diff --git a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol index d536529f22..ed5745e5d6 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol @@ -43,7 +43,6 @@ interface IStakingPoolRewardVault { uint256 amount ); - /// @dev Record a deposit of an amount of ETH for `poolId` into the vault. /// The staking contract should pay this contract the ETH owed in the /// same transaction. diff --git a/contracts/staking/contracts/src/stake/MixinStake.sol b/contracts/staking/contracts/src/stake/MixinStake.sol index 0ef2ef8801..b68d080be5 100644 --- a/contracts/staking/contracts/src/stake/MixinStake.sol +++ b/contracts/staking/contracts/src/stake/MixinStake.sol @@ -125,7 +125,7 @@ contract MixinStake is { return; } else if (from.status == IStructs.StakeStatus.DELEGATED - && from.poolId == to.poolId) + && from.poolId == to.poolId) { return; } diff --git a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol index f61142afe3..ef0b4be8b7 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol @@ -72,14 +72,19 @@ contract MixinCumulativeRewards is view returns (bool) { - return ( - // Is there a value to unset - _isCumulativeRewardSet(_cumulativeRewardsByPool[poolId][epoch]) && - // No references to this CR - _cumulativeRewardsByPoolReferenceCounter[poolId][epoch] == 0 && - // This is *not* the most recent CR - _cumulativeRewardsByPoolLastStored[poolId] > epoch - ); + // Must be a value to unset + if (!_isCumulativeRewardSet(_cumulativeRewardsByPool[poolId][epoch])) { + return false; + } + // Must be no references to this CR + if (_cumulativeRewardsByPoolReferenceCounter[poolId][epoch] != 0) { + return false; + } + // Must not be the most recent CR. + if (_cumulativeRewardsByPoolLastStored[poolId] == epoch) { + return false; + } + return true; } /// @dev Tries to set a cumulative reward for `poolId` at `epoch`. @@ -93,8 +98,11 @@ contract MixinCumulativeRewards is ) internal { - if (_isCumulativeRewardSet(_cumulativeRewardsByPool[poolId][epoch])) { - // Do nothing; we don't want to override the current value + // Do nothing if it's in the past since we don't want to + // rewrite history. + if (epoch < currentEpoch + && _isCumulativeRewardSet(_cumulativeRewardsByPool[poolId][epoch])) + { return; } _forceSetCumulativeReward(poolId, epoch, value); @@ -113,7 +121,12 @@ contract MixinCumulativeRewards is internal { _cumulativeRewardsByPool[poolId][epoch] = value; - _trySetMostRecentCumulativeRewardEpoch(poolId, epoch); + + // Never set the most recent reward epoch to one in the future, because + // it may get removed if there are no more dependencies on it. + if (epoch <= currentEpoch) { + _trySetMostRecentCumulativeRewardEpoch(poolId, epoch); + } } /// @dev Tries to unset the cumulative reward for `poolId` at `epoch`. diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 991e8bfeb5..7065e4eb39 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -238,6 +238,34 @@ contract MixinStakingPoolRewards is address(uint160(address(rewardVault))).transfer(membersReward); } + /// @dev Split a pool reward between the operator and members based on + /// the `operatorShare` and `membersStake`. + /// @param operatorShare The fraction of rewards owed to the operator, + /// in PPM. + /// @param totalReward The pool reward. + /// @param membersStake The amount of member (non-operator) stake delegated + /// to the pool in the epoch the rewards were earned. + function _splitStakingPoolRewards( + uint32 operatorShare, + uint256 totalReward, + uint256 membersStake + ) + internal + pure + returns (uint256 operatorReward, uint256 membersReward) + { + if (membersStake == 0) { + operatorReward = totalReward; + } else { + operatorReward = LibMath.getPartialAmountCeil( + uint256(operatorShare), + PPM_DENOMINATOR, + totalReward + ); + membersReward = totalReward - operatorReward; + } + } + /// @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. @@ -278,34 +306,6 @@ contract MixinStakingPoolRewards is ); } - /// @dev Split a pool reward between the operator and members based on - /// the `operatorShare` and `membersStake`. - /// @param operatorShare The fraction of rewards owed to the operator, - /// in PPM. - /// @param totalReward The pool reward. - /// @param membersStake The amount of member (non-operator) stake delegated - /// to the pool in the epoch the rewards were earned. - function _splitStakingPoolRewards( - uint32 operatorShare, - uint256 totalReward, - uint256 membersStake - ) - internal - pure - returns (uint256 operatorReward, uint256 membersReward) - { - if (membersStake == 0) { - operatorReward = totalReward; - } else { - operatorReward = LibMath.getPartialAmountCeil( - uint256(operatorShare), - PPM_DENOMINATOR, - totalReward - ); - membersReward = totalReward - operatorReward; - } - } - /// @dev Computes the reward balance in ETH of a specific member of a pool. /// @param poolId Unique id of pool. /// @param unsyncedStake Unsynced delegated stake to pool by owner @@ -413,19 +413,6 @@ contract MixinStakingPoolRewards is IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo = _getMostRecentCumulativeRewardInfo(poolId); - // Record dependency on the next epoch - uint256 nextEpoch = currentEpoch.safeAdd(1); - if (_delegatedStakeToPoolByOwner.currentEpoch > 0 - && _delegatedStakeToPoolByOwner.nextEpochBalance != 0) - { - _addOrRemoveDependencyOnCumulativeReward( - poolId, - nextEpoch, - mostRecentCumulativeRewardInfo, - isDependent - ); - } - // Record dependency on current epoch. if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 || _delegatedStakeToPoolByOwner.nextEpochBalance != 0) @@ -437,5 +424,15 @@ contract MixinStakingPoolRewards is isDependent ); } + + // Record dependency on the next epoch + if (_delegatedStakeToPoolByOwner.nextEpochBalance != 0) { + _addOrRemoveDependencyOnCumulativeReward( + poolId, + uint256(_delegatedStakeToPoolByOwner.currentEpoch).safeAdd(1), + mostRecentCumulativeRewardInfo, + isDependent + ); + } } } diff --git a/contracts/staking/contracts/src/vaults/EthVault.sol b/contracts/staking/contracts/src/vaults/EthVault.sol index c3c7510c4b..fb31f44ed7 100644 --- a/contracts/staking/contracts/src/vaults/EthVault.sol +++ b/contracts/staking/contracts/src/vaults/EthVault.sol @@ -37,7 +37,7 @@ contract EthVault is // solhint-disable no-empty-blocks /// @dev Payable fallback for bulk-deposits. - function () payable external {} + function () external payable {} /// @dev Record a deposit of an amount of ETH for `owner` into the vault. /// The staking contract should pay this contract the ETH owed in the diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 12c56c2e06..3e25a5dc91 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -43,7 +43,7 @@ contract StakingPoolRewardVault is // solhint-disable no-empty-blocks /// @dev Payable fallback for bulk-deposits. - function () payable external {} + function () external payable {} /// @dev Record a deposit of an amount of ETH for `poolId` into the vault. /// The staking contract should pay this contract the ETH owed in the diff --git a/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol b/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol index 7d9bf080d7..54d5896d9a 100644 --- a/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol +++ b/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol @@ -24,7 +24,6 @@ import "./TestStaking.sol"; contract TestCumulativeRewardTracking is TestStaking { - event SetCumulativeReward( bytes32 poolId, uint256 epoch @@ -40,6 +39,9 @@ contract TestCumulativeRewardTracking is uint256 epoch ); + // solhint-disable-next-line no-empty-blocks + function init(address, address, address payable, address) public {} + function _forceSetCumulativeReward( bytes32 poolId, uint256 epoch, @@ -76,14 +78,4 @@ contract TestCumulativeRewardTracking is newMostRecentEpoch ); } - - function _assertParamsNotInitialized() - internal - view - {} // solhint-disable-line no-empty-blocks - - function _assertSchedulerNotInitialized() - internal - view - {} // solhint-disable-line no-empty-blocks } diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 0fc807e617..0feb81c1bf 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -87,6 +87,7 @@ contract TestDelegatorRewards is membersReward: membersReward, membersStake: membersStake }); + _setOperatorShare(poolId, operatorReward, membersReward); } /// @dev Advance the epoch. @@ -104,6 +105,7 @@ contract TestDelegatorRewards is ) external { + _initGenesisCumulativeRewards(poolId); IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = @@ -130,6 +132,7 @@ contract TestDelegatorRewards is ) external { + _initGenesisCumulativeRewards(poolId); IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = @@ -158,6 +161,7 @@ contract TestDelegatorRewards is ) external { + _initGenesisCumulativeRewards(poolId); IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = @@ -234,7 +238,7 @@ contract TestDelegatorRewards is unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; delete unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; - _setOperatorShare(poolId, operatorReward, membersReward); + _setOperatorShare(poolId, reward.operatorReward, reward.membersReward); uint256 totalRewards = reward.operatorReward + reward.membersReward; membersStake = reward.membersStake; @@ -258,6 +262,19 @@ contract TestDelegatorRewards is membersStake = reward.membersStake; } + /// @dev Create a cumulative rewards entry for a pool if one doesn't + /// already exist to get around having to create pools in advance. + function _initGenesisCumulativeRewards(bytes32 poolId) + private + { + uint256 lastRewardEpoch = _cumulativeRewardsByPoolLastStored[poolId]; + IStructs.Fraction memory cumulativeReward = + _cumulativeRewardsByPool[poolId][lastRewardEpoch]; + if (!_isCumulativeRewardSet(cumulativeReward)) { + _initializeCumulativeRewards(poolId); + } + } + /// @dev Set the operator share of a pool based on reward ratios. function _setOperatorShare( bytes32 poolId, @@ -266,9 +283,13 @@ contract TestDelegatorRewards is ) private { - uint32 operatorShare = uint32( - operatorReward * PPM_DENOMINATOR / (operatorReward + membersReward) - ); + uint32 operatorShare = 0; + uint256 totalReward = operatorReward + membersReward; + if (totalReward != 0) { + operatorShare = uint32( + operatorReward * PPM_DENOMINATOR / totalReward + ); + } _poolById[poolId].operatorShare = operatorShare; } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index ccf038c53c..16a4c9b110 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -103,8 +103,8 @@ contract TestFinalizer is returns (FinalizedPoolRewards memory reward) { (reward.operatorReward, - reward.membersReward, - reward.membersStake) = _finalizePool(poolId); + reward.membersReward, + reward.membersStake) = _finalizePool(poolId); } /// @dev Get finalization-related state variables. diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index ed5b182502..29be592894 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -37,13 +37,13 @@ contract TestProtocolFees is mapping(address => bytes32) private _makersToTestPoolIds; constructor(address exchangeAddress, address wethProxyAddress) public { - validExchanges[exchangeAddress] = true; - _initMixinParams( + init( wethProxyAddress, address(1), // vault addresses must be non-zero address(1), address(1) ); + validExchanges[exchangeAddress] = true; } function addMakerToPool(bytes32 poolId, address makerAddress) diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 227554959c..6524ed8f68 100644 --- a/contracts/staking/package.json +++ b/contracts/staking/package.json @@ -63,6 +63,7 @@ "chai-bignumber": "^3.0.0", "decimal.js": "^10.2.0", "dirty-chai": "^2.0.1", + "js-combinatorics": "^0.5.3", "make-promises-safe": "^1.1.0", "mocha": "^4.1.0", "npm-run-all": "^4.1.2", diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 4697cbf197..ee737dcaca 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -128,10 +128,8 @@ export class FinalizerActor extends BaseActor { private async _getDelegatorBalancesByPoolIdAsync( delegatorsByPoolId: DelegatorsByPoolId, ): Promise { - const computeRewardBalanceOfDelegator = - this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; - const computeRewardBalanceOfOperator = - this._stakingApiWrapper.stakingContract.computeRewardBalanceOfOperator; + const computeRewardBalanceOfDelegator = this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; + const computeRewardBalanceOfOperator = this._stakingApiWrapper.stakingContract.computeRewardBalanceOfOperator; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { @@ -154,8 +152,7 @@ export class FinalizerActor extends BaseActor { private async _getDelegatorStakesByPoolIdAsync( delegatorsByPoolId: DelegatorsByPoolId, ): Promise { - const getStakeDelegatedToPoolByOwner = - this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner; + const getStakeDelegatedToPoolByOwner = this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { const delegators = delegatorsByPoolId[poolId]; @@ -201,14 +198,9 @@ export class FinalizerActor extends BaseActor { rewardVaultBalance: BigNumber, operatorShare: BigNumber, ): Promise<[BigNumber, BigNumber]> { - const totalStakeDelegatedToPool = (await - this._stakingApiWrapper - .stakingContract - .getTotalStakeDelegatedToPool - .callAsync( - poolId, - ) - ).currentEpochBalance; + const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync( + poolId, + )).currentEpochBalance; const operatorPortion = totalStakeDelegatedToPool.eq(0) ? reward : reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT); @@ -243,12 +235,7 @@ export class FinalizerActor extends BaseActor { const operatorShareByPoolId: OperatorShareByPoolId = {}; for (const poolId of poolIds) { operatorShareByPoolId[poolId] = new BigNumber( - (await this - ._stakingApiWrapper - .stakingContract - .getStakingPool - .callAsync(poolId) - ).operatorShare, + (await this._stakingApiWrapper.stakingContract.getStakingPool.callAsync(poolId)).operatorShare, ); } return operatorShareByPoolId; diff --git a/contracts/staking/test/cumulative_reward_tracking_test.ts b/contracts/staking/test/cumulative_reward_tracking_test.ts index e7e2f6f6cc..35e2258f97 100644 --- a/contracts/staking/test/cumulative_reward_tracking_test.ts +++ b/contracts/staking/test/cumulative_reward_tracking_test.ts @@ -34,371 +34,511 @@ blockchainTests.resets('Cumulative Reward Tracking', env => { }); describe('Tracking Cumulative Rewards (CR)', () => { - it('should set CR when a pool is created at epoch 0', async () => { + it('pool 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 () => { + it('pool 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 () => { + it('delegating in the same epoch pool is created', async () => { await simulation.runTestAsync( [ - TestAction.CreatePool, // creates CR in epoch 0 + // Creates CR for epoch 0 + TestAction.CreatePool, ], - [ - TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch. + // Updates CR for epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, ], - [], + [{ event: 'SetCumulativeReward', epoch: 0 }, { event: 'SetCumulativeReward', epoch: 1 }], ); }); - it('should not set CR or Most Recent CR when user re-delegates and values already exist for the current epoch', async () => { + it('re-delegating in the same 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. + // Creates CR for epoch 0 + TestAction.CreatePool, ], [ - TestAction.Delegate, // does nothing wrt CR, as there is alread a CR set for this epoch. + // Updates CR for epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Updates CR for epoch 0 + // Updates CR for epoch 1 + TestAction.Delegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 1 }, + { event: 'SetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 1 }, ], - [], ); }); - it('should not set CR or Most Recent CR when user undelegagtes and values already exist for the current epoch', async () => { + it('delegating then undelegating in the same 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. + // Creates CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, ], [ - TestAction.Undelegate, // does nothing wrt CR, as there is alread a CR set for this epoch. + // Unsets the CR for epoch 1 + TestAction.Undelegate, ], - [], + [{ event: 'UnsetCumulativeReward', epoch: 1 }], ); }); - it('should (i) set CR and Most Recent CR when delegating, and (ii) unset previous Most Recent CR if there are no dependencies', async () => { + it('delegating in new epoch', 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], + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + ], + [ + // Creates a CR for epoch 1 + // Sets MRCR to epoch 1 + // Unsets the CR for epoch 0 + // Creates a CR for epoch 2 + TestAction.Delegate, + ], [ { event: 'SetCumulativeReward', epoch: 1 }, { event: 'SetMostRecentCumulativeReward', epoch: 1 }, { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 2 }, ], ); }); - it('should (i) set CR and Most Recent CR when delegating, and (ii) NOT unset previous Most Recent CR if there are dependencies', async () => { + it('re-delegating in a new 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.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 + // Creates CR in epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Creates CR for epoch 1 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 + // Moves to epoch 1 + TestAction.Finalize, ], [ - TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, ], [ + { event: 'SetCumulativeReward', epoch: 1 }, + { event: 'SetMostRecentCumulativeReward', epoch: 1 }, { 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 () => { + it('delegate then undelegate to remove all dependencies', 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 + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + // Creates CR for epoch 1 + // Sets MRCR to epoch 1 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // Clears CR from epoch 2 + // Does NOT clear CR from epoch 1 because it is the current + // epoch. + TestAction.Undelegate, + ], + [{ event: 'UnsetCumulativeReward', epoch: 2 }], + ); + }); + it('delegating in epoch 1 then again in epoch 2', async () => { + await simulation.runTestAsync( + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + // Creates CR for epoch 1 + // Sets MRCR to epoch 1 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + // Move to epoch 2 + TestAction.Finalize, + ], + [ + // Updates CR for epoch 2 + // Sets MRCR to epoch 2 + // Creates CR for epoch 3 + // Clears CR for epoch 1 + TestAction.Delegate, ], [ { event: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 3 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, ], ); }); - it('should set CR and Most Recent CR when a reward is earned', async () => { + it('delegate in epoch 1 then undelegate in epoch 2', 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 + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + // Creates CR for epoch 1 + // Sets MRCR to epoch 0 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + ], + [ + // Update CR for epoch 2 + // Set MRCR to epoch 2 + // Clear CR for epoch 1 + TestAction.Undelegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 2 }, + { event: 'SetMostRecentCumulativeReward', epoch: 2 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, + ], + ); + }); + it('delegate in epoch 0 and epoch 1, then undelegate half in epoch 2', async () => { + await simulation.runTestAsync( + [ + // Create CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + ], + [ + // Updates CR for epoch 2 + // Sets MRCR to epoch 2 + // Creates CR for epoch 3 (because there will still be stake) + // Clears CR for epoch 1 + TestAction.Undelegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 2 }, + { event: 'SetMostRecentCumulativeReward', epoch: 2 }, + { event: 'SetCumulativeReward', epoch: 3 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, + ], + ); + }); + it('delegate in epoch 1 and 2 then again in 3', async () => { + await simulation.runTestAsync( + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + ], + [ + // Updates CR for epoch 2 + // Sets MRCR to epoch 2 + // Creates CR for epoch 3 + // Clears CR for epoch 1 + TestAction.Delegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 2 }, + { event: 'SetMostRecentCumulativeReward', epoch: 2 }, + { event: 'SetCumulativeReward', epoch: 3 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, + ], + ); + }); + it('delegate in epoch 0, earn reward in epoch 1', async () => { + await simulation.runTestAsync( + [ + // Create CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Credits pool with rewards 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. + // Moves to epoch 2 + // Creates CR for epoch 2 + // Sets MRCR to epoch 2 + TestAction.Finalize, ], [{ 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 () => { + it('delegate in epoch 0, epoch 2, earn reward in epoch 3, then delegate', 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 + // Create CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + // Credits pool with rewards + TestAction.PayProtocolFee, + // Moves to epoch 3 + // Creates CR for epoch 3 + // Sets MRCR to epoch 3 + TestAction.Finalize, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3. + // Updates CR for epoch 3 + // Creates CR for epoch 4 + // Clears CR for epoch 1 + // Clears CR for epoch 2 + TestAction.Delegate, ], [ + { event: 'SetCumulativeReward', epoch: 3 }, { event: 'SetCumulativeReward', epoch: 4 }, - { event: 'SetMostRecentCumulativeReward', epoch: 4 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, { event: 'UnsetCumulativeReward', epoch: 2 }, ], ); }); - it('should set/unset CR and update Most Recent CR re-delegating after one full epoch', async () => { + it('delegate in epoch 0 and 1, earn reward in epoch 3, then undelegate half', 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 + // Create CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + // Credits pool with rewards + TestAction.PayProtocolFee, + // Moves to epoch 3 + // Creates CR for epoch 3 + // Sets MRCR to epoch 3 + TestAction.Finalize, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3. + // Updates CR for epoch 3 + // Creates CR for epoch 4 (because there is still stake remaming) + // Clears CR for epoch 1 + // Clears CR for epoch 2 + TestAction.Undelegate, ], [ - { event: 'SetCumulativeReward', epoch: 2 }, - { event: 'SetMostRecentCumulativeReward', epoch: 2 }, { event: 'SetCumulativeReward', epoch: 3 }, - { event: 'SetMostRecentCumulativeReward', epoch: 3 }, + { event: 'SetCumulativeReward', epoch: 4 }, { event: 'UnsetCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 2 }, ], ); }); - it('should set/unset CR and update Most Recent CR when redelegating after receiving a reward', async () => { + it('delegate in epoch 1, 2, earn rewards in epoch 3, skip to epoch 4, then delegate', 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 + // Create CR for epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Sets MRCR to epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + // Credits pool with rewards + TestAction.PayProtocolFee, + // Moves to epoch 3 + // Creates CR for epoch 3 + // Sets MRCR to epoch 3 + TestAction.Finalize, + // Moves to epoch 4 + TestAction.Finalize, ], [ - TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // Creates CR for epoch 4 + // Sets MRCR to epoch 4 + // Clears CR for epoch 3 + // Creates CR for epoch 5 + // Clears CR for epoch 1 + TestAction.Delegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 4 }, + { event: 'SetMostRecentCumulativeReward', epoch: 4 }, + { event: 'UnsetCumulativeReward', epoch: 3 }, + { event: 'SetCumulativeReward', epoch: 5 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 2 }, + ], + ); + }); + it('earn reward in epoch 1 with no stake, then delegate', async () => { + await simulation.runTestAsync( + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Credit pool with rewards + TestAction.PayProtocolFee, + // Moves to epoch 1 + // That's it, because there's no active pools. + TestAction.Finalize, + ], + [ + // Updates CR to epoch 1 + // Sets MRCR to epoch 1 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 1 }, + { event: 'SetMostRecentCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 2 }, + ], + ); + }); + it('delegate in epoch 1, 3, then delegate in epoch 4', async () => { + await simulation.runTestAsync( + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + // Creates CR for epoch 1 + // Sets MRCR to epoch 0 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + // Moves to epoch 3 + TestAction.Finalize, + // Creates CR for epoch 3 + // Sets MRCR to epoch 3 + // Clears CR for epoch 1 + // Creates CR for epoch 4 + // Clears CR for epoch 2 + TestAction.Delegate, + // Moves to epoch 4 + TestAction.Finalize, + ], + [ + // Updates CR for epoch 4 + // Sets MRCR to epoch 4 + // Clears CR for epoch 3 + // Creates CR for epoch 5 + TestAction.Delegate, + ], + [ + { event: 'SetCumulativeReward', epoch: 4 }, + { event: 'SetMostRecentCumulativeReward', epoch: 4 }, + { event: 'SetCumulativeReward', epoch: 5 }, + { event: 'UnsetCumulativeReward', epoch: 3 }, + ], + ); + }); + it('delegate in epoch 1, then epoch 3', async () => { + await simulation.runTestAsync( + [ + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, + // Creates CR for epoch 1 + // Sets MRCR to epoch 0 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, + // Moves to epoch 3 + TestAction.Finalize, + ], + [ + // Creates CR for epoch 3 + // Sets MRCR to epoch 3 + // Clears CR for epoch 1 + // Creates CR for epoch 4 + // Clears CR for epoch 2 + TestAction.Delegate, ], [ { event: 'SetCumulativeReward', epoch: 3 }, { event: 'SetMostRecentCumulativeReward', epoch: 3 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 4 }, { event: 'UnsetCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 2 }, ], ); }); diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index 010115c6cf..0b0cfce7e4 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -195,7 +195,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); assertNoWETHTransferLogs(receipt.logs); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -208,7 +208,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); assertNoWETHTransferLogs(receipt.logs); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(ZERO_AMOUNT); }); @@ -226,7 +226,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(); await payAsync(); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); }); @@ -266,7 +266,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: ZERO_AMOUNT }, ); assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -279,7 +279,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: ZERO_AMOUNT }, ); assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(ZERO_AMOUNT); }); @@ -297,7 +297,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(); await payAsync(); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); @@ -317,7 +317,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(true); await payAsync(false); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); }); @@ -325,7 +325,10 @@ blockchainTests('Protocol Fee Unit Tests', env => { describe('Multiple makers', () => { it('fees paid to different makers in the same pool go to that pool', async () => { const otherMakerAddress = randomAddress(); - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress, otherMakerAddress] }); + const poolId = await createTestPoolAsync({ + operatorStake: minimumStake, + makers: [makerAddress, otherMakerAddress], + }); const payAsync = async (_makerAddress: string) => { await testContract.payProtocolFee.awaitTransactionSuccessAsync( _makerAddress, @@ -337,7 +340,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(makerAddress); await payAsync(otherMakerAddress); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = getProtocolFeesAsync(poolId); + const poolFees = await getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); @@ -345,7 +348,10 @@ blockchainTests('Protocol Fee Unit Tests', env => { const [fee, otherFee] = _.times(2, () => getRandomPortion(DEFAULT_PROTOCOL_FEE_PAID)); const otherMakerAddress = randomAddress(); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); - const otherPoolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [otherMakerAddress]}); + const otherPoolId = await createTestPoolAsync({ + operatorStake: minimumStake, + makers: [otherMakerAddress], + }); const payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => { // prettier-ignore await testContract.payProtocolFee.awaitTransactionSuccessAsync( @@ -368,14 +374,17 @@ blockchainTests('Protocol Fee Unit Tests', env => { describe('Dust stake', () => { it('credits pools with stake > minimum', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake.plus(1), makers: [makerAddress] }); + const poolId = await createTestPoolAsync({ + operatorStake: minimumStake.plus(1), + makers: [makerAddress], + }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = getProtocolFeesAsync(poolId); + const feesCredited = await getProtocolFeesAsync(poolId); expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -387,19 +396,22 @@ blockchainTests('Protocol Fee Unit Tests', env => { DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = getProtocolFeesAsync(poolId); + const feesCredited = await getProtocolFeesAsync(poolId); expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); it('does not credit pools with stake < minimum', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake.minus(1), makers: [makerAddress] }); + const poolId = await createTestPoolAsync({ + operatorStake: minimumStake.minus(1), + makers: [makerAddress], + }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = getProtocolFeesAsync(poolId); + const feesCredited = await getProtocolFeesAsync(poolId); expect(feesCredited).to.bignumber.eq(0); }); }); diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index a931373492..e44742491f 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -14,7 +14,7 @@ import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './ // tslint:disable:no-unnecessary-type-assertion // tslint:disable:max-file-line-count -blockchainTests.resets('Testing Rewards', env => { +blockchainTests.resets.skip('Testing Rewards', env => { // tokens & addresses let accounts: string[]; let owner: string; diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index a6901a7b83..4f6d6c45a7 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -17,7 +17,11 @@ import { TestDelegatorRewardsRecordDepositToRewardVaultEventArgs as RewardVaultDepositEventArgs, } from '../../src'; -import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; +import { + assertIntegerRoughlyEquals as assertRoughlyEquals, + getRandomInteger, + toBaseUnitAmount, +} from '../utils/number_utils'; blockchainTests.resets('delegator unit rewards', env => { let testContract: TestDelegatorRewardsContract; @@ -52,6 +56,11 @@ blockchainTests.resets('delegator unit rewards', env => { new BigNumber(_opts.membersReward), new BigNumber(_opts.membersStake), ); + // Because the operator share is implicitly defined by the member and + // operator reward, and is stored as a uint32, there will be precision + // loss when the reward is combined then split again in the contracts. + // So we perform this transformation on the values and return them. + [_opts.operatorReward, _opts.membersReward] = toActualRewards(_opts.operatorReward, _opts.membersReward); return _opts; } @@ -73,9 +82,30 @@ blockchainTests.resets('delegator unit rewards', env => { new BigNumber(_opts.membersReward), new BigNumber(_opts.membersStake), ); + // Because the operator share is implicitly defined by the member and + // operator reward, and is stored as a uint32, there will be precision + // loss when the reward is combined then split again in the contracts. + // So we perform this transformation on the values and return them. + [_opts.operatorReward, _opts.membersReward] = toActualRewards(_opts.operatorReward, _opts.membersReward); return _opts; } + // Converts pre-split rewards to the amounts the contracts will calculate + // after suffering precision loss from the low-precision `operatorShare`. + function toActualRewards(operatorReward: Numberish, membersReward: Numberish): [BigNumber, BigNumber] { + const totalReward = BigNumber.sum(operatorReward, membersReward); + const operatorSharePPM = new BigNumber(operatorReward) + .times(constants.PPM_100_PERCENT) + .dividedBy(totalReward) + .integerValue(BigNumber.ROUND_DOWN); + const _operatorReward = totalReward + .times(operatorSharePPM) + .dividedBy(constants.PPM_100_PERCENT) + .integerValue(BigNumber.ROUND_UP); + const _membersReward = totalReward.minus(_operatorReward); + return [_operatorReward, _membersReward]; + } + type ResultWithDeposits = T & { ethVaultDeposit: BigNumber; rewardVaultDeposit: BigNumber; @@ -138,12 +168,12 @@ blockchainTests.resets('delegator unit rewards', env => { logs, TestDelegatorRewardsEvents.RecordDepositToEthVault, ); - if (ethVaultDepositArgs.length > 0) { - expect(ethVaultDepositArgs.length).to.eq(1); - if (delegator !== undefined) { - expect(ethVaultDepositArgs[0].owner).to.eq(delegator); + if (ethVaultDepositArgs.length > 0 && delegator !== undefined) { + for (const event of ethVaultDepositArgs) { + if (event.owner === delegator) { + ethVaultDeposit = ethVaultDeposit.plus(event.amount); + } } - ethVaultDeposit = ethVaultDepositArgs[0].amount; } const rewardVaultDepositArgs = filterLogsToArguments( logs, @@ -284,7 +314,7 @@ blockchainTests.resets('delegator unit rewards', env => { // rewards paid for stake in epoch 1. const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); - expect(deposit).to.bignumber.eq(reward); + assertRoughlyEquals(deposit, reward); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -297,7 +327,7 @@ blockchainTests.resets('delegator unit rewards', env => { // rewards paid for stake in epoch 1. const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); - expect(deposit).to.bignumber.eq(reward); + assertRoughlyEquals(deposit, reward); await delegateStakeAsync(poolId, { delegator, stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); @@ -458,7 +488,7 @@ blockchainTests.resets('delegator unit rewards', env => { }); describe('with unfinalized rewards', async () => { - it('nothing with only unfinalized rewards from epoch 1 for deleator with nothing delegated', async () => { + it('nothing with only unfinalized rewards from epoch 1 for delegator with nothing delegated', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId, { stake: 0 }); await advanceEpochAsync(); // epoch 1 @@ -467,7 +497,7 @@ blockchainTests.resets('delegator unit rewards', env => { expect(reward).to.bignumber.eq(0); }); - it('nothing with only unfinalized rewards from epoch 1 for deleator delegating in epoch 0', async () => { + it('nothing with only unfinalized rewards from epoch 1 for delegator delegating in epoch 0', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 @@ -481,10 +511,12 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { membersReward: unfinalizedReward } = - await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ + poolId, + membersStake: stake, + }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); - expect(reward).to.bignumber.eq(unfinalizedReward); + assertRoughlyEquals(reward, unfinalizedReward); }); it('returns unfinalized rewards from epoch 3 for delegator delegating in epoch 0', async () => { @@ -493,10 +525,12 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 await advanceEpochAsync(); // epoch 3 - const { membersReward: unfinalizedReward } = - await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ + poolId, + membersStake: stake, + }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); - expect(reward).to.bignumber.eq(unfinalizedReward); + assertRoughlyEquals(reward, unfinalizedReward); }); it('returns unfinalized rewards from epoch 3 + rewards from epoch 2 for delegator delegating in epoch 0', async () => { @@ -506,10 +540,13 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 2 const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 - const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ + poolId, + membersStake: stake, + }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); - expect(reward).to.bignumber.eq(expectedReward); + assertRoughlyEquals(reward, expectedReward); }); it('returns unfinalized rewards from epoch 4 + rewards from epoch 2 for delegator delegating in epoch 1', async () => { @@ -520,11 +557,13 @@ blockchainTests.resets('delegator unit rewards', env => { const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 - const { membersReward: unfinalizedReward } = - await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ + poolId, + membersStake: stake, + }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); - expect(reward).to.bignumber.eq(expectedReward); + assertRoughlyEquals(reward, expectedReward); }); it('returns correct rewards if unfinalized stake is different from previous rewards', async () => { @@ -538,11 +577,13 @@ blockchainTests.resets('delegator unit rewards', env => { }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 - const { membersReward: unfinalizedReward, membersStake: unfinalizedStake } = - await setUnfinalizedPoolRewardAsync({ - poolId, - membersStake: new BigNumber(stake).times(5), - }); + const { + membersReward: unfinalizedReward, + membersStake: unfinalizedStake, + } = await setUnfinalizedPoolRewardAsync({ + poolId, + membersStake: new BigNumber(stake).times(5), + }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum( computeDelegatorRewards(prevReward, stake, prevStake), diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 077359b1cc..1555738e2c 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -192,10 +192,12 @@ blockchainTests.resets('finalizer unit tests', env => { // Make sure they all sum up to the totals. const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs); { - const totalDepositedOperatorRewards = - BigNumber.sum(...depositStakingPoolRewardsEvents.map(e => e.operatorReward)); - const totalDepositedMembersRewards = - BigNumber.sum(...depositStakingPoolRewardsEvents.map(e => e.membersReward)); + const totalDepositedOperatorRewards = BigNumber.sum( + ...depositStakingPoolRewardsEvents.map(e => e.operatorReward), + ); + const totalDepositedMembersRewards = BigNumber.sum( + ...depositStakingPoolRewardsEvents.map(e => e.membersReward), + ); assertRoughlyEquals(totalDepositedOperatorRewards, totalOperatorRewards); assertRoughlyEquals(totalDepositedMembersRewards, totalMembersRewards); } @@ -211,10 +213,7 @@ blockchainTests.resets('finalizer unit tests', env => { await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards); } - async function assertReceiverBalancesAsync( - operatorRewards: Numberish, - membersRewards: Numberish, - ): Promise { + async function assertReceiverBalancesAsync(operatorRewards: Numberish, membersRewards: Numberish): Promise { const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver); assertRoughlyEquals(operatorRewardsBalance, operatorRewards); const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver); @@ -272,11 +271,17 @@ blockchainTests.resets('finalizer unit tests', env => { } function getRecordStakingPoolRewardsEvents(logs: LogEntry[]): RecordStakingPoolRewardsEventArgs[] { - return filterLogsToArguments(logs, TestFinalizerEvents.RecordStakingPoolRewards); + return filterLogsToArguments( + logs, + TestFinalizerEvents.RecordStakingPoolRewards, + ); } function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] { - return filterLogsToArguments(logs, TestFinalizerEvents.DepositStakingPoolRewards); + return filterLogsToArguments( + logs, + TestFinalizerEvents.DepositStakingPoolRewards, + ); } function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] { @@ -706,7 +711,7 @@ blockchainTests.resets('finalizer unit tests', env => { const expectedPoolRewards = await calculatePoolRewardsAsync(INITIAL_BALANCE, pools); const [pool, reward] = _.sampleSize(shortZip(pools, expectedPoolRewards), 1)[0]; return assertUnfinalizedPoolRewardsAsync(pool.poolId, { - totalReward: reward as any as BigNumber, + totalReward: (reward as any) as BigNumber, membersStake: pool.membersStake, }); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index d83122fae1..722c569cd0 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -78,7 +78,7 @@ export class StakingApiWrapper { await this.stakingContract.getLogsAsync( StakingEvents.StakingPoolActivated, { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, - { epoch: _epoch }, + { epoch: new BigNumber(_epoch) }, ), StakingEvents.StakingPoolActivated, ); @@ -253,6 +253,8 @@ export async function deployAndConfigureContractsAsync( await zrxVaultContract.setStakingProxy.awaitTransactionSuccessAsync(stakingProxyContract.address); // set staking proxy contract in reward vault await rewardVaultContract.setStakingProxy.awaitTransactionSuccessAsync(stakingProxyContract.address); + // set staking proxy contract in eth vault + await ethVaultContract.setStakingProxy.awaitTransactionSuccessAsync(stakingProxyContract.address); return new StakingApiWrapper( env, ownerAddress, diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index 04be9518e3..56f7ef9d6e 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -63,6 +63,7 @@ export class CumulativeRewardTrackingSimulation { for (let i = 0; i < expectedSequence.length; i++) { const expectedLog = expectedSequence[i]; const actualLog = logs[i]; + expect(expectedLog.event).to.exist(''); expect(expectedLog.event, `testing event name of ${JSON.stringify(expectedLog)}`).to.be.equal( actualLog.event, ); diff --git a/contracts/staking/test/utils/number_utils.ts b/contracts/staking/test/utils/number_utils.ts index aa07ad3d44..474bd9e5a0 100644 --- a/contracts/staking/test/utils/number_utils.ts +++ b/contracts/staking/test/utils/number_utils.ts @@ -94,7 +94,10 @@ export function assertRoughlyEquals(actual: Numberish, expected: Numberish, prec * Asserts that two numbers are equal with up to `maxError` difference between them. */ export function assertIntegerRoughlyEquals(actual: Numberish, expected: Numberish, maxError: number = 1): void { - const diff = new BigNumber(actual).minus(expected).abs().toNumber(); + const diff = new BigNumber(actual) + .minus(expected) + .abs() + .toNumber(); if (diff <= maxError) { return; }