Merge pull request #2154 from 0xProject/feature/staking/refCountRewards

Reference counting for rewards, plus unit tests for cumulative rewards tracking
This commit is contained in:
Greg Hysz 2019-09-16 19:20:55 -07:00 committed by GitHub
commit 5d84d40a2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1354 additions and 152 deletions

View File

@ -41,6 +41,10 @@
{
"note": "Replace `MixinDeploymentConstants` with `MixinParams`.",
"pr": 2131
},
{
"note": "Reference counting for cumulative rewards.",
"pr": 2154
}
]
}

View File

@ -95,6 +95,9 @@ contract MixinStorage is
// mapping from Pool Id to Epoch to Reward Ratio
mapping (bytes32 => mapping (uint256 => IStructs.Fraction)) internal cumulativeRewardsByPool;
// mapping from Pool Id to Epoch to Cumulative Rewards Reference Counter
mapping (bytes32 => mapping (uint256 => uint256)) internal cumulativeRewardsByPoolReferenceCounter;
// mapping from Pool Id to Epoch
mapping (bytes32 => uint256) internal cumulativeRewardsByPoolLastStored;

View File

@ -28,7 +28,7 @@ interface IStakingEvents {
uint8 fromStatus,
bytes32 indexed fromPool,
uint8 toStatus,
bytes32 indexed toProol
bytes32 indexed toPool
);
/// @dev Emitted by MixinExchangeManager when an exchange is added.

View File

@ -37,11 +37,13 @@ interface IStructs {
/// Note that these balances may be stale if the current epoch
/// is greater than `currentEpoch`.
/// Always load this struct using _loadAndSyncBalance or _loadUnsyncedBalance.
/// @param isInitialized
/// @param currentEpoch the current epoch
/// @param currentEpochBalance balance in the current epoch.
/// @param nextEpochBalance balance in the next epoch.
struct StoredBalance {
uint64 currentEpoch;
bool isInitialized;
uint32 currentEpoch;
uint96 currentEpochBalance;
uint96 nextEpochBalance;
}
@ -85,4 +87,12 @@ interface IStructs {
bytes32 poolId;
bool confirmed;
}
/// @dev Encapsulates the epoch and value of a cumulative reward.
/// @param cumulativeRewardEpoch Epoch of the reward.
/// @param cumulativeReward Value of the reward.
struct CumulativeRewardInfo {
uint256 cumulativeRewardEpoch;
IStructs.Fraction cumulativeReward;
}
}

View File

@ -57,4 +57,21 @@ library LibSafeDowncast {
}
return b;
}
/// @dev Safely downcasts to a uint32
/// Note that this reverts if the input value is too large.
function downcastToUint32(uint256 a)
internal
pure
returns (uint32 b)
{
b = uint32(a);
if (uint256(b) != a) {
LibRichErrors.rrevert(LibSafeMathRichErrors.Uint256DowncastError(
LibSafeMathRichErrors.DowncastErrorCodes.VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT32,
a
));
}
return b;
}
}

View File

@ -51,6 +51,12 @@ library LibStakingRichErrors {
PoolIsFull
}
enum CumulativeRewardIntervalErrorCode {
BeginEpochMustBeLessThanEndEpoch,
BeginEpochDoesNotHaveReward,
EndEpochDoesNotHaveReward
}
// bytes4(keccak256("MiscalculatedRewardsError(uint256,uint256)"))
bytes4 internal constant MISCALCULATED_REWARDS_ERROR_SELECTOR =
0xf7806c4e;
@ -147,6 +153,10 @@ library LibStakingRichErrors {
bytes internal constant INVALID_WETH_ASSET_DATA_ERROR =
hex"24bf322c";
// bytes4(keccak256("CumulativeRewardIntervalError(uint8,bytes32,uint256,uint256)"))
bytes4 internal constant CUMULATIVE_REWARD_INTERVAL_ERROR_SELECTOR =
0x1f806d55;
// solhint-disable func-name-mixedcase
function MiscalculatedRewardsError(
uint256 totalRewardsPaid,
@ -455,4 +465,23 @@ library LibStakingRichErrors {
{
return INVALID_WETH_ASSET_DATA_ERROR;
}
function CumulativeRewardIntervalError(
CumulativeRewardIntervalErrorCode errorCode,
bytes32 poolId,
uint256 beginEpoch,
uint256 endEpoch
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
CUMULATIVE_REWARD_INTERVAL_ERROR_SELECTOR,
errorCode,
poolId,
beginEpoch,
endEpoch
);
}
}

View File

@ -181,18 +181,18 @@ contract MixinStake is
)
private
{
// transfer any rewards from the transient pool vault to the eth vault;
// this must be done before we can modify the owner's portion of the delegator pool.
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, owner);
// sync cumulative rewards that we'll need for future computations
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
// cache amount delegated to pool by owner
IStructs.StoredBalance memory initDelegatedStakeToPoolByOwner = _loadUnsyncedBalance(delegatedStakeToPoolByOwner[owner][poolId]);
// increment how much stake the owner has delegated to the input pool
_incrementNextBalance(delegatedStakeToPoolByOwner[owner][poolId], amount);
// increment how much stake has been delegated to pool
_incrementNextBalance(delegatedStakeByPoolId[poolId], amount);
// synchronizes reward state in the pool that the staker is delegating to
IStructs.StoredBalance memory finalDelegatedStakeToPoolByOwner = _loadAndSyncBalance(delegatedStakeToPoolByOwner[owner][poolId]);
_syncRewardsForDelegator(poolId, owner, initDelegatedStakeToPoolByOwner, finalDelegatedStakeToPoolByOwner);
}
/// @dev Un-Delegates a owners stake from a staking pool.
@ -206,18 +206,18 @@ contract MixinStake is
)
private
{
// transfer any rewards from the transient pool vault to the eth vault;
// this must be done before we can modify the owner's portion of the delegator pool.
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, owner);
// sync cumulative rewards that we'll need for future computations
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
// cache amount delegated to pool by owner
IStructs.StoredBalance memory initDelegatedStakeToPoolByOwner = _loadUnsyncedBalance(delegatedStakeToPoolByOwner[owner][poolId]);
// decrement how much stake the owner has delegated to the input pool
_decrementNextBalance(delegatedStakeToPoolByOwner[owner][poolId], amount);
// decrement how much stake has been delegated to pool
_decrementNextBalance(delegatedStakeByPoolId[poolId], amount);
// synchronizes reward state in the pool that the staker is undelegating from
IStructs.StoredBalance memory finalDelegatedStakeToPoolByOwner = _loadAndSyncBalance(delegatedStakeToPoolByOwner[owner][poolId]);
_syncRewardsForDelegator(poolId, owner, initDelegatedStakeToPoolByOwner, finalDelegatedStakeToPoolByOwner);
}
/// @dev Returns a storage pointer to a user's stake in a given status.

View File

@ -95,7 +95,7 @@ contract MixinStakeStorage is
// sync
uint256 currentEpoch = getCurrentEpoch();
if (currentEpoch > balance.currentEpoch) {
balance.currentEpoch = currentEpoch.downcastToUint64();
balance.currentEpoch = currentEpoch.downcastToUint32();
balance.currentEpochBalance = balance.nextEpochBalance;
}
return balance;
@ -185,6 +185,7 @@ contract MixinStakeStorage is
{
// note - this compresses into a single `sstore` when optimizations are enabled,
// since the StakeBalance struct occupies a single word of storage.
balancePtr.isInitialized = true;
balancePtr.currentEpoch = balance.currentEpoch;
balancePtr.nextEpochBalance = balance.nextEpochBalance;
balancePtr.currentEpochBalance = balance.currentEpochBalance;

View File

@ -0,0 +1,333 @@
/*
Copyright 2019 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;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibFractions.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../stake/MixinStakeBalances.sol";
import "./MixinStakingPoolRewardVault.sol";
contract MixinCumulativeRewards is
IStakingEvents,
MixinConstants,
Ownable,
MixinStorage,
MixinZrxVault,
MixinStakingPoolRewardVault,
MixinScheduler,
MixinStakeStorage,
MixinStakeBalances
{
using LibSafeMath for uint256;
/// @dev Initializes Cumulative Rewards for a given pool.
/// @param poolId Unique id of pool.
function _initializeCumulativeRewards(bytes32 poolId)
internal
{
uint256 currentEpoch = getCurrentEpoch();
// sets the default cumulative reward
_forceSetCumulativeReward(
poolId,
currentEpoch,
IStructs.Fraction({
numerator: 0,
denominator: MIN_TOKEN_VALUE
})
);
}
/// @dev returns true iff Cumulative Rewards are set
function _isCumulativeRewardSet(IStructs.Fraction memory cumulativeReward)
internal
pure
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;
}
/// Returns true iff the cumulative reward for `poolId` at `epoch` can be unset.
/// @param poolId Unique id of pool.
/// @param epoch of the cumulative reward.
function _canUnsetCumulativeReward(bytes32 poolId, uint256 epoch)
internal
view
returns (bool)
{
return (
_isCumulativeRewardSet(cumulativeRewardsByPool[poolId][epoch]) && // is there a value to unset
cumulativeRewardsByPoolReferenceCounter[poolId][epoch] == 0 && // no references to this CR
cumulativeRewardsByPoolLastStored[poolId] > epoch // this is *not* the most recent CR
);
}
/// @dev Tries to set a cumulative reward for `poolId` at `epoch`.
/// @param poolId Unique Id of pool.
/// @param epoch of cumulative reward.
/// @param value of cumulative reward.
function _trySetCumulativeReward(
bytes32 poolId,
uint256 epoch,
IStructs.Fraction memory value
)
internal
{
if (_isCumulativeRewardSet(cumulativeRewardsByPool[poolId][epoch])) {
// do nothing; we don't want to override the current value
return;
}
_forceSetCumulativeReward(poolId, epoch, value);
}
/// @dev Sets a cumulative reward for `poolId` at `epoch`.
/// This can be used to overwrite an existing value.
/// @param poolId Unique Id of pool.
/// @param epoch of cumulative reward.
/// @param value of cumulative reward.
function _forceSetCumulativeReward(
bytes32 poolId,
uint256 epoch,
IStructs.Fraction memory value
)
internal
{
cumulativeRewardsByPool[poolId][epoch] = value;
_trySetMostRecentCumulativeRewardEpoch(poolId, epoch);
}
/// @dev Tries to unset the cumulative reward for `poolId` at `epoch`.
/// @param poolId Unique id of pool.
/// @param epoch of cumulative reward to unset.
function _tryUnsetCumulativeReward(bytes32 poolId, uint256 epoch)
internal
{
if (!_canUnsetCumulativeReward(poolId, epoch)) {
return;
}
_forceUnsetCumulativeReward(poolId, epoch);
}
/// @dev Unsets the cumulative reward for `poolId` at `epoch`.
/// @param poolId Unique id of pool.
/// @param epoch of cumulative reward to unset.
function _forceUnsetCumulativeReward(bytes32 poolId, uint256 epoch)
internal
{
cumulativeRewardsByPool[poolId][epoch] = IStructs.Fraction({numerator: 0, denominator: 0});
}
/// @dev Returns info on most recent cumulative reward.
function _getMostRecentCumulativeRewardInfo(bytes32 poolId)
internal
returns (IStructs.CumulativeRewardInfo memory)
{
// fetch the last epoch at which we stored a cumulative reward for this pool
uint256 cumulativeRewardsLastStored = cumulativeRewardsByPoolLastStored[poolId];
// query and return cumulative reward info for this pool
return IStructs.CumulativeRewardInfo({
cumulativeReward: cumulativeRewardsByPool[poolId][cumulativeRewardsLastStored],
cumulativeRewardEpoch: cumulativeRewardsLastStored
});
}
/// @dev Tries to set the epoch of the most recent cumulative reward.
/// The value will only be set if the input epoch is greater than the current
/// most recent value.
/// @param poolId Unique Id of pool.
/// @param epoch of the most recent cumulative reward.
function _trySetMostRecentCumulativeRewardEpoch(bytes32 poolId, uint256 epoch)
internal
{
// check if we should do any work
uint256 currentMostRecentEpoch = cumulativeRewardsByPoolLastStored[poolId];
if (epoch == currentMostRecentEpoch) {
return;
}
// update state to reflect the most recent cumulative reward
_forceSetMostRecentCumulativeRewardEpoch(
poolId,
currentMostRecentEpoch,
epoch
);
}
/// @dev Forcefully sets the epoch of the most recent cumulative reward.
/// @param poolId Unique Id of pool.
/// @param currentMostRecentEpoch of the most recent cumulative reward.
/// @param newMostRecentEpoch of the new most recent cumulative reward.
function _forceSetMostRecentCumulativeRewardEpoch(
bytes32 poolId,
uint256 currentMostRecentEpoch,
uint256 newMostRecentEpoch
)
internal
{
// sanity check that we're not trying to go back in time
assert(newMostRecentEpoch >= currentMostRecentEpoch);
cumulativeRewardsByPoolLastStored[poolId] = newMostRecentEpoch;
// unset the previous most recent reward, if it is no longer needed
_tryUnsetCumulativeReward(poolId, currentMostRecentEpoch);
}
/// @dev Adds a dependency on a cumulative reward for a given epoch.
/// @param poolId Unique Id of pool.
/// @param epoch to remove dependency from.
/// @param mostRecentCumulativeRewardInfo Info for the most recent cumulative reward (value and epoch)
/// @param isDependent True iff there is a dependency on the cumulative reward for `poolId` at `epoch`
function _addOrRemoveDependencyOnCumulativeReward(
bytes32 poolId,
uint256 epoch,
IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo,
bool isDependent
)
internal
{
if (isDependent) {
_addDependencyOnCumulativeReward(
poolId,
epoch,
mostRecentCumulativeRewardInfo
);
} else {
_removeDependencyOnCumulativeReward(
poolId,
epoch
);
}
}
/// @dev Adds a dependency on a cumulative reward for a given epoch.
/// @param poolId Unique Id of pool.
/// @param epoch to remove dependency from.
/// @param mostRecentCumulativeRewardInfo Info on the most recent cumulative reward.
function _addDependencyOnCumulativeReward(
bytes32 poolId,
uint256 epoch,
IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo
)
internal
{
// add dependency by increasing the reference counter
cumulativeRewardsByPoolReferenceCounter[poolId][epoch] = cumulativeRewardsByPoolReferenceCounter[poolId][epoch].safeAdd(1);
// set CR to most recent reward (if it is not already set)
_trySetCumulativeReward(
poolId,
epoch,
mostRecentCumulativeRewardInfo.cumulativeReward
);
}
/// @dev Removes a dependency on a cumulative reward for a given epoch.
/// @param poolId Unique Id of pool.
/// @param epoch to remove dependency from.
function _removeDependencyOnCumulativeReward(
bytes32 poolId,
uint256 epoch
)
internal
{
// remove dependency by decreasing reference counter
uint256 newReferenceCounter = cumulativeRewardsByPoolReferenceCounter[poolId][epoch].safeSub(1);
cumulativeRewardsByPoolReferenceCounter[poolId][epoch] = newReferenceCounter;
// clear cumulative reward from state, if it is no longer needed
_tryUnsetCumulativeReward(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 member 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
)
internal
view
returns (uint256)
{
// sanity check inputs
if (memberStakeOverInterval == 0) {
return 0;
}
// sanity check interval
if (beginEpoch >= endEpoch) {
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors.CumulativeRewardIntervalErrorCode.BeginEpochMustBeLessThanEndEpoch,
poolId,
beginEpoch,
endEpoch
)
);
}
// sanity check begin reward
IStructs.Fraction memory beginReward = cumulativeRewardsByPool[poolId][beginEpoch];
if (!_isCumulativeRewardSet(beginReward)) {
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors.CumulativeRewardIntervalErrorCode.BeginEpochDoesNotHaveReward,
poolId,
beginEpoch,
endEpoch
)
);
}
// sanity check end reward
IStructs.Fraction memory endReward = cumulativeRewardsByPool[poolId][endEpoch];
if (!_isCumulativeRewardSet(endReward)) {
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors.CumulativeRewardIntervalErrorCode.EndEpochDoesNotHaveReward,
poolId,
beginEpoch,
endEpoch
)
);
}
// compute reward
uint256 reward = LibFractions.scaleFractionalDifference(
endReward.numerator,
endReward.denominator,
beginReward.numerator,
beginReward.denominator,
memberStakeOverInterval
);
return reward;
}
}

View File

@ -25,6 +25,7 @@ import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../stake/MixinStakeBalances.sol";
import "./MixinStakingPoolRewardVault.sol";
import "./MixinCumulativeRewards.sol";
contract MixinStakingPoolRewards is
@ -36,7 +37,8 @@ contract MixinStakingPoolRewards is
MixinStakingPoolRewardVault,
MixinScheduler,
MixinStakeStorage,
MixinStakeBalances
MixinStakeBalances,
MixinCumulativeRewards
{
using LibSafeMath for uint256;
@ -50,110 +52,51 @@ contract MixinStakingPoolRewards is
view
returns (uint256 totalReward)
{
// cache some values to reduce sloads
IStructs.StoredBalance memory delegatedStake = _loadUnsyncedBalance(delegatedStakeToPoolByOwner[member][poolId]);
uint256 currentEpoch = getCurrentEpoch();
// value is always zero in these two scenarios:
// 1. The owner's delegated is current as of this epoch: their rewards have been moved to the ETH vault.
// 2. The current epoch is zero: delegation begins at epoch 1
if (delegatedStake.currentEpoch == currentEpoch || currentEpoch == 0) return 0;
// compute reward accumulated during `delegatedStake.currentEpoch`;
uint256 rewardsAccumulatedDuringLastStoredEpoch = (delegatedStake.currentEpochBalance != 0)
? _computeMemberRewardOverInterval(
return _computeRewardBalanceOfDelegator(
poolId,
delegatedStake.currentEpochBalance,
delegatedStake.currentEpoch - 1,
delegatedStake.currentEpoch
)
: 0;
// compute the reward accumulated by the `next` balance;
// this starts at `delegatedStake.currentEpoch + 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.currentEpoch)
? _computeMemberRewardOverInterval(
poolId,
delegatedStake.nextEpochBalance,
delegatedStake.currentEpoch,
cumulativeRewardsByPoolLastStored[poolId]
)
: 0;
// compute the total reward
totalReward = rewardsAccumulatedDuringLastStoredEpoch.safeAdd(rewardsAccumulatedAfterLastStoredEpoch);
return totalReward;
_loadUnsyncedBalance(delegatedStakeToPoolByOwner[member][poolId]),
getCurrentEpoch()
);
}
/// @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.
/// @dev Syncs rewards for a delegator. This includes transferring rewards from
/// the Reward Vault to the Eth Vault, and adding/removing dependencies on cumulative rewards.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
function _transferDelegatorsAccumulatedRewardsToEthVault(bytes32 poolId, address member)
internal
{
// there are no delegators in the first epoch
uint256 currentEpoch = getCurrentEpoch();
if (currentEpoch == 0) {
return;
}
// 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)
/// @param member of the pool.
/// @param initialDelegatedStakeToPoolByOwner The member's delegated balance at the beginning of this transaction.
/// @param finalDelegatedStakeToPoolByOwner The member's delegated balance at the end of this transaction.
function _syncRewardsForDelegator(
bytes32 poolId,
address member,
IStructs.StoredBalance memory initialDelegatedStakeToPoolByOwner,
IStructs.StoredBalance memory finalDelegatedStakeToPoolByOwner
)
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;
}
// transfer any rewards from the transient pool vault to the eth vault;
// this must be done before we can modify the owner's portion of the delegator pool.
_transferDelegatorRewardsToEthVault(
poolId,
member,
initialDelegatedStakeToPoolByOwner,
currentEpoch
);
// cache a storage pointer to the cumulative rewards for `poolId` indexed by epoch.
mapping (uint256 => IStructs.Fraction) storage cumulativeRewardsByPoolPtr = cumulativeRewardsByPool[poolId];
// add dependencies on cumulative rewards for this epoch and the previous epoch, if necessary.
_setCumulativeRewardDependenciesForDelegator(
poolId,
finalDelegatedStakeToPoolByOwner,
true
);
// 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;
}
// remove dependencies on previous cumulative rewards, if they are no longer needed.
_setCumulativeRewardDependenciesForDelegator(
poolId,
initialDelegatedStakeToPoolByOwner,
false
);
}
/// @dev Records a reward for delegators. This adds to the `cumulativeRewardsByPool`.
@ -191,51 +134,129 @@ contract MixinStakingPoolRewards is
denominator.safeDiv(MIN_TOKEN_VALUE)
);
// store cumulative rewards
cumulativeRewardsByPoolPtr[epoch] = IStructs.Fraction({
// store cumulative rewards and set most recent
_forceSetCumulativeReward(
poolId,
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(
/// @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 _transferDelegatorRewardsToEthVault(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
address member,
IStructs.StoredBalance memory unsyncedDelegatedStakeToPoolByOwner,
uint256 currentEpoch
)
private
{
// compute balance owed to delegator
uint256 balance = _computeRewardBalanceOfDelegator(
poolId,
unsyncedDelegatedStakeToPoolByOwner,
currentEpoch
);
if (balance == 0) {
return;
}
// transfer from transient Reward Pool vault to ETH Vault
_transferMemberBalanceToEthVault(poolId, member, balance);
}
/// @dev Computes the reward balance in ETH of a specific member of a pool.
/// @param poolId Unique id of pool.
/// @param unsyncedDelegatedStakeToPoolByOwner Unsynced delegated stake to pool by owner
/// @param currentEpoch The epoch in which this call is executing
/// @return totalReward Balance in ETH.
function _computeRewardBalanceOfDelegator(
bytes32 poolId,
IStructs.StoredBalance memory unsyncedDelegatedStakeToPoolByOwner,
uint256 currentEpoch
)
private
view
returns (uint256)
returns (uint256 totalReward)
{
IStructs.Fraction memory beginRatio = cumulativeRewardsByPool[poolId][beginEpoch];
IStructs.Fraction memory endRatio = cumulativeRewardsByPool[poolId][endEpoch];
uint256 reward = LibFractions.scaleFractionalDifference(
endRatio.numerator,
endRatio.denominator,
beginRatio.numerator,
beginRatio.denominator,
memberStakeOverInterval
);
return reward;
// reward balance is always zero in these two scenarios:
// 1. The owner's delegated stake is current as of this epoch: their rewards have been moved to the ETH vault.
// 2. The current epoch is zero: delegation begins at epoch 1
if (unsyncedDelegatedStakeToPoolByOwner.currentEpoch == currentEpoch || currentEpoch == 0) return 0;
// compute reward accumulated during `delegatedStake.currentEpoch`;
uint256 rewardsAccumulatedDuringLastStoredEpoch = (unsyncedDelegatedStakeToPoolByOwner.currentEpochBalance != 0)
? _computeMemberRewardOverInterval(
poolId,
unsyncedDelegatedStakeToPoolByOwner.currentEpochBalance,
uint256(unsyncedDelegatedStakeToPoolByOwner.currentEpoch).safeSub(1),
unsyncedDelegatedStakeToPoolByOwner.currentEpoch
)
: 0;
// compute the reward accumulated by the `next` balance;
// this starts at `delegatedStake.currentEpoch + 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] > unsyncedDelegatedStakeToPoolByOwner.currentEpoch)
? _computeMemberRewardOverInterval(
poolId,
unsyncedDelegatedStakeToPoolByOwner.nextEpochBalance,
unsyncedDelegatedStakeToPoolByOwner.currentEpoch,
cumulativeRewardsByPoolLastStored[poolId]
)
: 0;
// compute the total reward
totalReward = rewardsAccumulatedDuringLastStoredEpoch.safeAdd(rewardsAccumulatedAfterLastStoredEpoch);
return totalReward;
}
/// @dev returns true iff Cumulative Rewards are set
function _isCumulativeRewardSet(IStructs.Fraction memory cumulativeReward)
/// @dev Adds or removes cumulative reward dependencies for a delegator.
/// A delegator always depends on the cumulative reward for the current epoch.
/// They will also depend on the previous epoch's reward, if they are already staked with the input pool.
/// @param poolId Unique id of pool.
/// @param delegatedStakeToPoolByOwner Amount of stake the member has delegated to the pool.
/// @param isDependent is true iff adding a dependency. False, otherwise.
function _setCumulativeRewardDependenciesForDelegator(
bytes32 poolId,
IStructs.StoredBalance memory delegatedStakeToPoolByOwner,
bool isDependent
)
private
pure
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;
// if this delegator is not yet initialized then there's no dependency to unset.
if (!isDependent && !delegatedStakeToPoolByOwner.isInitialized) {
return;
}
// get the most recent cumulative reward, which will serve as a reference point when updating dependencies
IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo = _getMostRecentCumulativeRewardInfo(poolId);
// record dependency on `lastEpoch`
if (delegatedStakeToPoolByOwner.currentEpoch > 0 && delegatedStakeToPoolByOwner.currentEpochBalance != 0) {
_addOrRemoveDependencyOnCumulativeReward(
poolId,
uint256(delegatedStakeToPoolByOwner.currentEpoch).safeSub(1),
mostRecentCumulativeRewardInfo,
isDependent
);
}
// record dependency on current epoch.
if (delegatedStakeToPoolByOwner.currentEpochBalance != 0 || delegatedStakeToPoolByOwner.nextEpochBalance != 0) {
_addOrRemoveDependencyOnCumulativeReward(
poolId,
delegatedStakeToPoolByOwner.currentEpoch,
mostRecentCumulativeRewardInfo,
isDependent
);
}
}
}

View File

@ -102,8 +102,11 @@ contract MixinParams is
_cobbDouglasAlphaDenomintor = cobbDouglasAlphaDenomintor;
}
/// @dev Initialzize storage belonging to this mixin.
function _initMixinParams() internal {
/// @dev Assert param values before initializing them.
/// This must be updated for each migration.
function _assertMixinParamsBeforeInit()
internal
{
// Ensure state is uninitialized.
if (epochDurationInSeconds != 0 &&
rewardDelegatedStakeWeight != 0 &&
@ -118,6 +121,15 @@ contract MixinParams is
)
);
}
}
/// @dev Initialize storage belonging to this mixin.
function _initMixinParams()
internal
{
// assert the current values before overwriting them.
_assertMixinParamsBeforeInit();
// Set up defaults.
epochDurationInSeconds = 2 weeks;
rewardDelegatedStakeWeight = (90 * PPM_DENOMINATOR) / 100; // 90%

View File

@ -75,9 +75,9 @@ contract MixinScheduler is
return getCurrentEpochStartTimeInSeconds().safeAdd(epochDurationInSeconds);
}
/// @dev Initializes state owned by this mixin.
/// Fails if state was already initialized.
function _initMixinScheduler()
/// @dev Assert scheduler state before initializing it.
/// This must be updated for each migration.
function _assertMixinSchedulerBeforeInit()
internal
{
if (currentEpochStartTimeInSeconds != 0) {
@ -87,6 +87,16 @@ contract MixinScheduler is
)
);
}
}
/// @dev Initializes state owned by this mixin.
/// Fails if state was already initialized.
function _initMixinScheduler()
internal
{
// assert the current values before overwriting them.
_assertMixinSchedulerBeforeInit();
// solhint-disable-next-line
currentEpochStartTimeInSeconds = block.timestamp;
}

View File

@ -0,0 +1,87 @@
/*
Copyright 2019 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;
pragma experimental ABIEncoderV2;
import "./TestStaking.sol";
contract TestCumulativeRewardTracking is
TestStaking
{
event SetCumulativeReward(
bytes32 poolId,
uint256 epoch
);
event UnsetCumulativeReward(
bytes32 poolId,
uint256 epoch
);
event SetMostRecentCumulativeReward(
bytes32 poolId,
uint256 epoch
);
function _forceSetCumulativeReward(
bytes32 poolId,
uint256 epoch,
IStructs.Fraction memory value
)
internal
{
emit SetCumulativeReward(poolId, epoch);
MixinCumulativeRewards._forceSetCumulativeReward(
poolId,
epoch,
value
);
}
function _forceUnsetCumulativeReward(bytes32 poolId, uint256 epoch)
internal
{
emit UnsetCumulativeReward(poolId, epoch);
MixinCumulativeRewards._forceUnsetCumulativeReward(poolId, epoch);
}
function _forceSetMostRecentCumulativeRewardEpoch(
bytes32 poolId,
uint256 currentMostRecentEpoch,
uint256 newMostRecentEpoch
)
internal
{
emit SetMostRecentCumulativeReward(poolId, newMostRecentEpoch);
MixinCumulativeRewards._forceSetMostRecentCumulativeRewardEpoch(
poolId,
currentMostRecentEpoch,
newMostRecentEpoch
);
}
function _assertMixinParamsBeforeInit()
internal
{} // solhint-disable-line no-empty-blocks
function _assertMixinSchedulerBeforeInit()
internal
{} // solhint-disable-line no-empty-blocks
}

View File

@ -97,6 +97,9 @@ contract TestStorageLayout is
if sub(cumulativeRewardsByPool_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(cumulativeRewardsByPoolReferenceCounter_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)
if sub(cumulativeRewardsByPoolLastStored_slot, slot) { revertIncorrectStorageSlot() }
slot := add(slot, 1)

View File

@ -37,7 +37,7 @@
},
"config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStorageInit|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json"
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStorageInit|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestCumulativeRewardTracking|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@ -21,6 +21,7 @@ import * as LibProxy from '../generated-artifacts/LibProxy.json';
import * as LibSafeDowncast from '../generated-artifacts/LibSafeDowncast.json';
import * as LibStakingRichErrors from '../generated-artifacts/LibStakingRichErrors.json';
import * as MixinConstants from '../generated-artifacts/MixinConstants.json';
import * as MixinCumulativeRewards from '../generated-artifacts/MixinCumulativeRewards.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';
@ -41,6 +42,7 @@ import * as Staking from '../generated-artifacts/Staking.json';
import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json';
import * as StakingProxy from '../generated-artifacts/StakingProxy.json';
import * as TestCobbDouglas from '../generated-artifacts/TestCobbDouglas.json';
import * as TestCumulativeRewardTracking from '../generated-artifacts/TestCumulativeRewardTracking.json';
import * as TestInitTarget from '../generated-artifacts/TestInitTarget.json';
import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json';
import * as TestLibProxy from '../generated-artifacts/TestLibProxy.json';
@ -79,6 +81,7 @@ export const artifacts = {
MixinStakeBalances: MixinStakeBalances as ContractArtifact,
MixinStakeStorage: MixinStakeStorage as ContractArtifact,
MixinZrxVault: MixinZrxVault as ContractArtifact,
MixinCumulativeRewards: MixinCumulativeRewards as ContractArtifact,
MixinEthVault: MixinEthVault as ContractArtifact,
MixinStakingPool: MixinStakingPool as ContractArtifact,
MixinStakingPoolRewardVault: MixinStakingPoolRewardVault as ContractArtifact,
@ -90,6 +93,7 @@ export const artifacts = {
StakingPoolRewardVault: StakingPoolRewardVault as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,
TestCobbDouglas: TestCobbDouglas as ContractArtifact,
TestCumulativeRewardTracking: TestCumulativeRewardTracking as ContractArtifact,
TestInitTarget: TestInitTarget as ContractArtifact,
TestLibFixedMath: TestLibFixedMath as ContractArtifact,
TestLibProxy: TestLibProxy as ContractArtifact,

View File

@ -19,6 +19,7 @@ export * from '../generated-wrappers/lib_proxy';
export * from '../generated-wrappers/lib_safe_downcast';
export * from '../generated-wrappers/lib_staking_rich_errors';
export * from '../generated-wrappers/mixin_constants';
export * from '../generated-wrappers/mixin_cumulative_rewards';
export * from '../generated-wrappers/mixin_deployment_constants';
export * from '../generated-wrappers/mixin_eth_vault';
export * from '../generated-wrappers/mixin_exchange_fees';
@ -39,6 +40,7 @@ export * from '../generated-wrappers/staking';
export * from '../generated-wrappers/staking_pool_reward_vault';
export * from '../generated-wrappers/staking_proxy';
export * from '../generated-wrappers/test_cobb_douglas';
export * from '../generated-wrappers/test_cumulative_reward_tracking';
export * from '../generated-wrappers/test_init_target';
export * from '../generated-wrappers/test_lib_fixed_math';
export * from '../generated-wrappers/test_lib_proxy';

View File

@ -0,0 +1,407 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, describe } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { artifacts } from '../src';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { CumulativeRewardTrackingSimulation, TestAction } from './utils/cumulative_reward_tracking_simulation';
// tslint:disable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count
blockchainTests.resets('Cumulative Reward Tracking', env => {
// tokens & addresses
let accounts: string[];
let owner: string;
// wrappers
let stakingApiWrapper: StakingApiWrapper;
let simulation: CumulativeRewardTrackingSimulation;
// let testWrapper: TestRewardBalancesContract;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
// create accounts
accounts = await env.getAccountAddressesAsync();
owner = accounts[0];
const actors = accounts.slice(1);
// set up ERC20Wrapper
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
// deploy staking contracts
stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking);
simulation = new CumulativeRewardTrackingSimulation(stakingApiWrapper, actors);
await simulation.deployAndConfigureTestContractsAsync(env);
});
describe('Tracking Cumulative Rewards (CR)', () => {
it('should set CR when a pool is created at epoch 0', async () => {
await simulation.runTestAsync([], [TestAction.CreatePool], [{ event: 'SetCumulativeReward', epoch: 0 }]);
});
it('should set CR and Most Recent CR when a pool is created in epoch >0', async () => {
await simulation.runTestAsync(
[TestAction.Finalize],
[TestAction.CreatePool],
[{ event: 'SetCumulativeReward', epoch: 1 }, { event: 'SetMostRecentCumulativeReward', epoch: 1 }],
);
});
it('should not set CR or Most Recent CR when values already exist for the current epoch', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
],
[
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
],
[],
);
});
it('should not set CR or Most Recent CR when user re-delegates and values already exist for the current epoch', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
],
[
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
],
[],
);
});
it('should not set CR or Most Recent CR when user undelegagtes and values already exist for the current epoch', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
],
[
TestAction.Undelegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
],
[],
);
});
it('should (i) set CR and Most Recent CR when delegating, and (ii) unset previous Most Recent CR if there are no dependencies', async () => {
// since there was no delegation in epoch 0 there is no longer a dependency on the CR for epoch 0
await simulation.runTestAsync(
[TestAction.CreatePool, TestAction.Finalize],
[TestAction.Delegate],
[
{ event: 'SetCumulativeReward', epoch: 1 },
{ event: 'SetMostRecentCumulativeReward', epoch: 1 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
],
);
});
it('should (i) set CR and Most Recent CR when delegating, and (ii) NOT unset previous Most Recent CR if there are dependencies', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
TestAction.Finalize, // moves to epoch 1
],
[
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
],
[{ event: 'SetCumulativeReward', epoch: 1 }, { event: 'SetMostRecentCumulativeReward', epoch: 1 }],
);
});
it('should not unset the current Most Recent CR, even if there are no dependencies', async () => {
// note - we never unset the current Most Recent CR; only ever a previous value - given there are no depencies from delegators.
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
],
[
TestAction.Undelegate, // does nothing. This delegator no longer has dependency, but the most recent CR is 1 so we don't remove.
],
[],
);
});
it('should set CR and update Most Recent CR when delegating more stake', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[{ event: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }],
);
});
it('should set CR and update Most Recent CR when undelegating', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
],
[
TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[{ event: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }],
);
});
it('should set CR and update Most Recent CR when undelegating, plus remove the CR that is no longer depends on.', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate,
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
],
[
TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 2 },
{ event: 'SetMostRecentCumulativeReward', epoch: 2 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
],
);
});
it('should set CR and update Most Recent CR when redelegating, plus remove the CR that it no longer depends on.', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 2 },
{ event: 'SetMostRecentCumulativeReward', epoch: 2 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
],
);
});
it('should set CR and Most Recent CR when a reward is earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch.
TestAction.Finalize, // moves to epoch 1
TestAction.PayProtocolFee,
],
[
TestAction.Finalize, // adds a CR for epoch 1, plus updates most recent CR
],
[{ event: 'SetCumulativeReward', epoch: 1 }, { event: 'SetMostRecentCumulativeReward', epoch: 1 }],
);
});
it('should set/unset CR and update Most Recent CR when redelegating, the epoch following a reward was earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 2; moves to epoch 3
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
it('should set/unset CR and update Most Recent CR when redelegating, the epoch following a reward was earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 2; moves to epoch 3
],
[
TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
it('should set/unset CR and update Most Recent CR when redelegating, one full epoch after a reward was earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 2; moves to epoch 3
TestAction.Finalize, // moves to epoch 4
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 2 },
{ event: 'SetCumulativeReward', epoch: 4 },
{ event: 'SetMostRecentCumulativeReward', epoch: 4 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
it('should set/unset CR and update Most Recent CR when redelegating, one full epoch after a reward was earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 2; moves to epoch 3
],
[
TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
it('should set/unset CR and update Most Recent CR when delegating for the first time in an epoch with no CR', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 0; moves to epoch 1
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 1 },
{ event: 'SetMostRecentCumulativeReward', epoch: 1 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
],
);
});
it('should set/unset CR and update Most Recent CR when delegating for the first time in an epoch with no CR, after an epoch where a reward was earned', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // creates new CR for epoch 0; moves to epoch 1
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 1 },
{ event: 'SetMostRecentCumulativeReward', epoch: 1 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
],
);
});
it('should set CR and update Most Recent CR when delegating in two subsequent epochs', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1.
TestAction.Finalize, // moves to epoch 1
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[{ event: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }],
);
});
it('should set/unset CR and update Most Recent CR when delegating in two subsequent epochs, when there is an old CR to clear', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
TestAction.Finalize, // moves to epoch 2
TestAction.Finalize, // moves to epoch 3
TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3.
TestAction.Finalize, // moves to epoch 4
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3.
],
[
{ event: 'SetCumulativeReward', epoch: 4 },
{ event: 'SetMostRecentCumulativeReward', epoch: 4 },
{ event: 'UnsetCumulativeReward', epoch: 2 },
],
);
});
it('should set/unset CR and update Most Recent CR re-delegating after one full epoch', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
TestAction.Finalize, // moves to epoch 2
TestAction.Finalize, // moves to epoch 3
],
[
TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3.
],
[
{ event: 'SetCumulativeReward', epoch: 2 },
{ event: 'SetMostRecentCumulativeReward', epoch: 2 },
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
it('should set/unset CR and update Most Recent CR when redelegating after receiving a reward', async () => {
await simulation.runTestAsync(
[
TestAction.CreatePool, // creates CR in epoch 0
TestAction.Delegate, // does nothing wrt CR
TestAction.Finalize, // moves to epoch 1
TestAction.Delegate, // copies CR from epoch 0 to epoch 1. Sets most recent CR to epoch 1.
TestAction.Finalize, // moves to epoch 2
TestAction.PayProtocolFee, // this means a CR will be available upon finalization
TestAction.Finalize, // creates new CR for epoch 2; moves to epoch 3
],
[
TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2.
],
[
{ event: 'SetCumulativeReward', epoch: 3 },
{ event: 'SetMostRecentCumulativeReward', epoch: 3 },
{ event: 'UnsetCumulativeReward', epoch: 0 },
{ event: 'UnsetCumulativeReward', epoch: 1 },
],
);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -177,6 +177,15 @@ blockchainTests.resets('Testing Rewards', env => {
await finalizer.finalizeAsync([{ reward: fee, poolId }]);
};
const ZERO = new BigNumber(0);
it('Reward balance should be zero if not delegated', async () => {
// sanity balances - all zero
await validateEndBalances({});
});
it('Reward balance should be zero if not delegated, when epoch is greater than 0', async () => {
await payProtocolFeeAndFinalize();
// sanity balances - all zero
await validateEndBalances({});
});
it('Reward balance should be zero in same epoch as delegation', async () => {
const amount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(amount);
@ -269,6 +278,7 @@ blockchainTests.resets('Testing Rewards', env => {
});
it('Should split pool reward between delegators, when they join in different epochs', async () => {
// first staker delegates (epoch 0)
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
const totalStakeAmount = toBaseUnitAmount(10);
await stakers[0].stakeAsync(stakeAmounts[0]);
@ -277,8 +287,10 @@ blockchainTests.resets('Testing Rewards', env => {
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[0],
);
// skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize();
// second staker delegates (epoch 1)
await stakers[1].stakeAsync(stakeAmounts[1]);
await stakers[1].moveStakeAsync(
@ -286,9 +298,11 @@ blockchainTests.resets('Testing Rewards', env => {
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[1],
);
// skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize();
// finalize
const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward);
// sanity check final balances
@ -499,15 +513,19 @@ blockchainTests.resets('Testing Rewards', env => {
await payProtocolFeeAndFinalize();
// earn reward
await payProtocolFeeAndFinalize(rewardForDelegator);
// undelegate stake and finalize epoch
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolId),
new StakeInfo(StakeStatus.Active),
stakeAmount,
);
await payProtocolFeeAndFinalize();
// this should not go do the delegator
await payProtocolFeeAndFinalize(rewardNotForDelegator);
// sanity check final balances
await validateEndBalances({
stakerEthVaultBalance_1: rewardForDelegator,
@ -602,6 +620,44 @@ blockchainTests.resets('Testing Rewards', env => {
membersRewardVaultBalance: rewardsForDelegator[1],
});
});
it('Should collect fees correctly when re-delegating after un-delegating', async () => {
// Note - there are two ranges over which payouts are computed (see _computeRewardBalanceOfDelegator).
// This triggers the first range (rewards for `delegatedStake.currentEpoch`), but not the second.
// first staker delegates (epoch 0)
const rewardForDelegator = toBaseUnitAmount(10);
const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize();
// undelegate stake and finalize epoch
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolId),
new StakeInfo(StakeStatus.Active),
stakeAmount,
);
// this should go to the delegator
await payProtocolFeeAndFinalize(rewardForDelegator);
// delegate stake ~ this will result in a payout where rewards are computed on
// the balance's `currentEpochBalance` field but not the `nextEpochBalance` field.
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// sanity check final balances
await validateEndBalances({
stakerRewardVaultBalance_1: ZERO,
stakerEthVaultBalance_1: rewardForDelegator,
operatorRewardVaultBalance: ZERO,
poolRewardVaultBalance: ZERO,
membersRewardVaultBalance: ZERO,
});
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -0,0 +1,173 @@
import { BlockchainTestsEnvironment, expect, txDefaults } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DecodedLogArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { TestCumulativeRewardTrackingContract } from '../../generated-wrappers/test_cumulative_reward_tracking';
import { artifacts } from '../../src';
import { StakingApiWrapper } from './api_wrapper';
import { toBaseUnitAmount } from './number_utils';
import { StakeInfo, StakeStatus } from './types';
export enum TestAction {
Finalize,
Delegate,
Undelegate,
PayProtocolFee,
CreatePool,
}
interface TestLog {
event: string;
epoch: number;
}
export class CumulativeRewardTrackingSimulation {
private readonly _amountToStake = toBaseUnitAmount(100);
private readonly _protocolFeeAmount = new BigNumber(10);
private readonly _stakingApiWrapper: StakingApiWrapper;
private readonly _staker: string;
private readonly _poolOperator: string;
private readonly _takerAddress: string;
private readonly _exchangeAddress: string;
private _testCumulativeRewardTrackingContract?: TestCumulativeRewardTrackingContract;
private _poolId: string;
private static _extractTestLogs(txReceiptLogs: DecodedLogArgs[]): TestLog[] {
const logs = [];
for (const log of txReceiptLogs) {
if (log.event === 'SetMostRecentCumulativeReward') {
logs.push({
event: 'SetMostRecentCumulativeReward',
epoch: log.args.epoch.toNumber(),
});
} else if (log.event === 'SetCumulativeReward') {
logs.push({
event: 'SetCumulativeReward',
epoch: log.args.epoch.toNumber(),
});
} else if (log.event === 'UnsetCumulativeReward') {
logs.push({
event: 'UnsetCumulativeReward',
epoch: log.args.epoch.toNumber(),
});
}
}
return logs;
}
private static _assertTestLogs(expectedSequence: TestLog[], txReceiptLogs: DecodedLogArgs[]): void {
const logs = CumulativeRewardTrackingSimulation._extractTestLogs(txReceiptLogs);
expect(logs.length).to.be.equal(expectedSequence.length);
for (let i = 0; i < expectedSequence.length; i++) {
const expectedLog = expectedSequence[i];
const actualLog = logs[i];
expect(expectedLog.event, `testing event name of ${JSON.stringify(expectedLog)}`).to.be.equal(
actualLog.event,
);
expect(expectedLog.epoch, `testing epoch of ${JSON.stringify(expectedLog)}`).to.be.equal(actualLog.epoch);
}
}
constructor(stakingApiWrapper: StakingApiWrapper, actors: string[]) {
this._stakingApiWrapper = stakingApiWrapper;
// setup actors
this._staker = actors[0];
this._poolOperator = actors[1];
this._takerAddress = actors[2];
this._exchangeAddress = actors[3];
this._poolId = '';
}
public async deployAndConfigureTestContractsAsync(env: BlockchainTestsEnvironment): Promise<void> {
// set exchange address
await this._stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(
this._exchangeAddress,
);
this._testCumulativeRewardTrackingContract = await TestCumulativeRewardTrackingContract.deployFrom0xArtifactAsync(
artifacts.TestCumulativeRewardTracking,
env.provider,
txDefaults,
artifacts,
);
}
public getTestCumulativeRewardTrackingContract(): TestCumulativeRewardTrackingContract {
if (this._testCumulativeRewardTrackingContract === undefined) {
throw new Error(`Contract has not been deployed. Run 'deployAndConfigureTestContractsAsync'.`);
}
return this._testCumulativeRewardTrackingContract;
}
public async runTestAsync(
initActions: TestAction[],
testActions: TestAction[],
expectedTestLogs: TestLog[],
): Promise<void> {
await this._executeActionsAsync(initActions);
await this._stakingApiWrapper.stakingProxyContract.attachStakingContract.awaitTransactionSuccessAsync(
this.getTestCumulativeRewardTrackingContract().address,
);
const testLogs = await this._executeActionsAsync(testActions);
CumulativeRewardTrackingSimulation._assertTestLogs(expectedTestLogs, testLogs);
}
private async _executeActionsAsync(actions: TestAction[]): Promise<DecodedLogArgs[]> {
let logs: DecodedLogArgs[] = [];
for (const action of actions) {
let txReceipt: TransactionReceiptWithDecodedLogs;
switch (action) {
case TestAction.Finalize:
txReceipt = await this._stakingApiWrapper.utils.skipToNextEpochAsync();
break;
case TestAction.Delegate:
await this._stakingApiWrapper.stakingContract.stake.sendTransactionAsync(this._amountToStake, {
from: this._staker,
});
txReceipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, this._poolId),
this._amountToStake,
{ from: this._staker },
);
break;
case TestAction.Undelegate:
txReceipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync(
new StakeInfo(StakeStatus.Delegated, this._poolId),
new StakeInfo(StakeStatus.Active),
this._amountToStake,
{ from: this._staker },
);
break;
case TestAction.PayProtocolFee:
txReceipt = await this._stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync(
this._poolOperator,
this._takerAddress,
this._protocolFeeAmount,
{ from: this._exchangeAddress, value: this._protocolFeeAmount },
);
break;
case TestAction.CreatePool:
txReceipt = await this._stakingApiWrapper.stakingContract.createStakingPool.awaitTransactionSuccessAsync(
0,
true,
{ from: this._poolOperator },
);
const createStakingPoolLog = txReceipt.logs[0];
// tslint:disable-next-line no-unnecessary-type-assertion
this._poolId = (createStakingPoolLog as DecodedLogArgs).args.poolId;
break;
default:
throw new Error('Unrecognized test action');
}
logs = logs.concat(txReceipt.logs);
}
return logs;
}
}

View File

@ -19,6 +19,7 @@
"generated-artifacts/LibSafeDowncast.json",
"generated-artifacts/LibStakingRichErrors.json",
"generated-artifacts/MixinConstants.json",
"generated-artifacts/MixinCumulativeRewards.json",
"generated-artifacts/MixinDeploymentConstants.json",
"generated-artifacts/MixinEthVault.json",
"generated-artifacts/MixinExchangeFees.json",
@ -39,6 +40,7 @@
"generated-artifacts/StakingPoolRewardVault.json",
"generated-artifacts/StakingProxy.json",
"generated-artifacts/TestCobbDouglas.json",
"generated-artifacts/TestCumulativeRewardTracking.json",
"generated-artifacts/TestInitTarget.json",
"generated-artifacts/TestLibFixedMath.json",
"generated-artifacts/TestLibProxy.json",

View File

@ -27,6 +27,7 @@ library LibSafeMathRichErrors {
}
enum DowncastErrorCodes {
VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT32,
VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT64,
VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT96
}

View File

@ -93,6 +93,10 @@
{
"note": "Add `InitializationError`, `InvalidParamValue` to `StakingRevertErrors`.",
"pr": 2131
},
{
"note": "Add `CumulativeRewardIntervalError`.",
"pr": 2154
}
]
},

View File

@ -30,6 +30,12 @@ export enum InitializationErrorCode {
MixinParamsAlreadyInitialized,
}
export enum CumulativeRewardIntervalErrorCode {
BeginEpochMustBeLessThanEndEpoch,
BeginEpochDoesNotHaveReward,
EndEpochDoesNotHaveReward,
}
export class MiscalculatedRewardsError extends RevertError {
constructor(totalRewardsPaid?: BigNumber | number | string, initialContractBalance?: BigNumber | number | string) {
super(
@ -225,6 +231,21 @@ export class ProxyDestinationCannotBeNilError extends RevertError {
}
}
export class CumulativeRewardIntervalError extends RevertError {
constructor(
errorCode?: CumulativeRewardIntervalErrorCode,
poolId?: string,
beginEpoch?: BigNumber | number | string,
endEpoch?: BigNumber | number | string,
) {
super(
'CumulativeRewardIntervalError',
'CumulativeRewardIntervalError(uint8 errorCode, bytes32 poolId, uint256 beginEpoch, uint256 endEpoch)',
{ errorCode, poolId, beginEpoch, endEpoch },
);
}
}
const types = [
AmountExceedsBalanceOfPoolError,
BlockTimestampTooLowError,
@ -249,6 +270,7 @@ const types = [
RewardVaultNotSetError,
WithdrawAmountExceedsMemberBalanceError,
ProxyDestinationCannotBeNilError,
CumulativeRewardIntervalError,
];
// Register the types we've defined.

View File

@ -11,6 +11,7 @@ export enum BinOpErrorCodes {
}
export enum DowncastErrorCodes {
ValueTooLargeToDowncastToUint32,
ValueTooLargeToDowncastToUint64,
ValueTooLargeToDowncastToUint96,
}