New staking mechanics: delay on entry opposed to exit

This commit is contained in:
Greg Hysen 2019-08-26 11:04:28 -07:00
parent 88e56356c4
commit eb6ad7d29d
39 changed files with 1392 additions and 1509 deletions

View File

@ -17,6 +17,7 @@
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./interfaces/IStaking.sol";
import "./fees/MixinExchangeManager.sol";
@ -24,9 +25,7 @@ import "./stake/MixinZrxVault.sol";
import "./staking_pools/MixinStakingPoolRewardVault.sol";
import "./sys/MixinScheduler.sol";
import "./stake/MixinStakeBalances.sol";
import "./stake/MixinTimeLockedStake.sol";
import "./stake/MixinStake.sol";
import "./stake/MixinDelegatedStake.sol";
import "./staking_pools/MixinStakingPool.sol";
import "./fees/MixinExchangeFees.sol";
import "./staking_pools/MixinStakingPoolRewards.sol";
@ -36,19 +35,19 @@ contract Staking is
IStaking,
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinExchangeManager,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeStorage,
MixinStakeBalances,
MixinStake,
MixinStakingPoolRewards,
MixinExchangeFees,
MixinDelegatedStake
MixinStake,
MixinStakingPool,
MixinExchangeFees
{
// this contract can receive ETH
// solhint-disable no-empty-blocks

View File

@ -17,6 +17,7 @@
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
@ -45,14 +46,17 @@ import "./MixinExchangeManager.sol";
contract MixinExchangeFees is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinExchangeManager,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances
MixinStakeStorage,
MixinStakeBalances,
MixinStakingPoolRewards,
MixinStakingPool
{
using LibSafeMath for uint256;
@ -205,8 +209,8 @@ contract MixinExchangeFees is
bytes32 poolId = activePoolsThisEpoch[i];
// compute weighted stake
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId);
uint256 stakeHeldByPoolOperator = getStakeDelegatedToPoolByOwner(getStakingPoolOperator(poolId), poolId);
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId).current;
uint256 stakeHeldByPoolOperator = getStakeDelegatedToPoolByOwner(getStakingPoolOperator(poolId), poolId).current; // @TODO Update
uint256 weightedStake = stakeHeldByPoolOperator.safeAdd(
totalStakeDelegatedToPool
.safeSub(stakeHeldByPoolOperator)
@ -218,6 +222,7 @@ contract MixinExchangeFees is
activePools[i].poolId = poolId;
activePools[i].feesCollected = protocolFeesThisEpochByPool[poolId];
activePools[i].weightedStake = weightedStake;
activePools[i].delegatedStake = totalStakeDelegatedToPool;
// update cumulative amounts
totalFeesCollected = totalFeesCollected.safeAdd(activePools[i].feesCollected);
@ -251,9 +256,20 @@ contract MixinExchangeFees is
);
// record reward in vault
_recordDepositInStakingPoolRewardVault(activePools[i].poolId, reward);
bool rewardForOperatorOnly = activePools[i].delegatedStake == 0;
(, uint256 poolPortion) = rewardVault.recordDepositFor(activePools[i].poolId, reward, rewardForOperatorOnly);
totalRewardsPaid = totalRewardsPaid.safeAdd(reward);
// sync cumulative rewards, if necessary.
if (poolPortion > 0) {
_recordRewardForDelegators(
activePools[i].poolId,
poolPortion,
activePools[i].delegatedStake,
currentEpoch
);
}
// clear state for gas refunds
protocolFeesThisEpochByPool[activePools[i].poolId] = 0;
activePoolsThisEpoch[i] = 0;

View File

@ -32,6 +32,7 @@ import "../immutable/MixinStorage.sol";
contract MixinExchangeManager is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{

View File

@ -41,4 +41,6 @@ contract MixinConstants is
uint64 constant internal INITIAL_EPOCH = 0;
uint64 constant internal INITIAL_TIMELOCK_PERIOD = INITIAL_EPOCH;
uint256 constant internal MIN_TOKEN_VALUE = 1000000000000000000; // 10**18
}

View File

@ -21,6 +21,7 @@ pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/Ownable.sol";
import "./MixinConstants.sol";
import "../interfaces/IZrxVault.sol";
import "../interfaces/IEthVault.sol";
import "../interfaces/IStakingPoolRewardVault.sol";
import "../interfaces/IStructs.sol";
@ -28,8 +29,8 @@ import "../interfaces/IStructs.sol";
// solhint-disable max-states-count, no-empty-blocks
contract MixinStorage is
MixinDeploymentConstants,
MixinConstants,
Ownable
Ownable,
MixinConstants
{
constructor()
@ -40,26 +41,23 @@ contract MixinStorage is
// address of staking contract
address internal stakingContract;
// mapping from Owner to Amount Staked
mapping (address => uint256) internal stakeByOwner;
// mapping from Owner to Amount of Active Stake
mapping (address => IStructs.DelayedBalance) internal activeStakeByOwner;
// mapping from Owner to Amount of Instactive Stake
mapping (address => uint256) internal activatedStakeByOwner;
// mapping from Owner to Amount TimeLocked
mapping (address => IStructs.TimeLock) internal timeLockedStakeByOwner;
// mapping from Owner to Amount of Inactive Stake
mapping (address => IStructs.DelayedBalance) internal inactiveStakeByOwner;
// mapping from Owner to Amount Delegated
mapping (address => uint256) internal delegatedStakeByOwner;
mapping (address => IStructs.DelayedBalance) internal delegatedStakeByOwner;
// mapping from Owner to Pool Id to Amount Delegated
mapping (address => mapping (bytes32 => uint256)) internal delegatedStakeToPoolByOwner;
mapping (address => mapping (bytes32 => IStructs.DelayedBalance)) internal delegatedStakeToPoolByOwner;
// mapping from Pool Id to Amount Delegated
mapping (bytes32 => uint256) internal delegatedStakeByPoolId;
mapping (bytes32 => IStructs.DelayedBalance) internal delegatedStakeByPoolId;
// total activated stake in the system
uint256 internal totalActivatedStake;
// mapping from Owner to Amount of Withdrawable Stake
mapping (address => uint256) internal withdrawableStakeByOwner;
// tracking Pool Id
bytes32 internal nextPoolId = INITIAL_POOL_ID;
@ -80,23 +78,16 @@ contract MixinStorage is
// current epoch start time
uint256 internal currentEpochStartTimeInSeconds;
// current withdrawal period
uint256 internal currentTimeLockPeriod = INITIAL_TIMELOCK_PERIOD;
// current epoch start time
uint256 internal currentTimeLockPeriodStartEpoch = INITIAL_EPOCH;
// fees collected this epoch
mapping (bytes32 => uint256) internal protocolFeesThisEpochByPool;
// pools that were active in the current epoch
bytes32[] internal activePoolsThisEpoch;
// mapping from POol Id to Shadow Rewards
mapping (bytes32 => uint256) internal shadowRewardsByPoolId;
// reward ratios by epoch
mapping (bytes32 => mapping (uint256 => IStructs.Fraction)) internal cumulativeRewardsByPool;
// shadow balances by
mapping (address => mapping (bytes32 => uint256)) internal shadowRewardsInPoolByOwner;
mapping (bytes32 => uint256) internal cumulativeRewardsByPoolLastStored;
// registered 0x Exchange contracts
mapping (address => bool) internal validExchanges;
@ -104,6 +95,9 @@ contract MixinStorage is
// ZRX vault
IZrxVault internal zrxVault;
// Rebate Vault
IEthVault internal ethVault;
// Rebate Vault
IStakingPoolRewardVault internal rewardVault;
@ -113,3 +107,4 @@ contract MixinStorage is
// Denominator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaDenomintor = 6;
}

View File

@ -0,0 +1,71 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
/// @dev This vault manages Ether.
interface IEthVault {
/// @dev Emitted when Ether are deposited into the vault.
/// @param sender Address of sender (`msg.sender`).
/// @param owner of Ether.
/// @param amount of Ether deposited.
event EthDepositedIntoVault(
address indexed sender,
address indexed owner,
uint256 amount
);
/// @dev Emitted when Ether are withdrawn from the vault.
/// @param sender Address of sender (`msg.sender`).
/// @param owner of Ether.
/// @param amount of Ether withdrawn.
event EthWithdrawnFromVault(
address indexed sender,
address indexed owner,
uint256 amount
);
/// @dev Deposit an `amount` of ETH from `owner` into the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of ETH Tokens.
function depositFor(address owner)
external
payable;
/// @dev Withdraw an `amount` of ETH to `msg.sender` from the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param amount of ETH to withdraw.
function withdraw(uint256 amount)
external;
/// @dev Withdraw ALL ETH to `msg.sender` from the vault.
function withdrawAll()
external
returns (uint256);
/// @dev Returns the balance in ETH of the `owner`
/// @return Balance in ETH.
function balanceOf(address owner)
external
view
returns (uint256);
}

View File

@ -3,22 +3,34 @@ pragma solidity ^0.5.9;
interface IStakingEvents {
/// @dev Emitted by MixinStake when new Stake is minted.
/// @param owner of Stake.
/// @param amount of Stake minted.
event StakeMinted(
address owner,
/// @dev Emitted by MixinStake when ZRX is staked.
/// @param owner of ZRX.
/// @param amount of ZRX staked.
event Stake(
address indexed owner,
uint256 amount
);
/// @dev Emitted by MixinStake when Stake is burned.
/// @param owner of Stake.
/// @param amount of Stake burned.
event StakeBurned(
address owner,
/// @dev Emitted by MixinStake when ZRX is unstaked.
/// @param owner of ZRX.
/// @param amount of ZRX unstaked.
event Unstake(
address indexed owner,
uint256 amount
);
/// @dev Emitted by MixinStake when ZRX is unstaked.
/// @param owner of ZRX.
/// @param amount of ZRX unstaked.
event MoveStake(
address indexed owner,
uint256 amount,
uint8 fromState,
bytes32 indexed fromPool,
uint8 toState,
bytes32 indexed toProol
);
/// @dev Emitted by MixinExchangeManager when an exchange is added.
/// @param exchangeAddress Address of new exchange.
event ExchangeAdded(

View File

@ -39,6 +39,12 @@ interface IStakingPoolRewardVault {
uint96 membersBalance;
}
/// @dev Emitted when the eth vault is changed
/// @param newEthVault address of new rth vault.
event EthVaultChanged(
address newEthVault
);
/// @dev Emitted when reward is deposited.
/// @param poolId The pool the reward was deposited for.
/// Note that a poolId of "0" means "unknown" at time of deposit.
@ -75,44 +81,57 @@ interface IStakingPoolRewardVault {
uint32 operatorShare
);
/// @dev Default constructor.
/// @dev Fallback function.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
function ()
external
payable;
/// @dev Deposit a reward in ETH for a specific pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
function depositFor(bytes32 poolId)
external
payable;
function setEthVault(address ethVaultAddress)
external;
/// @dev Record a deposit for a pool. This deposit should be in the same transaction,
/// @dev Record a deposit for a pool. This deposit should be in the same transaction,
/// which is enforced by the staking contract. We do not enforce it here to save (a lot of) gas.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function recordDepositFor(bytes32 poolId, uint256 amount)
external;
/// @param operatorOnly Only attribute amount to operator.
/// @return operatorPortion Portion of amount attributed to the operator.
/// @return operatorPortion Portion of amount attributed to the delegators.
function recordDepositFor(
bytes32 poolId,
uint256 amount,
bool operatorOnly
)
external
returns (
uint256 operatorPortion,
uint256 delegatorsPortion
);
/// @dev Withdraw some amount in ETH of an operator's reward.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForOperator(bytes32 poolId, uint256 amount)
function transferOperatorBalanceToEthVault(
bytes32 poolId,
address operator,
uint256 amount
)
external;
/// @dev Withdraw some amount in ETH of a pool member.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForMember(bytes32 poolId, uint256 amount)
/// @param amount Amount in ETH to transfer.
function transferMemberBalanceToEthVault(
bytes32 poolId,
address member,
uint256 amount
)
external;
/// @dev Register a new staking pool.

View File

@ -56,15 +56,44 @@ interface IStructs {
bytes32 poolId;
uint256 feesCollected;
uint256 weightedStake;
uint256 delegatedStake;
}
/// @dev Tracks timeLocked stake (see MixinTimeLockedStake).
/// @param lockedAt The TimeLock Period that stake was most recently locked at.
/// @param total Amount of stake that is timeLocked.
/// @param pending Stake pending to be un-TimeLocked next TimeLock Period.
struct TimeLock {
uint64 lockedAt;
uint96 total;
uint96 pending;
/// @dev A delayed balance allows values to be computed
struct DelayedBalance {
uint96 current;
uint96 next;
uint64 lastStored;
}
/// @dev Balance struct for stake.
/// @param current Balance in the current epoch.
/// @param next Balance in the next epoch.
struct StakeBalance {
uint256 current;
uint256 next;
}
/// @dev States that stake can exist in.
enum StakeState {
ACTIVE,
INACTIVE,
DELEGATED
}
/// @dev Info used to describe a state.
/// @param state of the stake.
/// @param poolId Unique Id of pool. This is set when state=DELEGATED.
struct StakeStateInfo {
StakeState state;
bytes32 poolId;
}
/// @dev Struct to represent a fraction.
/// @param numerator of fraction.
/// @param denominator of fraction.
struct Fraction {
uint256 numerator;
uint256 denominator;
}
}

View File

@ -1,127 +0,0 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
/// @dev This library contains logic for computing the reward balances of staking pool members.
/// *** READ MixinStakingPoolRewards BEFORE CONTINUING ***
library LibRewardMath {
using LibSafeMath for uint256;
/// @dev Computes a member's payout denominated in the real asset (ETH).
/// Use this function when a member is liquidating their position in the pool (undelegating all their stake);
/// their shadow balance must be reset to zero so there is no need to compute it here.
/// @param amountDelegatedByOwner Amount of Stake delegated by the member to the staking pool.
/// @param totalAmountDelegated Total amount of Stake delegated by all members of the staking pool.
/// @param amountOfShadowAssetHeldByOwner The shadow balance of the member.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computePayoutDenominatedInRealAsset(
uint256 amountDelegatedByOwner,
uint256 totalAmountDelegated,
uint256 amountOfShadowAssetHeldByOwner,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (uint256)
{
uint256 combinedPayout = amountDelegatedByOwner
.safeMul(totalAmountOfShadowAsset.safeAdd(totalAmountOfRealAsset))
.safeDiv(totalAmountDelegated);
// we round up the amount of shadow assets when computing buy-ins.
// the result is that sometimes the amount of actual assets in the pool
// is less than the shadow eth. in this case, we'll end up with a floating imbalance.
uint256 payoutInRealAsset = combinedPayout < amountOfShadowAssetHeldByOwner ?
0 :
combinedPayout - amountOfShadowAssetHeldByOwner;
return payoutInRealAsset;
}
/// @dev Computes a member's payout denominated in the real asset (ETH).
/// Use this function when a member is undelegating a portion (but not all) of their stake.
/// @param partialAmountDelegatedByOwner Amount of Stake being undelegated by the member to the staking pool.
/// @param amountDelegatedByOwner Amount of Stake delegated by the member to the staking pool.
/// This includes `partialAmountDelegatedByOwner`.
/// @param totalAmountDelegated Total amount of Stake delegated by all members of the staking pool.
/// @param amountOfShadowAssetHeldByOwner The shadow balance of the member.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computePartialPayout(
uint256 partialAmountDelegatedByOwner,
uint256 amountDelegatedByOwner,
uint256 totalAmountDelegated,
uint256 amountOfShadowAssetHeldByOwner,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (
uint256 payoutInRealAsset,
uint256 payoutInShadowAsset
)
{
payoutInShadowAsset = amountOfShadowAssetHeldByOwner
.safeMul(partialAmountDelegatedByOwner)
.safeDiv(amountDelegatedByOwner);
payoutInRealAsset = _computePayoutDenominatedInRealAsset(
partialAmountDelegatedByOwner,
totalAmountDelegated,
payoutInShadowAsset,
totalAmountOfShadowAsset,
totalAmountOfRealAsset
);
return (payoutInRealAsset, payoutInShadowAsset);
}
/// @dev Computes how much shadow asset to mint a member who wants to
/// join (or delegate more stake to) a staking pool.
/// See MixinStakingPoolRewards for more information on shadow assets.
/// @param amountToDelegateByOwner Amount of Stake the new member would delegate.
/// @param totalAmountDelegated Total amount currently delegated to the pool.
/// This does *not* include `amountToDelegateByOwner`.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computeBuyInDenominatedInShadowAsset(
uint256 amountToDelegateByOwner,
uint256 totalAmountDelegated,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (uint256)
{
if (totalAmountDelegated == 0) {
return 0;
}
return amountToDelegateByOwner
.safeMul(totalAmountOfShadowAsset.safeAdd(totalAmountOfRealAsset))
.safeAdd(totalAmountDelegated.safeSub(1)) // we round up when computing shadow asset
.safeDiv(totalAmountDelegated);
}
}

View File

@ -1,167 +0,0 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "./MixinZrxVault.sol";
import "../staking_pools/MixinStakingPoolRewardVault.sol";
import "../sys/MixinScheduler.sol";
import "./MixinStakeBalances.sol";
import "./MixinTimeLockedStake.sol";
import "./MixinStake.sol";
import "../staking_pools/MixinStakingPoolRewards.sol";
/// @dev This mixin contains logic for managing delegated stake.
/// **** Read MixinStake before continuing ****
/// Stake can be delegated to staking pools in order to trustlessly
/// leverage the weight of several stakers. The meaning of this
/// leverage depends on the context in which stake the is being utilized.
/// For example, the amount of fee-based rewards a market maker receives
/// is correlated to how much stake has been delegated to their pool (see MixinExchangeFees).
contract MixinDelegatedStake is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances,
MixinStake,
MixinStakingPoolRewards
{
using LibSafeMath for uint256;
/// @dev Deposit Zrx and mint stake in the "Activated & Delegated" state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndDelegateToStakingPool(bytes32 poolId, uint256 amount)
external
{
address payable owner = msg.sender;
_mintStake(owner, amount);
activateStake(amount);
_delegateStake(owner, poolId, amount);
}
/// @dev Activates stake that is presently in the Deactivated & Withdrawable state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// The newly activated stake is then delegated to a staking pool.
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Stake to activate & delegate.
function activateAndDelegateStake(
bytes32 poolId,
uint256 amount
)
public
{
activateStake(amount);
address payable owner = msg.sender;
_delegateStake(owner, poolId, amount);
}
/// @dev Deactivate & TimeLock stake that is currently in the Activated & Delegated state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// @param poolId Unique Id of staking pool that the Stake is currently delegated to.
/// @param amount of Stake to deactivate and timeLock.
function deactivateAndTimeLockDelegatedStake(bytes32 poolId, uint256 amount)
public
{
deactivateAndTimeLockStake(amount);
address payable owner = msg.sender;
_undelegateStake(owner, poolId, amount);
}
/// @dev Delegates stake from `owner` to the staking pool with id `poolId`
/// @param owner of Stake
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Stake to delegate.
function _delegateStake(
address payable owner,
bytes32 poolId,
uint256 amount
)
private
{
// take snapshot of parameters before any computation
uint256 _delegatedStakeByOwner = delegatedStakeByOwner[owner];
uint256 _delegatedStakeToPoolByOwner = delegatedStakeToPoolByOwner[owner][poolId];
uint256 _delegatedStakeByPoolId = delegatedStakeByPoolId[poolId];
// join staking pool
_joinStakingPool(
poolId,
owner,
amount,
_delegatedStakeByPoolId
);
// increment how much stake the owner has delegated
delegatedStakeByOwner[owner] = _delegatedStakeByOwner.safeAdd(amount);
// increment how much stake the owner has delegated to the input pool
delegatedStakeToPoolByOwner[owner][poolId] = _delegatedStakeToPoolByOwner.safeAdd(amount);
// increment how much stake has been delegated to pool
delegatedStakeByPoolId[poolId] = _delegatedStakeByPoolId.safeAdd(amount);
}
/// @dev Undelegates stake of `owner` from the staking pool with id `poolId`
/// @param owner of Stake
/// @param poolId Unique Id of staking pool to undelegate stake from.
/// @param amount of Stake to undelegate.
function _undelegateStake(
address payable owner,
bytes32 poolId,
uint256 amount
)
private
{
// take snapshot of parameters before any computation
uint256 _delegatedStakeByOwner = delegatedStakeByOwner[owner];
uint256 _delegatedStakeToPoolByOwner = delegatedStakeToPoolByOwner[owner][poolId];
uint256 _delegatedStakeByPoolId = delegatedStakeByPoolId[poolId];
// leave the staking pool
_leaveStakingPool(
poolId,
owner,
amount,
_delegatedStakeToPoolByOwner,
_delegatedStakeByPoolId
);
// decrement how much stake the owner has delegated
delegatedStakeByOwner[owner] = _delegatedStakeByOwner.safeSub(amount);
// decrement how much stake the owner has delegated to the input pool
delegatedStakeToPoolByOwner[owner][poolId] = _delegatedStakeToPoolByOwner.safeSub(amount);
// decrement how much stake has been delegated to pool
delegatedStakeByPoolId[poolId] = _delegatedStakeByPoolId.safeSub(amount);
}
}

View File

@ -17,188 +17,217 @@
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibStakingRichErrors.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "./MixinZrxVault.sol";
import "../staking_pools/MixinStakingPoolRewardVault.sol";
import "../staking_pools/MixinStakingPoolRewards.sol";
import "../sys/MixinScheduler.sol";
import "./MixinStakeBalances.sol";
import "./MixinTimeLockedStake.sol";
import "./MixinStakeStorage.sol";
/// @dev This mixin contains logic for managing ZRX tokens and Stake.
/// Stake is minted when ZRX is deposited and burned when ZRX is withdrawn.
/// Stake can exist in one of many states:
/// 1. Activated
/// 2. Activated & Delegated
/// 3. Deactivated & TimeLocked
/// 4. Deactivated & Withdrawable
///
/// -- State Definitions --
/// Activated Stake
/// Stake in this state can be used as a utility within the 0x ecosystem.
/// For example, it carries weight when computing fee-based rewards (see MixinExchangeFees).
/// In the future, it may be used to participate in the 0x governance system.
///
/// Activated & Delegated Stake
/// Stake in this state also serves as a utility that is shared between the delegator and delegate.
/// For example, if delegated to a staking pool then it carries weight when computing fee-based rewards for
/// the staking pool; however, in this case, delegated stake carries less weight that regular stake (see MixinStakingPool).
///
/// Deactivated & TimeLocked Stake
/// Stake in this state cannot be used as a utility within the 0x ecosystem.
/// Stake is timeLocked when it moves out of activated states (Activated / Activated & Delagated).
/// By limiting the portability of stake, we mitigate undesirable behavior such as switching staking pools
/// in the middle of an epoch.
///
/// Deactivated & Withdrawable
/// Stake in this state cannot be used as a utility with in the 0x ecosystem.
/// This stake can, however, be burned and withdrawn as Zrx tokens.
/// ----------------------------
///
/// -- Valid State Transtions --
/// Activated -> Deactivated & TimeLocked
///
/// Activated & Delegated -> Deactivated & TimeLocked
///
/// Deactivated & TimeLocked -> Deactivated & Withdrawable
///
/// Deactivated & Withdrawable -> Activated
/// Deactivated & Withdrawable -> Activated & Delegated
/// Deactivated & Withdrawable -> Deactivated & Withdrawable
/// ----------------------------
///
/// Freshly minted stake is in the "Deactvated & Withdrawable" State, so it can
/// either be activated, delegated or withdrawn.
/// See MixinDelegatedStake and MixinTimeLockedStake for more on respective state transitions.
contract MixinStake is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinTimeLockedStake,
MixinStakeBalances
MixinStakeStorage,
MixinStakeBalances,
MixinStakingPoolRewards
{
using LibSafeMath for uint256;
/// @dev Deposit Zrx. This mints stake for the sender that is in the "Deactivated & Withdrawable" state.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndMintDeactivatedStake(uint256 amount)
/// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. Unstake to retrieve the ZRX.
/// Stake is in the 'Active' state.
/// @param amount of ZRX to stake.
function stake(uint256 amount)
external
{
_mintStake(msg.sender, amount);
}
address payable owner = msg.sender;
/// @dev Deposit Zrx and mint stake in the activated stake.
/// This is a convenience function, and can be used in-place of
/// calling `depositZrxAndMintDeactivatedStake` and `activateStake`.
/// This mints stake for the sender that is in the "Activated" state.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndMintActivatedStake(uint256 amount)
external
{
_mintStake(msg.sender, amount);
activateStake(amount);
}
/// @dev Burns deactivated stake and withdraws the corresponding amount of Zrx.
/// @param amount of Stake to burn / Zrx to withdraw
function burnDeactivatedStakeAndWithdrawZrx(uint256 amount)
external
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
if (amount > getDeactivatedStake(owner)) {
LibRichErrors.rrevert(LibStakingRichErrors.InsufficientBalanceError(
amount,
getDeactivatedStake(owner)
));
}
_burnStake(owner, amount);
}
/// @dev Activates stake that is presently in the Deactivated & Withdrawable state.
/// @param amount of Stake to activate.
function activateStake(uint256 amount)
public
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
if (amount > getActivatableStake(owner)) {
LibRichErrors.rrevert(LibStakingRichErrors.InsufficientBalanceError(
amount,
getActivatableStake(owner)
));
}
activatedStakeByOwner[owner] = activatedStakeByOwner[owner].safeAdd(amount);
totalActivatedStake = totalActivatedStake.safeAdd(amount);
}
/// @dev Deactivate & TimeLock stake that is currently in the Activated state.
/// @param amount of Stake to deactivate and timeLock.
function deactivateAndTimeLockStake(uint256 amount)
public
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
if (amount > getActivatedStake(owner)) {
LibRichErrors.rrevert(LibStakingRichErrors.InsufficientBalanceError(
amount,
getActivatedStake(owner)
));
}
activatedStakeByOwner[owner] = activatedStakeByOwner[owner].safeSub(amount);
totalActivatedStake = totalActivatedStake.safeSub(amount);
_timeLockStake(owner, amount);
}
/// @dev Mints Stake in the Deactivated & Withdrawable state.
/// @param owner to mint Stake for.
/// @param amount of Stake to mint.
function _mintStake(address owner, uint256 amount)
internal
{
// deposit equivalent amount of ZRX into vault
zrxVault.depositFrom(owner, amount);
_depositFromOwnerIntoZrxVault(owner, amount);
// mint stake
stakeByOwner[owner] = stakeByOwner[owner].safeAdd(amount);
_mintBalance(activeStakeByOwner[owner], amount);
// emit stake event
emit StakeMinted(
// notify
emit Stake(
owner,
amount
);
}
/// @dev Burns Stake in the Deactivated & Withdrawable state.
/// @param owner to mint Stake for.
/// @param amount of Stake to mint.
function _burnStake(address owner, uint256 amount)
internal
/// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to the owner.
/// Stake must be in the 'inactive' state for at least one full epoch to unstake.
/// @param amount of ZRX to unstake.
function unstake(uint256 amount)
external
{
// burn stake
stakeByOwner[owner] = stakeByOwner[owner].safeSub(amount);
address payable owner = msg.sender;
// sanity check
uint256 currentWithdrawableStake = getWithdrawableStake(owner);
require(
amount <= currentWithdrawableStake,
"INSUFFICIENT_FUNDS"
);
// burn inactive stake
_burnBalance(inactiveStakeByOwner[owner], amount);
// update withdrawable field
withdrawableStakeByOwner[owner] = currentWithdrawableStake.safeSub(amount);
// withdraw equivalent amount of ZRX from vault
zrxVault.withdrawFrom(owner, amount);
_withdrawToOwnerFromZrxVault(owner, amount);
// emit stake event
emit StakeBurned(
emit Unstake(
owner,
amount
);
}
/// @dev Moves stake between states: 'active', 'inactive' or 'delegated'.
/// This change comes into effect next epoch.
/// @param from state to move stake out of.
/// @param to state to move stake into.
/// @param amount of stake to move.
function moveStake(IStructs.StakeStateInfo calldata from, IStructs.StakeStateInfo calldata to, uint256 amount)
external
{
// sanity check - do nothing if moving stake between the same state
if (from.state != IStructs.StakeState.DELEGATED && from.state == to.state) {
return;
} else if (from.state == IStructs.StakeState.DELEGATED && from.poolId == to.poolId) {
return;
}
address payable owner = msg.sender;
// handle delegation; this must be done before moving stake as the current
// (out-of-sync) state is used during delegation.
if (from.state == IStructs.StakeState.DELEGATED) {
_undelegateStake(
from.poolId,
owner,
amount
);
}
if (to.state == IStructs.StakeState.DELEGATED) {
_delegateStake(
to.poolId,
owner,
amount
);
}
// cache the current withdrawal state if we're moving out of the inactive state.
uint256 cachedWithdrawableStakeByOwner = (from.state == IStructs.StakeState.INACTIVE)
? getWithdrawableStake(owner)
: 0;
// execute move
IStructs.DelayedBalance storage fromPtr = _getBalancePtrFromState(from);
IStructs.DelayedBalance storage toPtr = _getBalancePtrFromState(to);
_moveStake(fromPtr, toPtr, amount);
// update withdrawable field, if necessary
if (from.state == IStructs.StakeState.INACTIVE) {
withdrawableStakeByOwner[owner] = _computeWithdrawableStake(owner, cachedWithdrawableStakeByOwner);
}
// notify
emit MoveStake(
owner,
amount,
uint8(from.state),
from.poolId,
uint8(to.state),
to.poolId
);
}
/// @dev Delegates an owners stake to a staking pool.
/// @param poolId Id of pool to delegate to.
/// @param owner of stake to delegate.
/// @param amount of stake to delegate.
function _delegateStake(
bytes32 poolId,
address payable owner,
uint256 amount
)
private
{
// transfer any rewards from the transient pool vault to the eth vault;
// this must be done before we can modify the staker's portion of the delegator pool.
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, owner);
// sync cumulative rewards that we'll need for future computations
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
// decrement how much stake the owner has delegated to the input pool
_incrementBalance(delegatedStakeToPoolByOwner[owner][poolId], amount);
// increment how much stake has been delegated to pool
_incrementBalance(delegatedStakeByPoolId[poolId], amount);
}
/// @dev Delegates an owners stake to a staking pool.
/// @param poolId Id of pool to delegate to.
/// @param owner of stake to delegate.
/// @param amount of stake to delegate.
function _undelegateStake(
bytes32 poolId,
address payable owner,
uint256 amount
)
private
{
// transfer any rewards from the transient pool vault to the eth vault;
// this must be done before we can modify the staker's portion of the delegator pool.
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, owner);
// sync cumulative rewards that we'll need for future computations
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
// decrement how much stake the owner has delegated to the input pool
_decrementBalance(delegatedStakeToPoolByOwner[owner][poolId], amount);
// decrement how much stake has been delegated to pool
_decrementBalance(delegatedStakeByPoolId[poolId], amount);
}
/// @dev Returns a storage pointer to a user's stake in a given state.
/// @param state of user's stake to lookup.
/// @return a storage pointer to the corresponding stake stake
function _getBalancePtrFromState(IStructs.StakeStateInfo memory state)
private
returns (IStructs.DelayedBalance storage)
{
// lookup state
address owner = msg.sender;
if (state.state == IStructs.StakeState.ACTIVE) {
return activeStakeByOwner[owner];
} else if (state.state == IStructs.StakeState.INACTIVE) {
return inactiveStakeByOwner[owner];
} else if (state.state == IStructs.StakeState.DELEGATED) {
return delegatedStakeByOwner[owner];
}
// not found
revert("Unrecognized State");
}
}

View File

@ -24,7 +24,8 @@ import "../interfaces/IStructs.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinScheduler.sol";
import "./MixinTimeLockedStake.sol";
import "./MixinZrxVault.sol";
import "./MixinStakeStorage.sol";
/// @dev This mixin contains logic for querying stake balances.
@ -32,157 +33,132 @@ import "./MixinTimeLockedStake.sol";
contract MixinStakeBalances is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinScheduler,
MixinTimeLockedStake
MixinStakeStorage
{
using LibSafeMath for uint256;
/// @dev Returns the total activated stake across all owners.
/// This stake is in the "Activated" OR "Activated & Delegated" states.
/// @return Total active stake.
function getActivatedStakeAcrossAllOwners()
public
view
returns (uint256)
{
return totalActivatedStake;
}
/// @dev Returns the total stake for a given owner.
/// This stake can be in any state.
/// @param owner to query.
/// @param owner of stake.
/// @return Total active stake for owner.
function getTotalStake(address owner)
public
view
returns (uint256)
{
return stakeByOwner[owner];
return _balanceOfOwnerInZrxVault(owner);
}
/// @dev Returns the activated stake for a given owner.
/// This stake is in the "Activated" OR "Activated & Delegated" states.
/// @param owner to query.
/// @return Activated stake for owner.
function getActivatedStake(address owner)
/// @dev Returns the active stake for a given owner.
/// @param owner of stake.
/// @return Active stake for owner.
function getActiveStake(address owner)
public
view
returns (uint256)
returns (IStructs.StakeBalance memory balance)
{
return activatedStakeByOwner[owner];
IStructs.DelayedBalance memory storedBalance = _syncBalanceDestructive(activeStakeByOwner[owner]);
return IStructs.StakeBalance({
current: storedBalance.current,
next: storedBalance.next
});
}
/// @dev Returns the deactivated stake for a given owner.
/// This stake is in the "Deactivated & TimeLocked" OR "Deactivated & Withdrawable" states.
/// @param owner to query.
/// @return Deactivated stake for owner.
function getDeactivatedStake(address owner)
/// @dev Returns the inactive stake for a given owner.
/// @param owner of stake.
/// @return Inactive stake for owner.
function getInactiveStake(address owner)
public
view
returns (uint256)
returns (IStructs.StakeBalance memory balance)
{
return getTotalStake(owner).safeSub(getActivatedStake(owner));
IStructs.DelayedBalance memory storedBalance = _syncBalanceDestructive(inactiveStakeByOwner[owner]);
return IStructs.StakeBalance({
current: storedBalance.current,
next: storedBalance.next
});
}
/// @dev Returns the activated & undelegated stake for a given owner.
/// This stake is in the "Activated" state.
/// @param owner to query.
/// @return Activated stake for owner.
function getActivatedAndUndelegatedStake(address owner)
public
view
returns (uint256)
{
return activatedStakeByOwner[owner].safeSub(getStakeDelegatedByOwner(owner));
}
/// @dev Returns the stake that can be activated for a given owner.
/// This stake is in the "Deactivated & Withdrawable" state.
/// @param owner to query.
/// @return Activatable stake for owner.
function getActivatableStake(address owner)
public
view
returns (uint256)
{
return getDeactivatedStake(owner).safeSub(getTimeLockedStake(owner));
}
/// @dev Returns the stake that can be withdrawn for a given owner.
/// This stake is in the "Deactivated & Withdrawable" state.
/// @param owner to query.
/// @dev Returns the amount stake that can be withdrawn for a given owner.
/// @param owner of stake.
/// @return Withdrawable stake for owner.
function getWithdrawableStake(address owner)
public
view
returns (uint256)
{
return getActivatableStake(owner);
uint256 cachedWithdrawableStakeByOwner = withdrawableStakeByOwner[owner];
return _computeWithdrawableStake(owner, cachedWithdrawableStakeByOwner);
}
/// @dev Returns the stake delegated by a given owner.
/// This stake is in the "Activated & Delegated" state.
/// @param owner to query.
/// @param owner of stake.
/// @return Delegated stake for owner.
function getStakeDelegatedByOwner(address owner)
public
view
returns (uint256)
returns (IStructs.StakeBalance memory balance)
{
return delegatedStakeByOwner[owner];
IStructs.DelayedBalance memory storedBalance = _syncBalanceDestructive(delegatedStakeByOwner[owner]);
return IStructs.StakeBalance({
current: storedBalance.current,
next: storedBalance.next
});
}
/// @dev Returns the stake delegated to a specific staking pool, by a given owner.
/// This stake is in the "Activated & Delegated" state.
/// @param owner to query.
/// @param owner of stake.
/// @param poolId Unique Id of pool.
/// @return Stake delegaated to pool by owner.
function getStakeDelegatedToPoolByOwner(address owner, bytes32 poolId)
public
view
returns (uint256)
returns (IStructs.StakeBalance memory balance)
{
return delegatedStakeToPoolByOwner[owner][poolId];
IStructs.DelayedBalance memory storedBalance = _syncBalanceDestructive(delegatedStakeToPoolByOwner[owner][poolId]);
return IStructs.StakeBalance({
current: storedBalance.current,
next: storedBalance.next
});
}
/// @dev Returns the total stake delegated to a specific staking pool, across all members.
/// This stake is in the "Activated & Delegated" state.
/// @param poolId Unique Id of pool.
/// @return Total stake delegaated to pool.
/// @return Total stake delegated to pool.
function getTotalStakeDelegatedToPool(bytes32 poolId)
public
view
returns (uint256)
returns (IStructs.StakeBalance memory balance)
{
return delegatedStakeByPoolId[poolId];
IStructs.DelayedBalance memory storedBalance = _syncBalanceDestructive(delegatedStakeByPoolId[poolId]);
return IStructs.StakeBalance({
current: storedBalance.current,
next: storedBalance.next
});
}
/// @dev Returns the timeLocked stake for a given owner.
/// This stake is in the "Deactivated & TimeLocked" state.
/// @dev Returns the stake that can be withdrawn for a given owner.
/// This stake is in the "Deactive & Withdrawable" state.
/// @param owner to query.
/// @return TimeLocked stake for owner.
function getTimeLockedStake(address owner)
public
/// @return Withdrawable stake for owner.
function _computeWithdrawableStake(address owner, uint256 cachedWithdrawableStakeByOwner)
internal
view
returns (uint256)
{
(IStructs.TimeLock memory timeLock,) = _getSynchronizedTimeLock(owner);
return timeLock.total;
}
/// @dev Returns the starting TimeLock Period of timeLocked state for a given owner.
/// This stake is in the "Deactivated & TimeLocked" state.
/// See MixinScheduling and MixinTimeLock.
/// @param owner to query.
/// @return Start of timeLock for owner's timeLocked stake.
function getTimeLockStart(address owner)
public
view
returns (uint256)
{
(IStructs.TimeLock memory timeLock,) = _getSynchronizedTimeLock(owner);
return timeLock.lockedAt;
// stake cannot be withdrawn if it has been reallocated for the `next` epoch;
// so the upper bound of withdrawable stake is always limited by the value of `next`.
IStructs.DelayedBalance memory storedBalance = inactiveStakeByOwner[owner];
if (storedBalance.lastStored == currentEpoch) {
return storedBalance.next < cachedWithdrawableStakeByOwner ? storedBalance.next : cachedWithdrawableStakeByOwner;
} else if (uint256(storedBalance.lastStored).safeAdd(1) == currentEpoch) {
return storedBalance.next < storedBalance.current ? storedBalance.next : storedBalance.current;
} else {
return storedBalance.next;
}
}
}

View File

@ -0,0 +1,196 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "../libs/LibSafeDowncast.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../interfaces/IStructs.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinScheduler.sol";
import "./MixinZrxVault.sol";
/// @dev This mixin contains logic for managing stake storage.
contract MixinStakeStorage is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinScheduler
{
using LibSafeMath for uint256;
/// @dev Moves stake between states: 'active', 'inactive' or 'delegated'.
/// This change comes into effect next epoch.
/// @param fromPtr pointer to storage location of `from` stake.
/// @param toPtr pointer to storage location of `to` stake.
/// @param amount of stake to move.
function _moveStake(
IStructs.DelayedBalance storage fromPtr,
IStructs.DelayedBalance storage toPtr,
uint256 amount
)
internal
{
// do nothing if pointers are equal
if (_arePointersEqual(fromPtr, toPtr)) {
return;
}
// load balance from storage and synchronize it
IStructs.DelayedBalance memory from = _syncBalanceDestructive(fromPtr);
IStructs.DelayedBalance memory to = _syncBalanceDestructive(toPtr);
// sanity check on balance
if (amount > from.next) {
revert("Insufficient Balance");
}
// move stake for next epoch
from.next = LibSafeDowncast.downcastToUint96(uint256(from.next).safeSub(amount));
to.next = LibSafeDowncast.downcastToUint96(uint256(to.next).safeAdd(amount));
// update state in storage
_storeBalance(fromPtr, from);
_storeBalance(toPtr, to);
}
/// @dev Synchronizes the fields of a stored balance.
/// The structs `current` field is set to `next` if the
/// current epoch is greater than the epoch in which the struct
/// was stored.
/// @param balance to update. This will be equal to the return value after calling.
/// @return synchronized balance.
function _syncBalanceDestructive(IStructs.DelayedBalance memory balance)
internal
view
returns (IStructs.DelayedBalance memory)
{
uint256 currentEpoch = getCurrentEpoch();
if (currentEpoch > balance.lastStored) {
balance.lastStored = LibSafeDowncast.downcastToUint64(currentEpoch);
balance.current = balance.next;
}
return balance;
}
/// @dev Mints new value in a balance.
/// This causes both the `current` and `next` fields to increase immediately.
/// @param balancePtr storage pointer to balance.
/// @param amount to mint.
function _mintBalance(IStructs.DelayedBalance storage balancePtr, uint256 amount)
internal
{
// Remove stake from balance
IStructs.DelayedBalance memory balance = _syncBalanceDestructive(balancePtr);
balance.next = LibSafeDowncast.downcastToUint96(uint256(balance.next).safeAdd(amount));
balance.current = LibSafeDowncast.downcastToUint96(uint256(balance.current).safeAdd(amount));
// update state
_storeBalance(balancePtr, balance);
}
/// @dev Burns existing value in a balance.
/// This causes both the `current` and `next` fields to decrease immediately.
/// @param balancePtr storage pointer to balance.
/// @param amount to mint.
function _burnBalance(IStructs.DelayedBalance storage balancePtr, uint256 amount)
internal
{
// Remove stake from balance
IStructs.DelayedBalance memory balance = _syncBalanceDestructive(balancePtr);
balance.next = LibSafeDowncast.downcastToUint96(uint256(balance.next).safeSub(amount));
balance.current = LibSafeDowncast.downcastToUint96(uint256(balance.current).safeSub(amount));
// update state
_storeBalance(balancePtr, balance);
}
/// @dev Increments a balance.
/// Ths updates the `next` field but not the `current` field.
/// @param balancePtr storage pointer to balance.
/// @param amount to increment by.
function _incrementBalance(IStructs.DelayedBalance storage balancePtr, uint256 amount)
internal
{
// Add stake to balance
IStructs.DelayedBalance memory balance = _syncBalanceDestructive(balancePtr);
balance.next = LibSafeDowncast.downcastToUint96(uint256(balance.next).safeAdd(amount));
// update state
_storeBalance(balancePtr, balance);
}
/// @dev Decrements a balance.
/// Ths updates the `next` field but not the `current` field.
/// @param balancePtr storage pointer to balance.
/// @param amount to decrement by.
function _decrementBalance(IStructs.DelayedBalance storage balancePtr, uint256 amount)
internal
{
// Remove stake from balance
IStructs.DelayedBalance memory balance = _syncBalanceDestructive(balancePtr);
balance.next = LibSafeDowncast.downcastToUint96(uint256(balance.next).safeSub(amount));
// update state
_storeBalance(balancePtr, balance);
}
/// @dev Stores a balance in storage.
/// @param balancePtr points to where `balance` will be stored.
/// @param balance to save to storage.
function _storeBalance(
IStructs.DelayedBalance storage balancePtr,
IStructs.DelayedBalance memory balance
)
private
{
// note - this compresses into a single `sstore` when optimizations are enabled,
// since the StakeBalance struct occupies a single word of storage.
balancePtr.lastStored = balance.lastStored;
balancePtr.next = balance.next;
balancePtr.current = balance.current;
}
/// @dev Returns true iff storage pointers resolve to same storage location.
/// @param balancePtrA first storage pointer.
/// @param balancePtrB second storage pointer.
/// @return true iff pointers are equal.
function _arePointersEqual(
// solhint-disable-next-line no-unused-vars
IStructs.DelayedBalance storage balancePtrA,
// solhint-disable-next-line no-unused-vars
IStructs.DelayedBalance storage balancePtrB
)
private
returns (bool areEqual)
{
assembly {
areEqual := and(
eq(balancePtrA_slot, balancePtrB_slot),
eq(balancePtrA_offset, balancePtrB_offset)
)
}
return areEqual;
}
}

View File

@ -1,150 +0,0 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibSafeDowncast.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "../sys/MixinScheduler.sol";
/// @dev This mixin contains logic for timeLocking stake.
/// **** Read MixinStake before continuing ****
/// When stake moves from an Activated state it must first go to
/// the Deactivated & TimeLocked state. The stake will be timeLocked
/// for a period of time, called a TimeLock Period, which is measured in epochs.
/// (see MixinScheduler).
/// Stake remains timeLocked for at least one full TimeLock Period; so,
/// if your stake is locked sometime during TimeLock Period #1 then it will
/// be un-TimeLocked at TimeLock Period #3.
/// Note that no action is required by the user to un-TimeLock their stake, and
/// when stake is un-TimeLocked it is moved to the state Deactivated & Withdrawable.
/// (see MixinStake).
///
/// -- The TimeLocking Data Structure --
/// Three fields are used to represent a timeLock:
/// 1. Total timeLocked stake (called `total`)
/// 2. TimeLocked stake pending removal of timeLock, on next TimeLock Period (called `pending`)
/// 3. The most recent TimeLock Period in which stake was timeLocked. (called `lockedAt`)
///
/// Each user has exactly one instance of this timeLock struct, which manages all of
/// their timeLocked stake. This data structure is defined in `IStructs.TimeLock`.
/// This data structure was designed to fit into one word of storage, as a gas optimization.
/// Its fields are updated only when a user interacts with their stake.
/// ------------------------------------
///
/// -- TimeLocking Example --
/// In the example below, the user executes a series of actions on their stake (`Action`) during `TimeLock Period` N.
/// The fields of the user's timeLocked struct (`lockedAt`, `total`, `pending`) are illustrated exactly as
/// they would be represented in storage.
/// The field `un-TimeLocked` is the amount of un-TimeLocked stake, as represented *in storage*; however, because
/// state is only updated when the user interacts with their stake, this field may lag.
/// The field `un-TimeLocked (virtual)` is the true amount of un-TimeLocked stake, as represented in the system;
/// the value in this field represents stake that has moved from the state
/// "Deactivated & TimeLocke" to "Deactivated & Withdrawable" (see MixinStake).
///
/// | Action | TimeLock Period | lockedAt | total | pending | un-TimeLocked | un-TimeLocked (virtual) |
/// | | 0 | 0 | 0 | 0 | 0 | 0 |
/// | lock(5) | 1 | 1 | 5 | 0 | 0 | 0 |
/// | | 2 | 1 | 5 | 0 | 0 | 0 |
/// | lock(10) | 2 | 2 | 15 | 5 | 0 | 0 |
/// | | 3 | 2 | 15 | 5 | 0 | 5 |
/// | lock(15) | 3 | 3 | 30 | 15 | 5 | 5 |
/// | | 4 | 3 | 30 | 15 | 5 | 15 |
/// | | 5 | 3 | 30 | 15 | 5 | 30 |
/// | lock(0) | 5 | 5 | 30 | 30 | 30 | 30 |
/// | lock(20) | 6 | 6 | 50 | 30 | 30 | 30 |
/// | unlock(30) | 6 | 6 | 20 | 0 | 0 | 0 |
/// | | 7 | 6 | 20 | 0 | 0 | 0 |
/// | | 8 | 6 | 20 | 0 | 0 | 20 |
/// -------------------------------------------------------------------------------------------------------------
contract MixinTimeLockedStake is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinScheduler
{
using LibSafeMath for uint256;
using LibSafeDowncast for uint256;
/// @dev Forces the timeLock data structure to sync to state.
/// This is not necessary but may optimize some subsequent calls.
/// @param owner of Stake.
function forceTimeLockSync(address owner)
external
{
_syncTimeLockedStake(owner);
}
/// @dev TimeLocks Stake
/// This moves state into the Deactivated & TimeLocked state.
/// @param owner of Stake.
/// @param amount of Stake to timeLock.
function _timeLockStake(address owner, uint256 amount)
internal
{
(IStructs.TimeLock memory ownerTimeLock,) = _getSynchronizedTimeLock(owner);
uint256 total = uint256(ownerTimeLock.total);
ownerTimeLock.total = total.safeAdd(amount).downcastToUint96();
timeLockedStakeByOwner[owner] = ownerTimeLock;
}
/// @dev Updates storage to reflect the most up-to-date timeLock data structure for a given owner.
/// @param owner of Stake.
function _syncTimeLockedStake(address owner)
internal
{
(IStructs.TimeLock memory ownerTimeLock, bool isOutOfSync) = _getSynchronizedTimeLock(owner);
if (!isOutOfSync) {
return;
}
timeLockedStakeByOwner[owner] = ownerTimeLock;
}
/// @dev Returns the most up-to-date timeLock data structure for a given owner.
/// @param owner of Stake.
function _getSynchronizedTimeLock(address owner)
internal
view
returns (
IStructs.TimeLock memory ownerTimeLock,
bool isOutOfSync
)
{
uint256 currentTimeLockPeriod = getCurrentTimeLockPeriod();
ownerTimeLock = timeLockedStakeByOwner[owner];
isOutOfSync = false;
uint256 ownerLockedAt = uint256(ownerTimeLock.lockedAt);
if (currentTimeLockPeriod == ownerLockedAt.safeAdd(1)) {
// shift n periods
ownerTimeLock.pending = ownerTimeLock.total;
isOutOfSync = true;
} else if (currentTimeLockPeriod > ownerLockedAt) {
// TimeLock has expired - zero out
ownerTimeLock.lockedAt = 0;
ownerTimeLock.total = 0;
ownerTimeLock.pending = 0;
}
return (ownerTimeLock, isOutOfSync);
}
}

View File

@ -25,6 +25,9 @@ import "../immutable/MixinStorage.sol";
/// @dev This mixin contains logic for managing and interfacing with the Zrx Vault.
/// (see vaults/ZrxVault.sol).
contract MixinZrxVault is
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{
/// @dev Set the Zrx Vault.
@ -45,4 +48,47 @@ contract MixinZrxVault is
{
return address(zrxVault);
}
/// @dev Deposits Zrx Tokens from the `owner` into the vault.
/// @param owner of Zrx Tokens
/// @param amount of tokens to deposit.
function _depositFromOwnerIntoZrxVault(address owner, uint256 amount)
internal
{
IZrxVault _zrxVault = zrxVault;
require(
address(_zrxVault) != address(0),
"INVALID_ZRX_VAULT"
);
_zrxVault.depositFrom(owner, amount);
}
/// @dev Withdraws Zrx Tokens from to `owner` from the vault.
/// @param owner of deposited Zrx Tokens
/// @param amount of tokens to withdraw.
function _withdrawToOwnerFromZrxVault(address owner, uint256 amount)
internal
{
IZrxVault _zrxVault = zrxVault;
require(
address(_zrxVault) != address(0),
"INVALID_ZRX_VAULT"
);
_zrxVault.withdrawFrom(owner, amount);
}
/// @dev Returns balance of `owner` in the ZRX ault.
/// @param owner of deposited Zrx Tokens.
function _balanceOfOwnerInZrxVault(address owner)
internal
view
returns (uint256)
{
IZrxVault _zrxVault = zrxVault;
require(
address(_zrxVault) != address(0),
"INVALID_ZRX_VAULT"
);
return _zrxVault.balanceOf(owner);
}
}

View File

@ -0,0 +1,54 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "../interfaces/IStakingEvents.sol";
import "../interfaces/IEthVault.sol";
import "../immutable/MixinStorage.sol";
/// @dev This mixin contains logic for managing and interfacing with the Eth Vault.
/// (see vaults/EthVault.sol).
contract MixinEthVault is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{
/// @dev Set the Eth Vault.
/// @param ethVaultAddress Address of the Eth Vault.
function setEthVault(address ethVaultAddress)
external
onlyOwner
{
ethVault = IEthVault(ethVaultAddress);
}
/// @dev Return the current Eth Vault
/// @return Eth Vault
function getEthVault()
public
view
returns (address)
{
return address(ethVault);
}
}

View File

@ -28,7 +28,9 @@ import "../interfaces/IStructs.sol";
import "../interfaces/IStakingEvents.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinScheduler.sol";
import "./MixinStakingPoolRewardVault.sol";
import "./MixinStakingPoolRewards.sol";
/// @dev This mixin contains logic for staking pools.
@ -57,9 +59,15 @@ import "./MixinStakingPoolRewardVault.sol";
contract MixinStakingPool is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinStakingPoolRewardVault
MixinZrxVault,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinStakeStorage,
MixinStakeBalances,
MixinStakingPoolRewards
{
using LibSafeMath for uint256;
@ -117,8 +125,12 @@ contract MixinStakingPool is
});
poolById[poolId] = pool;
// initialize cumulative rewards for this pool;
// this is used to track rewards earned by delegators.
_initializeCumulativeRewards(poolId);
// register pool in reward vault
_registerStakingPoolInRewardVault(poolId, operatorShare);
rewardVault.registerStakingPool(poolId, operatorShare);
// notify
emit StakingPoolCreated(poolId, operatorAddress, operatorShare);

View File

@ -29,6 +29,7 @@ import "../immutable/MixinStorage.sol";
contract MixinStakingPoolRewardVault is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{
@ -53,39 +54,6 @@ contract MixinStakingPoolRewardVault is
return address(rewardVault);
}
/// @dev Returns the total balance in ETH of a staking pool, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalBalanceInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOf(poolId);
}
/// @dev Returns the balance in ETH of the staking pool operator, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getBalanceOfOperatorInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOfOperator(poolId);
}
/// @dev Returns the balance in ETH co-owned by the members of a pool, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getBalanceOfMembersInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOfMembers(poolId);
}
/// @dev Registers a staking pool in the reward vault.
/// @param poolId Unique id of pool.
/// @param operatorShare Portion of rewards owned by the operator, in ppm.
@ -98,24 +66,6 @@ contract MixinStakingPoolRewardVault is
);
}
/// @dev Withdraws an amount in ETH of the reward for a pool operator.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function _withdrawFromOperatorInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
internal
{
rewardVault.withdrawForOperator(poolId, amount);
}
/// @dev Withdraws an amount in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function _withdrawFromMemberInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
internal
{
rewardVault.withdrawForMember(poolId, amount);
}
/// @dev Deposits an amount in ETH into the reward vault.
/// @param amount The amount in ETH to deposit.
function _depositIntoStakingPoolRewardVault(uint256 amount)
@ -125,12 +75,21 @@ contract MixinStakingPoolRewardVault is
rewardVaultAddress.transfer(amount);
}
/// @dev Records an amount deposited into the reward vault for a specific pool.
/// @param poolId Unique id of pool.
/// @param amount The amount in ETH to record.
function _recordDepositInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
/// @dev Transfer from transient Reward Pool vault to ETH Vault.
/// @param poolId Unique Id of pool.
/// @param member of pool to transfer ETH to.
/// @param amount The amount in ETH to transfer.
function _transferMemberBalanceToEthVault(
bytes32 poolId,
address member,
uint256 amount
)
internal
{
rewardVault.recordDepositFor(poolId, amount);
require(
address(rewardVault) != NIL_ADDRESS,
"REWARD_VAULT_NOT_SET"
);
rewardVault.transferMemberBalanceToEthVault(poolId, member, amount);
}
}

View File

@ -17,297 +17,225 @@
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibStakingRichErrors.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../stake/MixinStakeBalances.sol";
import "./MixinStakingPoolRewardVault.sol";
import "./MixinStakingPool.sol";
/// @dev This mixin contains logic for staking pool rewards.
/// Rewards for a pool are generated by their market makers trading on the 0x protocol (MixinStakingPool).
/// The operator of a pool receives a fixed percentage of all rewards; generally, the operator is the
/// sole market maker of a pool. The remaining rewards are divided among the members of a pool; each member
/// gets an amount proportional to how much stake they have delegated to the pool.
///
/// Note that members can freely join or leave a staking pool at any time, by delegating/undelegating their stake.
/// Moreover, there is no limit to how many members a pool can have. To limit the state-updates needed to track member balances,
/// we store only a single balance shared by all members. This state is updated every time a reward is paid to the pool - which
/// is currently at the end of each epoch. Additionally, each member has an associated "Shadow Balance" which is updated only
/// when a member delegates/undelegates stake to the pool, along with a "Total Shadow Balance" that represents the cumulative
/// Shadow Balances of all members in a pool.
///
/// -- Member Balances --
/// Terminology:
/// Real Balance - The reward balance in ETH of a member.
/// Total Real Balance - The sum total of reward balances in ETH across all members of a pool.
/// Shadow Balance - The realized reward balance of a member.
/// Total Shadow Balance - The sum total of realized reward balances across all members of a pool.
/// How it works:
/// 1. When a member delegates, their ownership of the pool increases; however, this new ownership applies
/// only to future rewards and must not change the rewards currently owned by other members. Thus, when a
/// member delegates stake, we *increase* their Shadow Balance and the Total Shadow Balance of the pool.
///
/// 2. When a member withdraws a portion of their reward, their realized balance increases but their ownership
/// within the pool remains unchanged. Thus, we simultaneously *decrease* their Real Balance and
/// *increase* their Shadow Balance by the amount withdrawn. The cumulative balance decrease and increase, respectively.
///
/// 3. When a member undelegates, the portion of their reward that corresponds to that stake is also withdrawn. Thus,
/// their realized balance *increases* while their ownership of the pool *decreases*. To reflect this, we
/// decrease their Shadow Balance, the Total Shadow Balance, their Real Balance, and the Total Real Balance.
contract MixinStakingPoolRewards is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinZrxVault,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeStorage,
MixinStakeBalances
{
using LibSafeMath for uint256;
/// @dev Withdraws an amount in ETH of the reward for the pool operator.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function withdrawRewardForStakingPoolOperator(bytes32 poolId, uint256 amount)
external
onlyStakingPoolOperator(poolId)
{
_withdrawFromOperatorInStakingPoolRewardVault(poolId, amount);
poolById[poolId].operatorAddress.transfer(amount);
}
/// @dev Withdraws the total balance in ETH of the reward for the pool operator.
/// @param poolId Unique id of pool.
/// @return The amount withdrawn.
function withdrawTotalRewardForStakingPoolOperator(bytes32 poolId)
external
onlyStakingPoolOperator(poolId)
returns (uint256)
{
uint256 amount = getBalanceOfOperatorInStakingPoolRewardVault(poolId);
_withdrawFromOperatorInStakingPoolRewardVault(poolId, amount);
poolById[poolId].operatorAddress.transfer(amount);
return amount;
}
/// @dev Withdraws an amount in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function withdrawRewardForStakingPoolMember(bytes32 poolId, uint256 amount)
external
{
// sanity checks
address payable member = msg.sender;
uint256 memberBalance = computeRewardBalanceOfStakingPoolMember(poolId, member);
if (amount > memberBalance) {
LibRichErrors.rrevert(LibStakingRichErrors.WithdrawAmountExceedsMemberBalanceError(
amount,
memberBalance
));
}
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId].safeAdd(amount);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId].safeAdd(amount);
// perform withdrawal
_withdrawFromMemberInStakingPoolRewardVault(poolId, amount);
member.transfer(amount);
}
/// @dev Withdraws the total balance in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @return The amount withdrawn.
function withdrawTotalRewardForStakingPoolMember(bytes32 poolId)
external
returns (uint256)
{
// sanity checks
address payable member = msg.sender;
uint256 amount = computeRewardBalanceOfStakingPoolMember(poolId, member);
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId].safeAdd(amount);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId].safeAdd(amount);
// perform withdrawal and return amount withdrawn
_withdrawFromMemberInStakingPoolRewardVault(poolId, amount);
member.transfer(amount);
return amount;
}
/// @dev Returns the sum total reward balance in ETH of a staking pool, across all members and the pool operator.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalRewardBalanceOfStakingPool(bytes32 poolId)
external
view
returns (uint256)
{
return getTotalBalanceInStakingPoolRewardVault(poolId);
}
/// @dev Returns the reward balance in ETH of the pool operator.
/// @param poolId Unique id of pool.
/// @return Balance.
function getRewardBalanceOfStakingPoolOperator(bytes32 poolId)
external
view
returns (uint256)
{
return getBalanceOfOperatorInStakingPoolRewardVault(poolId);
}
/// @dev Returns the reward balance in ETH co-owned by the members of a pool.
/// @param poolId Unique id of pool.
/// @return Balance.
function getRewardBalanceOfStakingPoolMembers(bytes32 poolId)
external
view
returns (uint256)
{
return getBalanceOfMembersInStakingPoolRewardVault(poolId);
}
/// @dev Returns the shadow balance of a specific member of a staking pool.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
/// @return Balance.
function getShadowBalanceOfStakingPoolMember(bytes32 poolId, address member)
public
view
returns (uint256)
{
return shadowRewardsInPoolByOwner[member][poolId];
}
/// @dev Returns the total shadow balance of a staking pool.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalShadowBalanceOfStakingPool(bytes32 poolId)
public
view
returns (uint256)
{
return shadowRewardsByPoolId[poolId];
}
/// @dev Computes the reward balance in ETH of a specific member of a pool.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
/// @return Balance.
function computeRewardBalanceOfStakingPoolMember(bytes32 poolId, address member)
/// @return Balance in ETH.
function computeRewardBalanceOfDelegator(bytes32 poolId, address member)
public
view
returns (uint256)
{
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
return LibRewardMath._computePayoutDenominatedInRealAsset(
delegatedStakeToPoolByOwner[member][poolId],
delegatedStakeByPoolId[poolId],
shadowRewardsInPoolByOwner[member][poolId],
shadowRewardsByPoolId[poolId],
poolBalance
);
// cache some values to reduce sloads
IStructs.DelayedBalance memory delegatedStake = delegatedStakeToPoolByOwner[member][poolId];
uint256 currentEpoch = getCurrentEpoch();
// value is always zero in these two scenarios:
// 1. The current epoch is zero: delegation begins at epoch 1
// 2. The owner's delegated is current as of this epoch: their rewards have been moved to the ETH vault.
if (currentEpoch == 0 || delegatedStake.lastStored == currentEpoch) return 0;
// compute reward accumulated during `lastStored` epoch;
// the `current` balance describes how much stake was collecting rewards when `lastStored` was set.
uint256 rewardsAccumulatedDuringLastStoredEpoch = (delegatedStake.current != 0)
? _computeMemberRewardOverInterval(
poolId,
delegatedStake.current,
delegatedStake.lastStored - 1,
delegatedStake.lastStored
)
: 0;
// compute the rewards accumulated by the `next` balance;
// this starts at `lastStored + 1` and goes up until the last epoch, during which
// rewards were accumulated. This is at most the most recently finalized epoch (current epoch - 1).
uint256 rewardsAccumulatedAfterLastStoredEpoch = (cumulativeRewardsByPoolLastStored[poolId] > delegatedStake.lastStored)
? _computeMemberRewardOverInterval(
poolId,
delegatedStake.next,
delegatedStake.lastStored,
cumulativeRewardsByPoolLastStored[poolId]
)
: 0;
// compute the total reward
uint256 totalReward = rewardsAccumulatedDuringLastStoredEpoch.safeAdd(rewardsAccumulatedAfterLastStoredEpoch);
return totalReward;
}
/// @dev A member joins a staking pool.
/// This function increments the shadow balance of the member, along
/// with the total shadow balance of the pool. This ensures that
/// any rewards belonging to existing members will not be diluted.
/// @param poolId Unique Id of pool to join.
/// @param member The member to join.
/// @param amountOfStakeToDelegate The stake to be delegated by `member` upon joining.
/// @param totalStakeDelegatedToPool The amount of stake currently delegated to the pool.
/// This does not include `amountOfStakeToDelegate`.
function _joinStakingPool(
bytes32 poolId,
address payable member,
uint256 amountOfStakeToDelegate,
uint256 totalStakeDelegatedToPool
)
/// @dev Transfers a delegators accumulated rewards from the transient pool Reward Pool vault
/// to the Eth Vault. This is required before the member's stake in the pool can be
/// modified.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
function _transferDelegatorsAccumulatedRewardsToEthVault(bytes32 poolId, address member)
internal
{
// update delegator's share of reward pool
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
uint256 buyIn = LibRewardMath._computeBuyInDenominatedInShadowAsset(
amountOfStakeToDelegate,
totalStakeDelegatedToPool,
shadowRewardsByPoolId[poolId],
poolBalance
);
// there are no delegators in the first epoch
uint256 currentEpoch = getCurrentEpoch();
if (currentEpoch == 0) {
return;
}
// the buy-in will be > 0 iff there exists a non-zero reward.
if (buyIn > 0) {
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId].safeAdd(buyIn);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId].safeAdd(buyIn);
// compute balance owed to delegator
uint256 balance = computeRewardBalanceOfDelegator(poolId, member);
if (balance == 0) {
return;
}
// transfer from transient Reward Pool vault to ETH Vault
_transferMemberBalanceToEthVault(poolId, member, balance);
}
/// @dev Initializes Cumulative Rewards for a given pool.
function _initializeCumulativeRewards(bytes32 poolId)
internal
{
uint256 currentEpoch = getCurrentEpoch();
cumulativeRewardsByPool[poolId][currentEpoch] = IStructs.Fraction({numerator: 0, denominator: MIN_TOKEN_VALUE});
cumulativeRewardsByPoolLastStored[poolId] = currentEpoch;
}
/// @dev To compute a delegator's reward we must know the cumulative reward
/// at the epoch before they delegated. If they were already delegated then
/// we also need to know the value at the epoch in which they modified
/// their delegated stake for this pool. See `computeRewardBalanceOfDelegator`.
/// @param poolId Unique Id of pool.
/// @param epoch at which the stake was delegated by the delegator.
function _syncCumulativeRewardsNeededByDelegator(bytes32 poolId, uint256 epoch)
internal
{
// set default value if staking at epoch 0
if (epoch == 0) {
return;
}
// cache a storage pointer to the cumulative rewards for `poolId` indexed by epoch.
mapping (uint256 => IStructs.Fraction) storage cumulativeRewardsByPoolPtr = cumulativeRewardsByPool[poolId];
// fetch the last epoch at which we stored an entry for this pool;
// this is the most up-to-date cumulative rewards for this pool.
uint256 cumulativeRewardsLastStored = cumulativeRewardsByPoolLastStored[poolId];
IStructs.Fraction memory mostRecentCumulativeRewards = cumulativeRewardsByPoolPtr[cumulativeRewardsLastStored];
// copy our most up-to-date cumulative rewards for last epoch, if necessary.
uint256 lastEpoch = currentEpoch.safeSub(1);
if (cumulativeRewardsLastStored != lastEpoch) {
cumulativeRewardsByPoolPtr[lastEpoch] = mostRecentCumulativeRewards;
cumulativeRewardsByPoolLastStored[poolId] = lastEpoch;
}
// copy our most up-to-date cumulative rewards for last epoch, if necessary.
// this is necessary if the pool does not earn any rewards this epoch;
// if it does then this value may be overwritten when the epoch is finalized.
if (!_isCumulativeRewardSet(cumulativeRewardsByPoolPtr[epoch])) {
cumulativeRewardsByPoolPtr[epoch] = mostRecentCumulativeRewards;
}
}
/// @dev A member leaves a staking pool.
/// This function decrements the shadow balance of the member, along
/// with the total shadow balance of the pool. This ensures that
/// any rewards belonging to co-members will not be inflated.
/// @param poolId Unique Id of pool to leave.
/// @param member The member to leave.
/// @param amountOfStakeToUndelegate The stake to be undelegated by `member` upon leaving.
/// @param totalStakeDelegatedToPoolByMember The amount of stake currently delegated to the pool by the member.
/// This includes `amountOfStakeToUndelegate`.
/// @param totalStakeDelegatedToPool The total amount of stake currently delegated to the pool, across all members.
/// This includes `amountOfStakeToUndelegate`.
function _leaveStakingPool(
/// @dev Records a reward for delegators. This adds to the `cumulativeRewardsByPool`.
/// @param poolId Unique Id of pool.
/// @param reward to record for delegators.
/// @param amountOfDelegatedStake the amount of delegated stake that will split this reward.
/// @param epoch at which this was earned.
function _recordRewardForDelegators(
bytes32 poolId,
address payable member,
uint256 amountOfStakeToUndelegate,
uint256 totalStakeDelegatedToPoolByMember,
uint256 totalStakeDelegatedToPool
uint256 reward,
uint256 amountOfDelegatedStake,
uint256 epoch
)
internal
{
// get payout
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
uint256 payoutInRealAsset = 0;
uint256 payoutInShadowAsset = 0;
if (totalStakeDelegatedToPoolByMember == amountOfStakeToUndelegate) {
// full payout; this is computed separately to avoid extra computation and rounding.
payoutInShadowAsset = shadowRewardsInPoolByOwner[member][poolId];
payoutInRealAsset = LibRewardMath._computePayoutDenominatedInRealAsset(
amountOfStakeToUndelegate,
totalStakeDelegatedToPool,
payoutInShadowAsset,
shadowRewardsByPoolId[poolId],
poolBalance
);
} else {
// partial payout
(payoutInRealAsset, payoutInShadowAsset) = LibRewardMath._computePartialPayout(
amountOfStakeToUndelegate,
totalStakeDelegatedToPoolByMember,
totalStakeDelegatedToPool,
shadowRewardsInPoolByOwner[member][poolId],
shadowRewardsByPoolId[poolId],
poolBalance
);
}
// cache a storage pointer to the cumulative rewards for `poolId` indexed by epoch.
mapping (uint256 => IStructs.Fraction) storage cumulativeRewardsByPoolPtr = cumulativeRewardsByPool[poolId];
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId].safeSub(payoutInShadowAsset);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId].safeSub(payoutInShadowAsset);
// fetch the last epoch at which we stored an entry for this pool;
// this is the most up-to-date cumulative rewards for this pool.
uint256 cumulativeRewardsLastStored = cumulativeRewardsByPoolLastStored[poolId];
IStructs.Fraction memory mostRecentCumulativeRewards = cumulativeRewardsByPoolPtr[cumulativeRewardsLastStored];
// withdraw payout for member
if (payoutInRealAsset > 0) {
_withdrawFromMemberInStakingPoolRewardVault(poolId, payoutInRealAsset);
member.transfer(payoutInRealAsset);
}
// compute new cumulative reward
(uint256 numerator, uint256 denominator) = LibSafeMath.addFractions(
mostRecentCumulativeRewards.numerator,
mostRecentCumulativeRewards.denominator,
reward,
amountOfDelegatedStake
);
// normalize fraction components by dividing by the min token value (10^18)
(uint256 numeratorNormalized, uint256 denominatorNormalized) = (
numerator.safeDiv(MIN_TOKEN_VALUE),
denominator.safeDiv(MIN_TOKEN_VALUE)
);
// store cumulative rewards
cumulativeRewardsByPoolPtr[epoch] = IStructs.Fraction({
numerator: numeratorNormalized,
denominator: denominatorNormalized
});
cumulativeRewardsByPoolLastStored[poolId] = epoch;
}
/// @dev Computes a member's reward over a given epoch interval.
/// @param poolId Uniqud Id of pool.
/// @param memberStakeOverInterval Stake delegated to pool by meber over the interval.
/// @param beginEpoch beginning of interval.
/// @param endEpoch end of interval.
/// @return rewards accumulated over interval [beginEpoch, endEpoch]
function _computeMemberRewardOverInterval(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
)
private
view
returns (uint256)
{
IStructs.Fraction memory beginRatio = cumulativeRewardsByPool[poolId][beginEpoch];
IStructs.Fraction memory endRatio = cumulativeRewardsByPool[poolId][endEpoch];
uint256 reward = LibSafeMath.scaleFractionalDifference(
endRatio.numerator,
endRatio.denominator,
beginRatio.numerator,
beginRatio.denominator,
memberStakeOverInterval
);
return reward;
}
/// @dev returns true iff Cumulative Rewards are set
function _isCumulativeRewardSet(IStructs.Fraction memory cumulativeReward)
private
returns (bool)
{
// we use the denominator as a proxy for whether the cumulative
// reward is set, as setting the cumulative reward always sets this
// field to at least 1.
return cumulativeReward.denominator != 0;
}
}

View File

@ -36,6 +36,7 @@ import "../interfaces/IStakingEvents.sol";
contract MixinScheduler is
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{
@ -85,49 +86,6 @@ contract MixinScheduler is
return getCurrentEpochStartTimeInSeconds().safeAdd(getEpochDurationInSeconds());
}
/// @dev Returns the current timeLock period.
/// @return TimeLock period.
function getCurrentTimeLockPeriod()
public
view
returns (uint256)
{
return currentTimeLockPeriod;
}
/// @dev Returns the length of a timeLock period, measured in epochs.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period end.
function getTimeLockDurationInEpochs()
public
pure
returns (uint256)
{
return TIMELOCK_DURATION_IN_EPOCHS;
}
/// @dev Returns the epoch that the current timeLock period started at.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period start.
function getCurrentTimeLockPeriodStartEpoch()
public
view
returns (uint256)
{
return currentTimeLockPeriodStartEpoch;
}
/// @dev Returns the epoch that the current timeLock period will end.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period.
function getCurrentTimeLockPeriodEndEpoch()
public
view
returns (uint256)
{
return getCurrentTimeLockPeriodStartEpoch().safeAdd(getTimeLockDurationInEpochs());
}
/// @dev Moves to the next epoch, given the current epoch period has ended.
/// Time intervals that are measured in epochs (like timeLocks) are also incremented, given
/// their periods have ended.
@ -152,26 +110,12 @@ contract MixinScheduler is
currentEpoch = nextEpoch;
currentEpochStartTimeInSeconds = currentBlockTimestamp;
uint256 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds.safeAdd(getEpochDurationInSeconds());
// notify of epoch change
emit EpochChanged(
currentEpoch,
currentEpochStartTimeInSeconds,
earliestEndTimeInSeconds
);
// increment timeLock period, if needed
if (getCurrentTimeLockPeriodEndEpoch() <= nextEpoch) {
currentTimeLockPeriod = currentTimeLockPeriod.safeAdd(1);
currentTimeLockPeriodStartEpoch = currentEpoch;
uint256 endEpoch = currentEpoch.safeAdd(getTimeLockDurationInEpochs());
// notify
emit TimeLockPeriodChanged(
currentTimeLockPeriod,
currentTimeLockPeriodStartEpoch,
endEpoch
);
}
}
}

View File

@ -0,0 +1,110 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../interfaces/IEthVault.sol";
import "./MixinVaultCore.sol";
/// @dev This vault manages ETH.
contract EthVault is
Authorizable,
IEthVault,
IVaultCore,
MixinVaultCore
{
using LibSafeMath for uint256;
// mapping from Owner to ETH balance
mapping (address => uint256) internal balances;
/// @dev Constructor.
// solhint-disable-next-line no-empty-blocks
constructor() public {}
/// @dev Deposit an `amount` of ETH from `owner` into the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of ETH Tokens.
function depositFor(address owner)
external
payable
{
// update balance
uint256 amount = msg.value;
balances[owner] = balances[owner].safeAdd(msg.value);
// notify
emit EthDepositedIntoVault(msg.sender, owner, amount);
}
/// @dev Withdraw an `amount` of ETH to `msg.sender` from the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param amount of ETH to withdraw.
function withdraw(uint256 amount)
external
{
_withdrawFrom(msg.sender, amount);
}
/// @dev Withdraw ALL ETH to `msg.sender` from the vault.
function withdrawAll()
external
returns (uint256)
{
// get total balance
address payable owner = msg.sender;
uint256 totalBalance = balances[owner];
// withdraw ETH to owner
_withdrawFrom(owner, totalBalance);
return totalBalance;
}
/// @dev Returns the balance in ETH of the `owner`
/// @return Balance in ETH.
function balanceOf(address owner)
external
view
returns (uint256)
{
return balances[owner];
}
/// @dev Withdraw an `amount` of ETH to `owner` from the vault.
/// @param owner of ETH.
/// @param amount of ETH to withdraw.
function _withdrawFrom(address payable owner, uint256 amount)
internal
{
// update balance
// note that this call will revert if trying to withdraw more
// than the current balance
balances[owner] = balances[owner].safeSub(amount);
// notify
emit EthWithdrawnFromVault(msg.sender, owner, amount);
// withdraw ETH to owner
owner.transfer(amount);
}
}

View File

@ -25,6 +25,7 @@ import "../libs/LibStakingRichErrors.sol";
import "../libs/LibSafeDowncast.sol";
import "./MixinVaultCore.sol";
import "../interfaces/IStakingPoolRewardVault.sol";
import "../interfaces/IEthVault.sol";
import "../immutable/MixinConstants.sol";
@ -40,6 +41,7 @@ import "../immutable/MixinConstants.sol";
contract StakingPoolRewardVault is
Authorizable,
IStakingPoolRewardVault,
IVaultCore,
MixinDeploymentConstants,
MixinConstants,
MixinVaultCore
@ -50,6 +52,9 @@ contract StakingPoolRewardVault is
// mapping from Pool to Reward Balance in ETH
mapping (bytes32 => Balance) internal balanceByPoolId;
// address of ether vault
IEthVault internal ethVault;
/// @dev Fallback function. This contract is payable, but only by the staking contract.
function ()
external
@ -60,24 +65,15 @@ contract StakingPoolRewardVault is
emit RewardDeposited(UNKNOWN_STAKING_POOL_ID, msg.value);
}
/// @dev Deposit a reward in ETH for a specific pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
function depositFor(bytes32 poolId)
/// @dev Sets the Eth Vault.
/// Note that only the contract owner can call this.
/// @param ethVaultAddress Address of the Eth Vault.
function setEthVault(address ethVaultAddress)
external
payable
onlyStakingContract
onlyNotInCatastrophicFailure
onlyOwner
{
// update balance of pool
uint256 amount = msg.value;
Balance memory balance = balanceByPoolId[poolId];
_incrementBalanceStruct(balance, amount);
balanceByPoolId[poolId] = balance;
// notify
emit RewardDeposited(poolId, amount);
ethVault = IEthVault(ethVaultAddress);
emit EthVaultChanged(ethVaultAddress);
}
/// @dev Record a deposit for a pool. This deposit should be in the same transaction,
@ -86,26 +82,50 @@ contract StakingPoolRewardVault is
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function recordDepositFor(bytes32 poolId, uint256 amount)
/// @param operatorOnly Only attribute amount to operator.
/// @return operatorPortion Portion of amount attributed to the operator.
/// @return poolPortion Portion of amount attributed to the pool.
function recordDepositFor(
bytes32 poolId,
uint256 amount,
bool operatorOnly
)
external
onlyStakingContract
onlyNotInCatastrophicFailure
returns (
uint256 operatorPortion,
uint256 poolPortion
)
{
// update balance of pool
Balance memory balance = balanceByPoolId[poolId];
_incrementBalanceStruct(balance, amount);
(operatorPortion, poolPortion) = _incrementBalanceStruct(balance, amount, operatorOnly);
balanceByPoolId[poolId] = balance;
return (operatorPortion, poolPortion);
}
/// @dev Withdraw some amount in ETH of an operator's reward.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForOperator(bytes32 poolId, uint256 amount)
function transferOperatorBalanceToEthVault(
bytes32 poolId,
address operator,
uint256 amount
)
external
onlyStakingContract
{
if (amount == 0) {
return;
}
// sanity check on eth vault
require(
address(ethVault) != address(0),
"ETH_VAULT_NOT_SET"
);
// sanity check - sufficient balance?
uint256 operatorBalance = uint256(balanceByPoolId[poolId].operatorBalance);
if (amount > operatorBalance) {
@ -117,7 +137,7 @@ contract StakingPoolRewardVault is
// update balance and transfer `amount` in ETH to staking contract
balanceByPoolId[poolId].operatorBalance = operatorBalance.safeSub(amount).downcastToUint96();
stakingContractAddress.transfer(amount);
ethVault.depositFor.value(amount)(operator);
// notify
emit RewardWithdrawnForOperator(poolId, amount);
@ -127,11 +147,21 @@ contract StakingPoolRewardVault is
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForMember(bytes32 poolId, uint256 amount)
/// @param amount Amount in ETH to transfer.
function transferMemberBalanceToEthVault(
bytes32 poolId,
address member,
uint256 amount
)
external
onlyStakingContract
{
// sanity check on eth vault
require(
address(ethVault) != address(0),
"ETH_VAULT_NOT_SET"
);
// sanity check - sufficient balance?
uint256 membersBalance = uint256(balanceByPoolId[poolId].membersBalance);
if (amount > membersBalance) {
@ -143,7 +173,7 @@ contract StakingPoolRewardVault is
// update balance and transfer `amount` in ETH to staking contract
balanceByPoolId[poolId].membersBalance = membersBalance.safeSub(amount).downcastToUint96();
stakingContractAddress.transfer(amount);
ethVault.depositFor.value(amount)(member);
// notify
emit RewardWithdrawnForMember(poolId, amount);
@ -222,21 +252,35 @@ contract StakingPoolRewardVault is
/// pool operator and members of the pool based on the pool operator's share.
/// @param balance Balance struct to increment.
/// @param amount Amount to add to balance.
function _incrementBalanceStruct(Balance memory balance, uint256 amount)
/// @param operatorOnly Only give this balance to the operator.
/// @return portion of amount given to operator and delegators, respectively.
function _incrementBalanceStruct(Balance memory balance, uint256 amount, bool operatorOnly)
private
pure
returns (uint256 operatorPortion, uint256 poolPortion)
{
// compute portions. One of the two must round down: the operator always receives the leftover from rounding.
uint256 operatorPortion = LibMath.getPartialAmountCeil(
uint256(balance.operatorShare), // Operator share out of 1e6
PPM_DENOMINATOR,
amount
operatorPortion = operatorOnly
? amount
: LibMath.getPartialAmountCeil(
uint256(balance.operatorShare),
PPM_DENOMINATOR,
amount
);
poolPortion = amount.safeSub(operatorPortion);
// compute new balances
uint256 newOperatorBalance = uint256(balance.operatorBalance).safeAdd(operatorPortion);
uint256 newMembersBalance = uint256(balance.membersBalance).safeAdd(poolPortion);
// save new balances
balance.operatorBalance = LibSafeDowncast.downcastToUint96(newOperatorBalance);
balance.membersBalance = LibSafeDowncast.downcastToUint96(newMembersBalance);
return (
operatorPortion,
poolPortion
);
uint256 poolPortion = amount.safeSub(operatorPortion);
// update balances
balance.operatorBalance = uint256(balance.operatorBalance).safeAdd(operatorPortion).downcastToUint96();
balance.membersBalance = uint256(balance.membersBalance).safeAdd(poolPortion).downcastToUint96();
}
}

View File

@ -35,6 +35,7 @@ import "./MixinVaultCore.sol";
/// corruption of related state in the staking contract.
contract ZrxVault is
Authorizable,
IVaultCore,
IZrxVault,
MixinVaultCore
{

View File

@ -16,6 +16,7 @@
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/Staking.sol";

View File

@ -25,6 +25,9 @@ import "../src/interfaces/IStructs.sol";
contract TestStorageLayout is
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage
{
function assertExpectedStorageLayout()
@ -39,30 +42,70 @@ contract TestStorageLayout is
mstore(64, 0x00000016494e434f52524543545f53544f524147455f534c4f54000000000000)
mstore(96, 0)
}
if sub(owner_slot, 0) { revertIncorrectStorageSlot() }
if sub(stakingContract_slot, 1) { revertIncorrectStorageSlot() }
if sub(stakeByOwner_slot, 2) { revertIncorrectStorageSlot() }
if sub(activatedStakeByOwner_slot, 3) { revertIncorrectStorageSlot() }
if sub(timeLockedStakeByOwner_slot, 4) { revertIncorrectStorageSlot() }
if sub(delegatedStakeByOwner_slot, 5) { revertIncorrectStorageSlot() }
if sub(delegatedStakeToPoolByOwner_slot, 6) { revertIncorrectStorageSlot() }
if sub(delegatedStakeByPoolId_slot, 7) { revertIncorrectStorageSlot() }
if sub(totalActivatedStake_slot, 8) { revertIncorrectStorageSlot() }
if sub(nextPoolId_slot, 9) { revertIncorrectStorageSlot() }
if sub(poolById_slot, 10) { revertIncorrectStorageSlot() }
if sub(poolIdByMakerAddress_slot, 11) { revertIncorrectStorageSlot() }
if sub(makerAddressesByPoolId_slot, 12) { revertIncorrectStorageSlot() }
if sub(currentEpoch_slot, 13) { revertIncorrectStorageSlot() }
if sub(currentEpochStartTimeInSeconds_slot, 14) { revertIncorrectStorageSlot() }
if sub(currentTimeLockPeriod_slot, 15) { revertIncorrectStorageSlot() }
if sub(currentTimeLockPeriodStartEpoch_slot, 16) { revertIncorrectStorageSlot() }
if sub(protocolFeesThisEpochByPool_slot, 17) { revertIncorrectStorageSlot() }
if sub(activePoolsThisEpoch_slot, 18) { revertIncorrectStorageSlot() }
if sub(shadowRewardsByPoolId_slot, 19) { revertIncorrectStorageSlot() }
if sub(shadowRewardsInPoolByOwner_slot, 20) { revertIncorrectStorageSlot() }
if sub(validExchanges_slot, 21) { revertIncorrectStorageSlot() }
if sub(zrxVault_slot, 22) { revertIncorrectStorageSlot() }
if sub(rewardVault_slot, 23) { revertIncorrectStorageSlot() }
let slot := 0
if sub(owner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(stakingContract_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(activeStakeByOwner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(inactiveStakeByOwner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(delegatedStakeByOwner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(delegatedStakeToPoolByOwner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(delegatedStakeByPoolId_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(withdrawableStakeByOwner_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(nextPoolId_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(poolById_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(poolIdByMakerAddress_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(makerAddressesByPoolId_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(currentEpoch_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(currentEpochStartTimeInSeconds_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(protocolFeesThisEpochByPool_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(activePoolsThisEpoch_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(cumulativeRewardsByPool_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(cumulativeRewardsByPoolLastStored_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(validExchanges_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(zrxVault_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(rewardVault_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
}
}
}

View File

@ -36,8 +36,8 @@
"compile:truffle": "truffle compile"
},
"config": {
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFixedMath|LibFixedMathRichErrors|LibRewardMath|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDelegatedStake|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinTimeLockedStake|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestStorageLayout|ZrxVault).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestStorageLayout|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@ -5,6 +5,8 @@
*/
import { ContractArtifact } from 'ethereum-types';
import * as EthVault from '../generated-artifacts/EthVault.json';
import * as IEthVault from '../generated-artifacts/IEthVault.json';
import * as IStaking from '../generated-artifacts/IStaking.json';
import * as IStakingEvents from '../generated-artifacts/IStakingEvents.json';
import * as IStakingPoolRewardVault from '../generated-artifacts/IStakingPoolRewardVault.json';
@ -16,23 +18,22 @@ import * as IZrxVault from '../generated-artifacts/IZrxVault.json';
import * as LibEIP712Hash from '../generated-artifacts/LibEIP712Hash.json';
import * as LibFixedMath from '../generated-artifacts/LibFixedMath.json';
import * as LibFixedMathRichErrors from '../generated-artifacts/LibFixedMathRichErrors.json';
import * as LibRewardMath from '../generated-artifacts/LibRewardMath.json';
import * as LibSafeDowncast from '../generated-artifacts/LibSafeDowncast.json';
import * as LibSignatureValidator from '../generated-artifacts/LibSignatureValidator.json';
import * as LibStakingRichErrors from '../generated-artifacts/LibStakingRichErrors.json';
import * as MixinConstants from '../generated-artifacts/MixinConstants.json';
import * as MixinDelegatedStake from '../generated-artifacts/MixinDelegatedStake.json';
import * as MixinDeploymentConstants from '../generated-artifacts/MixinDeploymentConstants.json';
import * as MixinEthVault from '../generated-artifacts/MixinEthVault.json';
import * as MixinExchangeFees from '../generated-artifacts/MixinExchangeFees.json';
import * as MixinExchangeManager from '../generated-artifacts/MixinExchangeManager.json';
import * as MixinScheduler from '../generated-artifacts/MixinScheduler.json';
import * as MixinStake from '../generated-artifacts/MixinStake.json';
import * as MixinStakeBalances from '../generated-artifacts/MixinStakeBalances.json';
import * as MixinStakeStorage from '../generated-artifacts/MixinStakeStorage.json';
import * as MixinStakingPool from '../generated-artifacts/MixinStakingPool.json';
import * as MixinStakingPoolRewards from '../generated-artifacts/MixinStakingPoolRewards.json';
import * as MixinStakingPoolRewardVault from '../generated-artifacts/MixinStakingPoolRewardVault.json';
import * as MixinStorage from '../generated-artifacts/MixinStorage.json';
import * as MixinTimeLockedStake from '../generated-artifacts/MixinTimeLockedStake.json';
import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.json';
import * as MixinZrxVault from '../generated-artifacts/MixinZrxVault.json';
import * as Staking from '../generated-artifacts/Staking.json';
@ -50,6 +51,7 @@ export const artifacts = {
MixinConstants: MixinConstants as ContractArtifact,
MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact,
MixinStorage: MixinStorage as ContractArtifact,
IEthVault: IEthVault as ContractArtifact,
IStaking: IStaking as ContractArtifact,
IStakingEvents: IStakingEvents as ContractArtifact,
IStakingPoolRewardVault: IStakingPoolRewardVault as ContractArtifact,
@ -61,19 +63,19 @@ export const artifacts = {
LibEIP712Hash: LibEIP712Hash as ContractArtifact,
LibFixedMath: LibFixedMath as ContractArtifact,
LibFixedMathRichErrors: LibFixedMathRichErrors as ContractArtifact,
LibRewardMath: LibRewardMath as ContractArtifact,
LibSafeDowncast: LibSafeDowncast as ContractArtifact,
LibSignatureValidator: LibSignatureValidator as ContractArtifact,
LibStakingRichErrors: LibStakingRichErrors as ContractArtifact,
MixinDelegatedStake: MixinDelegatedStake as ContractArtifact,
MixinStake: MixinStake as ContractArtifact,
MixinStakeBalances: MixinStakeBalances as ContractArtifact,
MixinTimeLockedStake: MixinTimeLockedStake as ContractArtifact,
MixinStakeStorage: MixinStakeStorage as ContractArtifact,
MixinZrxVault: MixinZrxVault as ContractArtifact,
MixinEthVault: MixinEthVault as ContractArtifact,
MixinStakingPool: MixinStakingPool as ContractArtifact,
MixinStakingPoolRewardVault: MixinStakingPoolRewardVault as ContractArtifact,
MixinStakingPoolRewards: MixinStakingPoolRewards as ContractArtifact,
MixinScheduler: MixinScheduler as ContractArtifact,
EthVault: EthVault as ContractArtifact,
MixinVaultCore: MixinVaultCore as ContractArtifact,
StakingPoolRewardVault: StakingPoolRewardVault as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,

View File

@ -3,6 +3,8 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/eth_vault';
export * from '../generated-wrappers/i_eth_vault';
export * from '../generated-wrappers/i_staking';
export * from '../generated-wrappers/i_staking_events';
export * from '../generated-wrappers/i_staking_pool_reward_vault';
@ -14,23 +16,22 @@ export * from '../generated-wrappers/i_zrx_vault';
export * from '../generated-wrappers/lib_e_i_p712_hash';
export * from '../generated-wrappers/lib_fixed_math';
export * from '../generated-wrappers/lib_fixed_math_rich_errors';
export * from '../generated-wrappers/lib_reward_math';
export * from '../generated-wrappers/lib_safe_downcast';
export * from '../generated-wrappers/lib_signature_validator';
export * from '../generated-wrappers/lib_staking_rich_errors';
export * from '../generated-wrappers/mixin_constants';
export * from '../generated-wrappers/mixin_delegated_stake';
export * from '../generated-wrappers/mixin_deployment_constants';
export * from '../generated-wrappers/mixin_eth_vault';
export * from '../generated-wrappers/mixin_exchange_fees';
export * from '../generated-wrappers/mixin_exchange_manager';
export * from '../generated-wrappers/mixin_scheduler';
export * from '../generated-wrappers/mixin_stake';
export * from '../generated-wrappers/mixin_stake_balances';
export * from '../generated-wrappers/mixin_stake_storage';
export * from '../generated-wrappers/mixin_staking_pool';
export * from '../generated-wrappers/mixin_staking_pool_reward_vault';
export * from '../generated-wrappers/mixin_staking_pool_rewards';
export * from '../generated-wrappers/mixin_storage';
export * from '../generated-wrappers/mixin_time_locked_stake';
export * from '../generated-wrappers/mixin_vault_core';
export * from '../generated-wrappers/mixin_zrx_vault';
export * from '../generated-wrappers/staking';

View File

@ -8,6 +8,7 @@ import { DelegatorBalances, StakerBalances } from '../utils/types';
import { StakerActor } from './staker_actor';
export class DelegatorActor extends StakerActor {
/**
constructor(owner: string, stakingWrapper: StakingWrapper) {
super(owner, stakingWrapper);
}
@ -155,4 +156,5 @@ export class DelegatorActor extends StakerActor {
).to.be.bignumber.equal(expectedBalances.stakeDelegatedToPool[i]);
}
}
*/
}

View File

@ -8,6 +8,7 @@ import { StakerBalances } from '../utils/types';
import { BaseActor } from './base_actor';
export class StakerActor extends BaseActor {
/*
constructor(owner: string, stakingWrapper: StakingWrapper) {
super(owner, stakingWrapper);
}
@ -136,27 +137,10 @@ export class StakerActor extends BaseActor {
expectedBalances.deactivatedStakeBalance,
);
}
public async forceTimeLockSyncAsync(): Promise<void> {
public async forceBalanceSyncAsync(): Promise<void> {
const initBalances = await this.getBalancesAsync();
await this._stakingWrapper.forceTimeLockSyncAsync(this._owner);
await this._stakingWrapper.stakeAsync(this._owner, new BigNumber(0));
await this.assertBalancesAsync(initBalances);
}
public async skipToNextTimeLockPeriodAsync(): Promise<void> {
// query some initial values
const initBalances = await this.getBalancesAsync();
const timeLockStart = await this._stakingWrapper.getTimeLockStartAsync(this._owner);
// skip to next period
await this._stakingWrapper.skipToNextTimeLockPeriodAsync();
// validate new balances
const expectedBalances = initBalances;
const currentTimeLockPeriod = await this._stakingWrapper.getCurrentTimeLockPeriodAsync();
if (currentTimeLockPeriod.minus(timeLockStart).isGreaterThan(1)) {
expectedBalances.activatableStakeBalance = initBalances.activatableStakeBalance.plus(
initBalances.timeLockedStakeBalance,
);
expectedBalances.withdrawableStakeBalance = expectedBalances.activatableStakeBalance;
expectedBalances.timeLockedStakeBalance = new BigNumber(0);
}
await this.assertBalancesAsync(expectedBalances);
}
*/
}

View File

@ -53,39 +53,22 @@ describe('Epochs', () => {
});
describe('Epochs & TimeLocks', () => {
it('basic epochs & timeLock periods', async () => {
///// 0/3 Validate Assumptions /////
///// 1/3 Validate Assumptions /////
expect(await stakingWrapper.getEpochDurationInSecondsAsync()).to.be.bignumber.equal(
stakingConstants.EPOCH_DURATION_IN_SECONDS,
);
expect(await stakingWrapper.getTimeLockDurationInEpochsAsync()).to.be.bignumber.equal(
stakingConstants.TIMELOCK_DURATION_IN_EPOCHS,
);
///// 1/3 Validate Initial Epoch & TimeLock Period /////
///// 2/3 Validate Initial Epoch & TimeLock Period /////
{
// epoch
const currentEpoch = await stakingWrapper.getCurrentEpochAsync();
expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH);
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD);
}
///// 2/3 Increment Epoch (TimeLock Should Not Increment) /////
///// 3/3 Increment Epoch (TimeLock Should Not Increment) /////
await stakingWrapper.skipToNextEpochAsync();
{
// epoch
const currentEpoch = await stakingWrapper.getCurrentEpochAsync();
expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH.plus(1));
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD);
}
///// 3/3 Increment Epoch (TimeLock Should Increment) /////
await stakingWrapper.skipToNextTimeLockPeriodAsync();
{
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD.plus(1));
}
});
});

View File

@ -9,6 +9,7 @@ import { Simulation } from './utils/Simulation';
import { StakingWrapper } from './utils/staking_wrapper';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('End-To-End Simulations', env => {
/*
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
const PPM_ONE = 1e6;
@ -96,7 +97,7 @@ blockchainTests('End-To-End Simulations', env => {
it('Should successfully simulate (delegators withdraw by undelegating / no shadow balances)', async () => {
// @TODO - get computations more accurate
/*
\ // the expected payouts were computed by hand
// @TODO - get computations more accurate
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Weighted) | Payout
@ -107,7 +108,7 @@ blockchainTests('End-To-End Simulations', env => {
Cumulative Fees = 43.75043836
Cumulative Weighted Stake = 386.8
Total Rewards = 43.75043836
*/
const simulationParams = {
users,
numberOfPools: 3,
@ -175,7 +176,7 @@ blockchainTests('End-To-End Simulations', env => {
it('Should successfully simulate (delegators withdraw by undelegating / includes shadow balances / delegators enter after reward payouts)', async () => {
// @TODO - get computations more accurate
/*
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Scaled)
0 | 0.304958 | 42 | 0 | 42
1 | 15.323258 | 84 | 0 | 84
@ -189,7 +190,7 @@ blockchainTests('End-To-End Simulations', env => {
// The first delegator got to claim it all. This is due to the necessary conservation of payouts.
// When a new delegator arrives, their new stake should not affect existing delegator payouts.
// In this case, there was unclaimed $$ in the delegator pool - which is claimed by the first delegator.
*/
const simulationParams = {
users,
numberOfPools: 3,
@ -250,7 +251,7 @@ blockchainTests('End-To-End Simulations', env => {
it('Should successfully simulate (delegators withdraw without undelegating / includes shadow balances / delegators enter after reward payouts)', async () => {
// @TODO - get computations more accurate
/*
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Scaled)
0 | 0.304958 | 42 | 0 | 42
1 | 15.323258 | 84 | 0 | 84
@ -264,7 +265,7 @@ blockchainTests('End-To-End Simulations', env => {
// The first delegator got to claim it all. This is due to the necessary conservation of payouts.
// When a new delegator arrives, their new stake should not affect existing delegator payouts.
// In this case, there was unclaimed $$ in the delegator pool - which is claimed by the first delegator.
*/
const simulationParams = {
users,
numberOfPools: 3,
@ -332,5 +333,6 @@ blockchainTests('End-To-End Simulations', env => {
await expect(tx).to.revertWith(revertError);
});
});
*/
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -39,60 +39,9 @@ blockchainTests('Staking & Delegating', env => {
await stakingWrapper.deployAndConfigureContractsAsync();
});
blockchainTests.resets('Staking', () => {
it('basic staking/unstaking', async () => {
// setup test parameters
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
const amountToDeactivate = StakingWrapper.toBaseUnitAmount(4);
const amountToReactivate = StakingWrapper.toBaseUnitAmount(1);
const amountToWithdraw = StakingWrapper.toBaseUnitAmount(1.5);
// run test - this actor will validate its own state
const staker = new StakerActor(stakers[0], stakingWrapper);
await staker.depositZrxAndMintActivatedStakeAsync(amountToStake);
await staker.deactivateAndTimeLockStakeAsync(amountToDeactivate);
// note - we cannot re-activate this timeLocked stake until at least one full timeLock period has passed.
// attempting to do so should revert.
const revertError = new StakingRevertErrors.InsufficientBalanceError(amountToReactivate, 0);
await staker.activateStakeAsync(amountToReactivate, revertError);
await staker.skipToNextTimeLockPeriodAsync();
await staker.activateStakeAsync(amountToReactivate, revertError);
await staker.skipToNextTimeLockPeriodAsync();
// this forces the internal state to update; it is not necessary to activate stake, but
// allows us to check that state is updated correctly after a timeLock period rolls over.
await staker.forceTimeLockSyncAsync();
// now we can activate stake
await staker.activateStakeAsync(amountToReactivate);
await staker.burnDeactivatedStakeAndWithdrawZrxAsync(amountToWithdraw);
});
});
blockchainTests.resets('Delegating', () => {
it('basic delegating/undelegating', async () => {
// setup test parameters
const amountToDelegate = StakingWrapper.toBaseUnitAmount(10);
const amountToDeactivate = StakingWrapper.toBaseUnitAmount(4);
const amountToReactivate = StakingWrapper.toBaseUnitAmount(1);
const amountToWithdraw = StakingWrapper.toBaseUnitAmount(1.5);
const poolOperator = stakers[1];
const operatorShare = 39;
const poolId = await stakingWrapper.createStakingPoolAsync(poolOperator, operatorShare);
// run test
const delegator = new DelegatorActor(stakers[0], stakingWrapper);
await delegator.depositZrxAndDelegateToStakingPoolAsync(poolId, amountToDelegate);
await delegator.deactivateAndTimeLockDelegatedStakeAsync(poolId, amountToDeactivate);
// note - we cannot re-activate this timeLocked stake until at least one full timeLock period has passed.
// attempting to do so should revert.
const revertError = new StakingRevertErrors.InsufficientBalanceError(amountToReactivate, 0);
await delegator.activateStakeAsync(amountToReactivate, revertError);
await delegator.skipToNextTimeLockPeriodAsync();
await delegator.activateStakeAsync(amountToReactivate, revertError);
await delegator.skipToNextTimeLockPeriodAsync();
// this forces the internal state to update; it is not necessary to activate stake, but
// allows us to check that state is updated correctly after a timeLock period rolls over.
await delegator.forceTimeLockSyncAsync();
// now we can activate stake
await delegator.activateAndDelegateStakeAsync(poolId, amountToReactivate);
await delegator.burnDeactivatedStakeAndWithdrawZrxAsync(amountToWithdraw);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -13,6 +13,9 @@ import { SimulationParams } from './types';
const REWARD_PRECISION = 12;
export class Simulation {
/*
private readonly _stakingWrapper: StakingWrapper;
private readonly _p: SimulationParams;
private _userQueue: Queue<string>;
@ -272,4 +275,5 @@ export class Simulation {
Simulation._assertRewardsEqual(reward, expectedReward, `reward withdrawn from pool ${poolId} for operator`);
}
}
*/
}

View File

@ -1,3 +1,4 @@
import { BaseContract } from '@0x/base-contract';
import { ERC20ProxyContract } from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { constants as testUtilsConstants, LogDecoder, txDefaults } from '@0x/contracts-test-utils';
@ -13,11 +14,12 @@ import {
StakingPoolRewardVaultContract,
StakingProxyContract,
ZrxVaultContract,
EthVaultContract,
} from '../../src';
import { ApprovalFactory } from './approval_factory';
import { constants } from './constants';
import { SignedStakingPoolApproval } from './types';
import { SignedStakingPoolApproval, StakeBalance } from './types';
export class StakingWrapper {
private readonly _web3Wrapper: Web3Wrapper;
@ -30,6 +32,7 @@ export class StakingWrapper {
private _stakingContractIfExists?: StakingContract;
private _stakingProxyContractIfExists?: StakingProxyContract;
private _zrxVaultContractIfExists?: ZrxVaultContract;
private _ethVaultContractIfExists?: EthVaultContract;
private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract;
public static toBaseUnitAmount(amount: BigNumber | number): BigNumber {
const decimals = 18;
@ -87,6 +90,10 @@ export class StakingWrapper {
this._validateDeployedOrThrow();
return this._zrxVaultContractIfExists as ZrxVaultContract;
}
public getEthVaultContract(): EthVaultContract {
this._validateDeployedOrThrow();
return this._ethVaultContractIfExists as EthVaultContract;
}
public getStakingPoolRewardVaultContract(): StakingPoolRewardVaultContract {
this._validateDeployedOrThrow();
return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract;
@ -101,6 +108,13 @@ export class StakingWrapper {
this._erc20ProxyContract.address,
this._zrxTokenContract.address,
);
// deploy eth vault
this._ethVaultContractIfExists = await EthVaultContract.deployFrom0xArtifactAsync(
artifacts.EthVault,
this._provider,
txDefaults,
artifacts,
);
// deploy reward vault
this._rewardVaultContractIfExists = await StakingPoolRewardVaultContract.deployFrom0xArtifactAsync(
artifacts.StakingPoolRewardVault,
@ -108,6 +122,8 @@ export class StakingWrapper {
txDefaults,
artifacts,
);
// set eth vault in reward vault
await this._rewardVaultContractIfExists.setEthVault.sendTransactionAsync(this._ethVaultContractIfExists.address);
// configure erc20 proxy to accept calls from zrx vault
await this._erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(
this._zrxVaultContractIfExists.address,
@ -165,88 +181,48 @@ export class StakingWrapper {
return balance;
}
///// STAKE /////
public async depositZrxAndMintDeactivatedStakeAsync(
public async stakeAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndMintDeactivatedStake.getABIEncodedTransactionData(
const calldata = this.getStakingContract().stake.getABIEncodedTransactionData(
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async depositZrxAndMintActivatedStakeAsync(
public async unstakeAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndMintActivatedStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async depositZrxAndDelegateToStakingPoolAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndDelegateToStakingPool.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner, new BigNumber(0), true);
return txReceipt;
}
public async activateStakeAsync(owner: string, amount: BigNumber): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().activateStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async activateAndDelegateStakeAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().activateAndDelegateStake.getABIEncodedTransactionData(
poolId,
const calldata = this.getStakingContract().unstake.getABIEncodedTransactionData(
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async deactivateAndTimeLockStakeAsync(
public async moveStakeAsync(
owner: string,
fromState: {
state: number,
poolId?: string
},
toState: {
state: number,
poolId?: string
},
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().deactivateAndTimeLockStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async deactivateAndTimeLockDelegatedStakeAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().deactivateAndTimeLockDelegatedStake.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner, new BigNumber(0), true);
return txReceipt;
}
public async burnDeactivatedStakeAndWithdrawZrxAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().burnDeactivatedStakeAndWithdrawZrx.getABIEncodedTransactionData(
fromState.poolId = fromState.poolId !== undefined ? fromState.poolId : constants.NIL_POOL_ID;
toState.poolId = fromState.poolId !== undefined ? toState.poolId : constants.NIL_POOL_ID;
const calldata = this.getStakingContract().moveStake.getABIEncodedTransactionData(
fromState as any,
toState as any,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async forceTimeLockSyncAsync(owner: string): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().forceTimeLockSync.getABIEncodedTransactionData(owner);
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddress);
return txReceipt;
}
///// STAKE BALANCES /////
public async getTotalStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalStake.getABIEncodedTransactionData(owner);
@ -254,22 +230,16 @@ export class StakingWrapper {
const value = this.getStakingContract().getTotalStake.getABIDecodedReturnData(returnData);
return value;
}
public async getActivatedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getActivatedStake.getABIEncodedTransactionData(owner);
public async getActiveStakeAsync(owner: string): Promise<StakeBalance> {
const calldata = this.getStakingContract().getActiveStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getActivatedStake.getABIDecodedReturnData(returnData);
const value = this.getStakingContract().getActiveStake.getABIDecodedReturnData(returnData);
return value;
}
public async getDeactivatedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getDeactivatedStake.getABIEncodedTransactionData(owner);
public async getInactiveStakeAsync(owner: string): Promise<StakeBalance> {
const calldata = this.getStakingContract().getInactiveStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getDeactivatedStake.getABIDecodedReturnData(returnData);
return value;
}
public async getActivatableStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getActivatableStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getActivatableStake.getABIDecodedReturnData(returnData);
const value = this.getStakingContract().getInactiveStake.getABIDecodedReturnData(returnData);
return value;
}
public async getWithdrawableStakeAsync(owner: string): Promise<BigNumber> {
@ -278,25 +248,13 @@ export class StakingWrapper {
const value = this.getStakingContract().getWithdrawableStake.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockedStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockedStake.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockStartAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockStart.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockStart.getABIDecodedReturnData(returnData);
return value;
}
public async getStakeDelegatedByOwnerAsync(owner: string): Promise<BigNumber> {
public async getStakeDelegatedByOwnerAsync(owner: string): Promise<StakeBalance> {
const calldata = this.getStakingContract().getStakeDelegatedByOwner.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getStakeDelegatedByOwner.getABIDecodedReturnData(returnData);
return value;
}
public async getStakeDelegatedToPoolByOwnerAsync(poolId: string, owner: string): Promise<BigNumber> {
public async getStakeDelegatedToPoolByOwnerAsync(poolId: string, owner: string): Promise<StakeBalance> {
const calldata = this.getStakingContract().getStakeDelegatedToPoolByOwner.getABIEncodedTransactionData(
owner,
poolId,
@ -305,7 +263,7 @@ export class StakingWrapper {
const value = this.getStakingContract().getStakeDelegatedToPoolByOwner.getABIDecodedReturnData(returnData);
return value;
}
public async getTotalStakeDelegatedToPoolAsync(poolId: string): Promise<BigNumber> {
public async getTotalStakeDelegatedToPoolAsync(poolId: string): Promise<StakeBalance> {
const calldata = this.getStakingContract().getTotalStakeDelegatedToPool.getABIEncodedTransactionData(poolId);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalStakeDelegatedToPool.getABIDecodedReturnData(returnData);
@ -406,56 +364,51 @@ export class StakingWrapper {
return signedStakingPoolApproval;
}
///// EPOCHS /////
/*
public async testFinalizefees(rewards: {reward: BigNumber, poolId: string}[]): Promise<TransactionReceiptWithDecodedLogs> {
await this.fastForwardToNextEpochAsync();
const totalRewards = _.sumBy(rewards, (v: any) => {return v.reward.toNumber();});
const calldata = this.getStakingContract().testFinalizeFees.getABIEncodedTransactionData(rewards);
const txReceipt = await this._executeTransactionAsync(calldata, undefined, new BigNumber(totalRewards), true);
return txReceipt;
}
*/
public async goToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().finalizeFees.getABIEncodedTransactionData();
const txReceipt = await this._executeTransactionAsync(calldata, undefined, new BigNumber(0), true);
logUtils.log(`Finalization costed ${txReceipt.gasUsed} gas`);
return txReceipt;
}
public async skipToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
// increase timestamp of next block
const epochDurationInSeconds = await this.getEpochDurationInSecondsAsync();
await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber());
// mine next block
public async fastForwardToNextEpochAsync(): Promise<void> {
// increase timestamp of next block
const epochDurationInSeconds = await this.getEpochDurationInSecondsAsync();
await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber());
// mine next block
await this._web3Wrapper.mineBlockAsync();
}
public async skipToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
await this.fastForwardToNextEpochAsync();
// increment epoch in contracts
const txReceipt = await this.goToNextEpochAsync();
await this._web3Wrapper.mineBlockAsync();
return txReceipt;
}
public async skipToNextTimeLockPeriodAsync(): Promise<void> {
const timeLockEndEpoch = await this.getCurrentTimeLockPeriodEndEpochAsync();
const currentEpoch = await this.getCurrentEpochAsync();
const nEpochsToJump = timeLockEndEpoch.minus(currentEpoch);
const nEpochsToJumpAsNumber = nEpochsToJump.toNumber();
for (let i = 0; i < nEpochsToJumpAsNumber; ++i) {
await this.skipToNextEpochAsync();
}
}
public async getEpochDurationInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getEpochDurationInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getEpochDurationInSeconds.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockDurationInEpochsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockDurationInEpochs.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockDurationInEpochs.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochStartTimeInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpochStartTimeInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentEpochStartTimeInSeconds.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentTimeLockPeriodStartEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriodStartEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriodStartEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochEarliestEndTimeInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpochEarliestEndTimeInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
@ -464,24 +417,12 @@ export class StakingWrapper {
);
return value;
}
public async getCurrentTimeLockPeriodEndEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriodEndEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriodEndEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentTimeLockPeriodAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriod.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriod.getABIDecodedReturnData(returnData);
return value;
}
///// PROTOCOL FEES /////
public async payProtocolFeeAsync(
makerAddress: string,
@ -536,116 +477,18 @@ export class StakingWrapper {
return txReceipt;
}
///// REWARDS /////
public async getTotalRewardBalanceOfStakingPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalRewardBalanceOfStakingPool.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalRewardBalanceOfStakingPool.getABIDecodedReturnData(returnData);
return value;
}
public async getRewardBalanceOfStakingPoolOperatorAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getRewardBalanceOfStakingPoolOperator.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getRewardBalanceOfStakingPoolOperator.getABIDecodedReturnData(
returnData,
);
return value;
}
public async getRewardBalanceOfStakingPoolMembersAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getRewardBalanceOfStakingPoolMembers.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getRewardBalanceOfStakingPoolMembers.getABIDecodedReturnData(
returnData,
);
return value;
}
public async computeRewardBalanceOfStakingPoolMemberAsync(poolId: string, owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().computeRewardBalanceOfStakingPoolMember.getABIEncodedTransactionData(
const calldata = this.getStakingContract().computeRewardBalanceOfDelegator.getABIEncodedTransactionData(
poolId,
owner,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().computeRewardBalanceOfStakingPoolMember.getABIDecodedReturnData(
const value = this.getStakingContract().computeRewardBalanceOfDelegator.getABIDecodedReturnData(
returnData,
);
return value;
}
public async getTotalShadowBalanceOfStakingPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalShadowBalanceOfStakingPool.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalShadowBalanceOfStakingPool.getABIDecodedReturnData(returnData);
return value;
}
public async getShadowBalanceOfStakingPoolMemberAsync(owner: string, poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getShadowBalanceOfStakingPoolMember.getABIEncodedTransactionData(
owner,
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getShadowBalanceOfStakingPoolMember.getABIDecodedReturnData(returnData);
return value;
}
public async withdrawRewardForStakingPoolOperatorAsync(
poolId: string,
amount: BigNumber,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawRewardForStakingPoolOperator.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async withdrawRewardForStakingPoolMemberAsync(
poolId: string,
amount: BigNumber,
owner: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawRewardForStakingPoolMember.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async withdrawTotalRewardForStakingPoolOperatorAsync(
poolId: string,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawTotalRewardForStakingPoolOperator.getABIEncodedTransactionData(
poolId,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async withdrawTotalRewardForStakingPoolMemberAsync(
poolId: string,
owner: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawTotalRewardForStakingPoolMember.getABIEncodedTransactionData(
poolId,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
///// REWARD VAULT /////
public async rewardVaultDepositForAsync(
poolId: string,
amount: BigNumber,
stakingContractAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingPoolRewardVaultContract().depositFor.getABIEncodedTransactionData(poolId);
const txReceipt = await this._executeTransactionAsync(calldata, stakingContractAddress, amount);
return txReceipt;
}
public async rewardVaultEnterCatastrophicFailureModeAsync(
zeroExMultisigAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {

View File

@ -48,3 +48,14 @@ export interface SimulationParams {
delegateInNextEpoch: boolean;
withdrawByUndelegating: boolean;
}
export interface StakeBalance {
current: BigNumber,
next: BigNumber,
}
export enum StakeStateId {
ACTIVE,
INACTIVE,
DELEGATED
};

View File

@ -3,6 +3,8 @@
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [
"generated-artifacts/EthVault.json",
"generated-artifacts/IEthVault.json",
"generated-artifacts/IStaking.json",
"generated-artifacts/IStakingEvents.json",
"generated-artifacts/IStakingPoolRewardVault.json",
@ -14,23 +16,22 @@
"generated-artifacts/LibEIP712Hash.json",
"generated-artifacts/LibFixedMath.json",
"generated-artifacts/LibFixedMathRichErrors.json",
"generated-artifacts/LibRewardMath.json",
"generated-artifacts/LibSafeDowncast.json",
"generated-artifacts/LibSignatureValidator.json",
"generated-artifacts/LibStakingRichErrors.json",
"generated-artifacts/MixinConstants.json",
"generated-artifacts/MixinDelegatedStake.json",
"generated-artifacts/MixinDeploymentConstants.json",
"generated-artifacts/MixinEthVault.json",
"generated-artifacts/MixinExchangeFees.json",
"generated-artifacts/MixinExchangeManager.json",
"generated-artifacts/MixinScheduler.json",
"generated-artifacts/MixinStake.json",
"generated-artifacts/MixinStakeBalances.json",
"generated-artifacts/MixinStakeStorage.json",
"generated-artifacts/MixinStakingPool.json",
"generated-artifacts/MixinStakingPoolRewardVault.json",
"generated-artifacts/MixinStakingPoolRewards.json",
"generated-artifacts/MixinStorage.json",
"generated-artifacts/MixinTimeLockedStake.json",
"generated-artifacts/MixinVaultCore.json",
"generated-artifacts/MixinZrxVault.json",
"generated-artifacts/Staking.json",

View File

@ -87,4 +87,62 @@ library LibSafeMath {
{
return a < b ? a : b;
}
/// @dev Safely adds two fractions `n1/d1 + n2/d2`
/// @param n1 numerator of `1`
/// @param d1 denominator of `1`
/// @param n2 numerator of `2`
/// @param d2 denominator of `2`
/// @return numerator of sum
/// @return denominator of sum
function addFractions(
uint256 n1,
uint256 d1,
uint256 n2,
uint256 d2
)
internal
pure
returns (
uint256 numerator,
uint256 denominator
)
{
numerator = safeAdd(
safeMul(n1, d2),
safeMul(n2, d1)
);
denominator = safeMul(d1, d2);
return (numerator, denominator);
}
/// @dev Safely scales the difference two fractions.
/// @param n1 numerator of `1`
/// @param d1 denominator of `1`
/// @param n2 numerator of `2`
/// @param d2 denominator of `2`
/// @param s scalar to multiply by difference.
/// @return result = `s * (n1/d1 - n2/d2)`.
function scaleFractionalDifference(
uint256 n1,
uint256 d1,
uint256 n2,
uint256 d2,
uint256 s
)
internal
pure
returns (uint256)
{
uint256 numerator = safeSub(
safeMul(n1, d2),
safeMul(n2, d1)
);
uint256 tmp = safeDiv(numerator, d2);
uint256 result = safeDiv(
safeMul(s, tmp),
d1
);
return result;
}
}