From bbae6b3de2d515db955b3d4f4256466d8670cda3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 10 Sep 2019 16:29:33 -0400 Subject: [PATCH 01/52] `@0x/contracts-staking`: Starting MBF. --- .../src/finalization/MixinFinalizer.sol | 169 ++++++++++++++++++ .../contracts/src/immutable/MixinStorage.sol | 27 +++ .../src/interfaces/IStakingEvents.sol | 24 +++ .../contracts/src/interfaces/IStructs.sol | 3 - 4 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 contracts/staking/contracts/src/finalization/MixinFinalizer.sol diff --git a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol new file mode 100644 index 0000000000..71e43980a0 --- /dev/null +++ b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol @@ -0,0 +1,169 @@ +/* + + 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/LibRichErrors.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; +import "../libs/LibStakingRichErrors.sol"; +import "../libs/LibFixedMath.sol"; +import "../immutable/MixinStorage.sol"; +import "../immutable/MixinConstants.sol"; +import "../interfaces/IStakingEvents.sol"; +import "../interfaces/IStructs.sol"; +import "../stake/MixinStakeBalances.sol"; +import "../sys/MixinScheduler.sol"; +import "../staking_pools/MixinStakingPool.sol"; +import "../staking_pools/MixinStakingPoolRewardVault.sol"; +import "./MixinExchangeManager.sol"; + + +/// @dev This mixin contains functions related to finalizing epochs. +/// Finalization occurs over multiple calls because we can only +/// discover the `totalRewardsPaid` to all pools by summing the +/// the reward function across all active pools at the end of an +/// epoch. Until this value is known for epoch `e`, we cannot finalize +/// epoch `e+1`, because the remaining balance (`balance - totalRewardsPaid`) +/// is the reward pool for finalizing the next epoch. +contract MixinFinalizer is + IStakingEvents, + MixinConstants, + Ownable, + MixinStorage, + MixinStakingPoolRewardVault, + MixinScheduler, + MixinStakeBalances, + MixinStakingPoolRewards +{ + using LibSafeMath for uint256; + + + /// @dev Begins a new epoch, preparing the prior one for finalization. + /// Throws if not enough time has passed between epochs or if the + /// previous epoch was not fully finalized. + /// If there were no active pools in the closing epoch, the epoch + /// will be instantly finalized here. Otherwise, `finalizePools()` + /// should be called on each active pool afterwards. + /// @return _unfinalizedPoolsRemaining The number of unfinalized pools. + function endEpoch() + external + returns (uint256 _unfinalizedPoolsRemaining) + { + // Make sure the previous epoch has been fully finalized. + if (unfinalizedPoolsRemaining != 0) { + LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalized( + currentEpoch - 1, + unfinalizedPoolsRemaining + )); + } + // Populate finalization state. + unfinalizedPoolsRemaining = numActivePoolsThisEpoch; + unfinalizedRewardsAvailable = address(this).balance; + unfinalizedTotalFeesCollected = totalFeesCollected; + unfinalizedTotalWeightedStake = totalWeightedStake; + totalRewardsPaid = 0; + // Reset current epoch state. + totalFeesCollected = 0; + totalWeightedStake = 0; + numActivePoolsThisEpoch = 0; + // Advance the epoch. This will revert if not enough time has passed. + _goToNextEpoch(); + // If there were no active pools, finalize the epoch now. + if (unfinalizedPoolsRemaining == 0) { + emit EpochFinalized(); + } + return _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; + } + + /// @dev Finalizes a pool that was active in the previous epoch, paying out + /// its rewards to the reward vault. Keepers should call this function + /// repeatedly until all active pools that were emitted in in a + /// `StakingPoolActivated` in the prior epoch have been finalized. + /// Pools that have already been finalized will be silently ignored. + /// @param poolIds List of active pool IDs to finalize. + /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. + function finalizePools(bytes32[] memory poolIds) external { + + } + + /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. + /// Note that in this function there is no limitation on alpha; we tend to get better rounding + /// on the simplified versions below. + /// @param totalRewards collected over an epoch. + /// @param ownerFees Fees attributed to the owner of the staking pool. + /// @param totalFees collected across all active staking pools in the epoch. + /// @param ownerStake Stake attributed to the owner of the staking pool. + /// @param totalStake collected across all active staking pools in the epoch. + /// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function. + /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. + /// @return ownerRewards Rewards for the owner. + function _cobbDouglas( + uint256 totalRewards, + uint256 ownerFees, + uint256 totalFees, + uint256 ownerStake, + uint256 totalStake, + uint256 alphaNumerator, + uint256 alphaDenominator + ) + internal + pure + returns (uint256 ownerRewards) + { + int256 feeRatio = LibFixedMath._toFixed(ownerFees, totalFees); + int256 stakeRatio = LibFixedMath._toFixed(ownerStake, totalStake); + if (feeRatio == 0 || stakeRatio == 0) { + return ownerRewards = 0; + } + + // The cobb-doublas function has the form: + // `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)` + // This is equivalent to: + // `totalRewards * stakeRatio * e^(alpha * (ln(feeRatio / stakeRatio)))` + // However, because `ln(x)` has the domain of `0 < x < 1` + // and `exp(x)` has the domain of `x < 0`, + // and fixed-point math easily overflows with multiplication, + // we will choose the following if `stakeRatio > feeRatio`: + // `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))` + + // Compute + // `e^(alpha * (ln(feeRatio/stakeRatio)))` if feeRatio <= stakeRatio + // or + // `e^(ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio + int256 n = feeRatio <= stakeRatio ? + LibFixedMath._div(feeRatio, stakeRatio) : + LibFixedMath._div(stakeRatio, feeRatio); + n = LibFixedMath._exp( + LibFixedMath._mulDiv( + LibFixedMath._ln(n), + int256(alphaNumerator), + int256(alphaDenominator) + ) + ); + // Compute + // `totalRewards * n` if feeRatio <= stakeRatio + // or + // `totalRewards / n` if stakeRatio > feeRatio + n = feeRatio <= stakeRatio ? + LibFixedMath._mul(stakeRatio, n) : + LibFixedMath._div(stakeRatio, n); + // Multiply the above with totalRewards. + ownerRewards = LibFixedMath._uintMul(n, totalRewards); + } +} diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 820f9ed2c6..744d8ece54 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -135,6 +135,33 @@ contract MixinStorage is // Denominator for cobb douglas alpha factor. uint32 public cobbDouglasAlphaDenominator; + /* Finalization states */ + + /// @dev The total fees collected in the current epoch, built up iteratively + /// in `payProtocolFee()`. + uint256 totalFeesCollectedThisEpoch; + /// @dev The total weighted stake in the current epoch, built up iteratively + /// in `payProtocolFee()`. + uint256 totalWeightedStakeThisEpoch; + /// @dev State information for each active pool in an epoch. + /// In practice, we only store state for `currentEpoch % 2`. + mapping(uint256 => mapping(bytes32 => ActivePool)) activePoolsByEpoch; + /// @dev Number of pools activated in the current epoch. + uint256 numActivePoolsThisEpoch; + /// @dev Rewards (ETH) available to the epoch being finalized (the previous + /// epoch). This is simply the balance of the contract at the end of + /// the epoch. + uint256 unfinalizedRewardsAvailable; + /// @dev The number of active pools in the last epoch that have yet to be + /// finalized through `finalizePools()`. + uint256 unfinalizedPoolsRemaining; + /// @dev The total fees collected for the epoch being finalized. + uint256 unfinalizedTotalFeesCollected; + /// @dev The total fees collected for the epoch being finalized. + uint256 unfinalizedTotalWeightedStake; + /// @dev How many rewards were paid at the end of finalization. + uint256 totalRewardsPaidLastEpoch; + /// @dev Adds owner as an authorized address. constructor() public diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index a017b66a32..dfe602ffec 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -53,6 +53,30 @@ interface IStakingEvents { uint256 earliestEndTimeInSeconds ); + /// @dev Emitted by MixinFinalizer when an epoch has ended. + /// @param epoch The closing epoch. + /// @param numActivePools Number of active pools in the closing epoch. + /// @param rewardsAvailable Rewards available to all active pools. + /// @param totalWeightedStake Total weighted stake across all active pools. + /// @param totalFeesCollected Total fees collected across all active pools. + event EpochFinalized( + uint256 epoch, + uint256 numActivePools, + uint256 rewardsAvailable, + uint256 totalWeightedStake, + uint256 totalFeesCollected + ); + + /// @dev Emitted by MixinFinalizer when an epoch is fully finalized. + /// @param epoch The epoch being finalized. + /// @param rewardsPaid Total amount of rewards paid out. + /// @param rewardsRemaining Rewards left over. + event EpochFinalized( + uint256 epoch, + uint256 rewardsPaid, + uint256 rewardsRemaining + ); + /// @dev Emitted whenever staking parameters are changed via the `setParams()` function. /// @param epochDurationInSeconds Minimum seconds between epochs. /// @param rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm. diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index df4deb2b49..67ba36c57b 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -23,14 +23,11 @@ interface IStructs { /// @dev Status for a pool that actively traded during the current epoch. /// (see MixinExchangeFees). - /// @param poolId Unique Id of staking pool. /// @param feesCollected Fees collected in ETH by this pool in the current epoch. /// @param weightedStake Amount of weighted stake currently held by the pool. struct ActivePool { - bytes32 poolId; uint256 feesCollected; uint256 weightedStake; - uint256 delegatedStake; } /// @dev Encapsulates a balance for the current and next epochs. From 73f1aca4a13d505ba7baed4e2080f58a943bdecc Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 10 Sep 2019 21:42:19 -0400 Subject: [PATCH 02/52] `@0x/contracts-staking`: More work on `MixinFinalizer`. --- .../src/finalization/MixinFinalizer.sol | 103 +++++++++++++++++- .../contracts/src/immutable/MixinStorage.sol | 2 +- .../src/interfaces/IStakingEvents.sol | 38 ++----- .../contracts/src/interfaces/IStructs.sol | 6 +- .../contracts/src/sys/MixinScheduler.sol | 7 -- 5 files changed, 115 insertions(+), 41 deletions(-) diff --git a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol index 71e43980a0..b9f21ff9d1 100644 --- a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol @@ -65,10 +65,11 @@ contract MixinFinalizer is external returns (uint256 _unfinalizedPoolsRemaining) { + uint256 closingEpoch = currentEpoch; // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalized( - currentEpoch - 1, + closingEpoch.sub(1), unfinalizedPoolsRemaining )); } @@ -78,15 +79,23 @@ contract MixinFinalizer is unfinalizedTotalFeesCollected = totalFeesCollected; unfinalizedTotalWeightedStake = totalWeightedStake; totalRewardsPaid = 0; + // Emit an event. + emit EpochEnded( + closingEpoch, + numActivePoolsThisEpoch, + rewardsAvailable, + totalWeightedStake, + totalFeesCollected + ); // Reset current epoch state. totalFeesCollected = 0; totalWeightedStake = 0; numActivePoolsThisEpoch = 0; // Advance the epoch. This will revert if not enough time has passed. _goToNextEpoch(); - // If there were no active pools, finalize the epoch now. + // If there were no active pools, the epoch is already finalized. if (unfinalizedPoolsRemaining == 0) { - emit EpochFinalized(); + emit EpochFinalized(closingEpoch, 0, unfinalizedRewardsAvailable); } return _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; } @@ -96,10 +105,96 @@ contract MixinFinalizer is /// repeatedly until all active pools that were emitted in in a /// `StakingPoolActivated` in the prior epoch have been finalized. /// Pools that have already been finalized will be silently ignored. + /// We deliberately try not to revert here in case multiple parties + /// are finalizing pools. /// @param poolIds List of active pool IDs to finalize. + /// @return rewardsPaid Total rewards paid to the pools passed in. /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. - function finalizePools(bytes32[] memory poolIds) external { + function finalizePools(bytes32[] memory poolIds) + external + returns (uint256 rewardsPaid, uint256 _unfinalizedPoolsRemaining) + { + uint256 epoch = currentEpoch.sub(1); + uint256 poolsRemaining = unfinalizedPoolsRemaining; + uint256 numPoolIds = poolIds.length; + uint256 rewardsPaid = 0; + // Pointer to the active pools in the last epoch. + // We use `(currentEpoch - 1) % 2` as the index to reuse state. + mapping(bytes32 => IStructs.ActivePool) storage activePools = + activePoolsByEpoch[epoch % 2]; + for (uint256 i = 0; i < numPoolIds && poolsRemaining != 0; i++) { + bytes32 poolId = poolIds[i]; + IStructs.ActivePool memory pool = activePools[poolId]; + // Ignore pools that aren't active. + if (pool.feesCollected != 0) { + // Credit the pool with rewards. + // We will transfer the total rewards to the vault at the end. + rewardsPaid = rewardsPaid.add(_creditRewardsToPool(poolId, pool)); + // Clear the pool state so we don't finalize it again, + // and to recoup some gas. + activePools[poolId] = IStructs.ActivePool(0, 0); + // Decrease the number of unfinalized pools left. + poolsRemaining = poolsRemaining.sub(1); + // Emit an event. + emit RewardsPaid(epoch, poolId, reward); + } + } + // Deposit all the rewards at once into the RewardVault. + _depositIntoStakingPoolRewardVault(rewardsPaid); + // Update finalization state. + totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.add(rewardsPaid); + _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining = poolsRemaining; + // If there are no more unfinalized pools remaining, the epoch is + // finalized. + if (poolsRemaining == 0) { + emit EpochFinalized( + epoch, + totalRewardsPaidLastEpoch, + unfinalizedRewardsAvailable.sub(totalRewardsPaidLastEpoch) + ); + } + } + /// @dev Computes the rewards owned for a pool during finalization and + /// credits it in the RewardVault. + /// @param The epoch being finalized. + /// @param poolId The pool's ID. + /// @param pool The pool. + /// @return rewards Amount of rewards for this pool. + function _creditRewardsToPool( + uint256 epoch, + bytes32 poolId, + IStructs.ActivePool memory pool + ) + internal + returns (uint256 rewards) + { + // Use the cobb-douglas function to compute the reward. + reward = _cobbDouglas( + unfinalizedRewardsAvailable, + pool.feesCollected, + unfinalizedTotalFeesCollected, + pool.weightedStake, + unfinalizedTotalWeightedStake, + cobbDouglasAlphaNumerator, + cobbDouglasAlphaDenomintor + ); + // Credit the pool the reward in the RewardVault. + (, uint256 membersPortionOfReward) = rewardVault.recordDepositFor( + poolId, + reward, + // If no delegated stake, all rewards go to the operator. + pool.delegatedStake == 0 + ); + // Sync delegator rewards. + if (membersPortionOfReward != 0) { + _recordRewardForDelegators( + poolId, + membersPortionOfReward, + pool.delegatedStake, + epoch + ); + } } /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 744d8ece54..59b6d9b58d 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -145,7 +145,7 @@ contract MixinStorage is uint256 totalWeightedStakeThisEpoch; /// @dev State information for each active pool in an epoch. /// In practice, we only store state for `currentEpoch % 2`. - mapping(uint256 => mapping(bytes32 => ActivePool)) activePoolsByEpoch; + mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) activePoolsByEpoch; /// @dev Number of pools activated in the current epoch. uint256 numActivePoolsThisEpoch; /// @dev Rewards (ETH) available to the epoch being finalized (the previous diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index dfe602ffec..84884cd67b 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -43,23 +43,13 @@ interface IStakingEvents { address exchangeAddress ); - /// @dev Emitted by MixinScheduler when the epoch is changed. - /// @param epoch The epoch we changed to. - /// @param startTimeInSeconds The start time of the epoch. - /// @param earliestEndTimeInSeconds The earliest this epoch can end. - event EpochChanged( - uint256 epoch, - uint256 startTimeInSeconds, - uint256 earliestEndTimeInSeconds - ); - /// @dev Emitted by MixinFinalizer when an epoch has ended. /// @param epoch The closing epoch. /// @param numActivePools Number of active pools in the closing epoch. /// @param rewardsAvailable Rewards available to all active pools. /// @param totalWeightedStake Total weighted stake across all active pools. /// @param totalFeesCollected Total fees collected across all active pools. - event EpochFinalized( + event EpochEnded( uint256 epoch, uint256 numActivePools, uint256 rewardsAvailable, @@ -77,6 +67,16 @@ interface IStakingEvents { uint256 rewardsRemaining ); + /// @dev Emitted by MixinFinalizer when rewards are paid out to a pool. + /// @param epoch The epoch when the rewards were earned. + /// @param poolId The pool's ID. + /// @param reward Amount of reward paid. + event RewardsPaid( + uint255 epoch, + bytes32 poolId, + uint255 reward + ); + /// @dev Emitted whenever staking parameters are changed via the `setParams()` function. /// @param epochDurationInSeconds Minimum seconds between epochs. /// @param rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm. @@ -111,22 +111,6 @@ interface IStakingEvents { uint256 endEpoch ); - /// @dev Emitted by MixinExchangeFees when rewards are paid out. - /// @param totalActivePools Total active pools this epoch. - /// @param totalFeesCollected Total fees collected this epoch, across all active pools. - /// @param totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less. - /// @param totalRewardsPaid Total rewards paid out across all active pools. - /// @param initialContractBalance Balance of this contract before paying rewards. - /// @param finalContractBalance Balance of this contract after paying rewards. - event RewardsPaid( - uint256 totalActivePools, - uint256 totalFeesCollected, - uint256 totalWeightedStake, - uint256 totalRewardsPaid, - uint256 initialContractBalance, - uint256 finalContractBalance - ); - /// @dev Emitted by MixinStakingPool when a new pool is created. /// @param poolId Unique id generated for pool. /// @param operator The operator (creator) of pool. diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index 67ba36c57b..13028afe69 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -23,11 +23,13 @@ interface IStructs { /// @dev Status for a pool that actively traded during the current epoch. /// (see MixinExchangeFees). - /// @param feesCollected Fees collected in ETH by this pool in the current epoch. - /// @param weightedStake Amount of weighted stake currently held by the pool. + /// @param feesCollected Fees collected in ETH by this pool. + /// @param weightedStake Amount of weighted stake in the pool. + /// @param delegatedStake Amount of delegated, non-operator stake in the pool. struct ActivePool { uint256 feesCollected; uint256 weightedStake; + uint256 delegatedStake; } /// @dev Encapsulates a balance for the current and next epochs. diff --git a/contracts/staking/contracts/src/sys/MixinScheduler.sol b/contracts/staking/contracts/src/sys/MixinScheduler.sol index 0414ee4139..a711c80d7c 100644 --- a/contracts/staking/contracts/src/sys/MixinScheduler.sol +++ b/contracts/staking/contracts/src/sys/MixinScheduler.sol @@ -87,13 +87,6 @@ contract MixinScheduler is uint256 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds.safeAdd( epochDurationInSeconds ); - - // notify of epoch change - emit EpochChanged( - currentEpoch, - currentEpochStartTimeInSeconds, - earliestEndTimeInSeconds - ); } /// @dev Assert scheduler state before initializing it. From e4b9d14f457c469d290d0f04c488d12f9a5807bd Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 11 Sep 2019 16:56:24 -0400 Subject: [PATCH 03/52] `@0x/contracts-staking`: Tinkering with crediting rewards. --- .../contracts/src/finalization/MixinFinalizer.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol index b9f21ff9d1..0c7c221244 100644 --- a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol @@ -53,7 +53,6 @@ contract MixinFinalizer is { using LibSafeMath for uint256; - /// @dev Begins a new epoch, preparing the prior one for finalization. /// Throws if not enough time has passed between epochs or if the /// previous epoch was not fully finalized. @@ -65,7 +64,7 @@ contract MixinFinalizer is external returns (uint256 _unfinalizedPoolsRemaining) { - uint256 closingEpoch = currentEpoch; + uint256 closingEpoch = getCurrentEpoch(); // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalized( @@ -114,7 +113,7 @@ contract MixinFinalizer is external returns (uint256 rewardsPaid, uint256 _unfinalizedPoolsRemaining) { - uint256 epoch = currentEpoch.sub(1); + uint256 epoch = getCurrentEpoch().sub(1); uint256 poolsRemaining = unfinalizedPoolsRemaining; uint256 numPoolIds = poolIds.length; uint256 rewardsPaid = 0; @@ -132,7 +131,7 @@ contract MixinFinalizer is rewardsPaid = rewardsPaid.add(_creditRewardsToPool(poolId, pool)); // Clear the pool state so we don't finalize it again, // and to recoup some gas. - activePools[poolId] = IStructs.ActivePool(0, 0); + activePools[poolId] = IStructs.ActivePool(0, 0, 0); // Decrease the number of unfinalized pools left. poolsRemaining = poolsRemaining.sub(1); // Emit an event. @@ -197,9 +196,10 @@ contract MixinFinalizer is } } - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note that in this function there is no limitation on alpha; we tend to get better rounding - /// on the simplified versions below. + /// @dev The cobb-douglas function used to compute fee-based rewards for + /// staking pools in a given epoch. Note that in this function there + /// is no limitation on alpha; we tend to get better rounding on the + /// simplified versions below. /// @param totalRewards collected over an epoch. /// @param ownerFees Fees attributed to the owner of the staking pool. /// @param totalFees collected across all active staking pools in the epoch. @@ -226,7 +226,6 @@ contract MixinFinalizer is if (feeRatio == 0 || stakeRatio == 0) { return ownerRewards = 0; } - // The cobb-doublas function has the form: // `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)` // This is equivalent to: From 9c47d22ff4c2568b8e85b530c961de26f3fe8731 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 11 Sep 2019 18:19:10 -0400 Subject: [PATCH 04/52] `@0x/contracts-staking`: Add some tests to check rewards in the EthVault to `_rewards_test`. --- contracts/staking/test/rewards_test.ts | 96 +++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 86ce5e9d0a..39e2aa3681 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -192,7 +192,8 @@ blockchainTests.resets('Testing Rewards', env => { operatorEthVaultBalance: reward, }); }); - it('Operator should receive entire reward if no delegators in their pool (staker joins this epoch but is active next epoch)', async () => { + it(`Operator should receive entire reward if no delegators in their pool + (staker joins this epoch but is active next epoch)`, async () => { // delegate const amount = toBaseUnitAmount(4); await stakers[0].stakeAsync(amount); @@ -663,6 +664,99 @@ blockchainTests.resets('Testing Rewards', env => { poolRewardVaultBalance: ZERO, }); }); + it(`should split rewards in the EthVault between two delegators when undelegating`, async () => { + const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; + const totalStakeAmount = BigNumber.sum(...stakeAmounts); + // stake both + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as + Array<[StakerActor, BigNumber]>; + for (const [staker, stakeAmount] of stakersAndStake) { + await staker.stakeAsync(stakeAmount); + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Active), + new StakeInfo(StakeStatus.Delegated, poolId), + stakeAmount, + ); + } + // skip epoch, so staker can start earning rewards + await payProtocolFeeAndFinalize(); + // finalize + const reward = toBaseUnitAmount(10); + await payProtocolFeeAndFinalize(reward); + // Undelegate 0 stake to move rewards from RewardVault into the EthVault. + for (const [staker] of stakersAndStake) { + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Delegated, poolId), + new StakeInfo(StakeStatus.Active), + toBaseUnitAmount(0), + ); + } + const expectedStakerRewards = stakeAmounts + .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + await validateEndBalances({ + stakerRewardVaultBalance_1: toBaseUnitAmount(0), + stakerRewardVaultBalance_2: toBaseUnitAmount(0), + stakerEthVaultBalance_1: expectedStakerRewards[0], + stakerEthVaultBalance_2: expectedStakerRewards[1], + poolRewardVaultBalance: new BigNumber(1), // Rounding error + membersRewardVaultBalance: new BigNumber(1), // Rounding error + }); + }); + it(`delegator should not be credited EthVault rewards twice in the same epoch by undelegating twice`, async () => { + const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; + const totalStakeAmount = BigNumber.sum(...stakeAmounts); + // stake both + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as + Array<[StakerActor, BigNumber]>; + for (const [staker, stakeAmount] of stakersAndStake) { + await staker.stakeAsync(stakeAmount); + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Active), + new StakeInfo(StakeStatus.Delegated, poolId), + stakeAmount, + ); + } + // skip epoch, so staker can start earning rewards + await payProtocolFeeAndFinalize(); + // finalize + const reward = toBaseUnitAmount(10); + await payProtocolFeeAndFinalize(reward); + const expectedStakerRewards = stakeAmounts + .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + await validateEndBalances({ + stakerRewardVaultBalance_1: expectedStakerRewards[0], + stakerRewardVaultBalance_2: expectedStakerRewards[1], + stakerEthVaultBalance_1: toBaseUnitAmount(0), + stakerEthVaultBalance_2: toBaseUnitAmount(0), + poolRewardVaultBalance: reward, + membersRewardVaultBalance: reward, + }); + const undelegateZeroAsync = async (staker: StakerActor) => { + return staker.moveStakeAsync( + new StakeInfo(StakeStatus.Delegated, poolId), + new StakeInfo(StakeStatus.Active), + toBaseUnitAmount(0), + ); + }; + // First staker will undelegate 0 to get rewards transferred to EthVault. + const sneakyStaker = stakers[0]; + const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0]; + await undelegateZeroAsync(sneakyStaker); + // Should have been credited the correct amount of rewards. + let sneakyStakerEthVaultBalance = await stakingApiWrapper + .ethVaultContract.balanceOf + .callAsync(sneakyStaker.getOwner()); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate') + .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + // Now he'll try to do it again to see if he gets credited twice. + await undelegateZeroAsync(sneakyStaker); + /// The total amount credited should remain the same. + sneakyStakerEthVaultBalance = await stakingApiWrapper + .ethVaultContract.balanceOf + .callAsync(sneakyStaker.getOwner()); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate') + .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + }); }); }); // tslint:enable:no-unnecessary-type-assertion From 94909f1a0f02cb7e221cae046b53739bc3ab9750 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 11 Sep 2019 19:01:31 -0400 Subject: [PATCH 05/52] `@0x/contracts-staking`: Add another delegator payout test. --- contracts/staking/test/rewards_test.ts | 41 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 39e2aa3681..5347f02f59 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -664,10 +664,43 @@ blockchainTests.resets('Testing Rewards', env => { poolRewardVaultBalance: ZERO, }); }); - it(`should split rewards in the EthVault between two delegators when undelegating`, async () => { + it(`payout should be based on stake at the time of rewards`, async () => { + const staker = stakers[0]; + const stakeAmount = toBaseUnitAmount(5); + // stake and delegate + await staker.stakeAsync(stakeAmount); + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Active), + new StakeInfo(StakeStatus.Delegated, poolId), + stakeAmount, + ); + // skip epoch, so staker can start earning rewards + await payProtocolFeeAndFinalize(); + // undelegate some stake + const unstakeAmount = toBaseUnitAmount(2.5); + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Delegated, poolId), + new StakeInfo(StakeStatus.Active), + unstakeAmount, + ); + // finalize + const reward = toBaseUnitAmount(10); + await payProtocolFeeAndFinalize(reward); + // Unstake nothing to move the rewards into the EthVault. + await staker.moveStakeAsync( + new StakeInfo(StakeStatus.Delegated, poolId), + new StakeInfo(StakeStatus.Active), + toBaseUnitAmount(0), + ); + await validateEndBalances({ + stakerRewardVaultBalance_1: toBaseUnitAmount(0), + stakerEthVaultBalance_1: reward, + }); + }); + it(`should split payout between two delegators when undelegating`, async () => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); - // stake both + // stake and delegate both const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { @@ -702,10 +735,10 @@ blockchainTests.resets('Testing Rewards', env => { membersRewardVaultBalance: new BigNumber(1), // Rounding error }); }); - it(`delegator should not be credited EthVault rewards twice in the same epoch by undelegating twice`, async () => { + it(`delegator should not be credited payout twice by undelegating twice`, async () => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); - // stake both + // stake and delegate both const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { From b57c0a2ebbdd11bd30305a98f03e65eaadb520b9 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 12 Sep 2019 16:19:52 -0400 Subject: [PATCH 06/52] `@0x/contracts-staking`: Fully implement MBF (I hope). --- .../contracts/src/fees/MixinExchangeFees.sol | 99 ++++++++++++------- .../src/finalization/MixinFinalizer.sol | 39 +++++--- .../contracts/src/immutable/MixinStorage.sol | 12 +-- .../src/interfaces/IStakingEvents.sol | 9 ++ .../contracts/src/interfaces/IStructs.sol | 4 +- .../staking_pools/MixinStakingPoolRewards.sol | 3 +- 6 files changed, 104 insertions(+), 62 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 0d820ceb8f..d1663de8f9 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -62,19 +62,7 @@ contract MixinExchangeFees is payable onlyExchange { - // If the protocol fee payment is invalid, revert with a rich error. - if ( - protocolFeePaid == 0 || - (msg.value != protocolFeePaid && msg.value != 0) - ) { - LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError( - protocolFeePaid == 0 ? - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid : - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, - protocolFeePaid, - msg.value - )); - } + _assertValidProtocolFee(protocolFeePaid); // Transfer the protocol fee to this address if it should be paid in WETH. if (msg.value == 0) { @@ -88,25 +76,60 @@ contract MixinExchangeFees is // Get the pool id of the maker address. bytes32 poolId = getStakingPoolIdOfMaker(makerAddress); - - // Only attribute the protocol fee payment to a pool if the maker is registered to a pool. - if (poolId != NIL_POOL_ID) { - uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; - // Ignore pools with dust stake. - if (poolStake >= minimumPoolStake) { - // Credit the pool. - uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId]; - protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(protocolFeePaid); - if (_feesCollectedThisEpoch == 0) { - activePoolsThisEpoch.push(poolId); - } - } + // Only attribute the protocol fee payment to a pool if the maker is + // registered to a pool. + if (poolId == NIL_POOL_ID) { + return; + } + uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; + // Ignore pools with dust stake. + if (poolStake < minimumPoolStake) { + return; + } + // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` + // because we only need to remember state in the current epoch and the + // epoch prior. + uint256 currentEpoch = getCurrentEpoch(); + mapping (bytes32 => IStructs.ActivePool) activePoolsThisEpoch = + activePoolsByEpoch[currentEpoch % 2]; + IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId] + // If the pool was previously inactive in this epoch, initialize it. + if (pool.feesCollected) { + // Compute weighted stake. + uint256 operatorStake = getStakeDelegatedToPoolByOwner( + rewardVault.operatorOf(poolId), + poolId + ).currentEpochBalance; + pool.weightedStake = operatorStake.safeAdd( + totalStakeDelegatedToPool + .safeSub(operatorStake) + .safeMul(delegatedStakeWeight) + .safeDiv(PPM_DENOMINATOR) + ); + // Compute delegated (non-operator) stake. + pool.delegatedStake = poolStake.safeSub(operatorStake); + // Increase the total weighted stake. + totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd( + pool.weightedStake + ); + // Increase the numberof active pools. + numActivePoolsThisEpoch += 1; + // Emit an event so keepers know what pools to pass into `finalize()`. + emit StakingPoolActivated(currentEpoch, poolId); } + // Credit the fees to the pool. + pool.feesCollected = protocolFeePaid; + // Increase the total fees collected this epoch. + totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd( + protocolFeePaid + ); + // Store the pool. + activePoolsThisEpoch[poolId] = pool; } /// @dev Pays the rebates for to market making pool that was active this epoch, - /// then updates the epoch and other time-based periods via the scheduler (see MixinScheduler). - /// This is intentionally permissionless, and may be called by anyone. + /// then updates the epoch and other time-based periods via the scheduler + /// (see MixinScheduler). This is intentionally permissionless, and may be called by anyone. function finalizeFees() external { @@ -140,15 +163,17 @@ contract MixinExchangeFees is return address(this).balance.safeAdd(wethBalance); } - /// @dev Withdraws the entire WETH balance of the contract. - function _unwrapWETH() - internal - { - uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); - - // Don't withdraw WETH if the WETH balance is zero as a gas optimization. - if (wethBalance != 0) { - IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + /// @dev Checks that the protocol fee passed into `payProtocolFee()` is valid. + /// @param protocolFeePaid The `protocolFeePaid` parameter to `payProtocolFee.` + function _assertValidProtocolFee(uint256 protocolFeePaid) private view { + if (protocolFeePaid == 0 || (msg.value != protocolFeePaid && msg.value != 0)) { + LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError( + protocolFeePaid == 0 ? + LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid : + LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, + protocolFeePaid, + msg.value + )); } } diff --git a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol index 0c7c221244..b5cb4490a6 100644 --- a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/finalization/MixinFinalizer.sol @@ -35,12 +35,10 @@ import "./MixinExchangeManager.sol"; /// @dev This mixin contains functions related to finalizing epochs. -/// Finalization occurs over multiple calls because we can only -/// discover the `totalRewardsPaid` to all pools by summing the -/// the reward function across all active pools at the end of an -/// epoch. Until this value is known for epoch `e`, we cannot finalize -/// epoch `e+1`, because the remaining balance (`balance - totalRewardsPaid`) -/// is the reward pool for finalizing the next epoch. +/// Finalization occurs AFTER the current epoch is ended/advanced and +/// over (potentially) multiple blocks/transactions. This pattern prevents +/// the contract from stalling while we finalize rewards for the previous +/// epoch. contract MixinFinalizer is IStakingEvents, MixinConstants, @@ -68,10 +66,12 @@ contract MixinFinalizer is // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalized( - closingEpoch.sub(1), + closingEpoch.safeSub(1), unfinalizedPoolsRemaining )); } + // Unrwap any WETH protocol fees. + _unwrapWETH(); // Populate finalization state. unfinalizedPoolsRemaining = numActivePoolsThisEpoch; unfinalizedRewardsAvailable = address(this).balance; @@ -99,8 +99,8 @@ contract MixinFinalizer is return _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; } - /// @dev Finalizes a pool that was active in the previous epoch, paying out - /// its rewards to the reward vault. Keepers should call this function + /// @dev Finalizes pools that were active in the previous epoch, paying out + /// rewards to the reward vault. Keepers should call this function /// repeatedly until all active pools that were emitted in in a /// `StakingPoolActivated` in the prior epoch have been finalized. /// Pools that have already been finalized will be silently ignored. @@ -113,7 +113,7 @@ contract MixinFinalizer is external returns (uint256 rewardsPaid, uint256 _unfinalizedPoolsRemaining) { - uint256 epoch = getCurrentEpoch().sub(1); + uint256 epoch = getCurrentEpoch().safeSub(1); uint256 poolsRemaining = unfinalizedPoolsRemaining; uint256 numPoolIds = poolIds.length; uint256 rewardsPaid = 0; @@ -128,12 +128,12 @@ contract MixinFinalizer is if (pool.feesCollected != 0) { // Credit the pool with rewards. // We will transfer the total rewards to the vault at the end. - rewardsPaid = rewardsPaid.add(_creditRewardsToPool(poolId, pool)); + rewardsPaid = rewardsPaid.safeAdd(_creditRewardsToPool(poolId, pool)); // Clear the pool state so we don't finalize it again, // and to recoup some gas. activePools[poolId] = IStructs.ActivePool(0, 0, 0); // Decrease the number of unfinalized pools left. - poolsRemaining = poolsRemaining.sub(1); + poolsRemaining = poolsRemaining.safeSub(1); // Emit an event. emit RewardsPaid(epoch, poolId, reward); } @@ -141,7 +141,7 @@ contract MixinFinalizer is // Deposit all the rewards at once into the RewardVault. _depositIntoStakingPoolRewardVault(rewardsPaid); // Update finalization state. - totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.add(rewardsPaid); + totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining = poolsRemaining; // If there are no more unfinalized pools remaining, the epoch is // finalized. @@ -149,7 +149,7 @@ contract MixinFinalizer is emit EpochFinalized( epoch, totalRewardsPaidLastEpoch, - unfinalizedRewardsAvailable.sub(totalRewardsPaidLastEpoch) + unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) ); } } @@ -190,12 +190,19 @@ contract MixinFinalizer is _recordRewardForDelegators( poolId, membersPortionOfReward, - pool.delegatedStake, - epoch + pool.delegatedStake ); } } + /// @dev Converts the entire WETH balance of the contract into ETH. + function _unwrapWETH() private { + uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); + if (wethBalance != 0) { + IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + } + } + /// @dev The cobb-douglas function used to compute fee-based rewards for /// staking pools in a given epoch. Note that in this function there /// is no limitation on alpha; we tend to get better rounding on the diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 59b6d9b58d..e56cd43d09 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -74,6 +74,12 @@ contract MixinStorage is // mapping from Owner to Amount of Withdrawable Stake mapping (address => uint256) internal _withdrawableStakeByOwner; + // Mapping from Owner to Pool Id to epoch of the last rewards collected. + // This is the last reward epoch for a pool that a delegator collected + // rewards from. This is different from the epoch when the rewards were + // collected This will always be `<= currentEpoch`. + mapping (address => mapping (bytes32 => uint256)) internal lastCollectedRewardsEpochToPoolByOwner; + // tracking Pool Id bytes32 public nextPoolId = INITIAL_POOL_ID; @@ -90,12 +96,6 @@ contract MixinStorage is // current epoch start time uint256 public currentEpochStartTimeInSeconds; - // fees collected this epoch - mapping (bytes32 => uint256) public protocolFeesThisEpochByPool; - - // pools that were active in the current epoch - bytes32[] public activePoolsThisEpoch; - // mapping from Pool Id to Epoch to Reward Ratio mapping (bytes32 => mapping (uint256 => IStructs.Fraction)) internal _cumulativeRewardsByPool; diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index 84884cd67b..d3d0bd4cdd 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -43,6 +43,15 @@ interface IStakingEvents { address exchangeAddress ); + /// @dev Emitted by MixinExchangeFees when a pool pays protocol fees + /// for the first time in an epoch. + /// @param epoch The epoch in which the pool was activated. + /// @param poolId The ID of the pool. + event StakingPoolActivated( + uint256 epoch, + bytes32 poolId + ); + /// @dev Emitted by MixinFinalizer when an epoch has ended. /// @param epoch The closing epoch. /// @param numActivePools Number of active pools in the closing epoch. diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index 13028afe69..ca7421c8cd 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -39,12 +39,14 @@ interface IStructs { /// @param isInitialized /// @param currentEpoch the current epoch /// @param currentEpochBalance balance in the current epoch. - /// @param nextEpochBalance balance in the next epoch. + /// @param nextEpochBalance balance in `currentEpoch+1`. + /// @param prevEpochBalance balance in `currentEpoch-1`. struct StoredBalance { bool isInitialized; uint32 currentEpoch; uint96 currentEpochBalance; uint96 nextEpochBalance; + uint96 prevEpochBalance; } /// @dev Balance struct for stake. diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 93b723c929..65b6630163 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -116,8 +116,7 @@ contract MixinStakingPoolRewards is function _handleStakingPoolReward( bytes32 poolId, uint256 reward, - uint256 amountOfDelegatedStake, - uint256 epoch + uint256 amountOfDelegatedStake ) internal { From 294be37afc1b336c180e26cd22046822ee21a175 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 12 Sep 2019 17:36:17 -0400 Subject: [PATCH 07/52] `@0x/contracts-staking`: Got the solidity compiling. --- .../contracts/src/fees/MixinExchangeFees.sol | 199 +++--------------- .../contracts/src/immutable/MixinStorage.sol | 26 ++- .../src/interfaces/IStakingEvents.sol | 8 +- .../src/libs/LibStakingRichErrors.sol | 19 ++ .../{finalization => sys}/MixinFinalizer.sol | 137 ++++++------ .../contracts/test/TestStorageLayout.sol | 33 ++- contracts/staking/src/artifacts.ts | 2 + contracts/staking/src/wrappers.ts | 1 + contracts/staking/tsconfig.json | 1 + 9 files changed, 170 insertions(+), 256 deletions(-) rename contracts/staking/contracts/src/{finalization => sys}/MixinFinalizer.sol (88%) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index d1663de8f9..2804cb3d1a 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -19,7 +19,6 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; @@ -90,20 +89,20 @@ contract MixinExchangeFees is // because we only need to remember state in the current epoch and the // epoch prior. uint256 currentEpoch = getCurrentEpoch(); - mapping (bytes32 => IStructs.ActivePool) activePoolsThisEpoch = + mapping (bytes32 => IStructs.ActivePool) storage activePoolsThisEpoch = activePoolsByEpoch[currentEpoch % 2]; - IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId] + IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId]; // If the pool was previously inactive in this epoch, initialize it. - if (pool.feesCollected) { + if (pool.feesCollected == 0) { // Compute weighted stake. uint256 operatorStake = getStakeDelegatedToPoolByOwner( rewardVault.operatorOf(poolId), poolId ).currentEpochBalance; pool.weightedStake = operatorStake.safeAdd( - totalStakeDelegatedToPool + poolStake .safeSub(operatorStake) - .safeMul(delegatedStakeWeight) + .safeMul(rewardDelegatedStakeWeight) .safeDiv(PPM_DENOMINATOR) ); // Compute delegated (non-operator) stake. @@ -127,45 +126,38 @@ contract MixinExchangeFees is activePoolsThisEpoch[poolId] = pool; } - /// @dev Pays the rebates for to market making pool that was active this epoch, - /// then updates the epoch and other time-based periods via the scheduler - /// (see MixinScheduler). This is intentionally permissionless, and may be called by anyone. - function finalizeFees() + /// @dev Returns the total amount of fees collected thus far, in the current epoch. + /// @return _totalFeesCollectedThisEpoch Total fees collected this epoch. + function getTotalProtocolFeesThisEpoch() external + view + returns (uint256 _totalFeesCollectedThisEpoch) { - // distribute fees to market maker pools as a reward - (uint256 totalActivePools, - uint256 totalFeesCollected, - uint256 totalWeightedStake, - uint256 totalRewardsPaid, - uint256 initialContractBalance, - uint256 finalContractBalance) = _distributeFeesAmongMakerPools(); - emit RewardsPaid( - totalActivePools, - totalFeesCollected, - totalWeightedStake, - totalRewardsPaid, - initialContractBalance, - finalContractBalance - ); - - _goToNextEpoch(); + _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; } - /// @dev Returns the total amount of fees collected thus far, in the current epoch. - /// @return Amount of fees. - function getTotalProtocolFeesThisEpoch() + /// @dev Returns the amount of fees attributed to the input pool this epoch. + /// @param poolId Pool Id to query. + /// @return feesCollectedByPool Amount of fees collected by the pool this epoch. + function getProtocolFeesThisEpochByPool(bytes32 poolId) external view - returns (uint256) + returns (uint256 feesCollected) { - uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); - return address(this).balance.safeAdd(wethBalance); + uint256 currentEpoch = getCurrentEpoch(); + // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` + // because we only need to remember state in the current epoch and the + // epoch prior. + IStructs.ActivePool memory pool = activePoolsByEpoch[getCurrentEpoch() % 2][poolId]; + feesCollected = pool.feesCollected; } /// @dev Checks that the protocol fee passed into `payProtocolFee()` is valid. /// @param protocolFeePaid The `protocolFeePaid` parameter to `payProtocolFee.` - function _assertValidProtocolFee(uint256 protocolFeePaid) private view { + function _assertValidProtocolFee(uint256 protocolFeePaid) + private + view + { if (protocolFeePaid == 0 || (msg.value != protocolFeePaid && msg.value != 0)) { LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError( protocolFeePaid == 0 ? @@ -176,143 +168,4 @@ contract MixinExchangeFees is )); } } - - /// @dev Pays rewards to market making pools that were active this epoch. - /// Each pool receives a portion of the fees generated this epoch (see LibCobbDouglas) that is - /// proportional to (i) the fee volume attributed to their pool over the epoch, and - /// (ii) the amount of stake provided by the maker and their delegators. Rebates are paid - /// into the Reward Vault where they can be withdraw by makers and - /// the members of their pool. There will be a small amount of ETH leftover in this contract - /// after paying out the rebates; at present, this rolls over into the next epoch. Eventually, - /// we plan to deposit this leftover into a DAO managed by the 0x community. - /// @return totalActivePools Total active pools this epoch. - /// @return totalFeesCollected Total fees collected this epoch, across all active pools. - /// @return totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less. - /// @return totalRewardsPaid Total rewards paid out across all active pools. - /// @return initialContractBalance Balance of this contract before paying rewards. - /// @return finalContractBalance Balance of this contract after paying rewards. - function _distributeFeesAmongMakerPools() - internal - returns ( - uint256 totalActivePools, - uint256 totalFeesCollected, - uint256 totalWeightedStake, - uint256 totalRewardsPaid, - uint256 initialContractBalance, - uint256 finalContractBalance - ) - { - // step 1/4 - withdraw the entire wrapped ether balance into this contract. WETH - // is unwrapped here to keep `payProtocolFee()` calls relatively cheap, - // and WETH is only withdrawn if this contract's WETH balance is nonzero. - _unwrapWETH(); - - // Initialize initial values - totalActivePools = activePoolsThisEpoch.length; - totalFeesCollected = 0; - totalWeightedStake = 0; - totalRewardsPaid = 0; - initialContractBalance = address(this).balance; - finalContractBalance = initialContractBalance; - - // sanity check - is there a balance to payout and were there any active pools? - if (initialContractBalance == 0 || totalActivePools == 0) { - return ( - totalActivePools, - totalFeesCollected, - totalWeightedStake, - totalRewardsPaid, - initialContractBalance, - finalContractBalance - ); - } - - // step 2/4 - compute stats for active maker pools - IStructs.ActivePool[] memory activePools = new IStructs.ActivePool[](totalActivePools); - uint32 delegatedStakeWeight = rewardDelegatedStakeWeight; - for (uint256 i = 0; i != totalActivePools; i++) { - bytes32 poolId = activePoolsThisEpoch[i]; - - // compute weighted stake - uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; - uint256 stakeHeldByPoolOperator = getStakeDelegatedToPoolByOwner(poolById[poolId].operator, poolId).currentEpochBalance; - uint256 weightedStake = stakeHeldByPoolOperator.safeAdd( - totalStakeDelegatedToPool - .safeSub(stakeHeldByPoolOperator) - .safeMul(delegatedStakeWeight) - .safeDiv(PPM_DENOMINATOR) - ); - - // store pool stats - activePools[i].poolId = poolId; - activePools[i].feesCollected = protocolFeesThisEpochByPool[poolId]; - activePools[i].weightedStake = weightedStake; - activePools[i].delegatedStake = totalStakeDelegatedToPool; - - // update cumulative amounts - totalFeesCollected = totalFeesCollected.safeAdd(activePools[i].feesCollected); - totalWeightedStake = totalWeightedStake.safeAdd(activePools[i].weightedStake); - } - - // sanity check - this is a gas optimization that can be used because we assume a non-zero - // split between stake and fees generated in the cobb-douglas computation (see below). - if (totalFeesCollected == 0) { - return ( - totalActivePools, - totalFeesCollected, - totalWeightedStake, - totalRewardsPaid, - initialContractBalance, - finalContractBalance - ); - } - - // step 3/4 - record reward for each pool - for (uint256 i = 0; i != totalActivePools; i++) { - // compute reward using cobb-douglas formula - uint256 reward = LibCobbDouglas.cobbDouglas( - initialContractBalance, - activePools[i].feesCollected, - totalFeesCollected, - totalWeightedStake != 0 ? activePools[i].weightedStake : 1, // only rewards are accounted for if no one has staked - totalWeightedStake != 0 ? totalWeightedStake : 1, // this is to avoid divide-by-zero in cobb douglas - cobbDouglasAlphaNumerator, - cobbDouglasAlphaDenominator - ); - - // pay reward to pool - _handleStakingPoolReward( - activePools[i].poolId, - reward, - activePools[i].delegatedStake, - currentEpoch - ); - - // clear state for gas refunds - protocolFeesThisEpochByPool[activePools[i].poolId] = 0; - activePoolsThisEpoch[i] = 0; - } - activePoolsThisEpoch.length = 0; - - // step 4/4 send total payout to vault - - // Sanity check rewards calculation - if (totalRewardsPaid > initialContractBalance) { - LibRichErrors.rrevert(LibStakingRichErrors.MiscalculatedRewardsError( - totalRewardsPaid, - initialContractBalance - )); - } - - finalContractBalance = address(this).balance; - - return ( - totalActivePools, - totalFeesCollected, - totalWeightedStake, - totalRewardsPaid, - initialContractBalance, - finalContractBalance - ); - } } diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index e56cd43d09..6422fca569 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -117,6 +117,8 @@ contract MixinStorage is // Rebate Vault (stores rewards for pools before they are moved to the eth vault on a per-user basis) IStakingPoolRewardVault public rewardVault; + /* Tweakable parameters */ + // Minimum seconds between epochs. uint256 public epochDurationInSeconds; @@ -139,26 +141,34 @@ contract MixinStorage is /// @dev The total fees collected in the current epoch, built up iteratively /// in `payProtocolFee()`. - uint256 totalFeesCollectedThisEpoch; + uint256 internal totalFeesCollectedThisEpoch; + /// @dev The total weighted stake in the current epoch, built up iteratively /// in `payProtocolFee()`. - uint256 totalWeightedStakeThisEpoch; + uint256 internal totalWeightedStakeThisEpoch; + /// @dev State information for each active pool in an epoch. /// In practice, we only store state for `currentEpoch % 2`. - mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) activePoolsByEpoch; + mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) internal activePoolsByEpoch; + /// @dev Number of pools activated in the current epoch. - uint256 numActivePoolsThisEpoch; + uint256 internal numActivePoolsThisEpoch; + /// @dev Rewards (ETH) available to the epoch being finalized (the previous /// epoch). This is simply the balance of the contract at the end of /// the epoch. - uint256 unfinalizedRewardsAvailable; + uint256 internal unfinalizedRewardsAvailable; + /// @dev The number of active pools in the last epoch that have yet to be /// finalized through `finalizePools()`. - uint256 unfinalizedPoolsRemaining; + uint256 internal unfinalizedPoolsRemaining; + /// @dev The total fees collected for the epoch being finalized. - uint256 unfinalizedTotalFeesCollected; + uint256 internal unfinalizedTotalFeesCollected; + /// @dev The total fees collected for the epoch being finalized. - uint256 unfinalizedTotalWeightedStake; + uint256 internal unfinalizedTotalWeightedStake; + /// @dev How many rewards were paid at the end of finalization. uint256 totalRewardsPaidLastEpoch; diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index d3d0bd4cdd..b6b321f23e 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -62,8 +62,8 @@ interface IStakingEvents { uint256 epoch, uint256 numActivePools, uint256 rewardsAvailable, - uint256 totalWeightedStake, - uint256 totalFeesCollected + uint256 totalFeesCollected, + uint256 totalWeightedStake ); /// @dev Emitted by MixinFinalizer when an epoch is fully finalized. @@ -81,9 +81,9 @@ interface IStakingEvents { /// @param poolId The pool's ID. /// @param reward Amount of reward paid. event RewardsPaid( - uint255 epoch, + uint256 epoch, bytes32 poolId, - uint255 reward + uint256 reward ); /// @dev Emitted whenever staking parameters are changed via the `setParams()` function. diff --git a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol index 15d83264b2..fcb5972d18 100644 --- a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol +++ b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol @@ -162,6 +162,10 @@ library LibStakingRichErrors { bytes4 internal constant CUMULATIVE_REWARD_INTERVAL_ERROR_SELECTOR = 0x1f806d55; + // bytes4(keccak256("PreviousEpochNotFinalizedError(uint256,uint256)")) + bytes4 internal constant PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR = + 0x614b800a; + // solhint-disable func-name-mixedcase function MiscalculatedRewardsError( uint256 totalRewardsPaid, @@ -491,4 +495,19 @@ library LibStakingRichErrors { endEpoch ); } + + function PreviousEpochNotFinalizedError( + uint256 unfinalizedEpoch, + uint256 unfinalizedPoolsRemaining + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR, + unfinalizedEpoch, + unfinalizedPoolsRemaining + ); + } } diff --git a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol similarity index 88% rename from contracts/staking/contracts/src/finalization/MixinFinalizer.sol rename to contracts/staking/contracts/src/sys/MixinFinalizer.sol index b5cb4490a6..1cba506574 100644 --- a/contracts/staking/contracts/src/finalization/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -19,19 +19,20 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; import "../libs/LibFixedMath.sol"; import "../immutable/MixinStorage.sol"; import "../immutable/MixinConstants.sol"; +import "../immutable/MixinDeploymentConstants.sol"; import "../interfaces/IStakingEvents.sol"; import "../interfaces/IStructs.sol"; import "../stake/MixinStakeBalances.sol"; -import "../sys/MixinScheduler.sol"; import "../staking_pools/MixinStakingPool.sol"; import "../staking_pools/MixinStakingPoolRewardVault.sol"; -import "./MixinExchangeManager.sol"; +import "./MixinScheduler.sol"; /// @dev This mixin contains functions related to finalizing epochs. @@ -42,6 +43,7 @@ import "./MixinExchangeManager.sol"; contract MixinFinalizer is IStakingEvents, MixinConstants, + MixinDeploymentConstants, Ownable, MixinStorage, MixinStakingPoolRewardVault, @@ -65,7 +67,7 @@ contract MixinFinalizer is uint256 closingEpoch = getCurrentEpoch(); // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { - LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalized( + LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalizedError( closingEpoch.safeSub(1), unfinalizedPoolsRemaining )); @@ -75,20 +77,20 @@ contract MixinFinalizer is // Populate finalization state. unfinalizedPoolsRemaining = numActivePoolsThisEpoch; unfinalizedRewardsAvailable = address(this).balance; - unfinalizedTotalFeesCollected = totalFeesCollected; - unfinalizedTotalWeightedStake = totalWeightedStake; - totalRewardsPaid = 0; + unfinalizedTotalFeesCollected = totalFeesCollectedThisEpoch; + unfinalizedTotalWeightedStake = totalWeightedStakeThisEpoch; + totalRewardsPaidLastEpoch = 0; // Emit an event. emit EpochEnded( closingEpoch, numActivePoolsThisEpoch, - rewardsAvailable, - totalWeightedStake, - totalFeesCollected + unfinalizedRewardsAvailable, + unfinalizedTotalFeesCollected, + unfinalizedTotalWeightedStake ); // Reset current epoch state. - totalFeesCollected = 0; - totalWeightedStake = 0; + totalFeesCollectedThisEpoch = 0; + totalWeightedStakeThisEpoch = 0; numActivePoolsThisEpoch = 0; // Advance the epoch. This will revert if not enough time has passed. _goToNextEpoch(); @@ -109,18 +111,19 @@ contract MixinFinalizer is /// @param poolIds List of active pool IDs to finalize. /// @return rewardsPaid Total rewards paid to the pools passed in. /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. - function finalizePools(bytes32[] memory poolIds) + function finalizePools(bytes32[] calldata poolIds) external returns (uint256 rewardsPaid, uint256 _unfinalizedPoolsRemaining) { - uint256 epoch = getCurrentEpoch().safeSub(1); + uint256 epoch = getCurrentEpoch(); + uint256 priorEpoch = epoch.safeSub(1); uint256 poolsRemaining = unfinalizedPoolsRemaining; uint256 numPoolIds = poolIds.length; uint256 rewardsPaid = 0; // Pointer to the active pools in the last epoch. // We use `(currentEpoch - 1) % 2` as the index to reuse state. mapping(bytes32 => IStructs.ActivePool) storage activePools = - activePoolsByEpoch[epoch % 2]; + activePoolsByEpoch[priorEpoch % 2]; for (uint256 i = 0; i < numPoolIds && poolsRemaining != 0; i++) { bytes32 poolId = poolIds[i]; IStructs.ActivePool memory pool = activePools[poolId]; @@ -128,7 +131,10 @@ contract MixinFinalizer is if (pool.feesCollected != 0) { // Credit the pool with rewards. // We will transfer the total rewards to the vault at the end. - rewardsPaid = rewardsPaid.safeAdd(_creditRewardsToPool(poolId, pool)); + // Note that we credit the pool the rewards at the current epoch + // even though they were earned in the prior epoch. + uint256 reward = _creditRewardToPool(epoch, poolId, pool); + rewardsPaid = rewardsPaid.safeAdd(reward); // Clear the pool state so we don't finalize it again, // and to recoup some gas. activePools[poolId] = IStructs.ActivePool(0, 0, 0); @@ -147,62 +153,13 @@ contract MixinFinalizer is // finalized. if (poolsRemaining == 0) { emit EpochFinalized( - epoch, + priorEpoch, totalRewardsPaidLastEpoch, unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) ); } } - /// @dev Computes the rewards owned for a pool during finalization and - /// credits it in the RewardVault. - /// @param The epoch being finalized. - /// @param poolId The pool's ID. - /// @param pool The pool. - /// @return rewards Amount of rewards for this pool. - function _creditRewardsToPool( - uint256 epoch, - bytes32 poolId, - IStructs.ActivePool memory pool - ) - internal - returns (uint256 rewards) - { - // Use the cobb-douglas function to compute the reward. - reward = _cobbDouglas( - unfinalizedRewardsAvailable, - pool.feesCollected, - unfinalizedTotalFeesCollected, - pool.weightedStake, - unfinalizedTotalWeightedStake, - cobbDouglasAlphaNumerator, - cobbDouglasAlphaDenomintor - ); - // Credit the pool the reward in the RewardVault. - (, uint256 membersPortionOfReward) = rewardVault.recordDepositFor( - poolId, - reward, - // If no delegated stake, all rewards go to the operator. - pool.delegatedStake == 0 - ); - // Sync delegator rewards. - if (membersPortionOfReward != 0) { - _recordRewardForDelegators( - poolId, - membersPortionOfReward, - pool.delegatedStake - ); - } - } - - /// @dev Converts the entire WETH balance of the contract into ETH. - function _unwrapWETH() private { - uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); - if (wethBalance != 0) { - IEtherToken(WETH_ADDRESS).withdraw(wethBalance); - } - } - /// @dev The cobb-douglas function used to compute fee-based rewards for /// staking pools in a given epoch. Note that in this function there /// is no limitation on alpha; we tend to get better rounding on the @@ -267,4 +224,54 @@ contract MixinFinalizer is // Multiply the above with totalRewards. ownerRewards = LibFixedMath._uintMul(n, totalRewards); } + + /// @dev Computes the reward owed to a pool during finalization and + /// credits it in the RewardVault. + /// @param epoch The epoch being finalized. + /// @param poolId The pool's ID. + /// @param pool The pool. + /// @return rewards Amount of rewards for this pool. + function _creditRewardToPool( + uint256 epoch, + bytes32 poolId, + IStructs.ActivePool memory pool + ) + private + returns (uint256 reward) + { + // Use the cobb-douglas function to compute the reward. + reward = _cobbDouglas( + unfinalizedRewardsAvailable, + pool.feesCollected, + unfinalizedTotalFeesCollected, + pool.weightedStake, + unfinalizedTotalWeightedStake, + cobbDouglasAlphaNumerator, + cobbDouglasAlphaDenomintor + ); + // Credit the pool the reward in the RewardVault. + (, uint256 membersPortionOfReward) = rewardVault.recordDepositFor( + poolId, + reward, + // If no delegated stake, all rewards go to the operator. + pool.delegatedStake == 0 + ); + // Sync delegator rewards. + if (membersPortionOfReward != 0) { + _recordRewardForDelegators( + poolId, + membersPortionOfReward, + pool.delegatedStake + ); + } + } + + /// @dev Converts the entire WETH balance of the contract into ETH. + function _unwrapWETH() private { + uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); + if (wethBalance != 0) { + IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + } + } + } diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index 9469f275d1..5f1899a733 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -88,12 +88,6 @@ contract TestStorageLayout is if sub(currentEpochStartTimeInSeconds_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) - if sub(protocolFeesThisEpochByPool_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - - if sub(activePoolsThisEpoch_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - if sub(_cumulativeRewardsByPool_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) @@ -129,6 +123,33 @@ contract TestStorageLayout is if sub(cobbDouglasAlphaDenominator_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) + + if sub(totalFeesCollectedThisEpoch_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(totalWeightedStakeThisEpoch_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(activePoolsByEpoch_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(numActivePoolsThisEpoch_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(unfinalizedRewardsAvailable_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(unfinalizedPoolsRemaining_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(unfinalizedTotalFeesCollected_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(unfinalizedTotalWeightedStake_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) + + if sub(totalRewardsPaidLastEpoch_slot, slot) { revertIncorrectStorageSlot() } + slot := add(slot, 1) } } } diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 247f09fdb4..cc9750e2c4 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -27,6 +27,7 @@ import * as MixinCumulativeRewards from '../generated-artifacts/MixinCumulativeR import * as MixinDeploymentConstants from '../generated-artifacts/MixinDeploymentConstants.json'; import * as MixinExchangeFees from '../generated-artifacts/MixinExchangeFees.json'; import * as MixinExchangeManager from '../generated-artifacts/MixinExchangeManager.json'; +import * as MixinFinalizer from '../generated-artifacts/MixinFinalizer.json'; import * as MixinParams from '../generated-artifacts/MixinParams.json'; import * as MixinScheduler from '../generated-artifacts/MixinScheduler.json'; import * as MixinStake from '../generated-artifacts/MixinStake.json'; @@ -63,6 +64,7 @@ export const artifacts = { StakingProxy: StakingProxy as ContractArtifact, MixinExchangeFees: MixinExchangeFees as ContractArtifact, MixinExchangeManager: MixinExchangeManager as ContractArtifact, + MixinFinalizer: MixinFinalizer as ContractArtifact, MixinConstants: MixinConstants as ContractArtifact, MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact, MixinStorage: MixinStorage as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index b0cadd3c4c..803cfa3c7b 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -25,6 +25,7 @@ export * from '../generated-wrappers/mixin_cumulative_rewards'; export * from '../generated-wrappers/mixin_deployment_constants'; export * from '../generated-wrappers/mixin_exchange_fees'; export * from '../generated-wrappers/mixin_exchange_manager'; +export * from '../generated-wrappers/mixin_finalizer'; export * from '../generated-wrappers/mixin_params'; export * from '../generated-wrappers/mixin_scheduler'; export * from '../generated-wrappers/mixin_stake'; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 09286c55b8..2ee3e56acb 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -25,6 +25,7 @@ "generated-artifacts/MixinDeploymentConstants.json", "generated-artifacts/MixinExchangeFees.json", "generated-artifacts/MixinExchangeManager.json", + "generated-artifacts/MixinFinalizer.json", "generated-artifacts/MixinParams.json", "generated-artifacts/MixinScheduler.json", "generated-artifacts/MixinStake.json", From 712b2569e6a64fcb84a9e805f6114e68ee493281 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 12 Sep 2019 17:40:20 -0400 Subject: [PATCH 08/52] `@0x/contracts-staking`: Fix solidity linter errors. --- .../staking/contracts/src/fees/MixinExchangeFees.sol | 1 - contracts/staking/contracts/src/sys/MixinFinalizer.sol | 8 ++------ contracts/staking/contracts/src/sys/MixinScheduler.sol | 3 --- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 2804cb3d1a..5febbc73bd 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -144,7 +144,6 @@ contract MixinExchangeFees is view returns (uint256 feesCollected) { - uint256 currentEpoch = getCurrentEpoch(); // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` // because we only need to remember state in the current epoch and the // epoch prior. diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 1cba506574..367e70e987 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -131,9 +131,7 @@ contract MixinFinalizer is if (pool.feesCollected != 0) { // Credit the pool with rewards. // We will transfer the total rewards to the vault at the end. - // Note that we credit the pool the rewards at the current epoch - // even though they were earned in the prior epoch. - uint256 reward = _creditRewardToPool(epoch, poolId, pool); + uint256 reward = _creditRewardToPool(poolId, pool); rewardsPaid = rewardsPaid.safeAdd(reward); // Clear the pool state so we don't finalize it again, // and to recoup some gas. @@ -226,13 +224,11 @@ contract MixinFinalizer is } /// @dev Computes the reward owed to a pool during finalization and - /// credits it in the RewardVault. - /// @param epoch The epoch being finalized. + /// credits it to that pool for the CURRENT epoch. /// @param poolId The pool's ID. /// @param pool The pool. /// @return rewards Amount of rewards for this pool. function _creditRewardToPool( - uint256 epoch, bytes32 poolId, IStructs.ActivePool memory pool ) diff --git a/contracts/staking/contracts/src/sys/MixinScheduler.sol b/contracts/staking/contracts/src/sys/MixinScheduler.sol index a711c80d7c..a512fc8527 100644 --- a/contracts/staking/contracts/src/sys/MixinScheduler.sol +++ b/contracts/staking/contracts/src/sys/MixinScheduler.sol @@ -84,9 +84,6 @@ contract MixinScheduler is uint256 nextEpoch = currentEpoch.safeAdd(1); currentEpoch = nextEpoch; currentEpochStartTimeInSeconds = currentBlockTimestamp; - uint256 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds.safeAdd( - epochDurationInSeconds - ); } /// @dev Assert scheduler state before initializing it. From 06b4d241af8ab7b3ca5b4f4c889967a9dd4e7236 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 04:11:59 -0400 Subject: [PATCH 09/52] `@0x/contracts-staking`: Working on MBF v2 --- .../contracts/src/fees/MixinExchangeFees.sol | 44 ++- .../contracts/src/immutable/MixinStorage.sol | 12 +- .../contracts/src/interfaces/IStructs.sol | 12 +- .../src/stake/MixinStakeBalances.sol | 21 +- .../staking_pools/MixinStakingPoolRewards.sol | 41 ++- .../contracts/src/sys/MixinAbstract.sol | 73 +++++ .../contracts/src/sys/MixinFinalizer.sol | 292 ++++++++++++------ .../src/vaults/StakingPoolRewardVault.sol | 2 +- contracts/staking/src/artifacts.ts | 4 +- contracts/staking/src/wrappers.ts | 1 + contracts/staking/tsconfig.json | 1 + .../utils/contracts/src/LibFractions.sol | 15 +- 12 files changed, 381 insertions(+), 137 deletions(-) create mode 100644 contracts/staking/contracts/src/sys/MixinAbstract.sol diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 5febbc73bd..5e714c6966 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -25,28 +25,35 @@ import "../libs/LibStakingRichErrors.sol"; import "../libs/LibCobbDouglas.sol"; import "../immutable/MixinDeploymentConstants.sol"; import "../interfaces/IStructs.sol"; +import "../stake/MixinStakeBalances.sol"; +import "../sys/MixinAbstract.sol"; import "../staking_pools/MixinStakingPool.sol"; import "./MixinExchangeManager.sol"; /// @dev This mixin contains the logic for 0x protocol fees. -/// Protocol fees are sent by 0x exchanges every time there is a trade. -/// If the maker has associated their address with a pool (see MixinStakingPool.sol), then -/// the fee will be attributed to their pool. At the end of an epoch the maker and -/// their pool will receive a rebate that is proportional to (i) the fee volume attributed -/// to their pool over the epoch, and (ii) the amount of stake provided by the maker and -/// their delegators. Note that delegated stake (see MixinStake) is weighted less than -/// stake provided by directly by the maker; this is a disincentive for market makers to -/// monopolize a single pool that they all delegate to. +/// Protocol fees are sent by 0x exchanges every time there is a trade. +/// If the maker has associated their address with a pool (see +/// MixinStakingPool.sol), then the fee will be attributed to their pool. +/// At the end of an epoch the maker and their pool will receive a rebate +/// that is proportional to (i) the fee volume attributed to their pool +/// over the epoch, and (ii) the amount of stake provided by the maker and +/// their delegators. Note that delegated stake (see MixinStake) is +/// weighted less than stake provided by directly by the maker; this is a +/// disincentive for market makers to monopolize a single pool that they +/// all delegate to. contract MixinExchangeFees is MixinDeploymentConstants, MixinExchangeManager, + MixinAbstract, + MixinStakeBalances, MixinStakingPool { using LibSafeMath for uint256; /// @dev Pays a protocol fee in ETH or WETH. - /// Only a known 0x exchange can call this method. See (MixinExchangeManager). + /// Only a known 0x exchange can call this method. See + /// (MixinExchangeManager). /// @param makerAddress The address of the order's maker. /// @param payerAddress The address of the protocol fee payer. /// @param protocolFeePaid The protocol fee that should be paid. @@ -80,18 +87,19 @@ contract MixinExchangeFees is if (poolId == NIL_POOL_ID) { return; } + uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; // Ignore pools with dust stake. if (poolStake < minimumPoolStake) { return; } - // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` - // because we only need to remember state in the current epoch and the - // epoch prior. + + // Look up the pool for this epoch. uint256 currentEpoch = getCurrentEpoch(); mapping (bytes32 => IStructs.ActivePool) storage activePoolsThisEpoch = - activePoolsByEpoch[currentEpoch % 2]; + _getActivePoolsFromEpoch(currentEpoch); IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId]; + // If the pool was previously inactive in this epoch, initialize it. if (pool.feesCollected == 0) { // Compute weighted stake. @@ -105,23 +113,30 @@ contract MixinExchangeFees is .safeMul(rewardDelegatedStakeWeight) .safeDiv(PPM_DENOMINATOR) ); + // Compute delegated (non-operator) stake. pool.delegatedStake = poolStake.safeSub(operatorStake); + // Increase the total weighted stake. totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd( pool.weightedStake ); + // Increase the numberof active pools. numActivePoolsThisEpoch += 1; + // Emit an event so keepers know what pools to pass into `finalize()`. emit StakingPoolActivated(currentEpoch, poolId); } + // Credit the fees to the pool. pool.feesCollected = protocolFeePaid; + // Increase the total fees collected this epoch. totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd( protocolFeePaid ); + // Store the pool. activePoolsThisEpoch[poolId] = pool; } @@ -147,7 +162,8 @@ contract MixinExchangeFees is // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` // because we only need to remember state in the current epoch and the // epoch prior. - IStructs.ActivePool memory pool = activePoolsByEpoch[getCurrentEpoch() % 2][poolId]; + IStructs.ActivePool memory pool = + _getActivePoolFromEpoch(getCurrentEpoch(), poolId); feesCollected = pool.feesCollected; } diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 6422fca569..95d9978521 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -55,19 +55,19 @@ contract MixinStorage is // (access using _loadAndSyncBalance or _loadUnsyncedBalance) mapping (address => IStructs.StoredBalance) internal _activeStakeByOwner; - // mapping from Owner to Amount of Inactive Stake + // Mapping from Owner to Amount of Inactive Stake // (access using _loadAndSyncBalance or _loadUnsyncedBalance) mapping (address => IStructs.StoredBalance) internal _inactiveStakeByOwner; - // mapping from Owner to Amount Delegated + // Mapping from Owner to Amount Delegated // (access using _loadAndSyncBalance or _loadUnsyncedBalance) mapping (address => IStructs.StoredBalance) internal _delegatedStakeByOwner; - // mapping from Owner to Pool Id to Amount Delegated + // Mapping from Owner to Pool Id to Amount Delegated // (access using _loadAndSyncBalance or _loadUnsyncedBalance) mapping (address => mapping (bytes32 => IStructs.StoredBalance)) internal _delegatedStakeToPoolByOwner; - // mapping from Pool Id to Amount Delegated + // Mapping from Pool Id to Amount Delegated // (access using _loadAndSyncBalance or _loadUnsyncedBalance) mapping (bytes32 => IStructs.StoredBalance) internal _delegatedStakeByPoolId; @@ -149,7 +149,9 @@ contract MixinStorage is /// @dev State information for each active pool in an epoch. /// In practice, we only store state for `currentEpoch % 2`. - mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) internal activePoolsByEpoch; + mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) + internal + activePoolsByEpoch; /// @dev Number of pools activated in the current epoch. uint256 internal numActivePoolsThisEpoch; diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index ca7421c8cd..69ba1291d2 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -32,6 +32,16 @@ interface IStructs { uint256 delegatedStake; } + /// @dev Rewards credited to a pool during finalization. + /// @param operatorReward The amount of reward credited to the pool operator. + /// @param membersReward The amount of reward credited to the pool members. + /// @param membersStake The amount of members/delegated stake in the pool. + struct PoolRewards { + uint256 operatorReward; + uint256 membersReward; + uint256 membersStake; + } + /// @dev Encapsulates a balance for the current and next epochs. /// Note that these balances may be stale if the current epoch /// is greater than `currentEpoch`. @@ -40,13 +50,11 @@ interface IStructs { /// @param currentEpoch the current epoch /// @param currentEpochBalance balance in the current epoch. /// @param nextEpochBalance balance in `currentEpoch+1`. - /// @param prevEpochBalance balance in `currentEpoch-1`. struct StoredBalance { bool isInitialized; uint32 currentEpoch; uint96 currentEpochBalance; uint96 nextEpochBalance; - uint96 prevEpochBalance; } /// @dev Balance struct for stake. diff --git a/contracts/staking/contracts/src/stake/MixinStakeBalances.sol b/contracts/staking/contracts/src/stake/MixinStakeBalances.sol index 92dc3106f6..d8003977e6 100644 --- a/contracts/staking/contracts/src/stake/MixinStakeBalances.sol +++ b/contracts/staking/contracts/src/stake/MixinStakeBalances.sol @@ -162,7 +162,8 @@ contract MixinStakeBalances is }); } - /// @dev Returns the total stake delegated to a specific staking pool, across all members. + /// @dev Returns the total stake delegated to a specific staking pool, + /// across all members. /// @param poolId Unique Id of pool. /// @return Total stake delegated to pool. function getTotalStakeDelegatedToPool(bytes32 poolId) @@ -179,9 +180,13 @@ contract MixinStakeBalances is /// @dev Returns the stake that can be withdrawn for a given owner. /// @param owner to query. - /// @param lastStoredWithdrawableStake The amount of withdrawable stake that was last stored. + /// @param lastStoredWithdrawableStake The amount of withdrawable stake + /// that was last stored. /// @return Withdrawable stake for owner. - function _computeWithdrawableStake(address owner, uint256 lastStoredWithdrawableStake) + function _computeWithdrawableStake( + address owner, + uint256 lastStoredWithdrawableStake + ) internal view returns (uint256) @@ -190,9 +195,15 @@ contract MixinStakeBalances is // so the upper bound of withdrawable stake is always limited by the value of `next`. IStructs.StoredBalance memory storedBalance = _loadUnsyncedBalance(_inactiveStakeByOwner[owner]); if (storedBalance.currentEpoch == currentEpoch) { - return LibSafeMath.min256(storedBalance.nextEpochBalance, lastStoredWithdrawableStake); + return LibSafeMath.min256( + storedBalance.nextEpochBalance, + lastStoredWithdrawableStake + ); } else if (uint256(storedBalance.currentEpoch).safeAdd(1) == currentEpoch) { - return LibSafeMath.min256(storedBalance.nextEpochBalance, storedBalance.currentEpochBalance); + return LibSafeMath.min256( + storedBalance.nextEpochBalance, + storedBalance.currentEpochBalance + ); } else { return storedBalance.nextEpochBalance; } diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 65b6630163..27eb1b9616 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -23,10 +23,12 @@ import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-utils/contracts/src/LibFractions.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "./MixinCumulativeRewards.sol"; +import "../sys/MixinAbstract.sol"; contract MixinStakingPoolRewards is - MixinCumulativeRewards + MixinCumulativeRewards, + MixinAbstract { using LibSafeMath for uint256; @@ -59,7 +61,35 @@ contract MixinStakingPoolRewards is function computeRewardBalanceOfDelegator(bytes32 poolId, address member) public view - returns (uint256 totalReward) + returns (uint256 reward) + { + IStructs.PoolRewards memory unfinalizedPoolReward = + _getUnfinalizedPoolReward(poolId); + reward = _computeRewardBalanceOfDelegator( + poolId, + member, + unfinalizedPoolReward.membersReward, + unfinalizedPoolReward.membersStake + ); + } + + /// @dev Computes the reward balance in ETH of a specific member of a pool. + /// @param poolId Unique id of pool. + /// @param member The member of the pool. + /// @param unfinalizedMembersReward Unfinalized memeber reward for + /// this pool in the current epoch. + /// @param unfinalizedDelegatedStake Unfinalized total delegated stake for + /// this pool in the current epoch. + /// @return totalReward Balance in ETH. + function _computeRewardBalanceOfDelegator( + bytes32 poolId, + address member, + uint256 unfinalizedMembersReward, + uint256 unfinalizedDelegatedStake + ) + internal + view + returns (uint256 reward) { return _computeRewardBalanceOfDelegator( poolId, @@ -144,12 +174,12 @@ contract MixinStakingPoolRewards is // cache a storage pointer to the cumulative rewards for `poolId` indexed by epoch. mapping (uint256 => IStructs.Fraction) storage _cumulativeRewardsByPoolPtr = _cumulativeRewardsByPool[poolId]; - // fetch the last epoch at which we stored an entry for this pool; + // 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]; - // compute new cumulative reward + // Compute new cumulative reward (uint256 numerator, uint256 denominator) = LibFractions.addFractions( mostRecentCumulativeRewards.numerator, mostRecentCumulativeRewards.denominator, @@ -157,7 +187,8 @@ contract MixinStakingPoolRewards is amountOfDelegatedStake ); - // normalize fraction components by dividing by the min token value (10^18) + // Normalize fraction components by dividing by the min token value + // (10^18) (uint256 numeratorNormalized, uint256 denominatorNormalized) = ( numerator.safeDiv(MIN_TOKEN_VALUE), denominator.safeDiv(MIN_TOKEN_VALUE) diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol new file mode 100644 index 0000000000..0d5b3f6fce --- /dev/null +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -0,0 +1,73 @@ +/* + + 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 "../interfaces/IStructs.sol"; + + +/// @dev Exposes some internal functions from various contracts to avoid +/// cyclical dependencies. +contract MixinAbstract { + + /// @dev Computes the reward owed to a pool during finalization. + /// Does nothing if the pool is already finalized. + /// @param poolId The pool's ID. + /// @return rewards Amount of rewards for this pool. + function _getUnfinalizedPoolReward(bytes32 poolId) + internal + view + returns (IStructs.PoolRewards memory rewards); + + /// @dev Get an active pool from an epoch by its ID. + /// @param epoch The epoch the pool was/will be active in. + /// @param poolId The ID of the pool. + /// @return pool The pool with ID `poolId` that was active in `epoch`. + function _getActivePoolFromEpoch( + uint256 epoch, + bytes32 poolId + ) + internal + view + returns (IStructs.ActivePool memory pool); + + /// @dev Get a mapping of active pools from an epoch. + /// This uses the formula `epoch % 2` as the epoch index in order + /// to reuse state, because we only need to remember, at most, two + /// epochs at once. + /// @return activePools The pools that were active in `epoch`. + function _getActivePoolsFromEpoch( + uint256 epoch + ) + internal + view + returns (mapping (bytes32 => IStructs.ActivePool) storage activePools); + + /// @dev Instantly finalizes a single pool that was active in the previous + /// epoch, crediting it rewards and sending those rewards to the reward + /// vault. This can be called by internal functions that need to + /// finalize a pool immediately. Does nothing if the pool is already + /// finalized. + /// @param poolId The pool ID to finalize. + /// @return rewards Rewards. + /// @return rewards The rewards credited to the pool. + function _finalizePool(bytes32 poolIds) + internal + returns (IStructs.PoolRewards memory rewards); +} diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 367e70e987..d44bcaff48 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -22,8 +22,8 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; +import "../libs/LibCobbDouglas.sol"; import "../libs/LibStakingRichErrors.sol"; -import "../libs/LibFixedMath.sol"; import "../immutable/MixinStorage.sol"; import "../immutable/MixinConstants.sol"; import "../immutable/MixinDeploymentConstants.sol"; @@ -44,7 +44,6 @@ contract MixinFinalizer is IStakingEvents, MixinConstants, MixinDeploymentConstants, - Ownable, MixinStorage, MixinStakingPoolRewardVault, MixinScheduler, @@ -65,21 +64,28 @@ contract MixinFinalizer is returns (uint256 _unfinalizedPoolsRemaining) { uint256 closingEpoch = getCurrentEpoch(); + // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { - LibRichErrors.rrevert(LibStakingRichErrors.PreviousEpochNotFinalizedError( - closingEpoch.safeSub(1), - unfinalizedPoolsRemaining - )); + LibRichErrors.rrevert( + LibStakingRichErrors.PreviousEpochNotFinalizedError( + closingEpoch.safeSub(1), + unfinalizedPoolsRemaining + ) + ); } + // Unrwap any WETH protocol fees. _unwrapWETH(); + // Populate finalization state. - unfinalizedPoolsRemaining = numActivePoolsThisEpoch; + unfinalizedPoolsRemaining = + _unfinalizedPoolsRemaining = numActivePoolsThisEpoch; unfinalizedRewardsAvailable = address(this).balance; unfinalizedTotalFeesCollected = totalFeesCollectedThisEpoch; unfinalizedTotalWeightedStake = totalWeightedStakeThisEpoch; totalRewardsPaidLastEpoch = 0; + // Emit an event. emit EpochEnded( closingEpoch, @@ -88,17 +94,19 @@ contract MixinFinalizer is unfinalizedTotalFeesCollected, unfinalizedTotalWeightedStake ); + // Reset current epoch state. totalFeesCollectedThisEpoch = 0; totalWeightedStakeThisEpoch = 0; numActivePoolsThisEpoch = 0; + // Advance the epoch. This will revert if not enough time has passed. _goToNextEpoch(); + // If there were no active pools, the epoch is already finalized. if (unfinalizedPoolsRemaining == 0) { emit EpochFinalized(closingEpoch, 0, unfinalizedRewardsAvailable); } - return _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; } /// @dev Finalizes pools that were active in the previous epoch, paying out @@ -109,44 +117,121 @@ contract MixinFinalizer is /// We deliberately try not to revert here in case multiple parties /// are finalizing pools. /// @param poolIds List of active pool IDs to finalize. - /// @return rewardsPaid Total rewards paid to the pools passed in. /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. function finalizePools(bytes32[] calldata poolIds) external - returns (uint256 rewardsPaid, uint256 _unfinalizedPoolsRemaining) + returns (_unfinalizedPoolsRemaining) { uint256 epoch = getCurrentEpoch(); - uint256 priorEpoch = epoch.safeSub(1); + // There are no pools to finalize at epoch 0. + if (epoch == 0) { + return; + } + uint256 poolsRemaining = unfinalizedPoolsRemaining; - uint256 numPoolIds = poolIds.length; - uint256 rewardsPaid = 0; + // If there are no more unfinalized pools remaining, there's nothing + // to do. + if (poolsRemaining == 0) { + return; + } + // Pointer to the active pools in the last epoch. - // We use `(currentEpoch - 1) % 2` as the index to reuse state. mapping(bytes32 => IStructs.ActivePool) storage activePools = - activePoolsByEpoch[priorEpoch % 2]; - for (uint256 i = 0; i < numPoolIds && poolsRemaining != 0; i++) { + _getActivePoolsFromEpoch(epoch - 1); + uint256 numPoolIds = poolIds.length; + uint256 rewardsPaid = 0; + + for (uint256 i = 0; i != numPoolIds && poolsRemaining != 0; ++i) { bytes32 poolId = poolIds[i]; IStructs.ActivePool memory pool = activePools[poolId]; + // Ignore pools that aren't active. - if (pool.feesCollected != 0) { - // Credit the pool with rewards. - // We will transfer the total rewards to the vault at the end. - uint256 reward = _creditRewardToPool(poolId, pool); - rewardsPaid = rewardsPaid.safeAdd(reward); - // Clear the pool state so we don't finalize it again, - // and to recoup some gas. - activePools[poolId] = IStructs.ActivePool(0, 0, 0); - // Decrease the number of unfinalized pools left. - poolsRemaining = poolsRemaining.safeSub(1); - // Emit an event. - emit RewardsPaid(epoch, poolId, reward); + if (pool.feesCollected == 0) { + continue; } + + // Clear the pool state so we don't finalize it again, and to + // recoup some gas. + delete activePools[poolId]; + + // Credit the pool with rewards. + // We will transfer the total rewards to the vault at the end. + uint256 reward = _creditRewardToPool(poolId, pool); + rewardsPaid = rewardsPaid.safeAdd(reward); + + // Decrease the number of unfinalized pools left. + poolsRemaining = poolsRemaining.safeSub(1); + + // Emit an event. + emit RewardsPaid(epoch, poolId, reward); + } + + // Deposit all the rewards at once into the RewardVault. + _depositIntoStakingPoolRewardVault(rewardsPaid); + + // Update finalization states. + totalRewardsPaidLastEpoch = + totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); + unfinalizedPoolsRemaining = _unfinalizedPoolsRemaining = poolsRemaining; + + // If there are no more unfinalized pools remaining, the epoch is + // finalized. + if (poolsRemaining == 0) { + emit EpochFinalized( + epoch - 1, + totalRewardsPaidLastEpoch, + unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) + ); + } + } + + /// @dev Instantly finalizes a single pool that was active in the previous + /// epoch, crediting it rewards and sending those rewards to the reward + /// vault. This can be called by internal functions that need to + /// finalize a pool immediately. Does nothing if the pool is already + /// finalized. + /// @param poolId The pool ID to finalize. + /// @return rewards Rewards. + /// @return rewards The rewards credited to the pool. + function _finalizePool(bytes32 poolId) + internal + returns (IStructs.PoolRewards memory rewards) + { + uint256 epoch = getCurrentEpoch(); + // There are no pools to finalize at epoch 0. + if (epoch == 0) { + return; + } + + // Get the active pool. + IStructs.ActivePool memory pool = + _getActivePoolFromEpoch(epoch - 1, poolId); + + // Ignore pools that weren't active. + if (pool.feesCollected == 0) { + return; } + + // Clear the pool state so we don't finalize it again, and to recoup + // some gas. + delete activePools[poolId]; + + // Credit the pool with rewards. + // We will transfer the total rewards to the vault at the end. + rewards = _creditRewardToPool(poolId, pool); + totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.safeAdd(reward); + + // Decrease the number of unfinalized pools left. + uint256 poolsRemaining = + unfinalizedPoolsRemaining = + unfinalizedPoolsRemaining.safeSub(1); + + // Emit an event. + emit RewardsPaid(epoch, poolId, reward); + // Deposit all the rewards at once into the RewardVault. _depositIntoStakingPoolRewardVault(rewardsPaid); - // Update finalization state. - totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); - _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining = poolsRemaining; + // If there are no more unfinalized pools remaining, the epoch is // finalized. if (poolsRemaining == 0) { @@ -158,69 +243,72 @@ contract MixinFinalizer is } } - /// @dev The cobb-douglas function used to compute fee-based rewards for - /// staking pools in a given epoch. Note that in this function there - /// is no limitation on alpha; we tend to get better rounding on the - /// simplified versions below. - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function. - /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. - /// @return ownerRewards Rewards for the owner. - function _cobbDouglas( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint256 alphaNumerator, - uint256 alphaDenominator + /// @dev Get an active pool from an epoch by its ID. + /// @param epoch The epoch the pool was/will be active in. + /// @param poolId The ID of the pool. + /// @return pool The pool with ID `poolId` that was active in `epoch`. + function _getActivePoolFromEpoch( + uint256 epoch, + bytes32 poolId ) internal - pure - returns (uint256 ownerRewards) + view + returns (IStructs.ActivePool memory pool) { - int256 feeRatio = LibFixedMath._toFixed(ownerFees, totalFees); - int256 stakeRatio = LibFixedMath._toFixed(ownerStake, totalStake); - if (feeRatio == 0 || stakeRatio == 0) { - return ownerRewards = 0; + pool = _getActivePoolFromEpoch(epoch)[poolId]; + } + + /// @dev Get a mapping of active pools from an epoch. + /// This uses the formula `epoch % 2` as the epoch index in order + /// to reuse state, because we only need to remember, at most, two + /// epochs at once. + /// @return activePools The pools that were active in `epoch`. + function _getActivePoolsFromEpoch( + uint256 epoch + ) + internal + view + returns (mapping (bytes32 => IStructs.ActivePool) storage activePools) + { + activePools = activePoolsByEpoch[epoch % 2]; + } + + /// @dev Computes the reward owed to a pool during finalization. + /// Does nothing if the pool is already finalized. + /// @param poolId The pool's ID. + /// @return rewards Amount of rewards for this pool. + function _getUnfinalizedPoolReward(bytes32 poolId) + internal + view + returns (IStructs.PoolRewards memory rewards) + { + uint256 epoch = getCurrentEpoch(); + // There can't be any rewards in the first epoch. + if (epoch == 0) { + return; } - // The cobb-doublas function has the form: - // `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)` - // This is equivalent to: - // `totalRewards * stakeRatio * e^(alpha * (ln(feeRatio / stakeRatio)))` - // However, because `ln(x)` has the domain of `0 < x < 1` - // and `exp(x)` has the domain of `x < 0`, - // and fixed-point math easily overflows with multiplication, - // we will choose the following if `stakeRatio > feeRatio`: - // `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))` - - // Compute - // `e^(alpha * (ln(feeRatio/stakeRatio)))` if feeRatio <= stakeRatio - // or - // `e^(ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio - int256 n = feeRatio <= stakeRatio ? - LibFixedMath._div(feeRatio, stakeRatio) : - LibFixedMath._div(stakeRatio, feeRatio); - n = LibFixedMath._exp( - LibFixedMath._mulDiv( - LibFixedMath._ln(n), - int256(alphaNumerator), - int256(alphaDenominator) - ) + + IStructs.ActivePool memory pool = + _getActivePoolFromEpoch(epoch - 1, poolId); + + // Use the cobb-douglas function to compute the total reward. + totalReward = LibCobbDouglas._cobbDouglas( + unfinalizedRewardsAvailable, + pool.feesCollected, + unfinalizedTotalFeesCollected, + pool.weightedStake, + unfinalizedTotalWeightedStake, + cobbDouglasAlphaNumerator, + cobbDouglasAlphaDenomintor ); - // Compute - // `totalRewards * n` if feeRatio <= stakeRatio - // or - // `totalRewards / n` if stakeRatio > feeRatio - n = feeRatio <= stakeRatio ? - LibFixedMath._mul(stakeRatio, n) : - LibFixedMath._div(stakeRatio, n); - // Multiply the above with totalRewards. - ownerRewards = LibFixedMath._uintMul(n, totalRewards); + + // Split the reward between the operator and delegators. + (rewards.operatorReward, rewards.membersReward) = + rewardVault.splitAmountBetweenOperatorAndMembers( + poolId, + totalReward + ); + rewards.delegatedStake = pool.delegatedStake; } /// @dev Computes the reward owed to a pool during finalization and @@ -233,10 +321,10 @@ contract MixinFinalizer is IStructs.ActivePool memory pool ) private - returns (uint256 reward) + returns (PoolRewards memory rewards) { - // Use the cobb-douglas function to compute the reward. - reward = _cobbDouglas( + // Use the cobb-douglas function to compute the total reward. + totalReward = LibCobbDouglas._cobbDouglas( unfinalizedRewardsAvailable, pool.feesCollected, unfinalizedTotalFeesCollected, @@ -245,13 +333,17 @@ contract MixinFinalizer is cobbDouglasAlphaNumerator, cobbDouglasAlphaDenomintor ); + // Credit the pool the reward in the RewardVault. - (, uint256 membersPortionOfReward) = rewardVault.recordDepositFor( - poolId, - reward, - // If no delegated stake, all rewards go to the operator. - pool.delegatedStake == 0 - ); + (rewards.operatorReward, rewards.membersReward) = + rewardVault.recordDepositFor( + poolId, + reward, + // If no delegated stake, all rewards go to the operator. + pool.delegatedStake == 0 + ); + rewards.delegatedStake = pool.delegatedStake; + // Sync delegator rewards. if (membersPortionOfReward != 0) { _recordRewardForDelegators( @@ -264,10 +356,10 @@ contract MixinFinalizer is /// @dev Converts the entire WETH balance of the contract into ETH. function _unwrapWETH() private { - uint256 wethBalance = IEtherToken(WETH_ADDRESS).balanceOf(address(this)); + uint256 wethBalance = IEtherToken(WETH_ADDRESS) + .balanceOf(address(this)); if (wethBalance != 0) { IEtherToken(WETH_ADDRESS).withdraw(wethBalance); } } - } diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 3fdcaa1e92..7eaf8cb9d2 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -81,7 +81,7 @@ contract StakingPoolRewardVault is function balanceOf(bytes32 poolId) external view - returns (uint256) + returns (uint256 balance) { return _balanceByPoolId[poolId]; } diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index cc9750e2c4..43b78853f4 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -22,6 +22,7 @@ import * as LibFixedMathRichErrors from '../generated-artifacts/LibFixedMathRich 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 MixinAbstract from '../generated-artifacts/MixinAbstract.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'; @@ -64,7 +65,6 @@ export const artifacts = { StakingProxy: StakingProxy as ContractArtifact, MixinExchangeFees: MixinExchangeFees as ContractArtifact, MixinExchangeManager: MixinExchangeManager as ContractArtifact, - MixinFinalizer: MixinFinalizer as ContractArtifact, MixinConstants: MixinConstants as ContractArtifact, MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact, MixinStorage: MixinStorage as ContractArtifact, @@ -92,6 +92,8 @@ export const artifacts = { MixinStakingPoolMakers: MixinStakingPoolMakers as ContractArtifact, MixinStakingPoolModifiers: MixinStakingPoolModifiers as ContractArtifact, MixinStakingPoolRewards: MixinStakingPoolRewards as ContractArtifact, + MixinAbstract: MixinAbstract as ContractArtifact, + MixinFinalizer: MixinFinalizer as ContractArtifact, MixinParams: MixinParams as ContractArtifact, MixinScheduler: MixinScheduler as ContractArtifact, EthVault: EthVault as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 803cfa3c7b..59a9e71b8f 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -20,6 +20,7 @@ export * from '../generated-wrappers/lib_fixed_math_rich_errors'; 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_abstract'; export * from '../generated-wrappers/mixin_constants'; export * from '../generated-wrappers/mixin_cumulative_rewards'; export * from '../generated-wrappers/mixin_deployment_constants'; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 2ee3e56acb..78deff5623 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -20,6 +20,7 @@ "generated-artifacts/LibProxy.json", "generated-artifacts/LibSafeDowncast.json", "generated-artifacts/LibStakingRichErrors.json", + "generated-artifacts/MixinAbstract.json", "generated-artifacts/MixinConstants.json", "generated-artifacts/MixinCumulativeRewards.json", "generated-artifacts/MixinDeploymentConstants.json", diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index caccd63591..98a894b7d3 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -27,12 +27,16 @@ library LibFractions { uint256 denominator ) { + if (n1 == 0) { + return (numerator = n2, denominator = d2); + } + if (n2 == 0) { + return (numerator = n1, denominator = d1); + } numerator = n1 .safeMul(d2) .safeAdd(n2.safeMul(d1)); - denominator = d1.safeMul(d2); - return (numerator, denominator); } /// @dev Safely scales the difference between two fractions. @@ -53,14 +57,17 @@ library LibFractions { pure returns (uint256 result) { + if (n2 == 0) { + return result = s + .safeMul(n1) + .safeDiv(d1); + } uint256 numerator = n1 .safeMul(d2) .safeSub(n2.safeMul(d1)); - uint256 tmp = numerator.safeDiv(d2); result = s .safeMul(tmp) .safeDiv(d1); - return result; } } From 102ca6b854a6dc85bfec9ac95181e098e2253efa Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 04:35:55 -0400 Subject: [PATCH 10/52] `@0x/contracts-staking`: Almost compiling... --- .../staking/contracts/src/ReadOnlyProxy.sol | 2 + contracts/staking/contracts/src/Staking.sol | 2 +- .../contracts/src/fees/MixinExchangeFees.sol | 1 + .../src/interfaces/IStakingEvents.sol | 6 +- .../contracts/src/sys/MixinFinalizer.sol | 84 ++++++++++++------- 5 files changed, 62 insertions(+), 33 deletions(-) diff --git a/contracts/staking/contracts/src/ReadOnlyProxy.sol b/contracts/staking/contracts/src/ReadOnlyProxy.sol index 4e3277c908..7aa9eba73d 100644 --- a/contracts/staking/contracts/src/ReadOnlyProxy.sol +++ b/contracts/staking/contracts/src/ReadOnlyProxy.sol @@ -23,6 +23,8 @@ import "./libs/LibProxy.sol"; contract ReadOnlyProxy is + MixinConstants, + Ownable, MixinStorage { using LibProxy for address; diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index 65a831ec04..6e329705eb 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -51,7 +51,7 @@ contract Staking is address _wethProxyAddress, address _ethVaultAddress, address payable _rewardVaultAddress, - address _zrxVaultAddress + address _zrxVaultAddress ) external onlyAuthorized diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 5e714c6966..4ce2cae889 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -47,6 +47,7 @@ contract MixinExchangeFees is MixinExchangeManager, MixinAbstract, MixinStakeBalances, + MixinStakingPoolRewards, MixinStakingPool { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index b6b321f23e..fe5a3a6f27 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -79,11 +79,13 @@ interface IStakingEvents { /// @dev Emitted by MixinFinalizer when rewards are paid out to a pool. /// @param epoch The epoch when the rewards were earned. /// @param poolId The pool's ID. - /// @param reward Amount of reward paid. + /// @param operatorReward Amount of reward paid to pool operator. + /// @param membersReward Amount of reward paid to pool members. event RewardsPaid( uint256 epoch, bytes32 poolId, - uint256 reward + uint256 operatorReward, + uint256 membersReward ); /// @dev Emitted whenever staking parameters are changed via the `setParams()` function. diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index d44bcaff48..50d7382f81 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -42,11 +42,15 @@ import "./MixinScheduler.sol"; /// epoch. contract MixinFinalizer is IStakingEvents, + MixinAbstract, MixinConstants, MixinDeploymentConstants, + Ownable, MixinStorage, - MixinStakingPoolRewardVault, + MixinZrxVault, MixinScheduler, + MixinStakingPoolRewardVault, + MixinStakeStorage, MixinStakeBalances, MixinStakingPoolRewards { @@ -120,19 +124,19 @@ contract MixinFinalizer is /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. function finalizePools(bytes32[] calldata poolIds) external - returns (_unfinalizedPoolsRemaining) + returns (uint256 _unfinalizedPoolsRemaining) { uint256 epoch = getCurrentEpoch(); // There are no pools to finalize at epoch 0. if (epoch == 0) { - return; + return _unfinalizedPoolsRemaining = 0; } uint256 poolsRemaining = unfinalizedPoolsRemaining; // If there are no more unfinalized pools remaining, there's nothing // to do. if (poolsRemaining == 0) { - return; + return _unfinalizedPoolsRemaining = 0; } // Pointer to the active pools in the last epoch. @@ -156,14 +160,22 @@ contract MixinFinalizer is // Credit the pool with rewards. // We will transfer the total rewards to the vault at the end. - uint256 reward = _creditRewardToPool(poolId, pool); - rewardsPaid = rewardsPaid.safeAdd(reward); + IStructs.PoolRewards memory poolRewards = + _creditRewardToPool(poolId, pool); + rewardsPaid = rewardsPaid.safeAdd( + poolRewards.operatorReward + poolRewards.membersReward + ); // Decrease the number of unfinalized pools left. poolsRemaining = poolsRemaining.safeSub(1); // Emit an event. - emit RewardsPaid(epoch, poolId, reward); + emit RewardsPaid( + epoch, + poolId, + poolRewards.operatorReward, + poolRewards.membersReward + ); } // Deposit all the rewards at once into the RewardVault. @@ -200,16 +212,17 @@ contract MixinFinalizer is uint256 epoch = getCurrentEpoch(); // There are no pools to finalize at epoch 0. if (epoch == 0) { - return; + return rewards; } // Get the active pool. - IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(epoch - 1, poolId); + mapping (bytes32 => IStructs.ActivePool) storage activePools = + _getActivePoolsFromEpoch(epoch - 1); + IStructs.ActivePool memory pool = activePools[poolId]; // Ignore pools that weren't active. if (pool.feesCollected == 0) { - return; + return rewards; } // Clear the pool state so we don't finalize it again, and to recoup @@ -219,7 +232,9 @@ contract MixinFinalizer is // Credit the pool with rewards. // We will transfer the total rewards to the vault at the end. rewards = _creditRewardToPool(poolId, pool); - totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.safeAdd(reward); + uint256 totalReward = rewards.membersReward + rewards.operatorReward; + totalRewardsPaidLastEpoch = + totalRewardsPaidLastEpoch.safeAdd(totalReward); // Decrease the number of unfinalized pools left. uint256 poolsRemaining = @@ -227,16 +242,21 @@ contract MixinFinalizer is unfinalizedPoolsRemaining.safeSub(1); // Emit an event. - emit RewardsPaid(epoch, poolId, reward); + emit RewardsPaid( + epoch, + poolId, + rewards.operatorReward, + rewards.membersReward + ); // Deposit all the rewards at once into the RewardVault. - _depositIntoStakingPoolRewardVault(rewardsPaid); + _depositIntoStakingPoolRewardVault(totalReward); // If there are no more unfinalized pools remaining, the epoch is // finalized. if (poolsRemaining == 0) { emit EpochFinalized( - priorEpoch, + epoch - 1, totalRewardsPaidLastEpoch, unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) ); @@ -255,7 +275,7 @@ contract MixinFinalizer is view returns (IStructs.ActivePool memory pool) { - pool = _getActivePoolFromEpoch(epoch)[poolId]; + pool = _getActivePoolsFromEpoch(epoch)[poolId]; } /// @dev Get a mapping of active pools from an epoch. @@ -285,14 +305,14 @@ contract MixinFinalizer is uint256 epoch = getCurrentEpoch(); // There can't be any rewards in the first epoch. if (epoch == 0) { - return; + return rewards; } IStructs.ActivePool memory pool = _getActivePoolFromEpoch(epoch - 1, poolId); // Use the cobb-douglas function to compute the total reward. - totalReward = LibCobbDouglas._cobbDouglas( + uint256 totalReward = LibCobbDouglas._cobbDouglas( unfinalizedRewardsAvailable, pool.feesCollected, unfinalizedTotalFeesCollected, @@ -303,12 +323,16 @@ contract MixinFinalizer is ); // Split the reward between the operator and delegators. - (rewards.operatorReward, rewards.membersReward) = - rewardVault.splitAmountBetweenOperatorAndMembers( - poolId, - totalReward - ); - rewards.delegatedStake = pool.delegatedStake; + if (pool.delegatedStake == 0) { + rewards.operatorReward = totalReward; + } else { + (rewards.operatorReward, rewards.membersReward) = + rewardVault.splitAmountBetweenOperatorAndMembers( + poolId, + totalReward + ); + } + rewards.membersStake = pool.delegatedStake; } /// @dev Computes the reward owed to a pool during finalization and @@ -321,10 +345,10 @@ contract MixinFinalizer is IStructs.ActivePool memory pool ) private - returns (PoolRewards memory rewards) + returns (IStructs.PoolRewards memory rewards) { // Use the cobb-douglas function to compute the total reward. - totalReward = LibCobbDouglas._cobbDouglas( + uint256 totalReward = LibCobbDouglas._cobbDouglas( unfinalizedRewardsAvailable, pool.feesCollected, unfinalizedTotalFeesCollected, @@ -338,17 +362,17 @@ contract MixinFinalizer is (rewards.operatorReward, rewards.membersReward) = rewardVault.recordDepositFor( poolId, - reward, + totalReward, // If no delegated stake, all rewards go to the operator. pool.delegatedStake == 0 ); - rewards.delegatedStake = pool.delegatedStake; + rewards.membersStake = pool.delegatedStake; // Sync delegator rewards. - if (membersPortionOfReward != 0) { + if (rewards.membersReward != 0) { _recordRewardForDelegators( poolId, - membersPortionOfReward, + rewards.membersReward, pool.delegatedStake ); } From 46b8bfe3388db0c10375b53c894c8e997a51d1f5 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 04:40:39 -0400 Subject: [PATCH 11/52] `@0x/contracts-staking`: It compiles! (again) --- contracts/staking/contracts/src/sys/MixinAbstract.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol index 0d5b3f6fce..e01a61b372 100644 --- a/contracts/staking/contracts/src/sys/MixinAbstract.sol +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -67,7 +67,7 @@ contract MixinAbstract { /// @param poolId The pool ID to finalize. /// @return rewards Rewards. /// @return rewards The rewards credited to the pool. - function _finalizePool(bytes32 poolIds) + function _finalizePool(bytes32 poolId) internal returns (IStructs.PoolRewards memory rewards); } From 6a8242a6ca614f3b6606292de5c62b22abe89397 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 04:49:58 -0400 Subject: [PATCH 12/52] `@0x/contracts-staking`: Fix past epoch reward accounting. --- contracts/staking/contracts/test/TestStorageLayout.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index 5f1899a733..27133485a2 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -29,7 +29,6 @@ contract TestStorageLayout is Ownable, MixinStorage { - function assertExpectedStorageLayout() public pure From 58a5ab45502008078c3de2f4da3b8c0b15697486 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 14:07:49 -0400 Subject: [PATCH 13/52] `@0x/contracts-test-utils`: Allow `hexSlice()` to take negative numbers, and a range. --- contracts/test-utils/CHANGELOG.json | 4 ++++ contracts/test-utils/src/hex_utils.ts | 31 +++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 52d1e62168..a5fff1105f 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -81,6 +81,10 @@ { "note": "Add `Numberish` type.", "pr": 2131 + }, + { + "note": "Tweaks/Upgrades to `hex_utils`, most notably `hexSlice()`", + "pr": "TODO" } ] }, diff --git a/contracts/test-utils/src/hex_utils.ts b/contracts/test-utils/src/hex_utils.ts index fef17f1e35..05b7a9c781 100644 --- a/contracts/test-utils/src/hex_utils.ts +++ b/contracts/test-utils/src/hex_utils.ts @@ -3,6 +3,7 @@ import * as crypto from 'crypto'; import * as ethUtil from 'ethereumjs-util'; import { constants } from './constants'; +import { Numberish } from './types'; const { WORD_LENGTH } = constants; const WORD_CEIL = new BigNumber(2).pow(WORD_LENGTH * 8); @@ -24,39 +25,44 @@ export function hexRandom(size: number = WORD_LENGTH): string { /** * Left-pad a hex number to a number of bytes. */ -export function hexLeftPad(n: string | BigNumber | number, size: number = WORD_LENGTH): string { +export function hexLeftPad(n: Numberish, size: number = WORD_LENGTH): string { return ethUtil.bufferToHex(ethUtil.setLengthLeft(toHex(n), size)); } /** * Right-pad a hex number to a number of bytes. */ -export function hexRightPad(n: string | BigNumber | number, size: number = WORD_LENGTH): string { +export function hexRightPad(n: Numberish, size: number = WORD_LENGTH): string { return ethUtil.bufferToHex(ethUtil.setLengthRight(toHex(n), size)); } /** * Inverts a hex word. */ -export function hexInvert(n: string | BigNumber | number, size: number = WORD_LENGTH): string { +export function hexInvert(n: Numberish, size: number = WORD_LENGTH): string { const buf = ethUtil.setLengthLeft(toHex(n), size); // tslint:disable-next-line: no-bitwise return ethUtil.bufferToHex(Buffer.from(buf.map(b => ~b))); } /** - * Slices off the desired number of bytes from a hex number. + * Slices a hex number. */ -export function hexSlice(n: string | BigNumber | number, size: number): string { - const hex = toHex(n); - return '0x'.concat(toHex(n).slice(size * 2 + 2, hex.length)); +export function hexSlice(n: Numberish, start: number, end?: number): string { + const hex = toHex(n).substr(2); + const sliceStart = start >= 0 ? start * 2 : Math.max(0, hex.length + start * 2); + let sliceEnd = hex.length; + if (end !== undefined) { + sliceEnd = end >= 0 ? end * 2 : Math.max(0, hex.length + end * 2); + } + return '0x'.concat(hex.substring(sliceStart, sliceEnd)); } /** * Convert a string, a number, or a BigNumber into a hex string. * Works with negative numbers, as well. */ -export function toHex(n: string | BigNumber | number, size: number = WORD_LENGTH): string { +export function toHex(n: Numberish, size: number = WORD_LENGTH): string { if (typeof n === 'string' && /^0x[0-9a-f]+$/i.test(n)) { // Already a hex. return n; @@ -64,7 +70,14 @@ export function toHex(n: string | BigNumber | number, size: number = WORD_LENGTH let _n = new BigNumber(n); if (_n.isNegative()) { // Perform two's-complement. - _n = new BigNumber(hexInvert(toHex(_n.abs()), size).substr(2), 16).plus(1).mod(WORD_CEIL); + // prettier-ignore + _n = new BigNumber( + hexInvert( + toHex(_n.abs()), + size, + ).substr(2), + 16, + ).plus(1).mod(WORD_CEIL); } return `0x${_n.toString(16)}`; } From d548ddac0dbfb0d8e66af72f28d4c442ab48ffaa Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 14:19:45 -0400 Subject: [PATCH 14/52] `@0x/contracts-staking`: Fixing tests. --- contracts/staking/.prettierrc | 10 ++ contracts/staking/compiler.json | 49 +++--- contracts/staking/contracts/src/Staking.sol | 3 +- .../contracts/src/sys/MixinFinalizer.sol | 18 +-- .../contracts/test/TestDelegatorRewards.sol | 29 ++++ contracts/staking/src/artifacts.ts | 2 +- .../staking/test/actors/finalizer_actor.ts | 2 +- contracts/staking/test/actors/staker_actor.ts | 2 +- contracts/staking/test/epoch_test.ts | 2 +- contracts/staking/test/rewards_test.ts | 34 ++-- .../unit_tests/delegator_reward_balance.ts | 22 +++ .../test/unit_tests/lib_proxy_unit_test.ts | 148 ++++++++++-------- contracts/staking/test/utils/api_wrapper.ts | 70 +++++++-- contracts/staking/test/utils/types.ts | 8 + 14 files changed, 260 insertions(+), 139 deletions(-) create mode 100644 contracts/staking/.prettierrc create mode 100644 contracts/staking/contracts/test/TestDelegatorRewards.sol create mode 100644 contracts/staking/test/unit_tests/delegator_reward_balance.ts diff --git a/contracts/staking/.prettierrc b/contracts/staking/.prettierrc new file mode 100644 index 0000000000..c568ca733a --- /dev/null +++ b/contracts/staking/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": "./test/**.ts", + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/contracts/staking/compiler.json b/contracts/staking/compiler.json index f432301d36..59b9936053 100644 --- a/contracts/staking/compiler.json +++ b/contracts/staking/compiler.json @@ -1,25 +1,30 @@ { - "artifactsDir": "./generated-artifacts", - "contractsDir": "./contracts", - "useDockerisedSolc": false, - "isOfflineMode": false, - "compilerSettings": { - "evmVersion": "constantinople", - "optimizer": { - "enabled": true, - "runs": 1000000, - "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode.object", - "evm.bytecode.sourceMap", - "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" - ] - } - } + "artifactsDir": "./generated-artifacts", + "contractsDir": "./contracts", + "useDockerisedSolc": false, + "isOfflineMode": false, + "compilerSettings": { + "evmVersion": "constantinople", + "optimizer": { + "enabled": true, + "runs": 1000000, + "details": { + "yul": true, + "deduplicate": true, + "cse": true, + "constantOptimizer": true + } + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } } + } } diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index 6e329705eb..18f0469eca 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2; import "./interfaces/IStaking.sol"; import "./sys/MixinParams.sol"; +import "./sys/MixinFinalizer.sol"; import "./stake/MixinStake.sol"; import "./staking_pools/MixinStakingPool.sol"; import "./fees/MixinExchangeFees.sol"; @@ -31,7 +32,7 @@ contract Staking is MixinParams, MixinStakingPool, MixinStake, - MixinExchangeFees + MixinExchangeFees, { // this contract can receive ETH // solhint-disable no-empty-blocks diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 50d7382f81..a226ed1633 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -335,6 +335,15 @@ contract MixinFinalizer is rewards.membersStake = pool.delegatedStake; } + /// @dev Converts the entire WETH balance of the contract into ETH. + function _unwrapWETH() internal { + uint256 wethBalance = IEtherToken(WETH_ADDRESS) + .balanceOf(address(this)); + if (wethBalance != 0) { + IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + } + } + /// @dev Computes the reward owed to a pool during finalization and /// credits it to that pool for the CURRENT epoch. /// @param poolId The pool's ID. @@ -377,13 +386,4 @@ contract MixinFinalizer is ); } } - - /// @dev Converts the entire WETH balance of the contract into ETH. - function _unwrapWETH() private { - uint256 wethBalance = IEtherToken(WETH_ADDRESS) - .balanceOf(address(this)); - if (wethBalance != 0) { - IEtherToken(WETH_ADDRESS).withdraw(wethBalance); - } - } } diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol new file mode 100644 index 0000000000..5184b2a341 --- /dev/null +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -0,0 +1,29 @@ +/* + + 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 "../src/Staking.sol"; + + +contract TestDelegatorRewards is + Staking +{ + // TODO +} diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 43b78853f4..2919f41aae 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -3,7 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -import { ContractArtifact } from 'ethereum-types'; +import { ContractArtifact } from "ethereum-types"; import * as EthVault from '../generated-artifacts/EthVault.json'; import * as IEthVault from '../generated-artifacts/IEthVault.json'; diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 11c370d7d1..d424861915 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -62,7 +62,7 @@ export class FinalizerActor extends BaseActor { memberRewardByPoolId, ); // finalize - await this._stakingApiWrapper.utils.skipToNextEpochAsync(); + await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // assert reward vault changes const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal( diff --git a/contracts/staking/test/actors/staker_actor.ts b/contracts/staking/test/actors/staker_actor.ts index 1bd47b1932..e2b130a017 100644 --- a/contracts/staking/test/actors/staker_actor.ts +++ b/contracts/staking/test/actors/staker_actor.ts @@ -151,7 +151,7 @@ export class StakerActor extends BaseActor { const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); const initBalances = await this._getBalancesAsync(); // go to next epoch - await this._stakingApiWrapper.utils.skipToNextEpochAsync(); + await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // check balances const expectedBalances = this._getNextEpochBalances(initBalances); await this._assertBalancesAsync(expectedBalances); diff --git a/contracts/staking/test/epoch_test.ts b/contracts/staking/test/epoch_test.ts index e32df331fb..3d7e9c5b0b 100644 --- a/contracts/staking/test/epoch_test.ts +++ b/contracts/staking/test/epoch_test.ts @@ -38,7 +38,7 @@ blockchainTests('Epochs', env => { expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH); } ///// 3/3 Increment Epoch (TimeLock Should Not Increment) ///// - await stakingApiWrapper.utils.skipToNextEpochAsync(); + await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); { // epoch const currentEpoch = await stakingApiWrapper.stakingContract.currentEpoch.callAsync(); diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 5347f02f59..faad8c8b78 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -701,8 +701,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as - Array<[StakerActor, BigNumber]>; + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeAsync(stakeAmount); await staker.moveStakeAsync( @@ -724,8 +723,7 @@ blockchainTests.resets('Testing Rewards', env => { toBaseUnitAmount(0), ); } - const expectedStakerRewards = stakeAmounts - .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); await validateEndBalances({ stakerRewardVaultBalance_1: toBaseUnitAmount(0), stakerRewardVaultBalance_2: toBaseUnitAmount(0), @@ -739,8 +737,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as - Array<[StakerActor, BigNumber]>; + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeAsync(stakeAmount); await staker.moveStakeAsync( @@ -754,8 +751,7 @@ blockchainTests.resets('Testing Rewards', env => { // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); - const expectedStakerRewards = stakeAmounts - .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); await validateEndBalances({ stakerRewardVaultBalance_1: expectedStakerRewards[0], stakerRewardVaultBalance_2: expectedStakerRewards[1], @@ -776,19 +772,21 @@ blockchainTests.resets('Testing Rewards', env => { const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0]; await undelegateZeroAsync(sneakyStaker); // Should have been credited the correct amount of rewards. - let sneakyStakerEthVaultBalance = await stakingApiWrapper - .ethVaultContract.balanceOf - .callAsync(sneakyStaker.getOwner()); - expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate') - .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( + sneakyStaker.getOwner(), + ); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate').to.bignumber.eq( + sneakyStakerExpectedEthVaultBalance, + ); // Now he'll try to do it again to see if he gets credited twice. await undelegateZeroAsync(sneakyStaker); /// The total amount credited should remain the same. - sneakyStakerEthVaultBalance = await stakingApiWrapper - .ethVaultContract.balanceOf - .callAsync(sneakyStaker.getOwner()); - expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate') - .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( + sneakyStaker.getOwner(), + ); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate').to.bignumber.eq( + sneakyStakerExpectedEthVaultBalance, + ); }); }); }); diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance.ts b/contracts/staking/test/unit_tests/delegator_reward_balance.ts new file mode 100644 index 0000000000..1fb12adac8 --- /dev/null +++ b/contracts/staking/test/unit_tests/delegator_reward_balance.ts @@ -0,0 +1,22 @@ +import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; + +import { artifacts, TestDelegatorRewardsContract } from '../../src'; + +blockchainTests('delegator rewards', env => { + let testContract: TestDelegatorRewardsContract; + + before(async () => { + testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync( + artifacts.TestLibFixedMath, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('computeRewardBalanceOfDelegator()', () => { + it('does stuff', () => { + // TODO + }); + }); +}); diff --git a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts index 129d72ef09..89326d93fb 100644 --- a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts +++ b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts @@ -8,6 +8,7 @@ import { testCombinatoriallyWithReferenceFunc, } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; +import { cartesianProduct } from 'js-combinatorics'; import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src'; @@ -196,79 +197,88 @@ blockchainTests.resets('LibProxy', env => { describe('Combinatorial Tests', () => { // Combinatorial Scenarios for `proxyCall()`. - const revertRuleScenarios: RevertRule[] = [ - RevertRule.RevertOnError, - RevertRule.AlwaysRevert, - RevertRule.NeverRevert, - ]; - const ignoreIngressScenarios: boolean[] = [false, true]; - const customEgressScenarios: string[] = [ - constants.NULL_BYTES4, - constructRandomFailureCalldata(), // Random failure calldata is used because it is nonzero and won't collide. - ]; - const calldataScenarios: string[] = [constructRandomFailureCalldata(), constructRandomSuccessCalldata()]; - - // A reference function that returns the expected success and returndata values of a given call to `proxyCall()`. - async function referenceFuncAsync( - revertRule: RevertRule, - customEgressSelector: string, - shouldIgnoreIngressSelector: boolean, - calldata: string, - ): Promise<[boolean, string]> { - // Determine whether or not the call should succeed. - let shouldSucceed = true; - if ( - ((shouldIgnoreIngressSelector && customEgressSelector !== constants.NULL_BYTES4) || - (!shouldIgnoreIngressSelector && customEgressSelector === constants.NULL_BYTES4)) && - calldata.length === 10 // This corresponds to a hex length of 4 - ) { - shouldSucceed = false; - } - - // Override the above success value if the RevertRule defines the success. - if (revertRule === RevertRule.AlwaysRevert) { - shouldSucceed = false; - } - if (revertRule === RevertRule.NeverRevert) { - shouldSucceed = true; - } - - // Construct the data that should be returned. - let returnData = calldata; - if (shouldIgnoreIngressSelector) { - returnData = hexSlice(returnData, 4); - } - if (customEgressSelector !== constants.NULL_BYTES4) { - returnData = hexConcat(customEgressSelector, returnData); - } - - // Return the success and return data values. - return [shouldSucceed, returnData]; + function getCombinatorialTestDescription(params: [RevertRule, boolean, string, string]): string { + const REVERT_RULE_NAMES = [ + 'RevertOnError', + 'AlwaysRevert', + 'NeverRevert', + ]; + return [ + `revertRule: ${REVERT_RULE_NAMES[params[0]]}`, + `ignoreIngressSelector: ${params[1]}`, + `customEgressSelector: ${params[2]}`, + `calldata: ${ + params[3].length / 2 - 2 > 4 + ? // tslint:disable-next-line + hexSlice(params[3], 0, 4) + '...' + : params[3] + }`, + ].join(', '); } - // A wrapper for `publicProxyCall()` that allow us to combinatorially test `proxyCall()` for the - // scenarios defined above. - async function testFuncAsync( - revertRule: RevertRule, - customEgressSelector: string, - shouldIgnoreIngressSelector: boolean, - calldata: string, - ): Promise<[boolean, string]> { - return publicProxyCallAsync({ - calldata, - customEgressSelector, - ignoreIngressSelector: shouldIgnoreIngressSelector, - revertRule, + const scenarios = [ + // revertRule + [ + RevertRule.RevertOnError, + RevertRule.AlwaysRevert, + RevertRule.NeverRevert, + ], + // ignoreIngressSelector + [false, true], + // customEgressSelector + [ + constants.NULL_BYTES4, + // Random failure calldata is used because it is nonzero and + // won't collide. + constructRandomFailureCalldata(), + ], + // calldata + [ + constructRandomFailureCalldata(), + constructRandomSuccessCalldata(), + ], + ] as [RevertRule[], boolean[], string[], string[]]; + + for (const params of cartesianProduct(...scenarios).toArray()) { + const [revertRule, shouldIgnoreIngressSelector, customEgressSelector, calldata] = params; + it(getCombinatorialTestDescription(params), async () => { + // Determine whether or not the call should succeed. + let shouldSucceed = true; + if ( + ((shouldIgnoreIngressSelector && customEgressSelector !== constants.NULL_BYTES4) || + (!shouldIgnoreIngressSelector && customEgressSelector === constants.NULL_BYTES4)) && + calldata.length === 10 // This corresponds to a hex length of 4 + ) { + shouldSucceed = false; + } + + // Override the above success value if the RevertRule defines the success. + if (revertRule === RevertRule.AlwaysRevert) { + shouldSucceed = false; + } + if (revertRule === RevertRule.NeverRevert) { + shouldSucceed = true; + } + + // Construct the data that should be returned. + let returnData = calldata; + if (shouldIgnoreIngressSelector) { + returnData = hexSlice(returnData, 4); + } + if (customEgressSelector !== constants.NULL_BYTES4) { + returnData = hexConcat(customEgressSelector, returnData); + } + + const [didSucceed, actualReturnData] = await publicProxyCallAsync({ + calldata, + customEgressSelector, + ignoreIngressSelector: shouldIgnoreIngressSelector, + revertRule, + }); + expect(didSucceed).to.be.eq(shouldSucceed); + expect(actualReturnData).to.be.eq(returnData); }); } - - // Combinatorially test proxy call. - testCombinatoriallyWithReferenceFunc('proxyCall', referenceFuncAsync, testFuncAsync, [ - revertRuleScenarios, - customEgressScenarios, - ignoreIngressScenarios, - calldataScenarios, - ]); }); }); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 6979649ba7..c9806da681 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -1,28 +1,34 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils'; +import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; -import { ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts, EthVaultContract, + IStakingEventsEpochEndedEventArgs, + IStakingEventsStakingPoolActivatedEventArgs, ReadOnlyProxyContract, StakingContract, + StakingEvents, StakingPoolRewardVaultContract, StakingProxyContract, ZrxVaultContract, } from '../../src'; import { constants as stakingConstants } from './constants'; -import { StakingParams } from './types'; +import { EndOfEpochInfo, StakingParams } from './types'; export class StakingApiWrapper { - public stakingContractAddress: string; // The address of the real Staking.sol contract - public stakingContract: StakingContract; // The StakingProxy.sol contract wrapped as a StakingContract to borrow API - public stakingProxyContract: StakingProxyContract; // The StakingProxy.sol contract as a StakingProxyContract + // The address of the real Staking.sol contract + public stakingContractAddress: string; + // The StakingProxy.sol contract wrapped as a StakingContract to borrow API + public stakingContract: StakingContract; + // The StakingProxy.sol contract as a StakingProxyContract + public stakingProxyContract: StakingProxyContract; public zrxVaultContract: ZrxVaultContract; public ethVaultContract: EthVaultContract; public rewardVaultContract: StakingPoolRewardVaultContract; @@ -30,21 +36,53 @@ export class StakingApiWrapper { public utils = { // Epoch Utils fastForwardToNextEpochAsync: async (): Promise => { - // increase timestamp of next block - const { epochDurationInSeconds } = await this.utils.getParamsAsync(); - await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber()); + // increase timestamp of next block by how many seconds we need to + // get to the next epoch. + const epochEndTime = await this.stakingContract.getCurrentEpochEarliestEndTimeInSeconds.callAsync(); + const lastBlockTime = await this._web3Wrapper.getBlockTimestampAsync('latest'); + const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber()); + await this._web3Wrapper.increaseTimeAsync(dt); // mine next block await this._web3Wrapper.mineBlockAsync(); }, - skipToNextEpochAsync: async (): Promise => { + skipToNextEpochAndFinalizeAsync: async (): Promise => { await this.utils.fastForwardToNextEpochAsync(); - // increment epoch in contracts - const txReceipt = await this.stakingContract.finalizeFees.awaitTransactionSuccessAsync(); - logUtils.log(`Finalization costed ${txReceipt.gasUsed} gas`); - // mine next block - await this._web3Wrapper.mineBlockAsync(); - return txReceipt; + const endOfEpochInfo = await this.utils.endEpochAsync(); + const receipt = await this.stakingContract.finalizePools.awaitTransactionSuccessAsync( + endOfEpochInfo.activePoolIds, + ); + logUtils.log(`Finalization cost ${receipt.gasUsed} gas`); + return receipt; + }, + + endEpochAsync: async (): Promise => { + const activePoolIds = await this.utils.findActivePoolIdsAsync(); + const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync(); + const [epochEndedEvent] = filterLogsToArguments( + receipt.logs, + StakingEvents.EpochEnded, + ); + return { + closingEpoch: epochEndedEvent.epoch, + activePoolIds, + rewardsAvailable: epochEndedEvent.rewardsAvailable, + totalFeesCollected: epochEndedEvent.totalFeesCollected, + totalWeightedStake: epochEndedEvent.totalWeightedStake, + }; + }, + + findActivePoolIdsAsync: async (epoch?: number): Promise => { + const _epoch = epoch !== undefined ? epoch : await this.stakingContract.getCurrentEpoch.callAsync(); + const events = filterLogsToArguments( + await this.stakingContract.getLogsAsync( + StakingEvents.StakingPoolActivated, + { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, + { epoch: _epoch }, + ), + StakingEvents.StakingPoolActivated, + ); + return events.map(e => e.poolId); }, // Other Utils diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index f9933da21e..9aa685b05d 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -53,6 +53,14 @@ export interface SimulationParams { withdrawByUndelegating: boolean; } +export interface EndOfEpochInfo { + closingEpoch: BigNumber; + activePoolIds: string[]; + rewardsAvailable: BigNumber; + totalFeesCollected: BigNumber; + totalWeightedStake: BigNumber; +} + export interface StakeBalance { currentEpochBalance: BigNumber; nextEpochBalance: BigNumber; From a1aad2e55e72bebca81f6051b3ddc23b14adaefe Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 19:48:54 -0400 Subject: [PATCH 15/52] `@0x/contracts-exchange`: Fixing tests and writing new ones. --- .../contracts/src/fees/MixinExchangeFees.sol | 106 +++++++---- .../contracts/src/interfaces/IStructs.sol | 4 +- .../src/staking_pools/MixinStakingPool.sol | 11 ++ .../contracts/src/sys/MixinFinalizer.sol | 76 ++++++-- .../staking/contracts/test/TestFinalizer.sol | 145 +++++++++++++++ .../contracts/test/TestProtocolFees.sol | 36 +++- contracts/staking/test/actors/staker_actor.ts | 9 + contracts/staking/test/protocol_fees.ts | 56 ++++-- contracts/staking/test/rewards_test.ts | 169 +++++------------- ...ce.ts => delegator_reward_balance_test.ts} | 2 +- .../staking/test/unit_tests/finalizer_test.ts | 55 ++++++ 11 files changed, 469 insertions(+), 200 deletions(-) create mode 100644 contracts/staking/contracts/test/TestFinalizer.sol rename contracts/staking/test/unit_tests/{delegator_reward_balance.ts => delegator_reward_balance_test.ts} (93%) create mode 100644 contracts/staking/test/unit_tests/finalizer_test.ts diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 4ce2cae889..9e61dcbb69 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -19,6 +19,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; @@ -71,7 +72,8 @@ contract MixinExchangeFees is { _assertValidProtocolFee(protocolFeePaid); - // Transfer the protocol fee to this address if it should be paid in WETH. + // Transfer the protocol fee to this address if it should be paid in + // WETH. if (msg.value == 0) { wethAssetProxy.transferFrom( WETH_ASSET_DATA, @@ -89,7 +91,8 @@ contract MixinExchangeFees is return; } - uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; + uint256 poolStake = + getTotalStakeDelegatedToPool(poolId).currentEpochBalance; // Ignore pools with dust stake. if (poolStake < minimumPoolStake) { return; @@ -103,35 +106,24 @@ contract MixinExchangeFees is // If the pool was previously inactive in this epoch, initialize it. if (pool.feesCollected == 0) { - // Compute weighted stake. - uint256 operatorStake = getStakeDelegatedToPoolByOwner( - rewardVault.operatorOf(poolId), - poolId - ).currentEpochBalance; - pool.weightedStake = operatorStake.safeAdd( - poolStake - .safeSub(operatorStake) - .safeMul(rewardDelegatedStakeWeight) - .safeDiv(PPM_DENOMINATOR) - ); - - // Compute delegated (non-operator) stake. - pool.delegatedStake = poolStake.safeSub(operatorStake); + // Compute member and total weighted stake. + (pool.membersStake, pool.weightedStake) = + _computeMembersAndWeightedStake(poolId, poolStake); // Increase the total weighted stake. - totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd( - pool.weightedStake - ); + totalWeightedStakeThisEpoch = + totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake); // Increase the numberof active pools. numActivePoolsThisEpoch += 1; - // Emit an event so keepers know what pools to pass into `finalize()`. + // Emit an event so keepers know what pools to pass into + // `finalize()`. emit StakingPoolActivated(currentEpoch, poolId); } // Credit the fees to the pool. - pool.feesCollected = protocolFeePaid; + pool.feesCollected = pool.feesCollected.safeAdd(protocolFeePaid); // Increase the total fees collected this epoch. totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd( @@ -142,7 +134,8 @@ contract MixinExchangeFees is activePoolsThisEpoch[poolId] = pool; } - /// @dev Returns the total amount of fees collected thus far, in the current epoch. + /// @dev Returns the total amount of fees collected thus far, in the current + /// epoch. /// @return _totalFeesCollectedThisEpoch Total fees collected this epoch. function getTotalProtocolFeesThisEpoch() external @@ -152,9 +145,21 @@ contract MixinExchangeFees is _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; } + /// @dev Returns the total balance of this contract, including WETH. + /// @return totalBalance Total balance. + function getTotalBalance() + external + view + returns (uint256 totalBalance) + { + totalBalance = address(this).balance + + IEtherToken(WETH_ADDRESS).balanceOf(address(this)); + } + /// @dev Returns the amount of fees attributed to the input pool this epoch. /// @param poolId Pool Id to query. - /// @return feesCollectedByPool Amount of fees collected by the pool this epoch. + /// @return feesCollectedByPool Amount of fees collected by the pool this + /// epoch. function getProtocolFeesThisEpochByPool(bytes32 poolId) external view @@ -168,20 +173,55 @@ contract MixinExchangeFees is feesCollected = pool.feesCollected; } - /// @dev Checks that the protocol fee passed into `payProtocolFee()` is valid. - /// @param protocolFeePaid The `protocolFeePaid` parameter to `payProtocolFee.` + /// @dev Computes the members and weighted stake for a pool at the current + /// epoch. + /// @param poolId ID of the pool. + /// @param totalStake Total (unweighted) stake in the pool. + /// @return membersStake Non-operator stake in the pool. + /// @return weightedStake Weighted stake of the pool. + function _computeMembersAndWeightedStake( + bytes32 poolId, + uint256 totalStake + ) + private + view + returns (uint256 membersStake, uint256 weightedStake) + { + uint256 operatorStake = getStakeDelegatedToPoolByOwner( + getPoolOperator(poolId), + poolId + ).currentEpochBalance; + membersStake = totalStake.safeSub(operatorStake); + weightedStake = operatorStake.safeAdd( + membersStake + .safeMul(rewardDelegatedStakeWeight) + .safeDiv(PPM_DENOMINATOR) + ); + } + + /// @dev Checks that the protocol fee passed into `payProtocolFee()` is + /// valid. + /// @param protocolFeePaid The `protocolFeePaid` parameter to + /// `payProtocolFee.` function _assertValidProtocolFee(uint256 protocolFeePaid) private view { - if (protocolFeePaid == 0 || (msg.value != protocolFeePaid && msg.value != 0)) { - LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError( - protocolFeePaid == 0 ? - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid : - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, - protocolFeePaid, - msg.value - )); + if (protocolFeePaid == 0 || + (msg.value != protocolFeePaid && msg.value != 0)) { + LibRichErrors.rrevert( + LibStakingRichErrors.InvalidProtocolFeePaymentError( + protocolFeePaid == 0 ? + LibStakingRichErrors + .ProtocolFeePaymentErrorCodes + .ZeroProtocolFeePaid : + LibStakingRichErrors + .ProtocolFeePaymentErrorCodes + .MismatchedFeeAndPayment, + protocolFeePaid, + msg.value + ) + ); } } } diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index 69ba1291d2..790c0c2bcf 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -25,11 +25,11 @@ interface IStructs { /// (see MixinExchangeFees). /// @param feesCollected Fees collected in ETH by this pool. /// @param weightedStake Amount of weighted stake in the pool. - /// @param delegatedStake Amount of delegated, non-operator stake in the pool. + /// @param membersStake Amount of non-operator stake in the pool. struct ActivePool { uint256 feesCollected; uint256 weightedStake; - uint256 delegatedStake; + uint256 membersStake; } /// @dev Rewards credited to a pool during finalization. diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol index 4f3ce21f8e..5b599350d7 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol @@ -113,6 +113,17 @@ contract MixinStakingPool is return poolById[poolId]; } + /// @dev Look up the operator of a pool. + /// @param poolId The ID of the pool. + /// @return operatorAddress The pool operator. + function getPoolOperator(bytes32 poolId) + public + view + returns (address operatorAddress) + { + return rewardVault.operatorOf(poolId); + } + /// @dev Computes the unique id that comes after the input pool id. /// @param poolId Unique id of pool. /// @return Next pool id after input pool. diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index a226ed1633..1420f010c1 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -93,7 +93,7 @@ contract MixinFinalizer is // Emit an event. emit EpochEnded( closingEpoch, - numActivePoolsThisEpoch, + unfinalizedPoolsRemaining, unfinalizedRewardsAvailable, unfinalizedTotalFeesCollected, unfinalizedTotalWeightedStake @@ -179,7 +179,9 @@ contract MixinFinalizer is } // Deposit all the rewards at once into the RewardVault. - _depositIntoStakingPoolRewardVault(rewardsPaid); + if (rewardsPaid != 0) { + _depositIntoStakingPoolRewardVault(rewardsPaid); + } // Update finalization states. totalRewardsPaidLastEpoch = @@ -310,6 +312,11 @@ contract MixinFinalizer is IStructs.ActivePool memory pool = _getActivePoolFromEpoch(epoch - 1, poolId); + // There can't be any rewards if the pool was active or if it has + // no stake. + if (pool.feesCollected == 0 || pool.weightedStake == 0) { + return rewards; + } // Use the cobb-douglas function to compute the total reward. uint256 totalReward = LibCobbDouglas._cobbDouglas( @@ -323,16 +330,13 @@ contract MixinFinalizer is ); // Split the reward between the operator and delegators. - if (pool.delegatedStake == 0) { + if (pool.membersStake == 0) { rewards.operatorReward = totalReward; } else { (rewards.operatorReward, rewards.membersReward) = - rewardVault.splitAmountBetweenOperatorAndMembers( - poolId, - totalReward - ); + _splitAmountBetweenOperatorAndMembers(poolId, totalReward); } - rewards.membersStake = pool.delegatedStake; + rewards.membersStake = pool.membersStake; } /// @dev Converts the entire WETH balance of the contract into ETH. @@ -344,6 +348,48 @@ contract MixinFinalizer is } } + /// @dev Splits an amount between the pool operator and members of the + /// pool based on the pool operator's share. + /// @param poolId The ID of the pool. + /// @param amount Amount to to split. + /// @return operatorPortion Portion of `amount` attributed to the operator. + /// @return membersPortion Portion of `amount` attributed to the pool. + function _splitAmountBetweenOperatorAndMembers( + bytes32 poolId, + uint256 amount + ) + internal + view + returns (uint256 operatorReward, uint256 membersReward) + { + (operatorReward, membersReward) = + rewardVault.splitAmountBetweenOperatorAndMembers(poolId, amount); + } + + /// @dev Record a deposit for a pool in the RewardVault. + /// @param poolId ID of the pool. + /// @param amount Amount in ETH to record. + /// @param operatorOnly Only attribute amount to operator. + /// @return operatorPortion Portion of `amount` attributed to the operator. + /// @return membersPortion Portion of `amount` attributed to the pool. + function _recordDepositInRewardVaultFor( + bytes32 poolId, + uint256 amount, + bool operatorOnly + ) + internal + returns ( + uint256 operatorPortion, + uint256 membersPortion + ) + { + (operatorPortion, membersPortion) = rewardVault.recordDepositFor( + poolId, + amount, + operatorOnly + ); + } + /// @dev Computes the reward owed to a pool during finalization and /// credits it to that pool for the CURRENT epoch. /// @param poolId The pool's ID. @@ -356,6 +402,12 @@ contract MixinFinalizer is private returns (IStructs.PoolRewards memory rewards) { + // There can't be any rewards if the pool was active or if it has + // no stake. + if (pool.feesCollected == 0 || pool.weightedStake == 0) { + return rewards; + } + // Use the cobb-douglas function to compute the total reward. uint256 totalReward = LibCobbDouglas._cobbDouglas( unfinalizedRewardsAvailable, @@ -369,20 +421,20 @@ contract MixinFinalizer is // Credit the pool the reward in the RewardVault. (rewards.operatorReward, rewards.membersReward) = - rewardVault.recordDepositFor( + _recordDepositInRewardVaultFor( poolId, totalReward, // If no delegated stake, all rewards go to the operator. - pool.delegatedStake == 0 + pool.membersStake == 0 ); - rewards.membersStake = pool.delegatedStake; + rewards.membersStake = pool.membersStake; // Sync delegator rewards. if (rewards.membersReward != 0) { _recordRewardForDelegators( poolId, rewards.membersReward, - pool.delegatedStake + pool.membersStake ); } } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol new file mode 100644 index 0000000000..b56013b09f --- /dev/null +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -0,0 +1,145 @@ +/* + + 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 "../src/interfaces/IStructs.sol"; +import "../src/Staking.sol"; + + +contract TestFinalizer is + Staking +{ + struct RecordedReward { + uint256 membersReward; + uint256 membersStake; + } + + struct DepositedReward { + uint256 totalReward; + bool operatorOnly; + } + mapping (bytes32 => uint32) internal _operatorSharesByPool; + mapping (bytes32 => RecordedReward) internal _recordedRewardsByPool; + mapping (bytes32 => DepositedReward) internal _depositedRewardsByPool; + + function getFinalizationState() + external + view + returns ( + uint256 _closingEpoch, + uint256 _unfinalizedPoolsRemaining, + uint256 _unfinalizedRewardsAvailable, + uint256 _unfinalizedTotalFeesCollected, + uint256 _unfinalizedTotalWeightedStake + ) + { + _closingEpoch = currentEpoch - 1; + _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; + _unfinalizedRewardsAvailable = unfinalizedRewardsAvailable; + _unfinalizedTotalFeesCollected = unfinalizedTotalFeesCollected; + _unfinalizedTotalWeightedStake = unfinalizedTotalWeightedStake; + } + + function addActivePool( + bytes32 poolId, + uint32 operatorShare, + uint256 feesCollected, + uint256 membersStake, + uint256 weightedStake + ) + external + { + mapping (bytes32 => IStructs.ActivePool) storage activePools = + _getActivePoolsFromEpoch(currentEpoch); + assert(activePools[poolId].feesCollected == 0); + _operatorSharesByPool[poolId] = operatorShare; + activePools[poolId] = IStructs.ActivePool({ + feesCollected: feesCollected, + membersStake: membersStake, + weightedStake: weightedStake + }); + totalFeesCollectedThisEpoch += feesCollected; + totalWeightedStakeThisEpoch += weightedStake; + numActivePoolsThisEpoch += 1; + } + + /// @dev Overridden to just store inputs. + function _recordRewardForDelegators( + bytes32 poolId, + uint256 membersReward, + uint256 membersStake + ) + internal + { + _recordedRewardsByPool[poolId] = RecordedReward({ + membersReward: membersReward, + membersStake: membersStake + }); + } + + /// @dev Overridden to store inputs and do some really basic math. + function _recordDepositInRewardVaultFor( + bytes32 poolId, + uint256 totalReward, + bool operatorOnly + ) + internal + returns ( + uint256 operatorPortion, + uint256 membersPortion + ) + { + _depositedRewardsByPool[poolId] = DepositedReward({ + totalReward: totalReward, + operatorOnly: operatorOnly + }); + + if (operatorOnly) { + operatorPortion = totalReward; + } else { + (operatorPortion, membersPortion) = + _splitAmountBetweenOperatorAndMembers(poolId, totalReward); + } + } + + /// @dev Overridden to do some really basic math. + function _splitAmountBetweenOperatorAndMembers( + bytes32 poolId, + uint256 amount + ) + internal + view + returns (uint256 operatorPortion, uint256 membersPortion) + { + uint32 operatorShare = _operatorSharesByPool[poolId]; + operatorPortion = operatorShare * amount / PPM_DENOMINATOR; + membersPortion = amount - operatorPortion; + } + + /// @dev Overriden to always succeed. + function _goToNextEpoch() internal { + currentEpoch += 1; + } + + /// @dev Overridden to do nothing. + function _unwrapWETH() internal { + // NOOP + } +} diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index 37f989d9e4..1df9efbf53 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -20,6 +20,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; +import "../src/interfaces/IStructs.sol"; import "../src/Staking.sol"; @@ -27,7 +28,8 @@ contract TestProtocolFees is Staking { struct TestPool { - uint256 stake; + uint256 operatorStake; + uint256 membersStake; mapping(address => bool) isMaker; } @@ -58,13 +60,15 @@ contract TestProtocolFees is /// @dev Create a test pool. function createTestPool( bytes32 poolId, - uint256 stake, + uint256 operatorStake, + uint256 membersStake, address[] memory makerAddresses ) public { TestPool storage pool = _testPools[poolId]; - pool.stake = stake; + pool.operatorStake = operatorStake; + pool.membersStake = membersStake; for (uint256 i = 0; i < makerAddresses.length; ++i) { pool.isMaker[makerAddresses[i]] = true; _makersToTestPoolIds[makerAddresses[i]] = poolId; @@ -86,10 +90,34 @@ contract TestProtocolFees is view returns (IStructs.StakeBalance memory balance) { - uint256 stake = _testPools[poolId].stake; + TestPool memory pool = _testPools[poolId]; + uint256 stake = pool.operatorStake + pool.membersStake; return IStructs.StakeBalance({ currentEpochBalance: stake, nextEpochBalance: stake }); } + + /// @dev Overridden to use test pools. + function getStakeDelegatedToPoolByOwner(address, bytes32 poolId) + public + view + returns (IStructs.StakeBalance memory balance) + { + TestPool memory pool = _testPools[poolId]; + return IStructs.StakeBalance({ + currentEpochBalance: pool.operatorStake, + nextEpochBalance: pool.operatorStake + }); + } + + /// @dev Overridden to use test pools. + function getPoolOperator(bytes32) + public + view + returns (address operatorAddress) + { + // Just return nil, we won't use it. + return address(0); + } } diff --git a/contracts/staking/test/actors/staker_actor.ts b/contracts/staking/test/actors/staker_actor.ts index e2b130a017..07423f5668 100644 --- a/contracts/staking/test/actors/staker_actor.ts +++ b/contracts/staking/test/actors/staker_actor.ts @@ -146,6 +146,15 @@ export class StakerActor extends BaseActor { expect(finalZrxBalanceOfVault, 'final balance of zrx vault').to.be.bignumber.equal(initZrxBalanceOfVault); } + public async stakeWithPoolAsync(poolId: string, amount: BigNumber): Promise { + await this.stakeAsync(amount); + await this.moveStakeAsync( + new StakeInfo(StakeStatus.Active), + new StakeInfo(StakeStatus.Delegated, poolId), + amount, + ); + } + public async goToNextEpochAsync(): Promise { // cache balances const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index 34ea87a11f..d96d9653f1 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -4,6 +4,7 @@ import { expect, filterLogsToArguments, hexRandom, + Numberish, randomAddress, } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; @@ -54,9 +55,26 @@ blockchainTests('Protocol Fee Unit Tests', env => { wethAssetData = await testContract.getWethAssetData.callAsync(); }); - async function createTestPoolAsync(stake: BigNumber, makers: string[]): Promise { + interface CreatePoolOpts { + operatorStake: Numberish; + membersStake: Numberish; + makers: string[]; + } + + async function createTestPoolAsync(opts: Partial): Promise { + const _opts = { + operatorStake: 0, + membersStake: 0, + makers: [], + ...opts, + }; const poolId = hexRandom(); - await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, stake, makers); + await testContract.createTestPool.awaitTransactionSuccessAsync( + poolId, + new BigNumber(_opts.operatorStake), + new BigNumber(_opts.membersStake), + _opts.makers, + ); return poolId; } @@ -154,7 +172,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { } it('should not transfer WETH if value is sent', async () => { - await createTestPoolAsync(minimumStake, []); + await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -164,8 +182,8 @@ blockchainTests('Protocol Fee Unit Tests', env => { assertNoWETHTransferLogs(receipt.logs); }); - it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + it('should credit pool if the maker is in a pool', async () => { + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -177,8 +195,8 @@ blockchainTests('Protocol Fee Unit Tests', env => { expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); - it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, []); + it('should not credit the pool if maker is not in a pool', async () => { + const poolId = await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -191,7 +209,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker should go to the same pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async () => { const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -225,7 +243,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { } it('should transfer WETH if no value is sent and the maker is not in a pool', async () => { - await createTestPoolAsync(minimumStake, []); + await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -236,7 +254,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -249,7 +267,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, []); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -262,7 +280,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker should go to the same pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async () => { const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -280,7 +298,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker in WETH then ETH should go to the same pool', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async (inWETH: boolean) => { await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -303,7 +321,7 @@ 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(minimumStake, [makerAddress, otherMakerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress, otherMakerAddress] }); const payAsync = async (_makerAddress: string) => { await testContract.payProtocolFee.awaitTransactionSuccessAsync( _makerAddress, @@ -322,8 +340,8 @@ blockchainTests('Protocol Fee Unit Tests', env => { it('fees paid to makers in different pools go to their respective pools', async () => { const [fee, otherFee] = _.times(2, () => getRandomPortion(DEFAULT_PROTOCOL_FEE_PAID)); const otherMakerAddress = randomAddress(); - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); - const otherPoolId = await createTestPoolAsync(minimumStake, [otherMakerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const otherPoolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [otherMakerAddress]}); const payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => { // prettier-ignore await testContract.payProtocolFee.awaitTransactionSuccessAsync( @@ -346,7 +364,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { describe('Dust stake', () => { it('credits pools with stake > minimum', async () => { - const poolId = await createTestPoolAsync(minimumStake.plus(1), [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake.plus(1), makers: [makerAddress] }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, @@ -358,7 +376,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('credits pools with stake == minimum', async () => { - const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, @@ -370,7 +388,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('does not credit pools with stake < minimum', async () => { - const poolId = await createTestPoolAsync(minimumStake.minus(1), [makerAddress]); + const poolId = await createTestPoolAsync({ operatorStake: minimumStake.minus(1), makers: [makerAddress] }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index faad8c8b78..739db688a4 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -6,6 +6,7 @@ import * as _ from 'lodash'; import { artifacts } from '../src'; import { FinalizerActor } from './actors/finalizer_actor'; +import { PoolOperatorActor } from './actors/pool_operator_actor'; import { StakerActor } from './actors/staker_actor'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { toBaseUnitAmount } from './utils/number_utils'; @@ -26,8 +27,9 @@ blockchainTests.resets('Testing Rewards', env => { let erc20Wrapper: ERC20Wrapper; // test parameters let stakers: StakerActor[]; + let poolOperatorStaker: StakerActor; let poolId: string; - let poolOperator: string; + let poolOperator: PoolOperatorActor; let finalizer: FinalizerActor; // tests before(async () => { @@ -43,7 +45,7 @@ blockchainTests.resets('Testing Rewards', env => { stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); // set up staking parameters await stakingApiWrapper.utils.setParamsAsync({ - minimumPoolStake: new BigNumber(0), + minimumPoolStake: new BigNumber(1), cobbDouglasAlphaNumerator: new BigNumber(1), cobbDouglasAlphaDenominator: new BigNumber(6), rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address, @@ -51,22 +53,26 @@ blockchainTests.resets('Testing Rewards', env => { zrxVaultAddress: stakingApiWrapper.zrxVaultContract.address, }); // setup stakers - stakers = [new StakerActor(actors[0], stakingApiWrapper), new StakerActor(actors[1], stakingApiWrapper)]; + stakers = actors.slice(0, 2).map(a => new StakerActor(a, stakingApiWrapper)); // setup pools - poolOperator = actors[2]; - poolId = await stakingApiWrapper.utils.createStakingPoolAsync(poolOperator, 0, true); // add operator as maker + poolOperator = new PoolOperatorActor(actors[2], stakingApiWrapper); + // Create a pool where all rewards go to members. + poolId = await poolOperator.createStakingPoolAsync(0, true); + // Stake something in the pool or else it won't get any rewards. + poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper); + await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(1)); // set exchange address await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress); // associate operators for tracking in Finalizer const operatorByPoolId: OperatorByPoolId = {}; - operatorByPoolId[poolId] = poolOperator; - operatorByPoolId[poolId] = poolOperator; + operatorByPoolId[poolId] = poolOperator.getOwner(); // associate actors with pools for tracking in Finalizer const membersByPoolId: MembersByPoolId = {}; membersByPoolId[poolId] = [actors[0], actors[1]]; - membersByPoolId[poolId] = [actors[0], actors[1]]; // create Finalizer actor finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, membersByPoolId); + // Skip to next epoch so operator stake is realized. + await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); }); describe('Reward Simulation', () => { interface EndBalances { @@ -154,7 +160,7 @@ blockchainTests.resets('Testing Rewards', env => { const fee = _fee !== undefined ? _fee : ZERO; if (!fee.eq(ZERO)) { await stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync( - poolOperator, + poolOperator.getOwner(), takerAddress, fee, { from: exchangeAddress, value: fee }, @@ -196,12 +202,7 @@ blockchainTests.resets('Testing Rewards', env => { (staker joins this epoch but is active next epoch)`, async () => { // delegate const amount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(amount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - amount, - ); + await stakers[0].stakeWithPoolAsync(poolId, amount); // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); @@ -213,12 +214,7 @@ blockchainTests.resets('Testing Rewards', env => { it('Should give pool reward to delegator', async () => { // delegate const amount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(amount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - amount, - ); + await stakers[0].stakeWithPoolAsync(poolId, amount); // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); // finalize @@ -232,22 +228,12 @@ blockchainTests.resets('Testing Rewards', env => { }); }); it('Should split pool reward between delegators', async () => { - // first staker delegates const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const totalStakeAmount = toBaseUnitAmount(10); - await stakers[0].stakeAsync(stakeAmounts[0]); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[0], - ); + // first staker delegates + await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]); // second staker delegates - await stakers[1].stakeAsync(stakeAmounts[1]); - await stakers[1].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[1], - ); + await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]); // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); // finalize @@ -299,24 +285,14 @@ blockchainTests.resets('Testing Rewards', env => { }); }); it('Should give pool reward to delegators only for the epoch during which they delegated', async () => { - // first staker delegates (epoch 0) const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const totalStakeAmount = toBaseUnitAmount(10); - await stakers[0].stakeAsync(stakeAmounts[0]); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[0], - ); + // first staker delegates (epoch 0) + await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // second staker delegates (epoch 1) - await stakers[1].stakeAsync(stakeAmounts[1]); - await stakers[1].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[1], - ); + await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]); // only the first staker will get this reward const rewardForOnlyFirstDelegator = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator); @@ -349,24 +325,14 @@ blockchainTests.resets('Testing Rewards', env => { return v.toNumber(); }); const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber); - // first staker delegates (epoch 0) const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const totalStakeAmount = toBaseUnitAmount(10); - await stakers[0].stakeAsync(stakeAmounts[0]); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[0], - ); + // first staker delegates (epoch 0) + await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // second staker delegates (epoch 1) - await stakers[1].stakeAsync(stakeAmounts[1]); - await stakers[1].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[1], - ); + await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]); // only the first staker will get this reward await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator); // earn a bunch of rewards @@ -386,14 +352,9 @@ blockchainTests.resets('Testing Rewards', env => { }); }); it('Should send existing rewards from reward vault to eth vault correctly when undelegating stake', async () => { - // first staker delegates (epoch 0) const stakeAmount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + // first staker delegates (epoch 0) + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // earn reward @@ -412,26 +373,16 @@ blockchainTests.resets('Testing Rewards', env => { }); }); it('Should send existing rewards from reward vault to eth vault correctly when delegating more stake', async () => { - // first staker delegates (epoch 0) const stakeAmount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + // first staker delegates (epoch 0) + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // earn reward const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); // add more stake - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // sanity check final balances await validateEndBalances({ stakerRewardVaultBalance_1: ZERO, @@ -453,23 +404,13 @@ blockchainTests.resets('Testing Rewards', env => { return v.toNumber(); }), ); - // first staker delegates (epoch 0) const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; - await stakers[0].stakeAsync(stakeAmounts[0]); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[0], - ); + // first staker delegates (epoch 0) + await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // second staker delegates (epoch 1) - await stakers[0].stakeAsync(stakeAmounts[1]); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmounts[1], - ); + await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]); // only the first staker will get this reward await payProtocolFeeAndFinalize(rewardBeforeAddingMoreStake); // earn a bunch of rewards @@ -488,12 +429,7 @@ blockchainTests.resets('Testing Rewards', env => { const rewardForDelegator = toBaseUnitAmount(10); const rewardNotForDelegator = toBaseUnitAmount(7); const stakeAmount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // earn reward @@ -534,12 +470,7 @@ blockchainTests.resets('Testing Rewards', env => { }), ); const stakeAmount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // earn reward @@ -566,12 +497,7 @@ blockchainTests.resets('Testing Rewards', env => { const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)]; const rewardNotForDelegator = toBaseUnitAmount(7); const stakeAmount = toBaseUnitAmount(4); - await stakers[0].stakeAsync(stakeAmount); - await stakers[0].moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards await payProtocolFeeAndFinalize(); // earn reward @@ -668,12 +594,7 @@ blockchainTests.resets('Testing Rewards', env => { const staker = stakers[0]; const stakeAmount = toBaseUnitAmount(5); // stake and delegate - await staker.stakeAsync(stakeAmount); - await staker.moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); // undelegate some stake @@ -703,12 +624,7 @@ blockchainTests.resets('Testing Rewards', env => { // stake and delegate both const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { - await staker.stakeAsync(stakeAmount); - await staker.moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await staker.stakeWithPoolAsync(poolId, stakeAmount); } // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); @@ -739,12 +655,7 @@ blockchainTests.resets('Testing Rewards', env => { // stake and delegate both const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { - await staker.stakeAsync(stakeAmount); - await staker.moveStakeAsync( - new StakeInfo(StakeStatus.Active), - new StakeInfo(StakeStatus.Delegated, poolId), - stakeAmount, - ); + await staker.stakeWithPoolAsync(poolId, stakeAmount); } // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance.ts b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts similarity index 93% rename from contracts/staking/test/unit_tests/delegator_reward_balance.ts rename to contracts/staking/test/unit_tests/delegator_reward_balance_test.ts index 1fb12adac8..42564bab3f 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts @@ -7,7 +7,7 @@ blockchainTests('delegator rewards', env => { before(async () => { testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync( - artifacts.TestLibFixedMath, + artifacts.TestDelegatorRewards, env.provider, env.txDefaults, artifacts, diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts new file mode 100644 index 0000000000..fbaea90989 --- /dev/null +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -0,0 +1,55 @@ +import { blockchainTests, expect, filterLogsToArguments, Numberish } from '@0x/contracts-test-utils'; + +import { + artifacts, + IStakingEventsEpochEndedEventArgs, + IStakingEventsEpochFinalizedEventArgs, + IStakingEventsEvents, + TestFinalizerContract, +} from '../../src'; + +blockchainTests.resets.only('finalization tests', env => { + let testContract: TestFinalizerContract; + const INITIAL_EPOCH = 0; + + before(async () => { + testContract = await TestFinalizerContract.deployFrom0xArtifactAsync( + artifacts.TestFinalizer, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('endEpoch()', () => { + it('emits an `EpochEnded` event', async () => { + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const [epochEndedEvent] = filterLogsToArguments( + receipt.logs, + IStakingEventsEvents.EpochEnded, + ); + expect(epochEndedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH); + expect(epochEndedEvent.numActivePools).to.bignumber.eq(0); + expect(epochEndedEvent.rewardsAvailable).to.bignumber.eq(0); + expect(epochEndedEvent.totalFeesCollected).to.bignumber.eq(0); + expect(epochEndedEvent.totalWeightedStake).to.bignumber.eq(0); + }); + + it('advances the epoch', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const currentEpoch = await testContract.getCurrentEpoch.callAsync(); + expect(currentEpoch).to.be.bignumber.eq(INITIAL_EPOCH + 1); + }); + + it('immediately finalizes if there are no active pools', async () => { + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const [epochFinalizedEvent] = filterLogsToArguments( + receipt.logs, + IStakingEventsEvents.EpochFinalized, + ); + expect(epochFinalizedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH); + expect(epochFinalizedEvent.rewardsPaid).to.bignumber.eq(0); + expect(epochFinalizedEvent.rewardsRemaining).to.bignumber.eq(0); + }); + }); +}); From ada1de429cec51b044332c482e4b13b842920dc5 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Sep 2019 21:34:48 -0400 Subject: [PATCH 16/52] `@0x/order-utils`: Add `PreviousEpochNotFinalizedError` to `StakingRevertErrors`. --- packages/order-utils/CHANGELOG.json | 4 ++++ .../order-utils/src/staking_revert_errors.ts | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 3d4ad7302a..1e0aa3b5db 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -97,6 +97,10 @@ { "note": "Add `CumulativeRewardIntervalError`.", "pr": 2154 + }, + { + "note": "Add `PreviousEpochNotFinalizedError` to `StakingRevertErrors`.", + "pr": 2155 } ] }, diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index cb32c497d8..227dbdbbae 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -254,9 +254,23 @@ export class CumulativeRewardIntervalError extends RevertError { } } +export class PreviousEpochNotFinalizedError extends RevertError { + constructor( + closingEpoch?: BigNumber | number | string, + unfinalizedPoolsRemaining?: BigNumber | number | string, + ) { + super( + 'PreviousEpochNotFinalizedError', + 'PreviousEpochNotFinalizedError(uint256 closingEpoch, uint256 unfinalizedPoolsRemaining)', + { closingEpoch, unfinalizedPoolsRemaining }, + ); + } +} + const types = [ AmountExceedsBalanceOfPoolError, BlockTimestampTooLowError, + CumulativeRewardIntervalError, EthVaultNotSetError, ExchangeAddressAlreadyRegisteredError, ExchangeAddressNotRegisteredError, @@ -275,10 +289,10 @@ const types = [ OnlyCallableIfNotInCatastrophicFailureError, OperatorShareError, PoolExistenceError, + PreviousEpochNotFinalizedError, + ProxyDestinationCannotBeNilError, RewardVaultNotSetError, WithdrawAmountExceedsMemberBalanceError, - ProxyDestinationCannotBeNilError, - CumulativeRewardIntervalError, ]; // Register the types we've defined. From f5ab1e6f867d98f8fedfa56ca3045639a902eb22 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 14 Sep 2019 04:28:56 -0400 Subject: [PATCH 17/52] `@0x/contracts-staking`: Reduce code duplication in `MixinFinalizer` and add unit tests for it. --- .../staking_pools/MixinStakingPoolRewards.sol | 8 +- .../contracts/src/sys/MixinAbstract.sol | 2 +- .../contracts/src/sys/MixinFinalizer.sol | 232 ++++----- .../staking/contracts/test/TestFinalizer.sol | 104 +++- .../staking/test/unit_tests/finalizer_test.ts | 480 +++++++++++++++++- 5 files changed, 668 insertions(+), 158 deletions(-) diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 27eb1b9616..a81005a0af 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -63,13 +63,13 @@ contract MixinStakingPoolRewards is view returns (uint256 reward) { - IStructs.PoolRewards memory unfinalizedPoolReward = - _getUnfinalizedPoolReward(poolId); + IStructs.PoolRewards memory unfinalizedPoolRewards = + _getUnfinalizedPoolRewards(poolId); reward = _computeRewardBalanceOfDelegator( poolId, member, - unfinalizedPoolReward.membersReward, - unfinalizedPoolReward.membersStake + unfinalizedPoolRewards.membersReward, + unfinalizedPoolRewards.membersStake ); } diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol index e01a61b372..d2be6624e0 100644 --- a/contracts/staking/contracts/src/sys/MixinAbstract.sol +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -30,7 +30,7 @@ contract MixinAbstract { /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. /// @return rewards Amount of rewards for this pool. - function _getUnfinalizedPoolReward(bytes32 poolId) + function _getUnfinalizedPoolRewards(bytes32 poolId) internal view returns (IStructs.PoolRewards memory rewards); diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 1420f010c1..6f4a679ced 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -154,28 +154,19 @@ contract MixinFinalizer is continue; } - // Clear the pool state so we don't finalize it again, and to - // recoup some gas. + // Clear the pool state so we don't finalize it again, and to recoup + // some gas. delete activePools[poolId]; - // Credit the pool with rewards. - // We will transfer the total rewards to the vault at the end. IStructs.PoolRewards memory poolRewards = - _creditRewardToPool(poolId, pool); + _finalizePool(epoch, poolId, pool, true); + rewardsPaid = rewardsPaid.safeAdd( poolRewards.operatorReward + poolRewards.membersReward ); // Decrease the number of unfinalized pools left. poolsRemaining = poolsRemaining.safeSub(1); - - // Emit an event. - emit RewardsPaid( - epoch, - poolId, - poolRewards.operatorReward, - poolRewards.membersReward - ); } // Deposit all the rewards at once into the RewardVault. @@ -216,53 +207,32 @@ contract MixinFinalizer is if (epoch == 0) { return rewards; } - - // Get the active pool. - mapping (bytes32 => IStructs.ActivePool) storage activePools = - _getActivePoolsFromEpoch(epoch - 1); - IStructs.ActivePool memory pool = activePools[poolId]; - - // Ignore pools that weren't active. - if (pool.feesCollected == 0) { - return rewards; - } - - // Clear the pool state so we don't finalize it again, and to recoup - // some gas. - delete activePools[poolId]; - - // Credit the pool with rewards. - // We will transfer the total rewards to the vault at the end. - rewards = _creditRewardToPool(poolId, pool); - uint256 totalReward = rewards.membersReward + rewards.operatorReward; - totalRewardsPaidLastEpoch = - totalRewardsPaidLastEpoch.safeAdd(totalReward); - - // Decrease the number of unfinalized pools left. - uint256 poolsRemaining = - unfinalizedPoolsRemaining = - unfinalizedPoolsRemaining.safeSub(1); - - // Emit an event. - emit RewardsPaid( + rewards = _finalizePool( epoch, poolId, - rewards.operatorReward, - rewards.membersReward + _getActivePoolFromEpoch(epoch - 1, poolId), + false ); + } - // Deposit all the rewards at once into the RewardVault. - _depositIntoStakingPoolRewardVault(totalReward); - - // If there are no more unfinalized pools remaining, the epoch is - // finalized. - if (poolsRemaining == 0) { - emit EpochFinalized( - epoch - 1, - totalRewardsPaidLastEpoch, - unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) - ); + /// @dev Computes the reward owed to a pool during finalization. + /// Does nothing if the pool is already finalized. + /// @param poolId The pool's ID. + /// @return rewards Amount of rewards for this pool. + function _getUnfinalizedPoolRewards(bytes32 poolId) + internal + view + returns (IStructs.PoolRewards memory rewards) + { + uint256 epoch = getCurrentEpoch(); + // There are no pools to finalize at epoch 0. + if (epoch == 0) { + return rewards; } + rewards = _getUnfinalizedPoolRewards( + poolId, + _getActivePoolFromEpoch(epoch - 1, poolId) + ); } /// @dev Get an active pool from an epoch by its ID. @@ -295,50 +265,6 @@ contract MixinFinalizer is activePools = activePoolsByEpoch[epoch % 2]; } - /// @dev Computes the reward owed to a pool during finalization. - /// Does nothing if the pool is already finalized. - /// @param poolId The pool's ID. - /// @return rewards Amount of rewards for this pool. - function _getUnfinalizedPoolReward(bytes32 poolId) - internal - view - returns (IStructs.PoolRewards memory rewards) - { - uint256 epoch = getCurrentEpoch(); - // There can't be any rewards in the first epoch. - if (epoch == 0) { - return rewards; - } - - IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(epoch - 1, poolId); - // There can't be any rewards if the pool was active or if it has - // no stake. - if (pool.feesCollected == 0 || pool.weightedStake == 0) { - return rewards; - } - - // Use the cobb-douglas function to compute the total reward. - uint256 totalReward = LibCobbDouglas._cobbDouglas( - unfinalizedRewardsAvailable, - pool.feesCollected, - unfinalizedTotalFeesCollected, - pool.weightedStake, - unfinalizedTotalWeightedStake, - cobbDouglasAlphaNumerator, - cobbDouglasAlphaDenomintor - ); - - // Split the reward between the operator and delegators. - if (pool.membersStake == 0) { - rewards.operatorReward = totalReward; - } else { - (rewards.operatorReward, rewards.membersReward) = - _splitAmountBetweenOperatorAndMembers(poolId, totalReward); - } - rewards.membersStake = pool.membersStake; - } - /// @dev Converts the entire WETH balance of the contract into ETH. function _unwrapWETH() internal { uint256 wethBalance = IEtherToken(WETH_ADDRESS) @@ -354,7 +280,7 @@ contract MixinFinalizer is /// @param amount Amount to to split. /// @return operatorPortion Portion of `amount` attributed to the operator. /// @return membersPortion Portion of `amount` attributed to the pool. - function _splitAmountBetweenOperatorAndMembers( + function _splitRewardAmountBetweenOperatorAndMembers( bytes32 poolId, uint256 amount ) @@ -390,21 +316,21 @@ contract MixinFinalizer is ); } - /// @dev Computes the reward owed to a pool during finalization and - /// credits it to that pool for the CURRENT epoch. + /// @dev Computes the reward owed to a pool during finalization. /// @param poolId The pool's ID. - /// @param pool The pool. + /// @param pool The active pool. /// @return rewards Amount of rewards for this pool. - function _creditRewardToPool( + function _getUnfinalizedPoolRewards( bytes32 poolId, IStructs.ActivePool memory pool ) private + view returns (IStructs.PoolRewards memory rewards) { // There can't be any rewards if the pool was active or if it has // no stake. - if (pool.feesCollected == 0 || pool.weightedStake == 0) { + if (pool.feesCollected == 0) { return rewards; } @@ -419,15 +345,59 @@ contract MixinFinalizer is cobbDouglasAlphaDenomintor ); - // Credit the pool the reward in the RewardVault. - (rewards.operatorReward, rewards.membersReward) = - _recordDepositInRewardVaultFor( - poolId, - totalReward, - // If no delegated stake, all rewards go to the operator. - pool.membersStake == 0 - ); + // Split the reward between the operator and delegators. + if (pool.membersStake == 0) { + rewards.operatorReward = totalReward; + } else { + (rewards.operatorReward, rewards.membersReward) = + _splitRewardAmountBetweenOperatorAndMembers( + poolId, + totalReward + ); + } rewards.membersStake = pool.membersStake; + } + + /// @dev Either fully or partially finalizes a single pool that was active + /// in the previous epoch. If `batchedMode` is `true`, this function + /// will NOT: + /// - transfer ether into the reward vault + /// - update `poolsRemaining` + /// - update `totalRewardsPaidLastEpoch` + /// - clear the pool from `activePoolsByEpoch` + /// - emit an `EpochFinalized` event. + /// @param epoch The current epoch. + /// @param poolId The pool ID to finalize. + /// @param pool The active pool to finalize. + /// @param batchedMode Only calculate and credit rewards. + /// @return rewards Rewards. + /// @return rewards The rewards credited to the pool. + function _finalizePool( + uint256 epoch, + bytes32 poolId, + IStructs.ActivePool memory pool, + bool batchedMode + ) + private + returns (IStructs.PoolRewards memory rewards) + { + // Ignore pools that weren't active. + if (pool.feesCollected == 0) { + return rewards; + } + + // Compute the rewards. + rewards = _getUnfinalizedPoolRewards(poolId, pool); + uint256 totalReward = + rewards.membersReward.safeAdd(rewards.operatorReward); + + // Credit the pool the rewards in the RewardVault. + _recordDepositInRewardVaultFor( + poolId, + totalReward, + // If no delegated stake, all rewards go to the operator. + pool.membersStake == 0 + ); // Sync delegator rewards. if (rewards.membersReward != 0) { @@ -437,5 +407,41 @@ contract MixinFinalizer is pool.membersStake ); } + + // Emit an event. + emit RewardsPaid( + epoch, + poolId, + rewards.operatorReward, + rewards.membersReward + ); + + if (batchedMode) { + return rewards; + } + + // Clear the pool state so we don't finalize it again, and to recoup + // some gas. + delete _getActivePoolsFromEpoch(epoch)[poolId]; + + if (totalReward > 0) { + totalRewardsPaidLastEpoch = + totalRewardsPaidLastEpoch.safeAdd(totalReward); + _depositIntoStakingPoolRewardVault(totalReward); + } + + // Decrease the number of unfinalized pools left. + uint256 poolsRemaining = unfinalizedPoolsRemaining; + unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1); + + // If there are no more unfinalized pools remaining, the epoch is + // finalized. + if (poolsRemaining == 0) { + emit EpochFinalized( + epoch - 1, + totalRewardsPaidLastEpoch, + unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) + ); + } } } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index b56013b09f..f98c4809a5 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -26,37 +26,58 @@ import "../src/Staking.sol"; contract TestFinalizer is Staking { - struct RecordedReward { - uint256 membersReward; - uint256 membersStake; - } + event RecordRewardForDelegatorsCall( + bytes32 poolId, + uint256 membersReward, + uint256 membersStake + ); + + event RecordDepositInRewardVaultForCall( + bytes32 poolId, + uint256 totalReward, + bool operatorOnly + ); + + event DepositIntoStakingPoolRewardVaultCall( + uint256 amount + ); - struct DepositedReward { - uint256 totalReward; - bool operatorOnly; - } mapping (bytes32 => uint32) internal _operatorSharesByPool; - mapping (bytes32 => RecordedReward) internal _recordedRewardsByPool; - mapping (bytes32 => DepositedReward) internal _depositedRewardsByPool; + constructor() public { + init(); + } + + /// @dev Get finalization-related state variables. function getFinalizationState() external view returns ( + uint256 _balance, + uint256 _currentEpoch, uint256 _closingEpoch, + uint256 _numActivePoolsThisEpoch, + uint256 _totalFeesCollectedThisEpoch, + uint256 _totalWeightedStakeThisEpoch, uint256 _unfinalizedPoolsRemaining, uint256 _unfinalizedRewardsAvailable, uint256 _unfinalizedTotalFeesCollected, uint256 _unfinalizedTotalWeightedStake ) { + _balance = address(this).balance; + _currentEpoch = currentEpoch; _closingEpoch = currentEpoch - 1; + _numActivePoolsThisEpoch = numActivePoolsThisEpoch; + _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; + _totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch; _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; _unfinalizedRewardsAvailable = unfinalizedRewardsAvailable; _unfinalizedTotalFeesCollected = unfinalizedTotalFeesCollected; _unfinalizedTotalWeightedStake = unfinalizedTotalWeightedStake; } + /// @dev Activate a pool in the current epoch. function addActivePool( bytes32 poolId, uint32 operatorShare, @@ -66,9 +87,10 @@ contract TestFinalizer is ) external { + require(feesCollected > 0, "FEES_MUST_BE_NONZERO"); mapping (bytes32 => IStructs.ActivePool) storage activePools = _getActivePoolsFromEpoch(currentEpoch); - assert(activePools[poolId].feesCollected == 0); + require(feesCollected > 0, "POOL_ALREADY_ADDED"); _operatorSharesByPool[poolId] = operatorShare; activePools[poolId] = IStructs.ActivePool({ feesCollected: feesCollected, @@ -80,6 +102,34 @@ contract TestFinalizer is numActivePoolsThisEpoch += 1; } + /// @dev Expose `_getUnfinalizedPoolReward()` + function internalGetUnfinalizedPoolRewards(bytes32 poolId) + external + view + returns (IStructs.PoolRewards memory rewards) + { + rewards = _getUnfinalizedPoolRewards(poolId); + } + + + /// @dev Expose `_getActivePoolFromEpoch`. + function internalGetActivePoolFromEpoch(uint256 epoch, bytes32 poolId) + external + view + returns (IStructs.ActivePool memory pool) + { + pool = _getActivePoolFromEpoch(epoch, poolId); + } + + + /// @dev Expose `_finalizePool()` + function internalFinalizePool(bytes32 poolId) + external + returns (IStructs.PoolRewards memory rewards) + { + rewards = _finalizePool(poolId); + } + /// @dev Overridden to just store inputs. function _recordRewardForDelegators( bytes32 poolId, @@ -88,10 +138,16 @@ contract TestFinalizer is ) internal { - _recordedRewardsByPool[poolId] = RecordedReward({ - membersReward: membersReward, - membersStake: membersStake - }); + emit RecordRewardForDelegatorsCall( + poolId, + membersReward, + membersStake + ); + } + + /// @dev Overridden to store inputs and do some really basic math. + function _depositIntoStakingPoolRewardVault(uint256 amount) internal { + emit DepositIntoStakingPoolRewardVaultCall(amount); } /// @dev Overridden to store inputs and do some really basic math. @@ -106,21 +162,25 @@ contract TestFinalizer is uint256 membersPortion ) { - _depositedRewardsByPool[poolId] = DepositedReward({ - totalReward: totalReward, - operatorOnly: operatorOnly - }); + emit RecordDepositInRewardVaultForCall( + poolId, + totalReward, + operatorOnly + ); if (operatorOnly) { operatorPortion = totalReward; } else { (operatorPortion, membersPortion) = - _splitAmountBetweenOperatorAndMembers(poolId, totalReward); + _splitRewardAmountBetweenOperatorAndMembers( + poolId, + totalReward + ); } } /// @dev Overridden to do some really basic math. - function _splitAmountBetweenOperatorAndMembers( + function _splitRewardAmountBetweenOperatorAndMembers( bytes32 poolId, uint256 amount ) @@ -133,7 +193,7 @@ contract TestFinalizer is membersPortion = amount - operatorPortion; } - /// @dev Overriden to always succeed. + /// @dev Overriden to just increase the epoch counter. function _goToNextEpoch() internal { currentEpoch += 1; } diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index fbaea90989..acae0318b9 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -1,55 +1,499 @@ -import { blockchainTests, expect, filterLogsToArguments, Numberish } from '@0x/contracts-test-utils'; +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + hexRandom, + Numberish, +} from '@0x/contracts-test-utils'; +import { StakingRevertErrors } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import { LogEntry } from 'ethereum-types'; +import * as _ from 'lodash'; import { artifacts, IStakingEventsEpochEndedEventArgs, IStakingEventsEpochFinalizedEventArgs, IStakingEventsEvents, + IStakingEventsRewardsPaidEventArgs, TestFinalizerContract, + TestFinalizerDepositIntoStakingPoolRewardVaultCallEventArgs, + TestFinalizerEvents, } from '../../src'; +import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; blockchainTests.resets.only('finalization tests', env => { - let testContract: TestFinalizerContract; + const { ONE_ETHER, ZERO_AMOUNT } = constants; const INITIAL_EPOCH = 0; + const INITIAL_BALANCE = toBaseUnitAmount(32); + let senderAddress: string; + let testContract: TestFinalizerContract; before(async () => { + [senderAddress] = await env.getAccountAddressesAsync(); testContract = await TestFinalizerContract.deployFrom0xArtifactAsync( artifacts.TestFinalizer, env.provider, env.txDefaults, artifacts, ); + // Give the contract a balance. + await sendEtherAsync(testContract.address, INITIAL_BALANCE); }); + async function sendEtherAsync(to: string, amount: Numberish): Promise { + await env.web3Wrapper.awaitTransactionSuccessAsync( + await env.web3Wrapper.sendTransactionAsync({ + from: senderAddress, + to, + value: new BigNumber(amount), + }), + ); + } + + interface ActivePoolOpts { + poolId: string; + operatorShare: number; + feesCollected: Numberish; + membersStake: Numberish; + weightedStake: Numberish; + } + + async function addActivePoolAsync(opts?: Partial): Promise { + const _opts = { + poolId: hexRandom(), + operatorShare: Math.random(), + feesCollected: getRandomInteger(0, ONE_ETHER), + membersStake: getRandomInteger(0, ONE_ETHER), + weightedStake: getRandomInteger(0, ONE_ETHER), + ...opts, + }; + await testContract.addActivePool.awaitTransactionSuccessAsync( + _opts.poolId, + new BigNumber(_opts.operatorShare * constants.PPM_DENOMINATOR).integerValue(), + new BigNumber(_opts.feesCollected), + new BigNumber(_opts.membersStake), + new BigNumber(_opts.weightedStake), + ); + return _opts; + } + + interface FinalizationState { + balance: Numberish; + currentEpoch: number; + closingEpoch: number; + numActivePoolsThisEpoch: number; + totalFeesCollectedThisEpoch: Numberish; + totalWeightedStakeThisEpoch: Numberish; + unfinalizedPoolsRemaining: number; + unfinalizedRewardsAvailable: Numberish; + unfinalizedTotalFeesCollected: Numberish; + unfinalizedTotalWeightedStake: Numberish; + } + + async function getFinalizationStateAsync(): Promise { + const r = await testContract.getFinalizationState.callAsync(); + return { + balance: r[0], + currentEpoch: r[1].toNumber(), + closingEpoch: r[2].toNumber(), + numActivePoolsThisEpoch: r[3].toNumber(), + totalFeesCollectedThisEpoch: r[4], + totalWeightedStakeThisEpoch: r[5], + unfinalizedPoolsRemaining: r[6].toNumber(), + unfinalizedRewardsAvailable: r[7], + unfinalizedTotalFeesCollected: r[8], + unfinalizedTotalWeightedStake: r[9], + }; + } + + async function assertFinalizationStateAsync( + expected: Partial, + ): Promise { + const actual = await getFinalizationStateAsync(); + if (expected.balance !== undefined) { + expect(actual.balance).to.bignumber.eq(expected.balance); + } + if (expected.currentEpoch !== undefined) { + expect(actual.currentEpoch).to.eq(expected.currentEpoch); + } + if (expected.closingEpoch !== undefined) { + expect(actual.closingEpoch).to.eq(expected.closingEpoch); + } + if (expected.numActivePoolsThisEpoch !== undefined) { + expect(actual.numActivePoolsThisEpoch) + .to.eq(expected.numActivePoolsThisEpoch); + } + if (expected.totalFeesCollectedThisEpoch !== undefined) { + expect(actual.totalFeesCollectedThisEpoch) + .to.bignumber.eq(expected.totalFeesCollectedThisEpoch); + } + if (expected.totalWeightedStakeThisEpoch !== undefined) { + expect(actual.totalWeightedStakeThisEpoch) + .to.bignumber.eq(expected.totalWeightedStakeThisEpoch); + } + if (expected.unfinalizedPoolsRemaining !== undefined) { + expect(actual.unfinalizedPoolsRemaining) + .to.eq(expected.unfinalizedPoolsRemaining); + } + if (expected.unfinalizedRewardsAvailable !== undefined) { + expect(actual.unfinalizedRewardsAvailable) + .to.bignumber.eq(expected.unfinalizedRewardsAvailable); + } + if (expected.unfinalizedTotalFeesCollected !== undefined) { + expect(actual.unfinalizedTotalFeesCollected) + .to.bignumber.eq(expected.unfinalizedTotalFeesCollected); + } + if (expected.unfinalizedTotalFeesCollected !== undefined) { + expect(actual.unfinalizedTotalFeesCollected) + .to.bignumber.eq(expected.unfinalizedTotalFeesCollected); + } + } + + function assertEpochEndedEvent( + logs: LogEntry[], + args: Partial, + ): void { + const events = filterLogsToArguments( + logs, + IStakingEventsEvents.EpochEnded, + ); + expect(events.length).to.eq(1); + if (args.epoch !== undefined) { + expect(events[0].epoch).to.bignumber.eq(INITIAL_EPOCH); + } + if (args.numActivePools !== undefined) { + expect(events[0].numActivePools).to.bignumber.eq(args.numActivePools); + } + if (args.rewardsAvailable !== undefined) { + expect(events[0].rewardsAvailable).to.bignumber.eq(args.rewardsAvailable); + } + if (args.totalFeesCollected !== undefined) { + expect(events[0].totalFeesCollected).to.bignumber.eq(args.totalFeesCollected); + } + if (args.totalWeightedStake !== undefined) { + expect(events[0].totalWeightedStake).to.bignumber.eq(args.totalWeightedStake); + } + } + + function assertEpochFinalizedEvent( + logs: LogEntry[], + args: Partial, + ): void { + const events = getEpochFinalizedEvents(logs); + expect(events.length).to.eq(1); + if (args.epoch !== undefined) { + expect(events[0].epoch).to.bignumber.eq(args.epoch); + } + if (args.rewardsPaid !== undefined) { + expect(events[0].rewardsPaid).to.bignumber.eq(args.rewardsPaid); + } + if (args.rewardsRemaining !== undefined) { + expect(events[0].rewardsRemaining).to.bignumber.eq(args.rewardsRemaining); + } + } + + function assertDepositIntoStakingPoolRewardVaultCallEvent( + logs: LogEntry[], + amount?: Numberish, + ): void { + const events = filterLogsToArguments( + logs, + TestFinalizerEvents.DepositIntoStakingPoolRewardVaultCall, + ); + expect(events.length).to.eq(1); + if (amount !== undefined) { + expect(events[0].amount).to.bignumber.eq(amount); + } + } + + function getEpochFinalizedEvents(logs: LogEntry[]): IStakingEventsEpochFinalizedEventArgs[] { + return filterLogsToArguments( + logs, + IStakingEventsEvents.EpochFinalized, + ); + } + + function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] { + return filterLogsToArguments( + logs, + IStakingEventsEvents.RewardsPaid, + ); + } + + async function getCurrentEpochAsync(): Promise { + return (await testContract.getCurrentEpoch.callAsync()).toNumber(); + } + describe('endEpoch()', () => { + it('advances the epoch', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const currentEpoch = await testContract.getCurrentEpoch.callAsync(); + expect(currentEpoch).to.bignumber.eq(INITIAL_EPOCH + 1); + }); + it('emits an `EpochEnded` event', async () => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const [epochEndedEvent] = filterLogsToArguments( + assertEpochEndedEvent( receipt.logs, - IStakingEventsEvents.EpochEnded, + { + epoch: new BigNumber(INITIAL_EPOCH), + numActivePools: ZERO_AMOUNT, + rewardsAvailable: INITIAL_BALANCE, + totalFeesCollected: ZERO_AMOUNT, + totalWeightedStake: ZERO_AMOUNT, + }, ); - expect(epochEndedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH); - expect(epochEndedEvent.numActivePools).to.bignumber.eq(0); - expect(epochEndedEvent.rewardsAvailable).to.bignumber.eq(0); - expect(epochEndedEvent.totalFeesCollected).to.bignumber.eq(0); - expect(epochEndedEvent.totalWeightedStake).to.bignumber.eq(0); }); - it('advances the epoch', async () => { - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const currentEpoch = await testContract.getCurrentEpoch.callAsync(); - expect(currentEpoch).to.be.bignumber.eq(INITIAL_EPOCH + 1); + it('immediately finalizes if there are no active pools', async () => { + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + assertEpochFinalizedEvent( + receipt.logs, + { + epoch: new BigNumber(INITIAL_EPOCH), + rewardsPaid: ZERO_AMOUNT, + rewardsRemaining: INITIAL_BALANCE, + }, + ); }); - it('immediately finalizes if there are no active pools', async () => { + it('does not immediately finalize if there is an active pool', async () => { + await addActivePoolAsync(); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const [epochFinalizedEvent] = filterLogsToArguments( + const events = filterLogsToArguments( receipt.logs, IStakingEventsEvents.EpochFinalized, ); - expect(epochFinalizedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH); - expect(epochFinalizedEvent.rewardsPaid).to.bignumber.eq(0); - expect(epochFinalizedEvent.rewardsRemaining).to.bignumber.eq(0); + expect(events).to.deep.eq([]); + }); + + it('clears the next epoch\'s finalization state', async () => { + // Add a pool so there is state to clear. + await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + assertFinalizationStateAsync({ + currentEpoch: INITIAL_EPOCH + 1, + closingEpoch: INITIAL_EPOCH, + numActivePoolsThisEpoch: 0, + totalFeesCollectedThisEpoch: 0, + totalWeightedStakeThisEpoch: 0, + }); + }); + + it('prepares finalization state', async () => { + // Add a pool so there is state to clear. + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + assertFinalizationStateAsync({ + unfinalizedPoolsRemaining: 1, + unfinalizedRewardsAvailable: INITIAL_BALANCE, + unfinalizedTotalFeesCollected: pool.feesCollected, + unfinalizedTotalWeightedStake: pool.weightedStake, + }); + }); + + it('reverts if the prior epoch is unfinalized', async () => { + await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const tx = testContract.endEpoch.awaitTransactionSuccessAsync(); + const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError( + INITIAL_EPOCH, + 1, + ); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('finalizePools()', () => { + it('does nothing if there were no active pools', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const poolId = hexRandom(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([poolId]); + expect(receipt.logs).to.deep.eq([]); + }); + + it('does nothing if no pools are passed in', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([]); + expect(receipt.logs).to.deep.eq([]); + }); + + it('can finalize a single pool', async () => { + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents.length).to.eq(1); + expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1); + expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId); + assertEpochFinalizedEvent( + receipt.logs, + { + epoch: new BigNumber(INITIAL_EPOCH), + rewardsPaid: INITIAL_BALANCE, + }, + ); + assertDepositIntoStakingPoolRewardVaultCallEvent( + receipt.logs, + INITIAL_BALANCE, + ); + }); + + it('can finalize multiple pools', async () => { + const nextEpoch = INITIAL_EPOCH + 1; + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const poolIds = pools.map(p => p.poolId); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents.length).to.eq(pools.length); + for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as + Array<[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]>) { + expect(event.epoch).to.bignumber.eq(nextEpoch); + expect(event.poolId).to.eq(pool.poolId); + } + assertEpochFinalizedEvent( + receipt.logs, + { epoch: new BigNumber(INITIAL_EPOCH) }, + ); + assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs); + }); + + it('ignores a non-active pool', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const nonActivePoolId = hexRandom(); + const poolIds = _.shuffle([...pools.map(p => p.poolId), nonActivePoolId]); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents.length).to.eq(pools.length); + for (const event of rewardsPaidEvents) { + expect(event.poolId).to.not.eq(nonActivePoolId); + } + }); + + it('ignores a finalized pool', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const poolIds = pools.map(p => p.poolId); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const [finalizedPool] = _.sampleSize(pools, 1); + await testContract.finalizePools.awaitTransactionSuccessAsync([finalizedPool.poolId]); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents.length).to.eq(pools.length - 1); + for (const event of rewardsPaidEvents) { + expect(event.poolId).to.not.eq(finalizedPool.poolId); + } + }); + + it('resets pool state after finalizing it', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pool = _.sample(pools) as ActivePoolOpts; + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + const poolState = await testContract + .internalGetActivePoolFromEpoch + .callAsync(new BigNumber(INITIAL_EPOCH), pool.poolId); + expect(poolState.feesCollected).to.bignumber.eq(0); + expect(poolState.weightedStake).to.bignumber.eq(0); + expect(poolState.membersStake).to.bignumber.eq(0); + }); + }); + + describe('lifecycle', () => { + it('can advance the epoch after the prior epoch is finalized', async () => { + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + return expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); + }); + + it('does not reward a pool that was only active 2 epochs ago', async () => { + const pool1 = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents).to.deep.eq([]); + }); + + it('does not reward a pool that was only active 3 epochs ago', async () => { + const pool1 = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 3); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents).to.deep.eq([]); + }); + }); + + interface PoolRewards { + operatorReward: Numberish; + membersReward: Numberish; + membersStake: Numberish; + } + + function assertPoolRewards(actual: PoolRewards, expected: Partial): void { + if (expected.operatorReward !== undefined) { + expect(actual.operatorReward).to.bignumber.eq(actual.operatorReward); + } + if (expected.membersReward !== undefined) { + expect(actual.membersReward).to.bignumber.eq(actual.membersReward); + } + if (expected.membersStake !== undefined) { + expect(actual.membersStake).to.bignumber.eq(actual.membersStake); + } + } + + describe('_getUnfinalizedPoolReward()', () => { + const ZERO_REWARDS = { + operatorReward: 0, + membersReward: 0, + membersStake: 0, + }; + + it('returns empty if epoch is 0', async () => { + const poolId = hexRandom(); + const rewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(poolId); + assertPoolRewards(rewards, ZERO_REWARDS); + }); + + it('returns empty if pool was not active', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const poolId = hexRandom(); + const rewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(poolId); + assertPoolRewards(rewards, ZERO_REWARDS); + }); + + it('returns empty if pool was only active in the 2 epochs ago', async () => { + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + const rewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(rewards, ZERO_REWARDS); + }); + + it('returns empty if pool was already finalized', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pool = _.sample(pools) as ActivePoolOpts; + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + const rewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(rewards, ZERO_REWARDS); }); }); }); From 03c59fdaf7732128901931a8abf13410c5b823e2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 14 Sep 2019 05:34:40 -0400 Subject: [PATCH 18/52] `@0x/contracts-staking`: More `MixinFinalizer` unit tests. --- .../contracts/src/sys/MixinFinalizer.sol | 2 +- .../staking/contracts/test/TestFinalizer.sol | 24 ++++ .../staking/test/unit_tests/finalizer_test.ts | 119 +++++++++++++++++- 3 files changed, 139 insertions(+), 6 deletions(-) diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 6f4a679ced..f3bc3d6f27 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -342,7 +342,7 @@ contract MixinFinalizer is pool.weightedStake, unfinalizedTotalWeightedStake, cobbDouglasAlphaNumerator, - cobbDouglasAlphaDenomintor + cobbDouglasAlphaDenominator ); // Split the reward between the operator and delegators. diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index f98c4809a5..83159f9f7e 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -20,6 +20,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "../src/interfaces/IStructs.sol"; +import "../src/libs/LibCobbDouglas.sol"; import "../src/Staking.sol"; @@ -102,6 +103,29 @@ contract TestFinalizer is numActivePoolsThisEpoch += 1; } + /// @dev Compute Cobb-Douglas. + function cobbDouglas( + uint256 totalRewards, + uint256 ownerFees, + uint256 totalFees, + uint256 ownerStake, + uint256 totalStake + ) + external + view + returns (uint256 ownerRewards) + { + ownerRewards = LibCobbDouglas._cobbDouglas( + totalRewards, + ownerFees, + totalFees, + ownerStake, + totalStake, + cobbDouglasAlphaNumerator, + cobbDouglasAlphaDenominator + ); + } + /// @dev Expose `_getUnfinalizedPoolReward()` function internalGetUnfinalizedPoolRewards(bytes32 poolId) external diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index acae0318b9..4232938581 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -63,7 +63,7 @@ blockchainTests.resets.only('finalization tests', env => { async function addActivePoolAsync(opts?: Partial): Promise { const _opts = { poolId: hexRandom(), - operatorShare: Math.random(), + operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR, feesCollected: getRandomInteger(0, ONE_ETHER), membersStake: getRandomInteger(0, ONE_ETHER), weightedStake: getRandomInteger(0, ONE_ETHER), @@ -445,16 +445,42 @@ blockchainTests.resets.only('finalization tests', env => { function assertPoolRewards(actual: PoolRewards, expected: Partial): void { if (expected.operatorReward !== undefined) { - expect(actual.operatorReward).to.bignumber.eq(actual.operatorReward); + expect(actual.operatorReward).to.bignumber.eq(expected.operatorReward); } if (expected.membersReward !== undefined) { - expect(actual.membersReward).to.bignumber.eq(actual.membersReward); + expect(actual.membersReward).to.bignumber.eq(expected.membersReward); } if (expected.membersStake !== undefined) { - expect(actual.membersStake).to.bignumber.eq(actual.membersStake); + expect(actual.membersStake).to.bignumber.eq(expected.membersStake); } } + async function callCobbDouglasAsync( + totalRewards: Numberish, + fees: Numberish, + totalFees: Numberish, + stake: Numberish, + totalStake: Numberish, + ): Promise { + return testContract.cobbDouglas.callAsync( + new BigNumber(totalRewards), + new BigNumber(fees), + new BigNumber(totalFees), + new BigNumber(stake), + new BigNumber(totalStake), + ); + } + + function splitRewards(pool: ActivePoolOpts, totalReward: Numberish): [BigNumber, BigNumber] { + if (new BigNumber(pool.membersStake).isZero()) { + return [new BigNumber(totalReward), ZERO_AMOUNT]; + } + const operatorShare = new BigNumber(totalReward) + .times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); + const membersShare = new BigNumber(totalReward).minus(operatorShare); + return [operatorShare, membersShare]; + } + describe('_getUnfinalizedPoolReward()', () => { const ZERO_REWARDS = { operatorReward: 0, @@ -477,6 +503,13 @@ blockchainTests.resets.only('finalization tests', env => { assertPoolRewards(rewards, ZERO_REWARDS); }); + it('returns empty if pool is active only in the current epoch', async () => { + const pool = await addActivePoolAsync(); + const rewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(rewards, ZERO_REWARDS); + }); + it('returns empty if pool was only active in the 2 epochs ago', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); @@ -488,12 +521,88 @@ blockchainTests.resets.only('finalization tests', env => { it('returns empty if pool was already finalized', async () => { const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); - const pool = _.sample(pools) as ActivePoolOpts; + const [pool] = _.sampleSize(pools, 1); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); const rewards = await testContract .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); + + it('computes one reward among one pool', async () => { + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const expectedTotalRewards = INITIAL_BALANCE; + const [expectedOperatorReward, expectedMembersReward] = + splitRewards(pool, expectedTotalRewards); + const actualRewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards( + actualRewards, + { + operatorReward: expectedOperatorReward, + membersReward: expectedMembersReward, + membersStake: pool.membersStake, + }, + ); + }); + + it('computes one reward among multiple pools', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const [pool] = _.sampleSize(pools, 1); + const totalFeesCollected = BigNumber.sum(...pools.map(p => p.feesCollected)); + const totalWeightedStake = BigNumber.sum(...pools.map(p => p.weightedStake)); + const expectedTotalRewards = await callCobbDouglasAsync( + INITIAL_BALANCE, + pool.feesCollected, + totalFeesCollected, + pool.weightedStake, + totalWeightedStake, + ); + const [expectedOperatorReward, expectedMembersReward] = + splitRewards(pool, expectedTotalRewards); + const actualRewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards( + actualRewards, + { + operatorReward: expectedOperatorReward, + membersReward: expectedMembersReward, + membersStake: pool.membersStake, + }, + ); + }); + + it('computes a reward with 0% operatorShare', async () => { + const pool = await addActivePoolAsync({ operatorShare: 0 }); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const actualRewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards( + actualRewards, + { + operatorReward: 0, + membersReward: INITIAL_BALANCE, + membersStake: pool.membersStake, + }, + ); + }); + + it('computes a reward with 100% operatorShare', async () => { + const pool = await addActivePoolAsync({ operatorShare: 1 }); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const actualRewards = await testContract + .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards( + actualRewards, + { + operatorReward: INITIAL_BALANCE, + membersReward: 0, + membersStake: pool.membersStake, + }, + ); + }); }); }); +// tslint:disable: max-file-line-count From b4929df1e6dace799e4e911e2e906653bdd07dc4 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 14 Sep 2019 19:35:07 -0400 Subject: [PATCH 19/52] `@0x/order-utils`: Ran prettier. --- packages/order-utils/src/staking_revert_errors.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index 227dbdbbae..6cdd0b9d9c 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -255,10 +255,7 @@ export class CumulativeRewardIntervalError extends RevertError { } export class PreviousEpochNotFinalizedError extends RevertError { - constructor( - closingEpoch?: BigNumber | number | string, - unfinalizedPoolsRemaining?: BigNumber | number | string, - ) { + constructor(closingEpoch?: BigNumber | number | string, unfinalizedPoolsRemaining?: BigNumber | number | string) { super( 'PreviousEpochNotFinalizedError', 'PreviousEpochNotFinalizedError(uint256 closingEpoch, uint256 unfinalizedPoolsRemaining)', From da0f6b5e8fda8c18bea073405c6262c5696ad2c5 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 14 Sep 2019 19:35:49 -0400 Subject: [PATCH 20/52] `@0x/contracts-test-utils`: Finish off finalizer tests... for now. --- contracts/staking/.prettierrc | 10 - contracts/staking/compiler.json | 54 +- .../contracts/src/sys/MixinFinalizer.sol | 95 ++-- .../staking/contracts/test/TestFinalizer.sol | 12 +- .../staking/test/unit_tests/finalizer_test.ts | 463 ++++++++++++------ .../test/unit_tests/lib_proxy_unit_test.ts | 17 +- 6 files changed, 387 insertions(+), 264 deletions(-) delete mode 100644 contracts/staking/.prettierrc diff --git a/contracts/staking/.prettierrc b/contracts/staking/.prettierrc deleted file mode 100644 index c568ca733a..0000000000 --- a/contracts/staking/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "overrides": [ - { - "files": "./test/**.ts", - "options": { - "printWidth": 80 - } - } - ] -} diff --git a/contracts/staking/compiler.json b/contracts/staking/compiler.json index 59b9936053..aa1f06e97c 100644 --- a/contracts/staking/compiler.json +++ b/contracts/staking/compiler.json @@ -1,30 +1,30 @@ { - "artifactsDir": "./generated-artifacts", - "contractsDir": "./contracts", - "useDockerisedSolc": false, - "isOfflineMode": false, - "compilerSettings": { - "evmVersion": "constantinople", - "optimizer": { - "enabled": true, - "runs": 1000000, - "details": { - "yul": true, - "deduplicate": true, - "cse": true, - "constantOptimizer": true - } - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode.object", - "evm.bytecode.sourceMap", - "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" - ] - } + "artifactsDir": "./generated-artifacts", + "contractsDir": "./contracts", + "useDockerisedSolc": false, + "isOfflineMode": false, + "compilerSettings": { + "evmVersion": "constantinople", + "optimizer": { + "enabled": true, + "runs": 1000000, + "details": { + "yul": true, + "deduplicate": true, + "cse": true, + "constantOptimizer": true + } + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } } - } } diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index f3bc3d6f27..c9bf932171 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -154,12 +154,8 @@ contract MixinFinalizer is continue; } - // Clear the pool state so we don't finalize it again, and to recoup - // some gas. - delete activePools[poolId]; - IStructs.PoolRewards memory poolRewards = - _finalizePool(epoch, poolId, pool, true); + _creditRewardsToPool(epoch, poolId, pool); rewardsPaid = rewardsPaid.safeAdd( poolRewards.operatorReward + poolRewards.membersReward @@ -194,6 +190,7 @@ contract MixinFinalizer is /// epoch, crediting it rewards and sending those rewards to the reward /// vault. This can be called by internal functions that need to /// finalize a pool immediately. Does nothing if the pool is already + /// finalized. Does nothing if the pool was not active or was already /// finalized. /// @param poolId The pool ID to finalize. /// @return rewards Rewards. @@ -207,12 +204,37 @@ contract MixinFinalizer is if (epoch == 0) { return rewards; } - rewards = _finalizePool( - epoch, - poolId, - _getActivePoolFromEpoch(epoch - 1, poolId), - false - ); + + IStructs.ActivePool memory pool = + _getActivePoolFromEpoch(epoch - 1, poolId); + // Do nothing if the pool was not active (has no fees). + if (pool.feesCollected == 0) { + return rewards; + } + + rewards = _creditRewardsToPool(epoch, poolId, pool); + uint256 totalReward = + rewards.membersReward.safeAdd(rewards.operatorReward); + + if (totalReward > 0) { + totalRewardsPaidLastEpoch = + totalRewardsPaidLastEpoch.safeAdd(totalReward); + _depositIntoStakingPoolRewardVault(totalReward); + } + + // Decrease the number of unfinalized pools left. + uint256 poolsRemaining = unfinalizedPoolsRemaining; + unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1); + + // If there are no more unfinalized pools remaining, the epoch is + // finalized. + if (poolsRemaining == 0) { + emit EpochFinalized( + epoch - 1, + totalRewardsPaidLastEpoch, + unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) + ); + } } /// @dev Computes the reward owed to a pool during finalization. @@ -358,33 +380,24 @@ contract MixinFinalizer is rewards.membersStake = pool.membersStake; } - /// @dev Either fully or partially finalizes a single pool that was active - /// in the previous epoch. If `batchedMode` is `true`, this function - /// will NOT: - /// - transfer ether into the reward vault - /// - update `poolsRemaining` - /// - update `totalRewardsPaidLastEpoch` - /// - clear the pool from `activePoolsByEpoch` - /// - emit an `EpochFinalized` event. + /// @dev Credits finalization rewards to a pool that was active in the + /// last epoch. /// @param epoch The current epoch. /// @param poolId The pool ID to finalize. /// @param pool The active pool to finalize. - /// @param batchedMode Only calculate and credit rewards. /// @return rewards Rewards. /// @return rewards The rewards credited to the pool. - function _finalizePool( + function _creditRewardsToPool( uint256 epoch, bytes32 poolId, - IStructs.ActivePool memory pool, - bool batchedMode + IStructs.ActivePool memory pool ) private returns (IStructs.PoolRewards memory rewards) { - // Ignore pools that weren't active. - if (pool.feesCollected == 0) { - return rewards; - } + // Clear the pool state so we don't finalize it again, and to recoup + // some gas. + delete _getActivePoolsFromEpoch(epoch - 1)[poolId]; // Compute the rewards. rewards = _getUnfinalizedPoolRewards(poolId, pool); @@ -415,33 +428,5 @@ contract MixinFinalizer is rewards.operatorReward, rewards.membersReward ); - - if (batchedMode) { - return rewards; - } - - // Clear the pool state so we don't finalize it again, and to recoup - // some gas. - delete _getActivePoolsFromEpoch(epoch)[poolId]; - - if (totalReward > 0) { - totalRewardsPaidLastEpoch = - totalRewardsPaidLastEpoch.safeAdd(totalReward); - _depositIntoStakingPoolRewardVault(totalReward); - } - - // Decrease the number of unfinalized pools left. - uint256 poolsRemaining = unfinalizedPoolsRemaining; - unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1); - - // If there are no more unfinalized pools remaining, the epoch is - // finalized. - if (poolsRemaining == 0) { - emit EpochFinalized( - epoch - 1, - totalRewardsPaidLastEpoch, - unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) - ); - } } } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 83159f9f7e..68b7d9b971 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -43,9 +43,13 @@ contract TestFinalizer is uint256 amount ); - mapping (bytes32 => uint32) internal _operatorSharesByPool; + address payable private _rewardReceiver; + mapping (bytes32 => uint32) private _operatorSharesByPool; - constructor() public { + /// @param rewardReceiver The address to transfer rewards into when + /// a pool is finalized. + constructor(address payable rewardReceiver) public { + _rewardReceiver = rewardReceiver; init(); } @@ -91,7 +95,8 @@ contract TestFinalizer is require(feesCollected > 0, "FEES_MUST_BE_NONZERO"); mapping (bytes32 => IStructs.ActivePool) storage activePools = _getActivePoolsFromEpoch(currentEpoch); - require(feesCollected > 0, "POOL_ALREADY_ADDED"); + IStructs.ActivePool memory pool = activePools[poolId]; + require(pool.feesCollected == 0, "POOL_ALREADY_ADDED"); _operatorSharesByPool[poolId] = operatorShare; activePools[poolId] = IStructs.ActivePool({ feesCollected: feesCollected, @@ -172,6 +177,7 @@ contract TestFinalizer is /// @dev Overridden to store inputs and do some really basic math. function _depositIntoStakingPoolRewardVault(uint256 amount) internal { emit DepositIntoStakingPoolRewardVaultCall(amount); + _rewardReceiver.transfer(amount); } /// @dev Overridden to store inputs and do some really basic math. diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 4232938581..fe7a78d372 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -23,20 +23,23 @@ import { } from '../../src'; import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; -blockchainTests.resets.only('finalization tests', env => { - const { ONE_ETHER, ZERO_AMOUNT } = constants; +blockchainTests.resets('finalizer tests', env => { + const { ZERO_AMOUNT } = constants; const INITIAL_EPOCH = 0; const INITIAL_BALANCE = toBaseUnitAmount(32); let senderAddress: string; + let rewardReceiverAddress: string; let testContract: TestFinalizerContract; before(async () => { [senderAddress] = await env.getAccountAddressesAsync(); + rewardReceiverAddress = hexRandom(constants.ADDRESS_LENGTH); testContract = await TestFinalizerContract.deployFrom0xArtifactAsync( artifacts.TestFinalizer, env.provider, env.txDefaults, artifacts, + rewardReceiverAddress, ); // Give the contract a balance. await sendEtherAsync(testContract.address, INITIAL_BALANCE); @@ -44,7 +47,7 @@ blockchainTests.resets.only('finalization tests', env => { async function sendEtherAsync(to: string, amount: Numberish): Promise { await env.web3Wrapper.awaitTransactionSuccessAsync( - await env.web3Wrapper.sendTransactionAsync({ + await env.web3Wrapper.sendTransactionAsync({ from: senderAddress, to, value: new BigNumber(amount), @@ -61,22 +64,23 @@ blockchainTests.resets.only('finalization tests', env => { } async function addActivePoolAsync(opts?: Partial): Promise { - const _opts = { - poolId: hexRandom(), - operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR, - feesCollected: getRandomInteger(0, ONE_ETHER), - membersStake: getRandomInteger(0, ONE_ETHER), - weightedStake: getRandomInteger(0, ONE_ETHER), - ...opts, - }; - await testContract.addActivePool.awaitTransactionSuccessAsync( - _opts.poolId, + const maxAmount = toBaseUnitAmount(1e9); + const _opts = { + poolId: hexRandom(), + operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR, + feesCollected: getRandomInteger(0, maxAmount), + membersStake: getRandomInteger(0, maxAmount), + weightedStake: getRandomInteger(0, maxAmount), + ...opts, + }; + await testContract.addActivePool.awaitTransactionSuccessAsync( + _opts.poolId, new BigNumber(_opts.operatorShare * constants.PPM_DENOMINATOR).integerValue(), new BigNumber(_opts.feesCollected), new BigNumber(_opts.membersStake), new BigNumber(_opts.weightedStake), - ); - return _opts; + ); + return _opts; } interface FinalizationState { @@ -108,9 +112,7 @@ blockchainTests.resets.only('finalization tests', env => { }; } - async function assertFinalizationStateAsync( - expected: Partial, - ): Promise { + async function assertFinalizationStateAsync(expected: Partial): Promise { const actual = await getFinalizationStateAsync(); if (expected.balance !== undefined) { expect(actual.balance).to.bignumber.eq(expected.balance); @@ -122,43 +124,30 @@ blockchainTests.resets.only('finalization tests', env => { expect(actual.closingEpoch).to.eq(expected.closingEpoch); } if (expected.numActivePoolsThisEpoch !== undefined) { - expect(actual.numActivePoolsThisEpoch) - .to.eq(expected.numActivePoolsThisEpoch); + expect(actual.numActivePoolsThisEpoch).to.eq(expected.numActivePoolsThisEpoch); } if (expected.totalFeesCollectedThisEpoch !== undefined) { - expect(actual.totalFeesCollectedThisEpoch) - .to.bignumber.eq(expected.totalFeesCollectedThisEpoch); + expect(actual.totalFeesCollectedThisEpoch).to.bignumber.eq(expected.totalFeesCollectedThisEpoch); } if (expected.totalWeightedStakeThisEpoch !== undefined) { - expect(actual.totalWeightedStakeThisEpoch) - .to.bignumber.eq(expected.totalWeightedStakeThisEpoch); + expect(actual.totalWeightedStakeThisEpoch).to.bignumber.eq(expected.totalWeightedStakeThisEpoch); } if (expected.unfinalizedPoolsRemaining !== undefined) { - expect(actual.unfinalizedPoolsRemaining) - .to.eq(expected.unfinalizedPoolsRemaining); + expect(actual.unfinalizedPoolsRemaining).to.eq(expected.unfinalizedPoolsRemaining); } if (expected.unfinalizedRewardsAvailable !== undefined) { - expect(actual.unfinalizedRewardsAvailable) - .to.bignumber.eq(expected.unfinalizedRewardsAvailable); + expect(actual.unfinalizedRewardsAvailable).to.bignumber.eq(expected.unfinalizedRewardsAvailable); } if (expected.unfinalizedTotalFeesCollected !== undefined) { - expect(actual.unfinalizedTotalFeesCollected) - .to.bignumber.eq(expected.unfinalizedTotalFeesCollected); + expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected); } if (expected.unfinalizedTotalFeesCollected !== undefined) { - expect(actual.unfinalizedTotalFeesCollected) - .to.bignumber.eq(expected.unfinalizedTotalFeesCollected); + expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected); } } - function assertEpochEndedEvent( - logs: LogEntry[], - args: Partial, - ): void { - const events = filterLogsToArguments( - logs, - IStakingEventsEvents.EpochEnded, - ); + function assertEpochEndedEvent(logs: LogEntry[], args: Partial): void { + const events = getEpochEndedEvents(logs); expect(events.length).to.eq(1); if (args.epoch !== undefined) { expect(events[0].epoch).to.bignumber.eq(INITIAL_EPOCH); @@ -177,10 +166,7 @@ blockchainTests.resets.only('finalization tests', env => { } } - function assertEpochFinalizedEvent( - logs: LogEntry[], - args: Partial, - ): void { + function assertEpochFinalizedEvent(logs: LogEntry[], args: Partial): void { const events = getEpochFinalizedEvents(logs); expect(events.length).to.eq(1); if (args.epoch !== undefined) { @@ -194,10 +180,7 @@ blockchainTests.resets.only('finalization tests', env => { } } - function assertDepositIntoStakingPoolRewardVaultCallEvent( - logs: LogEntry[], - amount?: Numberish, - ): void { + function assertDepositIntoStakingPoolRewardVaultCallEvent(logs: LogEntry[], amount?: Numberish): void { const events = filterLogsToArguments( logs, TestFinalizerEvents.DepositIntoStakingPoolRewardVaultCall, @@ -208,24 +191,31 @@ blockchainTests.resets.only('finalization tests', env => { } } + async function assertRewarReceiverBalanceAsync(expectedAmount: Numberish): Promise { + const balance = await getBalanceOfAsync(rewardReceiverAddress); + expect(balance).to.be.bignumber.eq(expectedAmount); + } + + function getEpochEndedEvents(logs: LogEntry[]): IStakingEventsEpochEndedEventArgs[] { + return filterLogsToArguments(logs, IStakingEventsEvents.EpochEnded); + } + function getEpochFinalizedEvents(logs: LogEntry[]): IStakingEventsEpochFinalizedEventArgs[] { - return filterLogsToArguments( - logs, - IStakingEventsEvents.EpochFinalized, - ); + return filterLogsToArguments(logs, IStakingEventsEvents.EpochFinalized); } function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] { - return filterLogsToArguments( - logs, - IStakingEventsEvents.RewardsPaid, - ); + return filterLogsToArguments(logs, IStakingEventsEvents.RewardsPaid); } async function getCurrentEpochAsync(): Promise { return (await testContract.getCurrentEpoch.callAsync()).toNumber(); } + async function getBalanceOfAsync(whom: string): Promise { + return env.web3Wrapper.getBalanceInWeiAsync(whom); + } + describe('endEpoch()', () => { it('advances the epoch', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); @@ -235,28 +225,22 @@ blockchainTests.resets.only('finalization tests', env => { it('emits an `EpochEnded` event', async () => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - assertEpochEndedEvent( - receipt.logs, - { - epoch: new BigNumber(INITIAL_EPOCH), - numActivePools: ZERO_AMOUNT, - rewardsAvailable: INITIAL_BALANCE, - totalFeesCollected: ZERO_AMOUNT, - totalWeightedStake: ZERO_AMOUNT, - }, - ); + assertEpochEndedEvent(receipt.logs, { + epoch: new BigNumber(INITIAL_EPOCH), + numActivePools: ZERO_AMOUNT, + rewardsAvailable: INITIAL_BALANCE, + totalFeesCollected: ZERO_AMOUNT, + totalWeightedStake: ZERO_AMOUNT, + }); }); it('immediately finalizes if there are no active pools', async () => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - assertEpochFinalizedEvent( - receipt.logs, - { - epoch: new BigNumber(INITIAL_EPOCH), - rewardsPaid: ZERO_AMOUNT, - rewardsRemaining: INITIAL_BALANCE, - }, - ); + assertEpochFinalizedEvent(receipt.logs, { + epoch: new BigNumber(INITIAL_EPOCH), + rewardsPaid: ZERO_AMOUNT, + rewardsRemaining: INITIAL_BALANCE, + }); }); it('does not immediately finalize if there is an active pool', async () => { @@ -269,7 +253,7 @@ blockchainTests.resets.only('finalization tests', env => { expect(events).to.deep.eq([]); }); - it('clears the next epoch\'s finalization state', async () => { + it("clears the next epoch's finalization state", async () => { // Add a pool so there is state to clear. await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); @@ -298,10 +282,7 @@ blockchainTests.resets.only('finalization tests', env => { await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); const tx = testContract.endEpoch.awaitTransactionSuccessAsync(); - const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError( - INITIAL_EPOCH, - 1, - ); + const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError(INITIAL_EPOCH, 1); return expect(tx).to.revertWith(expectedError); }); }); @@ -328,17 +309,13 @@ blockchainTests.resets.only('finalization tests', env => { expect(rewardsPaidEvents.length).to.eq(1); expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1); expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId); - assertEpochFinalizedEvent( - receipt.logs, - { - epoch: new BigNumber(INITIAL_EPOCH), - rewardsPaid: INITIAL_BALANCE, - }, - ); - assertDepositIntoStakingPoolRewardVaultCallEvent( - receipt.logs, - INITIAL_BALANCE, - ); + assertEpochFinalizedEvent(receipt.logs, { + epoch: new BigNumber(INITIAL_EPOCH), + rewardsPaid: INITIAL_BALANCE, + }); + assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + await assertRewarReceiverBalanceAsync(totalRewardsPaid); }); it('can finalize multiple pools', async () => { @@ -349,16 +326,37 @@ blockchainTests.resets.only('finalization tests', env => { const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); expect(rewardsPaidEvents.length).to.eq(pools.length); - for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as - Array<[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]>) { + for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< + [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] + >) { expect(event.epoch).to.bignumber.eq(nextEpoch); expect(event.poolId).to.eq(pool.poolId); } - assertEpochFinalizedEvent( - receipt.logs, - { epoch: new BigNumber(INITIAL_EPOCH) }, - ); + assertEpochFinalizedEvent(receipt.logs, { epoch: new BigNumber(INITIAL_EPOCH) }); assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + await assertRewarReceiverBalanceAsync(totalRewardsPaid); + }); + + it('can finalize multiple pools over multiple transactions', async () => { + const nextEpoch = INITIAL_EPOCH + 1; + const pools = await Promise.all(_.times(2, () => addActivePoolAsync())); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipts = await Promise.all( + pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + const rewardsPaidEvents = getRewardsPaidEvents(allLogs); + expect(rewardsPaidEvents.length).to.eq(pools.length); + for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< + [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] + >) { + expect(event.epoch).to.bignumber.eq(nextEpoch); + expect(event.poolId).to.eq(pool.poolId); + } + assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) }); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + await assertRewarReceiverBalanceAsync(totalRewardsPaid); }); it('ignores a non-active pool', async () => { @@ -393,13 +391,180 @@ blockchainTests.resets.only('finalization tests', env => { const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const poolState = await testContract - .internalGetActivePoolFromEpoch - .callAsync(new BigNumber(INITIAL_EPOCH), pool.poolId); + const poolState = await testContract.internalGetActivePoolFromEpoch.callAsync( + new BigNumber(INITIAL_EPOCH), + pool.poolId, + ); + expect(poolState.feesCollected).to.bignumber.eq(0); + expect(poolState.weightedStake).to.bignumber.eq(0); + expect(poolState.membersStake).to.bignumber.eq(0); + }); + + it('`rewardsPaid` is the sum of all pool rewards', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const poolIds = pools.map(p => p.poolId); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + const expectedTotalRewardsPaid = BigNumber.sum( + ...rewardsPaidEvents.map(e => e.membersReward.plus(e.operatorReward)), + ); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + expect(totalRewardsPaid).to.bignumber.eq(expectedTotalRewardsPaid); + }); + + it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const poolIds = pools.map(p => p.poolId); + let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); + receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + + it('`rewardsPaid` <= `rewardsAvailable` with two equal pools', async () => { + const pool1 = await addActivePoolAsync(); + const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId')); + const poolIds = [pool1, pool2].map(p => p.poolId); + let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + + blockchainTests.optional('`rewardsPaid` fuzzing', async () => { + const numTests = 32; + for (const i of _.times(numTests)) { + const numPools = _.random(1, 32); + it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => { + const pools = await Promise.all(_.times(numPools, () => addActivePoolAsync())); + const poolIds = pools.map(p => p.poolId); + let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + } + }); + }); + + describe('_finalizePool()', () => { + it('does nothing if there were no active pools', async () => { + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const poolId = hexRandom(); + const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); + expect(receipt.logs).to.deep.eq([]); + }); + + it('can finalize a pool', async () => { + const pool = await addActivePoolAsync(); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents.length).to.eq(1); + expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1); + expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId); + assertEpochFinalizedEvent(receipt.logs, { + epoch: new BigNumber(INITIAL_EPOCH), + rewardsPaid: INITIAL_BALANCE, + }); + assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; + await assertRewarReceiverBalanceAsync(totalRewardsPaid); + }); + + it('can finalize multiple pools over multiple transactions', async () => { + const nextEpoch = INITIAL_EPOCH + 1; + const pools = await Promise.all(_.times(2, () => addActivePoolAsync())); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipts = await Promise.all( + pools.map(pool => testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId)), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + const rewardsPaidEvents = getRewardsPaidEvents(allLogs); + expect(rewardsPaidEvents.length).to.eq(pools.length); + for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< + [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] + >) { + expect(event.epoch).to.bignumber.eq(nextEpoch); + expect(event.poolId).to.eq(pool.poolId); + } + assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) }); + const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + await assertRewarReceiverBalanceAsync(totalRewardsPaid); + }); + + it('ignores a finalized pool', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const [finalizedPool] = _.sampleSize(pools, 1); + await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); + const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); + const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + expect(rewardsPaidEvents).to.deep.eq([]); + }); + + it('resets pool state after finalizing it', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pool = _.sample(pools) as ActivePoolOpts; + await testContract.endEpoch.awaitTransactionSuccessAsync(); + await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId); + const poolState = await testContract.internalGetActivePoolFromEpoch.callAsync( + new BigNumber(INITIAL_EPOCH), + pool.poolId, + ); expect(poolState.feesCollected).to.bignumber.eq(0); expect(poolState.weightedStake).to.bignumber.eq(0); expect(poolState.membersStake).to.bignumber.eq(0); }); + + it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => { + const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); + const receipts = await Promise.all( + pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + + it('`rewardsPaid` <= `rewardsAvailable` with two equal pools', async () => { + const pool1 = await addActivePoolAsync(); + const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId')); + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + const receipts = await Promise.all( + [pool1, pool2].map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + + blockchainTests.optional('`rewardsPaid` fuzzing', async () => { + const numTests = 32; + for (const i of _.times(numTests)) { + const numPools = _.random(1, 32); + it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => { + const pools = await Promise.all(_.times(numPools, () => addActivePoolAsync())); + const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + const receipts = await Promise.all( + pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); + }); + } + }); }); describe('lifecycle', () => { @@ -435,6 +600,18 @@ blockchainTests.resets.only('finalization tests', env => { const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); expect(rewardsPaidEvents).to.deep.eq([]); }); + + it('rolls over leftover rewards into th next epoch', async () => { + const poolIds = _.times(3, () => hexRandom()); + await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id }))); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + let receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); + const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(receipt.logs)[0]; + await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id }))); + receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); + }); }); interface PoolRewards { @@ -475,8 +652,7 @@ blockchainTests.resets.only('finalization tests', env => { if (new BigNumber(pool.membersStake).isZero()) { return [new BigNumber(totalReward), ZERO_AMOUNT]; } - const operatorShare = new BigNumber(totalReward) - .times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); + const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); const membersShare = new BigNumber(totalReward).minus(operatorShare); return [operatorShare, membersShare]; } @@ -490,23 +666,20 @@ blockchainTests.resets.only('finalization tests', env => { it('returns empty if epoch is 0', async () => { const poolId = hexRandom(); - const rewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(poolId); + const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); it('returns empty if pool was not active', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); const poolId = hexRandom(); - const rewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(poolId); + const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); it('returns empty if pool is active only in the current epoch', async () => { const pool = await addActivePoolAsync(); - const rewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); @@ -514,8 +687,7 @@ blockchainTests.resets.only('finalization tests', env => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const rewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); @@ -524,8 +696,7 @@ blockchainTests.resets.only('finalization tests', env => { const [pool] = _.sampleSize(pools, 1); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const rewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); assertPoolRewards(rewards, ZERO_REWARDS); }); @@ -533,18 +704,13 @@ blockchainTests.resets.only('finalization tests', env => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); const expectedTotalRewards = INITIAL_BALANCE; - const [expectedOperatorReward, expectedMembersReward] = - splitRewards(pool, expectedTotalRewards); - const actualRewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards( - actualRewards, - { - operatorReward: expectedOperatorReward, - membersReward: expectedMembersReward, - membersStake: pool.membersStake, - }, - ); + const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: expectedOperatorReward, + membersReward: expectedMembersReward, + membersStake: pool.membersStake, + }); }); it('computes one reward among multiple pools', async () => { @@ -560,48 +726,35 @@ blockchainTests.resets.only('finalization tests', env => { pool.weightedStake, totalWeightedStake, ); - const [expectedOperatorReward, expectedMembersReward] = - splitRewards(pool, expectedTotalRewards); - const actualRewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards( - actualRewards, - { - operatorReward: expectedOperatorReward, - membersReward: expectedMembersReward, - membersStake: pool.membersStake, - }, - ); + const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: expectedOperatorReward, + membersReward: expectedMembersReward, + membersStake: pool.membersStake, + }); }); it('computes a reward with 0% operatorShare', async () => { const pool = await addActivePoolAsync({ operatorShare: 0 }); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards( - actualRewards, - { - operatorReward: 0, - membersReward: INITIAL_BALANCE, - membersStake: pool.membersStake, - }, - ); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: 0, + membersReward: INITIAL_BALANCE, + membersStake: pool.membersStake, + }); }); it('computes a reward with 100% operatorShare', async () => { const pool = await addActivePoolAsync({ operatorShare: 1 }); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract - .internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards( - actualRewards, - { - operatorReward: INITIAL_BALANCE, - membersReward: 0, - membersStake: pool.membersStake, - }, - ); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: INITIAL_BALANCE, + membersReward: 0, + membersStake: pool.membersStake, + }); }); }); }); diff --git a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts index 89326d93fb..8e76add5b8 100644 --- a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts +++ b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts @@ -198,11 +198,7 @@ blockchainTests.resets('LibProxy', env => { describe('Combinatorial Tests', () => { // Combinatorial Scenarios for `proxyCall()`. function getCombinatorialTestDescription(params: [RevertRule, boolean, string, string]): string { - const REVERT_RULE_NAMES = [ - 'RevertOnError', - 'AlwaysRevert', - 'NeverRevert', - ]; + const REVERT_RULE_NAMES = ['RevertOnError', 'AlwaysRevert', 'NeverRevert']; return [ `revertRule: ${REVERT_RULE_NAMES[params[0]]}`, `ignoreIngressSelector: ${params[1]}`, @@ -218,11 +214,7 @@ blockchainTests.resets('LibProxy', env => { const scenarios = [ // revertRule - [ - RevertRule.RevertOnError, - RevertRule.AlwaysRevert, - RevertRule.NeverRevert, - ], + [RevertRule.RevertOnError, RevertRule.AlwaysRevert, RevertRule.NeverRevert], // ignoreIngressSelector [false, true], // customEgressSelector @@ -233,10 +225,7 @@ blockchainTests.resets('LibProxy', env => { constructRandomFailureCalldata(), ], // calldata - [ - constructRandomFailureCalldata(), - constructRandomSuccessCalldata(), - ], + [constructRandomFailureCalldata(), constructRandomSuccessCalldata()], ] as [RevertRule[], boolean[], string[], string[]]; for (const params of cartesianProduct(...scenarios).toArray()) { From fa65452e2b543fb6cbc518edd79ba915acf6ecaa Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 14 Sep 2019 19:46:37 -0400 Subject: [PATCH 21/52] `@0x/contracts-staking`: OK, two more finalizer tests. --- contracts/staking/compiler.json | 7 +----- .../staking/test/unit_tests/finalizer_test.ts | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/contracts/staking/compiler.json b/contracts/staking/compiler.json index aa1f06e97c..f432301d36 100644 --- a/contracts/staking/compiler.json +++ b/contracts/staking/compiler.json @@ -8,12 +8,7 @@ "optimizer": { "enabled": true, "runs": 1000000, - "details": { - "yul": true, - "deduplicate": true, - "cse": true, - "constantOptimizer": true - } + "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } }, "outputSelection": { "*": { diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index fe7a78d372..f53f6d0a79 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -756,6 +756,28 @@ blockchainTests.resets('finalizer tests', env => { membersStake: pool.membersStake, }); }); + + it('gives all rewards to operator if membersStake is zero', async () => { + const pool = await addActivePoolAsync({ membersStake: 0 }); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: INITIAL_BALANCE, + membersReward: 0, + membersStake: pool.membersStake, + }); + }); + + it('gives all rewards to operator if membersStake is zero, even if operatorShare is zero', async () => { + const pool = await addActivePoolAsync({ membersStake: 0, operatorShare: 0 }); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); + assertPoolRewards(actualRewards, { + operatorReward: INITIAL_BALANCE, + membersReward: 0, + membersStake: pool.membersStake, + }); + }); }); }); // tslint:disable: max-file-line-count From 38b94ec5f899a129e280a97a434cafc6a34d60a7 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 16 Sep 2019 04:51:57 -0400 Subject: [PATCH 22/52] `@0x/order-utils`: Add `InvalidMinimumPoolStake` to `StakingRevertErrors.InvalidParamValueErrorCode`. --- packages/order-utils/CHANGELOG.json | 4 ++++ packages/order-utils/src/staking_revert_errors.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 1e0aa3b5db..2b46f2fe8f 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -101,6 +101,10 @@ { "note": "Add `PreviousEpochNotFinalizedError` to `StakingRevertErrors`.", "pr": 2155 + }, + { + "note": "Add `InvalidMinimumPoolStake` to `StakingRevertErrors.InvalidParamValueErrorCode`.", + "pr": 2155 } ] }, diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index 6cdd0b9d9c..c0346e663b 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -28,6 +28,7 @@ export enum InvalidParamValueErrorCode { InvalidRewardVaultAddress, InvalidZrxVaultAddress, InvalidEpochDuration, + InvalidMinimumPoolStake, } export enum InitializationErrorCode { From 7ef3c1272211ef4114d4e539fdce3669466c90db Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 16 Sep 2019 10:55:50 -0400 Subject: [PATCH 23/52] `@0x/contracts-staking`: Well, it almost worked. --- .../src/libs/LibStakingRichErrors.sol | 1 + .../staking_pools/MixinStakingPoolRewards.sol | 12 +- .../contracts/test/TestDelegatorRewards.sol | 168 +++++- .../staking/contracts/test/TestFinalizer.sol | 4 +- .../staking/test/actors/finalizer_actor.ts | 239 +++++--- contracts/staking/test/params.ts | 2 +- contracts/staking/test/rewards_test.ts | 36 +- .../delegator_reward_balance_test.ts | 560 +++++++++++++++++- .../staking/test/unit_tests/finalizer_test.ts | 6 +- contracts/staking/test/utils/types.ts | 4 +- .../utils/contracts/src/LibFractions.sol | 3 + 11 files changed, 910 insertions(+), 125 deletions(-) diff --git a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol index fcb5972d18..4ff7f2a0ef 100644 --- a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol +++ b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol @@ -42,6 +42,7 @@ library LibStakingRichErrors { InvalidCobbDouglasAlpha, InvalidRewardDelegatedStakeWeight, InvalidMaximumMakersInPool, + InvalidMinimumPoolStake, InvalidWethProxyAddress, InvalidEthVaultAddress, InvalidRewardVaultAddress, diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index a81005a0af..0625018666 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -187,11 +187,15 @@ contract MixinStakingPoolRewards is amountOfDelegatedStake ); - // Normalize fraction components by dividing by the min token value - // (10^18) + // Normalize fraction components by dividing by the minimum denominator. + uint256 minDenominator = + mostRecentCumulativeRewards.denominator <= amountOfDelegatedStake ? + mostRecentCumulativeRewards.denominator : + amountOfDelegatedStake; + minDenominator = minDenominator == 0 ? 1 : minDenominator; (uint256 numeratorNormalized, uint256 denominatorNormalized) = ( - numerator.safeDiv(MIN_TOKEN_VALUE), - denominator.safeDiv(MIN_TOKEN_VALUE) + numerator.safeDiv(minDenominator), + denominator.safeDiv(minDenominator) ); // store cumulative rewards and set most recent diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 5184b2a341..962a4effea 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -19,11 +19,173 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "../src/Staking.sol"; +import "../src/interfaces/IStructs.sol"; +import "./TestStaking.sol"; contract TestDelegatorRewards is - Staking + TestStaking { - // TODO + event Deposit( + bytes32 poolId, + address member, + uint256 balance + ); + + event FinalizePool( + bytes32 poolId, + uint256 reward, + uint256 stake + ); + + struct UnfinalizedMembersReward { + uint256 reward; + uint256 stake; + } + + constructor() public { + init(); + } + + mapping (uint256 => mapping (bytes32 => UnfinalizedMembersReward)) private + unfinalizedMembersRewardByPoolByEpoch; + + /// @dev Expose _finalizePool + function internalFinalizePool(bytes32 poolId) external { + _finalizePool(poolId); + } + + /// @dev Set unfinalized members reward for a pool in the current epoch. + function setUnfinalizedMembersRewards( + bytes32 poolId, + uint256 membersReward, + uint256 membersStake + ) + external + { + unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId] = + UnfinalizedMembersReward({ + reward: membersReward, + stake: membersStake + }); + } + + /// @dev Advance the epoch. + function advanceEpoch() external { + currentEpoch += 1; + } + + /// @dev Create and delegate stake that is active in the current epoch. + /// Only used to test purportedly unreachable states. + /// Also withdraws pending rewards to the eth vault. + function delegateStakeNow( + address delegator, + bytes32 poolId, + uint256 stake + ) + external + { + _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); + _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance storage _stake = + delegatedStakeToPoolByOwner[delegator][poolId]; + _stake.currentEpochBalance += uint96(stake); + _stake.nextEpochBalance += uint96(stake); + _stake.currentEpoch = uint64(currentEpoch); + } + + /// @dev Create and delegate stake that will occur in the next epoch + /// (normal behavior). + /// Also withdraws pending rewards to the eth vault. + function delegateStake( + address delegator, + bytes32 poolId, + uint256 stake + ) + external + { + _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); + _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance storage _stake = + delegatedStakeToPoolByOwner[delegator][poolId]; + if (_stake.currentEpoch < currentEpoch) { + _stake.currentEpochBalance = _stake.nextEpochBalance; + } + _stake.nextEpochBalance += uint96(stake); + _stake.currentEpoch = uint64(currentEpoch); + } + + /// @dev Clear stake that will occur in the next epoch + /// (normal behavior). + /// Also withdraws pending rewards to the eth vault. + function undelegateStake( + address delegator, + bytes32 poolId, + uint256 stake + ) + external + { + _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); + _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance storage _stake = + delegatedStakeToPoolByOwner[delegator][poolId]; + if (_stake.currentEpoch < currentEpoch) { + _stake.currentEpochBalance = _stake.nextEpochBalance; + } + _stake.nextEpochBalance -= uint96(stake); + _stake.currentEpoch = uint64(currentEpoch); + } + + /// @dev Expose `_recordDepositInRewardVaultFor`. + function recordRewardForDelegators( + bytes32 poolId, + uint256 reward, + uint256 amountOfDelegatedStake + ) + external + { + _recordRewardForDelegators(poolId, reward, amountOfDelegatedStake); + } + + /// @dev Overridden to just emit events. + function _transferMemberBalanceToEthVault( + bytes32 poolId, + address member, + uint256 balance + ) + internal + { + emit Deposit( + poolId, + member, + balance + ); + } + + /// @dev Overridden to realize unfinalizedMembersRewardByPoolByEpoch in + /// the current epoch and eit a event, + function _finalizePool(bytes32 poolId) + internal + returns (IStructs.PoolRewards memory rewards) + { + UnfinalizedMembersReward memory reward = + unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; + delete unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; + rewards.membersReward = reward.reward; + rewards.membersStake = reward.stake; + _recordRewardForDelegators(poolId, reward.reward, reward.stake); + emit FinalizePool(poolId, reward.reward, reward.stake); + } + + /// @dev Overridden to use unfinalizedMembersRewardByPoolByEpoch. + function _getUnfinalizedPoolRewards(bytes32 poolId) + internal + view + returns (IStructs.PoolRewards memory rewards) + { + UnfinalizedMembersReward storage reward = + unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; + rewards.membersReward = reward.reward; + rewards.membersStake = reward.stake; + } } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 68b7d9b971..ebd530fdd3 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -21,11 +21,11 @@ pragma experimental ABIEncoderV2; import "../src/interfaces/IStructs.sol"; import "../src/libs/LibCobbDouglas.sol"; -import "../src/Staking.sol"; +import "./TestStaking.sol"; contract TestFinalizer is - Staking + TestStaking { event RecordRewardForDelegatorsCall( bytes32 poolId, diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index d424861915..6ae34f7b2d 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -1,14 +1,15 @@ -import { expect } from '@0x/contracts-test-utils'; +import { constants, expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { StakingApiWrapper } from '../utils/api_wrapper'; import { - MemberBalancesByPoolId, - MembersByPoolId, - OperatorBalanceByPoolId, + DelegatorBalancesByPoolId, + DelegatorsByPoolId, OperatorByPoolId, OperatorShareByPoolId, + RewardByPoolId, + RewardVaultBalance, RewardVaultBalanceByPoolId, } from '../utils/types'; @@ -19,59 +20,67 @@ interface Reward { poolId: string; } +const { PPM_100_PERCENT } = constants; + +// tslint:disable: prefer-conditional-expression export class FinalizerActor extends BaseActor { private readonly _poolIds: string[]; private readonly _operatorByPoolId: OperatorByPoolId; - private readonly _membersByPoolId: MembersByPoolId; + private readonly _delegatorsByPoolId: DelegatorsByPoolId; constructor( owner: string, stakingApiWrapper: StakingApiWrapper, poolIds: string[], operatorByPoolId: OperatorByPoolId, - membersByPoolId: MembersByPoolId, + delegatorsByPoolId: DelegatorsByPoolId, ) { super(owner, stakingApiWrapper); this._poolIds = _.cloneDeep(poolIds); this._operatorByPoolId = _.cloneDeep(operatorByPoolId); - this._membersByPoolId = _.cloneDeep(membersByPoolId); + this._delegatorsByPoolId = _.cloneDeep(delegatorsByPoolId); } public async finalizeAsync(rewards: Reward[] = []): Promise { // cache initial info and balances - const operatorShareByPoolId = await this._getOperatorShareByPoolIdAsync(this._poolIds); - const rewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); - const memberBalancesByPoolId = await this._getMemberBalancesByPoolIdAsync(this._membersByPoolId); - const operatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId); + const operatorShareByPoolId = + await this._getOperatorShareByPoolIdAsync(this._poolIds); + const rewardVaultBalanceByPoolId = + await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); + const delegatorBalancesByPoolId = + await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); + const delegatorStakesByPoolId = + await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId); // compute expected changes - const [ - expectedOperatorBalanceByPoolId, - expectedRewardVaultBalanceByPoolId, - ] = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( - rewards, - operatorBalanceByPoolId, - rewardVaultBalanceByPoolId, - operatorShareByPoolId, - ); - const memberRewardByPoolId = _.mapValues(_.keyBy(rewards, 'poolId'), r => { - return r.reward.minus(r.reward.times(operatorShareByPoolId[r.poolId]).dividedToIntegerBy(100)); - }); - const expectedMemberBalancesByPoolId = await this._computeExpectedMemberBalancesByPoolIdAsync( - this._membersByPoolId, - memberBalancesByPoolId, - memberRewardByPoolId, - ); + const expectedRewardVaultBalanceByPoolId = + await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( + rewards, + rewardVaultBalanceByPoolId, + operatorShareByPoolId, + ); + const totalRewardsByPoolId = + _.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward')); + const expectedDelegatorBalancesByPoolId = + await this._computeExpectedDelegatorBalancesByPoolIdAsync( + this._delegatorsByPoolId, + delegatorBalancesByPoolId, + delegatorStakesByPoolId, + operatorShareByPoolId, + totalRewardsByPoolId, + ); // finalize await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // assert reward vault changes - const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); + const finalRewardVaultBalanceByPoolId = + await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal( expectedRewardVaultBalanceByPoolId, ); - // assert member balances - const finalMemberBalancesByPoolId = await this._getMemberBalancesByPoolIdAsync(this._membersByPoolId); - expect(finalMemberBalancesByPoolId, 'final delegator balances in reward vault').to.be.deep.equal( - expectedMemberBalancesByPoolId, + // assert delegator balances + const finalDelegatorBalancesByPoolId = + await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); + expect(finalDelegatorBalancesByPoolId, 'final delegator balances in reward vault').to.be.deep.equal( + expectedDelegatorBalancesByPoolId, ); // assert operator balances const finalOperatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId); @@ -80,55 +89,100 @@ export class FinalizerActor extends BaseActor { ); } - private async _computeExpectedMemberBalancesByPoolIdAsync( - membersByPoolId: MembersByPoolId, - memberBalancesByPoolId: MemberBalancesByPoolId, - rewardByPoolId: { [key: string]: BigNumber }, - ): Promise { - const expectedMemberBalancesByPoolId = _.cloneDeep(memberBalancesByPoolId); - for (const poolId of Object.keys(membersByPoolId)) { - if (rewardByPoolId[poolId] === undefined) { + private async _computeExpectedDelegatorBalancesByPoolIdAsync( + delegatorsByPoolId: DelegatorsByPoolId, + delegatorBalancesByPoolId: DelegatorBalancesByPoolId, + delegatorStakesByPoolId: DelegatorBalancesByPoolId, + operatorShareByPoolId: OperatorShareByPoolId, + totalRewardByPoolId: RewardByPoolId, + ): Promise { + const expectedDelegatorBalancesByPoolId = _.cloneDeep(delegatorBalancesByPoolId); + for (const poolId of Object.keys(delegatorsByPoolId)) { + if (totalRewardByPoolId[poolId] === undefined) { continue; } - const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync( - poolId, - )).currentEpochBalance; - for (const member of membersByPoolId[poolId]) { - if (totalStakeDelegatedToPool.eq(0)) { - expectedMemberBalancesByPoolId[poolId][member] = new BigNumber(0); + + const operator = this._operatorByPoolId[poolId]; + const [, membersStakeInPool] = + await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); + const operatorShare = operatorShareByPoolId[poolId].dividedBy(PPM_100_PERCENT); + const totalReward = totalRewardByPoolId[poolId]; + const operatorReward = membersStakeInPool.eq(0) ? + totalReward : + totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN); + const membersTotalReward = totalReward.minus(operatorReward); + + for (const delegator of delegatorsByPoolId[poolId]) { + let delegatorReward = new BigNumber(0); + if (delegator === operator) { + delegatorReward = operatorReward; + } else if (membersStakeInPool.gt(0)) { + const delegatorStake = delegatorStakesByPoolId[poolId][delegator]; + delegatorReward = delegatorStake + .times(membersTotalReward) + .dividedBy(membersStakeInPool) + .integerValue(BigNumber.ROUND_DOWN); + } + const currentBalance = expectedDelegatorBalancesByPoolId[poolId][delegator] || 0; + expectedDelegatorBalancesByPoolId[poolId][delegator] = delegatorReward.plus(currentBalance); + } + } + return expectedDelegatorBalancesByPoolId; + } + + private async _getDelegatorBalancesByPoolIdAsync( + delegatorsByPoolId: DelegatorsByPoolId, + ): Promise { + const computeRewardBalanceOfDelegator = + this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; + const rewardVaultBalanceOfOperator = + this._stakingApiWrapper.rewardVaultContract.balanceOfOperator; + const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; + + for (const poolId of Object.keys(delegatorsByPoolId)) { + const operator = this._operatorByPoolId[poolId]; + const delegators = delegatorsByPoolId[poolId]; + delegatorBalancesByPoolId[poolId] = {}; + for (const delegator of delegators) { + let balance = + new BigNumber(delegatorBalancesByPoolId[poolId][delegator] || 0); + if (delegator === operator) { + balance = balance.plus( + await rewardVaultBalanceOfOperator.callAsync(poolId), + ); } else { - const stakeDelegatedToPoolByMember = (await this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner.callAsync( - member, - poolId, - )).currentEpochBalance; - const rewardThisEpoch = rewardByPoolId[poolId] - .times(stakeDelegatedToPoolByMember) - .dividedToIntegerBy(totalStakeDelegatedToPool); - expectedMemberBalancesByPoolId[poolId][member] = - memberBalancesByPoolId[poolId][member] === undefined - ? rewardThisEpoch - : memberBalancesByPoolId[poolId][member].plus(rewardThisEpoch); + balance = balance.plus( + await computeRewardBalanceOfDelegator.callAsync( + poolId, + delegator, + ), + ); } + delegatorBalancesByPoolId[poolId][delegator] = balance; } } - return expectedMemberBalancesByPoolId; + return delegatorBalancesByPoolId; } - private async _getMemberBalancesByPoolIdAsync(membersByPoolId: MembersByPoolId): Promise { - const memberBalancesByPoolId: MemberBalancesByPoolId = {}; - for (const poolId of Object.keys(membersByPoolId)) { - const members = membersByPoolId[poolId]; - memberBalancesByPoolId[poolId] = {}; - for (const member of members) { - memberBalancesByPoolId[poolId][ - member - ] = await this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator.callAsync( + private async _getDelegatorStakesByPoolIdAsync( + delegatorsByPoolId: DelegatorsByPoolId, + ): Promise { + const getStakeDelegatedToPoolByOwner = + this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner; + const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; + for (const poolId of Object.keys(delegatorsByPoolId)) { + const delegators = delegatorsByPoolId[poolId]; + delegatorBalancesByPoolId[poolId] = {}; + for (const delegator of delegators) { + delegatorBalancesByPoolId[poolId][ + delegator + ] = (await getStakeDelegatedToPoolByOwner.callAsync( + delegator, poolId, - member, - ); + )).currentEpochBalance; } } - return memberBalancesByPoolId; + return delegatorBalancesByPoolId; } private async _computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( @@ -141,16 +195,13 @@ export class FinalizerActor extends BaseActor { const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId); for (const reward of rewards) { const operatorShare = operatorShareByPoolId[reward.poolId]; - [ - expectedOperatorBalanceByPoolId[reward.poolId], - expectedRewardVaultBalanceByPoolId[reward.poolId], - ] = await this._computeExpectedRewardVaultBalanceAsync( - reward.poolId, - reward.reward, - expectedOperatorBalanceByPoolId[reward.poolId], - expectedRewardVaultBalanceByPoolId[reward.poolId], - operatorShare, - ); + expectedRewardVaultBalanceByPoolId[reward.poolId] = + await this._computeExpectedRewardVaultBalanceAsync( + reward.poolId, + reward.reward, + expectedRewardVaultBalanceByPoolId[reward.poolId], + operatorShare, + ); } return [expectedOperatorBalanceByPoolId, expectedRewardVaultBalanceByPoolId]; } @@ -161,13 +212,11 @@ export class FinalizerActor extends BaseActor { operatorBalance: BigNumber, rewardVaultBalance: BigNumber, operatorShare: BigNumber, - ): Promise<[BigNumber, BigNumber]> { - const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync( - poolId, - )).currentEpochBalance; - const operatorPortion = totalStakeDelegatedToPool.eq(0) + ): Promise { + const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); + const operatorPortion = membersStakeInPool.eq(0) ? reward - : reward.times(operatorShare).dividedToIntegerBy(100); + : reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT); const membersPortion = reward.minus(operatorPortion); return [operatorBalance.plus(operatorPortion), rewardVaultBalance.plus(membersPortion)]; } @@ -184,6 +233,22 @@ export class FinalizerActor extends BaseActor { return operatorBalanceByPoolId; } + private async _getOperatorAndDelegatorsStakeInPoolAsync( + poolId: string, + ): Promise<[BigNumber, BigNumber]> { + const stakingContract = this._stakingApiWrapper.stakingContract; + const operator = await stakingContract.getPoolOperator.callAsync(poolId); + const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync( + poolId, + )).currentEpochBalance; + const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync( + operator, + poolId, + )).currentEpochBalance; + const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool); + return [operatorStakeInPool, membersStakeInPool]; + } + private async _getOperatorShareByPoolIdAsync(poolIds: string[]): Promise { const operatorShareByPoolId: OperatorShareByPoolId = {}; for (const poolId of poolIds) { diff --git a/contracts/staking/test/params.ts b/contracts/staking/test/params.ts index 7b22c61e8f..9e76ab9d6a 100644 --- a/contracts/staking/test/params.ts +++ b/contracts/staking/test/params.ts @@ -6,7 +6,7 @@ import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from import { constants as stakingConstants } from './utils/constants'; import { StakingParams } from './utils/types'; -blockchainTests('Configurable Parameters', env => { +blockchainTests('Configurable Parameters unit tests', env => { let testContract: MixinParamsContract; let authorizedAddress: string; let notAuthorizedAddress: string; diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 739db688a4..75f6fa8daf 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -10,7 +10,7 @@ import { PoolOperatorActor } from './actors/pool_operator_actor'; import { StakerActor } from './actors/staker_actor'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { toBaseUnitAmount } from './utils/number_utils'; -import { MembersByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './utils/types'; +import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './utils/types'; // tslint:disable:no-unnecessary-type-assertion // tslint:disable:max-file-line-count @@ -67,14 +67,14 @@ blockchainTests.resets('Testing Rewards', env => { const operatorByPoolId: OperatorByPoolId = {}; operatorByPoolId[poolId] = poolOperator.getOwner(); // associate actors with pools for tracking in Finalizer - const membersByPoolId: MembersByPoolId = {}; - membersByPoolId[poolId] = [actors[0], actors[1]]; + const stakersByPoolId: DelegatorsByPoolId = {}; + stakersByPoolId[poolId] = actors.slice(0, 3); // create Finalizer actor - finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, membersByPoolId); + finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, stakersByPoolId); // Skip to next epoch so operator stake is realized. await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); }); - describe('Reward Simulation', () => { + describe.skip('Reward Simulation', () => { interface EndBalances { // staker 1 stakerRewardVaultBalance_1?: BigNumber; @@ -399,12 +399,9 @@ blockchainTests.resets('Testing Rewards', env => { toBaseUnitAmount(0), toBaseUnitAmount(17), ]; - const totalRewardsAfterAddingMoreStake = new BigNumber( - _.sumBy(rewardsAfterAddingMoreStake, v => { - return v.toNumber(); - }), - ); + const totalRewardsAfterAddingMoreStake = BigNumber.sum(...rewardsAfterAddingMoreStake); const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; + const totalStake = BigNumber.sum(...stakeAmounts); // first staker delegates (epoch 0) await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]); // skip epoch, so first staker can start earning rewards @@ -419,7 +416,16 @@ blockchainTests.resets('Testing Rewards', env => { } // sanity check final balances await validateEndBalances({ - stakerRewardVaultBalance_1: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake), + stakerRewardVaultBalance_1: rewardBeforeAddingMoreStake.plus( + totalRewardsAfterAddingMoreStake + .times(stakeAmounts[0]) + .dividedBy(totalStake) + .integerValue(BigNumber.ROUND_DOWN), + ), + stakerRewardVaultBalance_2: totalRewardsAfterAddingMoreStake + .times(stakeAmounts[1]) + .dividedBy(totalStake) + .integerValue(BigNumber.ROUND_DOWN), poolRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake), membersRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake), }); @@ -464,11 +470,7 @@ blockchainTests.resets('Testing Rewards', env => { toBaseUnitAmount(0), toBaseUnitAmount(17), ]; - const totalRewardsNotForDelegator = new BigNumber( - _.sumBy(rewardsNotForDelegator, v => { - return v.toNumber(); - }), - ); + const totalRewardsNotForDelegator = BigNumber.sum(...rewardsNotForDelegator); const stakeAmount = toBaseUnitAmount(4); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount); // skip epoch, so first staker can start earning rewards @@ -492,7 +494,7 @@ blockchainTests.resets('Testing Rewards', env => { operatorEthVaultBalance: totalRewardsNotForDelegator, }); }); - it('Should collect fees correctly when leaving and returning to a pool', async () => { + it.only('Should collect fees correctly when leaving and returning to a pool', async () => { // first staker delegates (epoch 0) const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)]; const rewardNotForDelegator = toBaseUnitAmount(7); diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts index 42564bab3f..43956f8dde 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts @@ -1,8 +1,24 @@ -import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + hexRandom, + Numberish, +} from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import { LogEntry } from 'ethereum-types'; -import { artifacts, TestDelegatorRewardsContract } from '../../src'; +import { + artifacts, + TestDelegatorRewardsContract, + TestDelegatorRewardsDepositEventArgs, + TestDelegatorRewardsEvents, +} from '../../src'; -blockchainTests('delegator rewards', env => { +import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; + +blockchainTests.resets('delegator unit rewards', env => { let testContract: TestDelegatorRewardsContract; before(async () => { @@ -14,9 +30,543 @@ blockchainTests('delegator rewards', env => { ); }); + interface RewardPoolMembersOpts { + poolId: string; + reward: Numberish; + stake: Numberish; + } + + async function rewardPoolMembersAsync( + opts?: Partial, + ): Promise { + const _opts = { + poolId: hexRandom(), + reward: getRandomInteger(1, toBaseUnitAmount(100)), + stake: getRandomInteger(1, toBaseUnitAmount(10)), + ...opts, + }; + await testContract.recordRewardForDelegators.awaitTransactionSuccessAsync( + _opts.poolId, + new BigNumber(_opts.reward), + new BigNumber(_opts.stake), + ); + return _opts; + } + + interface SetUnfinalizedMembersRewardsOpts { + poolId: string; + reward: Numberish; + stake: Numberish; + } + + async function setUnfinalizedMembersRewardsAsync( + opts?: Partial, + ): Promise { + const _opts = { + poolId: hexRandom(), + reward: getRandomInteger(1, toBaseUnitAmount(100)), + stake: getRandomInteger(1, toBaseUnitAmount(10)), + ...opts, + }; + await testContract.setUnfinalizedMembersRewards.awaitTransactionSuccessAsync( + _opts.poolId, + new BigNumber(_opts.reward), + new BigNumber(_opts.stake), + ); + return _opts; + } + + type ResultWithDeposit = T & { + deposit: BigNumber; + }; + + interface DelegateStakeOpts { + delegator: string; + stake: Numberish; + } + + async function delegateStakeNowAsync( + poolId: string, + opts?: Partial, + ): Promise> { + return delegateStakeAsync(poolId, opts, true); + } + + async function delegateStakeAsync( + poolId: string, + opts?: Partial, + now?: boolean, + ): Promise> { + const _opts = { + delegator: randomAddress(), + stake: getRandomInteger(1, toBaseUnitAmount(10)), + ...opts, + }; + const fn = now ? testContract.delegateStakeNow : testContract.delegateStake; + const receipt = await fn.awaitTransactionSuccessAsync( + _opts.delegator, + poolId, + new BigNumber(_opts.stake), + ); + return { + ..._opts, + deposit: getDepositFromLogs(receipt.logs, poolId, _opts.delegator), + }; + } + + async function undelegateStakeAsync( + poolId: string, + delegator: string, + stake?: Numberish, + ): Promise> { + const _stake = new BigNumber( + stake || (await + testContract + .getStakeDelegatedToPoolByOwner + .callAsync(delegator, poolId) + ).currentEpochBalance, + ); + const receipt = await testContract.undelegateStake.awaitTransactionSuccessAsync( + delegator, + poolId, + _stake, + ); + return { + stake: _stake, + deposit: getDepositFromLogs(receipt.logs, poolId, delegator), + }; + } + + function getDepositFromLogs(logs: LogEntry[], poolId: string, delegator?: string): BigNumber { + const events = + filterLogsToArguments( + logs, + TestDelegatorRewardsEvents.Deposit, + ); + if (events.length > 0) { + expect(events.length).to.eq(1); + expect(events[0].poolId).to.eq(poolId); + if (delegator !== undefined) { + expect(events[0].member).to.eq(delegator); + } + return events[0].balance; + } + return constants.ZERO_AMOUNT; + } + + async function advanceEpochAsync(): Promise { + await testContract.advanceEpoch.awaitTransactionSuccessAsync(); + const epoch = await testContract.getCurrentEpoch.callAsync(); + return epoch.toNumber(); + } + + async function getDelegatorRewardAsync(poolId: string, delegator: string): Promise { + return testContract.computeRewardBalanceOfDelegator.callAsync( + poolId, + delegator, + ); + } + + async function touchStakeAsync(poolId: string, delegator: string): Promise> { + return undelegateStakeAsync(poolId, delegator, 0); + } + + async function finalizePoolAsync(poolId: string): Promise> { + const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); + return { + deposit: getDepositFromLogs(receipt.logs, poolId), + }; + } + + function randomAddress(): string { + return hexRandom(constants.ADDRESS_LENGTH); + } + + function computeDelegatorRewards( + totalRewards: Numberish, + delegatorStake: Numberish, + totalDelegatorStake: Numberish, + ): BigNumber { + return new BigNumber(totalRewards) + .times(delegatorStake) + .dividedBy(new BigNumber(totalDelegatorStake)) + .integerValue(BigNumber.ROUND_DOWN); + } + describe('computeRewardBalanceOfDelegator()', () => { - it('does stuff', () => { - // TODO + it('nothing in epoch 0 for delegator with no stake', async () => { + const { poolId } = await rewardPoolMembersAsync(); + const delegator = randomAddress(); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('nothing in epoch 1 for delegator with no stake', async () => { + await advanceEpochAsync(); // epoch 1 + const { poolId } = await rewardPoolMembersAsync(); + const delegator = randomAddress(); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('nothing in epoch 0 for delegator staked in epoch 0', async () => { + const { poolId } = await rewardPoolMembersAsync(); + // Assign active stake to pool in epoch 0, which is usuaslly not + // possible due to delegating delays. + const { delegator } = await delegateStakeNowAsync(poolId); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('nothing in epoch 1 for delegator delegating in epoch 1', async () => { + await advanceEpochAsync(); // epoch 1 + const { poolId } = await rewardPoolMembersAsync(); + const { delegator } = await delegateStakeAsync(poolId); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('nothing in epoch 1 for delegator delegating in epoch 0', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + // rewards paid for stake in epoch 0. + await rewardPoolMembersAsync({ poolId, stake }); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('all rewards from epoch 2 for delegator delegating in epoch 0', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(reward); + }); + + it('all rewards from epoch 2 and 3 for delegator delegating in epoch 0', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake }); + await advanceEpochAsync(); // epoch 3 + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake }); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(BigNumber.sum(reward1, reward2)); + }); + + it('partial rewards from epoch 2 and 3 for delegator partially delegating in epoch 0', async () => { + const poolId = hexRandom(); + const { delegator, stake: delegatorStake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + const { reward, stake: rewardStake } = await rewardPoolMembersAsync( + { poolId, stake: new BigNumber(delegatorStake).times(2) }, + ); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const expectedDelegatorRewards = computeDelegatorRewards(reward, delegatorStake, rewardStake); + assertRoughlyEquals(delegatorReward, expectedDelegatorRewards); + }); + + it.only('has correct reward immediately after unstaking', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + const { reward } = await rewardPoolMembersAsync( + { poolId, stake }, + ); + await undelegateStakeAsync(poolId, delegator); + await advanceEpochAsync(); // epoch 3 + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(reward); + }); + + it('computes correct rewards for 2 staggered delegators', async () => { + const poolId = hexRandom(); + const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake A now active) + const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId); + const totalStake = BigNumber.sum(stakeA, stakeB); + await advanceEpochAsync(); // epoch 2 (stake B now active) + // rewards paid for stake in epoch 1 (delegator A only) + const { reward: reward1 } = await rewardPoolMembersAsync( + { poolId, stake: stakeA }, + ); + await advanceEpochAsync(); // epoch 3 + // rewards paid for stake in epoch 2 (delegator A and B) + const { reward: reward2 } = await rewardPoolMembersAsync( + { poolId, stake: totalStake }, + ); + const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA); + const expectedDelegatorRewardA = BigNumber.sum( + computeDelegatorRewards(reward1, stakeA, stakeA), + computeDelegatorRewards(reward2, stakeA, totalStake), + ); + assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA); + const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB); + const expectedDelegatorRewardB = BigNumber.sum( + computeDelegatorRewards(reward2, stakeB, totalStake), + ); + assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB); + }); + + it('computes correct rewards for 2 staggered delegators with a 2 epoch gap between payments', async () => { + const poolId = hexRandom(); + const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake A now active) + const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId); + const totalStake = BigNumber.sum(stakeA, stakeB); + await advanceEpochAsync(); // epoch 2 (stake B now active) + // rewards paid for stake in epoch 1 (delegator A only) + const { reward: reward1 } = await rewardPoolMembersAsync( + { poolId, stake: stakeA }, + ); + await advanceEpochAsync(); // epoch 3 + await advanceEpochAsync(); // epoch 4 + // rewards paid for stake in epoch 3 (delegator A and B) + const { reward: reward2 } = await rewardPoolMembersAsync( + { poolId, stake: totalStake }, + ); + const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA); + const expectedDelegatorRewardA = BigNumber.sum( + computeDelegatorRewards(reward1, stakeA, stakeA), + computeDelegatorRewards(reward2, stakeA, totalStake), + ); + assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA); + const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB); + const expectedDelegatorRewardB = BigNumber.sum( + computeDelegatorRewards(reward2, stakeB, totalStake), + ); + assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB); + }); + + it('correct rewards for rewards with different stakes', async () => { + const poolId = hexRandom(); + const { delegator, stake: delegatorStake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + const { reward: reward1, stake: rewardStake1 } = await rewardPoolMembersAsync( + { poolId, stake: new BigNumber(delegatorStake).times(2) }, + ); + await advanceEpochAsync(); // epoch 3 + // rewards paid for stake in epoch 2 + const { reward: reward2, stake: rewardStake2 } = await rewardPoolMembersAsync( + { poolId, stake: new BigNumber(delegatorStake).times(3) }, + ); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const expectedDelegatorReward = BigNumber.sum( + computeDelegatorRewards(reward1, delegatorStake, rewardStake1), + computeDelegatorRewards(reward2, delegatorStake, rewardStake2), + ); + assertRoughlyEquals(delegatorReward, expectedDelegatorReward); + }); + + describe('with unfinalized rewards', async () => { + it('nothing with only unfinalized rewards from epoch 1 for deleator with nothing delegated', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId, { stake: 0 }); + await advanceEpochAsync(); // epoch 1 + await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + expect(reward).to.bignumber.eq(0); + }); + + it('nothing with only unfinalized rewards from epoch 1 for deleator delegating in epoch 0', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + expect(reward).to.bignumber.eq(0); + }); + + it('returns only unfinalized rewards from epoch 2 for delegator delegating in epoch 1', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await advanceEpochAsync(); // epoch 2 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + expect(reward).to.bignumber.eq(unfinalizedReward); + }); + + it('returns only unfinalized rewards from epoch 3 for delegator delegating in epoch 1', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await advanceEpochAsync(); // epoch 2 + await advanceEpochAsync(); // epoch 3 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + expect(reward).to.bignumber.eq(unfinalizedReward); + }); + + it('returns unfinalized rewards from epoch 3 + rewards from epoch 2 for delegator delegating in epoch 1', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await advanceEpochAsync(); // epoch 2 + const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake }); + await advanceEpochAsync(); // epoch 3 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); + expect(reward).to.bignumber.eq(expectedReward); + }); + + it('returns unfinalized rewards from epoch 4 + rewards from epoch 2 for delegator delegating in epoch 1', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await advanceEpochAsync(); // epoch 2 + const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake }); + await advanceEpochAsync(); // epoch 3 + await advanceEpochAsync(); // epoch 4 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const reward = await getDelegatorRewardAsync(poolId, delegator); + const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); + expect(reward).to.bignumber.eq(expectedReward); + }); + + it('returns correct rewards if unfinalized stake is different from previous rewards', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 + await advanceEpochAsync(); // epoch 2 + const { reward: prevReward, stake: prevStake } = await rewardPoolMembersAsync( + { poolId, stake: new BigNumber(stake).times(2) }, + ); + await advanceEpochAsync(); // epoch 3 + await advanceEpochAsync(); // epoch 4 + const { reward: unfinalizedReward, stake: unfinalizedStake } = + await setUnfinalizedMembersRewardsAsync( + { poolId, stake: new BigNumber(stake).times(5) }, + ); + const reward = await getDelegatorRewardAsync(poolId, delegator); + const expectedReward = BigNumber.sum( + computeDelegatorRewards(prevReward, stake, prevStake), + computeDelegatorRewards(unfinalizedReward, stake, unfinalizedStake), + ); + assertRoughlyEquals(reward, expectedReward); + }); + }); + }); + + describe('reward transfers', async () => { + it('transfers all rewards to eth vault when touching stake', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1 + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const { deposit } = await touchStakeAsync(poolId, delegator); + expect(deposit).to.bignumber.eq(reward); + }); + + it('does not collect extra rewards from delegating more stake in the reward epoch', async () => { + const poolId = hexRandom(); + const stakeResults = []; + // stake + stakeResults.push(await delegateStakeAsync(poolId)); + const { delegator, stake } = stakeResults[0]; + const totalStake = new BigNumber(stake).times(2); + await advanceEpochAsync(); // epoch 1 (stake now active) + // add more stake. + stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake })); + await advanceEpochAsync(); // epoch 1 (2 * stake now active) + // reward for epoch 1, using 2 * stake so delegator should + // only be entitled to a fraction of the rewards. + const { reward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + await advanceEpochAsync(); // epoch 2 + // touch the stake one last time + stakeResults.push(await touchStakeAsync(poolId, delegator)); + // Should only see deposits for epoch 2. + const expectedDeposit = computeDelegatorRewards(reward, stake, totalStake); + const allDeposits = stakeResults.map(r => r.deposit); + assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedDeposit); + }); + + it('only collects rewards from staked epochs', async () => { + const poolId = hexRandom(); + const stakeResults = []; + // stake + stakeResults.push(await delegateStakeAsync(poolId)); + const { delegator, stake } = stakeResults[0]; + await advanceEpochAsync(); // epoch 1 (stake now active) + // unstake before and after reward payout, to be extra sneaky. + const unstake1 = new BigNumber(stake).dividedToIntegerBy(2); + stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake1)); + // reward for epoch 0 + await rewardPoolMembersAsync({ poolId, stake }); + const unstake2 = new BigNumber(stake).minus(unstake1); + stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake2)); + await advanceEpochAsync(); // epoch 2 (no active stake) + // reward for epoch 1 + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + // re-stake + stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake })); + await advanceEpochAsync(); // epoch 3 (stake now active) + // reward for epoch 2 + await rewardPoolMembersAsync({ poolId, stake }); + // touch the stake one last time + stakeResults.push(await touchStakeAsync(poolId, delegator)); + // Should only see deposits for epoch 2. + const allDeposits = stakeResults.map(r => r.deposit); + assertRoughlyEquals(BigNumber.sum(...allDeposits), reward); + }); + + it('delegator B collects correct rewards after delegator A finalizes', async () => { + const poolId = hexRandom(); + const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); + const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId); + const totalStake = BigNumber.sum(stakeA, stakeB); + await advanceEpochAsync(); // epoch 1 (stakes now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1 + const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + await advanceEpochAsync(); // epoch 3 + // unfinalized rewards for stake in epoch 2 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake }); + const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); + // delegator A will finalize and collect rewards by touching stake. + const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake)); + // delegator B will collect rewards by touching stake + const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake)); + }); + + it('delegator A and B collect correct rewards after external finalization', async () => { + const poolId = hexRandom(); + const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); + const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId); + const totalStake = BigNumber.sum(stakeA, stakeB); + await advanceEpochAsync(); // epoch 1 (stakes now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1 + const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + await advanceEpochAsync(); // epoch 3 + // unfinalized rewards for stake in epoch 2 + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake }); + const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); + // finalize + await finalizePoolAsync(poolId); + // delegator A will collect rewards by touching stake. + const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake)); + // delegator B will collect rewards by touching stake + const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake)); }); }); }); +// tslint:disable: max-file-line-count diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index f53f6d0a79..11a8a15056 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -23,16 +23,14 @@ import { } from '../../src'; import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; -blockchainTests.resets('finalizer tests', env => { +blockchainTests.resets('finalizer unit tests', env => { const { ZERO_AMOUNT } = constants; const INITIAL_EPOCH = 0; const INITIAL_BALANCE = toBaseUnitAmount(32); - let senderAddress: string; let rewardReceiverAddress: string; let testContract: TestFinalizerContract; before(async () => { - [senderAddress] = await env.getAccountAddressesAsync(); rewardReceiverAddress = hexRandom(constants.ADDRESS_LENGTH); testContract = await TestFinalizerContract.deployFrom0xArtifactAsync( artifacts.TestFinalizer, @@ -48,7 +46,7 @@ blockchainTests.resets('finalizer tests', env => { async function sendEtherAsync(to: string, amount: Numberish): Promise { await env.web3Wrapper.awaitTransactionSuccessAsync( await env.web3Wrapper.sendTransactionAsync({ - from: senderAddress, + from: (await env.getAccountAddressesAsync())[0], to, value: new BigNumber(amount), }), diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index 9aa685b05d..a21918a09b 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -121,7 +121,7 @@ export interface RewardByPoolId { [key: string]: BigNumber; } -export interface MemberBalancesByPoolId { +export interface DelegatorBalancesByPoolId { [key: string]: BalanceByOwner; } @@ -129,6 +129,6 @@ export interface OperatorByPoolId { [key: string]: string; } -export interface MembersByPoolId { +export interface DelegatorsByPoolId { [key: string]: string[]; } diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index 98a894b7d3..9f3f7750c2 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -57,6 +57,9 @@ library LibFractions { pure returns (uint256 result) { + if (s == 0) { + return 0; + } if (n2 == 0) { return result = s .safeMul(n1) From 0196ce18f373a94e8d0cf42c0deb8ac7b5e65d6f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 16 Sep 2019 13:23:33 -0400 Subject: [PATCH 24/52] `@0x/contracts-staking`: Last call before embarking of V3 of staking integration. --- .../staking_pools/MixinStakingPoolRewards.sol | 102 +++++++++++++----- .../delegator_reward_balance_test.ts | 37 ++++++- .../utils/contracts/src/LibFractions.sol | 2 +- 3 files changed, 110 insertions(+), 31 deletions(-) diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 0625018666..4ab57b07f5 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -255,36 +255,80 @@ contract MixinStakingPoolRewards is view returns (uint256 totalReward) { - // 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( + uint256 currentEpoch = getCurrentEpoch(); + // There can be no rewards in epoch 0 because there is no delegated + // stake. + if (currentEpoch == 0) { + return reward = 0; + } + + IStructs.StoredBalance memory stake = + _loadUnsyncedBalance(delegatedStakeToPoolByOwner[member][poolId]); + // There can be no rewards if the last epoch when stake was synced is + // equal to the current epoch, because all prior rewards, including + // rewards finalized this epoch have been claimed. + if (stake.currentEpoch == currentEpoch) { + return reward = 0; + } + + // From here we know: + // 1. `currentEpoch > 0` + // 2. `stake.currentEpoch < currentEpoch`. + + // Get the last epoch where a reward was credited to this pool. + uint256 lastRewardEpoch = cumulativeRewardsByPoolLastStored[poolId]; + + // If there are unfinalized rewards this epoch, compute the member's + // share. + if (unfinalizedMembersReward != 0 && unfinalizedDelegatedStake != 0) { + // Unfinalized rewards are always earned from stake in + // the prior epoch so we want the stake at `currentEpoch-1`. + uint256 _stake = stake.currentEpoch == currentEpoch - 1 ? + stake.currentEpochBalance : + stake.nextEpochBalance; + if (_stake != 0) { + reward = _stake + .safeMul(unfinalizedMembersReward) + .safeDiv(unfinalizedDelegatedStake); + } + // Add rewards up to the last reward epoch. + if (lastRewardEpoch != 0 && lastRewardEpoch > stake.currentEpoch) { + reward = reward.safeAdd( + _computeMemberRewardOverInterval( + poolId, + stake, + stake.currentEpoch, + stake.currentEpoch + 1 + ) + ); + if (lastRewardEpoch > stake.currentEpoch + 1) { + reward = reward.safeAdd( + _computeMemberRewardOverInterval( + poolId, + stake, + stake.currentEpoch + 1, + lastRewardEpoch + ) + ); + } + } + // Otherwise get the rewards earned up to the last reward epoch. + } else if (stake.currentEpoch < lastRewardEpoch) { + reward = _computeMemberRewardOverInterval( poolId, - unsyncedDelegatedStakeToPoolByOwner.nextEpochBalance, - unsyncedDelegatedStakeToPoolByOwner.currentEpoch, - _cumulativeRewardsByPoolLastStored[poolId] - ) - : 0; - - // compute the total reward - totalReward = rewardsAccumulatedDuringLastStoredEpoch.safeAdd(rewardsAccumulatedAfterLastStoredEpoch); - return totalReward; + stake, + stake.currentEpoch, + stake.currentEpoch + 1 + ); + if (lastRewardEpoch > stake.currentEpoch + 1) { + reward = _computeMemberRewardOverInterval( + poolId, + stake, + stake.currentEpoch + 1, + lastRewardEpoch + ).safeSub(reward); + } + } } /// @dev Adds or removes cumulative reward dependencies for a delegator. diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts index 43956f8dde..64df0da462 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts @@ -273,7 +273,22 @@ blockchainTests.resets('delegator unit rewards', env => { assertRoughlyEquals(delegatorReward, expectedDelegatorRewards); }); - it.only('has correct reward immediately after unstaking', async () => { + it('has correct reward immediately after unstaking', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + const { reward } = await rewardPoolMembersAsync( + { poolId, stake }, + ); + const { deposit } = await undelegateStakeAsync(poolId, delegator); + expect(deposit).to.bignumber.eq(reward); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it('has correct reward immediately after unstaking and restaking', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) @@ -282,8 +297,28 @@ blockchainTests.resets('delegator unit rewards', env => { const { reward } = await rewardPoolMembersAsync( { poolId, stake }, ); + const { deposit } = await undelegateStakeAsync(poolId, delegator); + expect(deposit).to.bignumber.eq(reward); + await delegateStakeAsync(poolId, { delegator, stake }); + const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(0); + }); + + it.only('has correct reward immediately after unstaking, restaking, and rewarding fees', async () => { + const poolId = hexRandom(); + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1. + await rewardPoolMembersAsync({ poolId, stake }); await undelegateStakeAsync(poolId, delegator); + await delegateStakeAsync(poolId, { delegator, stake }); await advanceEpochAsync(); // epoch 3 + await advanceEpochAsync(); // epoch 4 + // rewards paid for stake in epoch 3. + const { reward } = await rewardPoolMembersAsync( + { poolId, stake }, + ); const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index 9f3f7750c2..01a44aabf8 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -57,7 +57,7 @@ library LibFractions { pure returns (uint256 result) { - if (s == 0) { + if (s == 0 || d1 == 0) { return 0; } if (n2 == 0) { From e267a0e855fda099b2a769cdcf4eabb51a60ed27 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 16 Sep 2019 16:02:14 -0400 Subject: [PATCH 25/52] `@0x/contracts-staking`: Transition to V3 --- .../staking_pools/MixinStakingPoolRewards.sol | 53 +++++++------------ .../delegator_reward_balance_test.ts | 8 +-- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 4ab57b07f5..8d8b27b855 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -276,7 +276,17 @@ contract MixinStakingPoolRewards is // 2. `stake.currentEpoch < currentEpoch`. // Get the last epoch where a reward was credited to this pool. - uint256 lastRewardEpoch = cumulativeRewardsByPoolLastStored[poolId]; + uint256 lastRewardEpoch = lastPoolRewardEpoch[poolId]; + // Get the last reward epoch for which we collected rewards from. + uint256 lastCollectedRewardEpoch = + lastCollectedRewardsEpochToPoolByOwner[member][poolId]; + + // If either of these are true, the most recent reward has already been + // claimed. + if (lastCollectedRewardEpoch == lastRewardEpoch + || stake.currentEpoch >= lastRewardEpoch) { + return reward = 0; + } // If there are unfinalized rewards this epoch, compute the member's // share. @@ -291,43 +301,18 @@ contract MixinStakingPoolRewards is .safeMul(unfinalizedMembersReward) .safeDiv(unfinalizedDelegatedStake); } - // Add rewards up to the last reward epoch. - if (lastRewardEpoch != 0 && lastRewardEpoch > stake.currentEpoch) { - reward = reward.safeAdd( - _computeMemberRewardOverInterval( - poolId, - stake, - stake.currentEpoch, - stake.currentEpoch + 1 - ) - ); - if (lastRewardEpoch > stake.currentEpoch + 1) { - reward = reward.safeAdd( - _computeMemberRewardOverInterval( - poolId, - stake, - stake.currentEpoch + 1, - lastRewardEpoch - ) - ); - } - } - // Otherwise get the rewards earned up to the last reward epoch. - } else if (stake.currentEpoch < lastRewardEpoch) { - reward = _computeMemberRewardOverInterval( - poolId, - stake, - stake.currentEpoch, - stake.currentEpoch + 1 - ); - if (lastRewardEpoch > stake.currentEpoch + 1) { - reward = _computeMemberRewardOverInterval( + } + + // Add rewards up to the last reward epoch. + if (lastRewardEpoch != 0) { + reward = reward.safeAdd( + _computeMemberRewardOverInterval( poolId, stake, stake.currentEpoch + 1, lastRewardEpoch - ).safeSub(reward); - } + ) + ); } } diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts index 64df0da462..6c91cd1949 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts @@ -18,7 +18,7 @@ import { import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; -blockchainTests.resets('delegator unit rewards', env => { +blockchainTests.resets.only('delegator unit rewards', env => { let testContract: TestDelegatorRewardsContract; before(async () => { @@ -304,7 +304,7 @@ blockchainTests.resets('delegator unit rewards', env => { expect(delegatorReward).to.bignumber.eq(0); }); - it.only('has correct reward immediately after unstaking, restaking, and rewarding fees', async () => { + it('has correct reward immediately after unstaking, restaking, and rewarding fees', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) @@ -323,7 +323,7 @@ blockchainTests.resets('delegator unit rewards', env => { expect(delegatorReward).to.bignumber.eq(reward); }); - it('computes correct rewards for 2 staggered delegators', async () => { + it.only('computes correct rewards for 2 staggered delegators', async () => { const poolId = hexRandom(); const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake A now active) @@ -347,7 +347,7 @@ blockchainTests.resets('delegator unit rewards', env => { assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA); const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB); const expectedDelegatorRewardB = BigNumber.sum( - computeDelegatorRewards(reward2, stakeB, totalStake), + computeDelegatorRewards(BigNumber.sum(reward1, reward2), stakeB, totalStake), ); assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB); }); From 527ec289156e4e3653acbf4e76d4092cc1b88410 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 17 Sep 2019 13:09:11 -0400 Subject: [PATCH 26/52] `@0x/contracts-utils`: Add auto-scaling and zero-value optimizations to `LibFractions`. --- contracts/utils/CHANGELOG.json | 4 ++++ contracts/utils/contracts/src/LibFractions.sol | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 9dc089de5e..1557a9c289 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -57,6 +57,10 @@ { "note": "Added LibFractions", "pr": 2118 + }, + { + "note": "Introduce automatic normalization and some zero-value shortcuts in `LibFractions`.", + "pr": 2155 } ] }, diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index 01a44aabf8..13e0c80370 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -7,6 +7,11 @@ library LibFractions { using LibSafeMath for uint256; + /// @dev Maximum value for addition result components. + uint256 constant internal RESCALE_THRESHOLD = 10 ** 27; + /// @dev Rescale factor for addition. + uint256 constant internal RESCALE_BASE = 10 ** 9; + /// @dev Safely adds two fractions `n1/d1 + n2/d2` /// @param n1 numerator of `1` /// @param d1 denominator of `1` @@ -37,6 +42,13 @@ library LibFractions { .safeMul(d2) .safeAdd(n2.safeMul(d1)); denominator = d1.safeMul(d2); + + // If either the numerator or the denominator are > RESCALE_THRESHOLD, + // re-scale them to prevent overflows in future operations. + if (numerator > RESCALE_THRESHOLD || denominator > RESCALE_THRESHOLD) { + numerator = numerator.safeDiv(RESCALE_BASE); + denominator = denominator.safeDiv(RESCALE_BASE); + } } /// @dev Safely scales the difference between two fractions. @@ -57,7 +69,7 @@ library LibFractions { pure returns (uint256 result) { - if (s == 0 || d1 == 0) { + if (s == 0) { return 0; } if (n2 == 0) { From a43b494302b039d5de3adcdec89320b5232ff2fe Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 17 Sep 2019 14:04:07 -0400 Subject: [PATCH 27/52] `@0x/contracts-staking`: All tests passing! --- contracts/staking/CHANGELOG.json | 4 + .../staking_pools/MixinStakingPoolRewards.sol | 50 ++-- .../contracts/src/sys/MixinFinalizer.sol | 15 +- .../staking/test/actors/finalizer_actor.ts | 111 +++---- contracts/staking/test/rewards_test.ts | 8 +- .../delegator_reward_balance_test.ts | 272 ++++++++++-------- .../staking/test/unit_tests/finalizer_test.ts | 38 +-- .../test/unit_tests/lib_proxy_unit_test.ts | 10 +- 8 files changed, 259 insertions(+), 249 deletions(-) diff --git a/contracts/staking/CHANGELOG.json b/contracts/staking/CHANGELOG.json index 6f7fd99b1f..625793f960 100644 --- a/contracts/staking/CHANGELOG.json +++ b/contracts/staking/CHANGELOG.json @@ -57,6 +57,10 @@ { "note": "Refactored out `_cobbDouglas()` into its own library", "pr": 2179 + }, + { + "note": "Introduce multi-block finalization.", + "pr": 2155 } ] } diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 8d8b27b855..92906d0a33 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -271,29 +271,12 @@ contract MixinStakingPoolRewards is return reward = 0; } - // From here we know: - // 1. `currentEpoch > 0` - // 2. `stake.currentEpoch < currentEpoch`. - - // Get the last epoch where a reward was credited to this pool. - uint256 lastRewardEpoch = lastPoolRewardEpoch[poolId]; - // Get the last reward epoch for which we collected rewards from. - uint256 lastCollectedRewardEpoch = - lastCollectedRewardsEpochToPoolByOwner[member][poolId]; - - // If either of these are true, the most recent reward has already been - // claimed. - if (lastCollectedRewardEpoch == lastRewardEpoch - || stake.currentEpoch >= lastRewardEpoch) { - return reward = 0; - } - // If there are unfinalized rewards this epoch, compute the member's // share. if (unfinalizedMembersReward != 0 && unfinalizedDelegatedStake != 0) { // Unfinalized rewards are always earned from stake in // the prior epoch so we want the stake at `currentEpoch-1`. - uint256 _stake = stake.currentEpoch == currentEpoch - 1 ? + uint256 _stake = stake.currentEpoch >= currentEpoch - 1 ? stake.currentEpochBalance : stake.nextEpochBalance; if (_stake != 0) { @@ -303,16 +286,35 @@ contract MixinStakingPoolRewards is } } - // Add rewards up to the last reward epoch. - if (lastRewardEpoch != 0) { + // Get the last epoch where a reward was credited to this pool. + uint256 lastRewardEpoch = lastPoolRewardEpoch[poolId]; + + // If the stake has been touched since the last reward epoch, + // it has already been claimed. + if (stake.currentEpoch >= lastRewardEpoch) { + return reward; + } + // From here we know: `stake.currentEpoch < currentEpoch > 0`. + + if (stake.currentEpoch < lastRewardEpoch) { reward = reward.safeAdd( _computeMemberRewardOverInterval( poolId, stake, - stake.currentEpoch + 1, - lastRewardEpoch + stake.currentEpoch, + stake.currentEpoch + 1 ) ); + if (stake.currentEpoch + 1 < lastRewardEpoch) { + reward = reward.safeAdd( + _computeMemberRewardOverInterval( + poolId, + stake, + stake.currentEpoch + 1, + lastRewardEpoch + ) + ); + } } } @@ -356,5 +358,9 @@ contract MixinStakingPoolRewards is isDependent ); } + uint256 nextEpoch = epoch.safeAdd(1); + if (!_isCumulativeRewardSet(cumulativeRewardsByPoolPtr[nextEpoch])) { + cumulativeRewardsByPoolPtr[nextEpoch] = mostRecentCumulativeRewards; + } } } diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index c9bf932171..77e042564c 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -165,14 +165,11 @@ contract MixinFinalizer is poolsRemaining = poolsRemaining.safeSub(1); } - // Deposit all the rewards at once into the RewardVault. + // Update finalization states. if (rewardsPaid != 0) { - _depositIntoStakingPoolRewardVault(rewardsPaid); + totalRewardsPaidLastEpoch = + totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); } - - // Update finalization states. - totalRewardsPaidLastEpoch = - totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); unfinalizedPoolsRemaining = _unfinalizedPoolsRemaining = poolsRemaining; // If there are no more unfinalized pools remaining, the epoch is @@ -184,6 +181,12 @@ contract MixinFinalizer is unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) ); } + + // Deposit all the rewards at once into the RewardVault. + if (rewardsPaid != 0) { + _depositIntoStakingPoolRewardVault(rewardsPaid); + } + } /// @dev Instantly finalizes a single pool that was active in the previous diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 6ae34f7b2d..6a5aa00953 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -43,42 +43,33 @@ export class FinalizerActor extends BaseActor { public async finalizeAsync(rewards: Reward[] = []): Promise { // cache initial info and balances - const operatorShareByPoolId = - await this._getOperatorShareByPoolIdAsync(this._poolIds); - const rewardVaultBalanceByPoolId = - await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); - const delegatorBalancesByPoolId = - await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); - const delegatorStakesByPoolId = - await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId); + const operatorShareByPoolId = await this._getOperatorShareByPoolIdAsync(this._poolIds); + const rewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); + const delegatorBalancesByPoolId = await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); + const delegatorStakesByPoolId = await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId); // compute expected changes - const expectedRewardVaultBalanceByPoolId = - await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( - rewards, - rewardVaultBalanceByPoolId, - operatorShareByPoolId, - ); - const totalRewardsByPoolId = - _.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward')); - const expectedDelegatorBalancesByPoolId = - await this._computeExpectedDelegatorBalancesByPoolIdAsync( - this._delegatorsByPoolId, - delegatorBalancesByPoolId, - delegatorStakesByPoolId, - operatorShareByPoolId, - totalRewardsByPoolId, - ); + const expectedRewardVaultBalanceByPoolId = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( + rewards, + rewardVaultBalanceByPoolId, + operatorShareByPoolId, + ); + const totalRewardsByPoolId = _.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward')); + const expectedDelegatorBalancesByPoolId = await this._computeExpectedDelegatorBalancesByPoolIdAsync( + this._delegatorsByPoolId, + delegatorBalancesByPoolId, + delegatorStakesByPoolId, + operatorShareByPoolId, + totalRewardsByPoolId, + ); // finalize await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // assert reward vault changes - const finalRewardVaultBalanceByPoolId = - await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); + const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal( expectedRewardVaultBalanceByPoolId, ); // assert delegator balances - const finalDelegatorBalancesByPoolId = - await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); + const finalDelegatorBalancesByPoolId = await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); expect(finalDelegatorBalancesByPoolId, 'final delegator balances in reward vault').to.be.deep.equal( expectedDelegatorBalancesByPoolId, ); @@ -103,13 +94,12 @@ export class FinalizerActor extends BaseActor { } const operator = this._operatorByPoolId[poolId]; - const [, membersStakeInPool] = - await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); + const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); const operatorShare = operatorShareByPoolId[poolId].dividedBy(PPM_100_PERCENT); const totalReward = totalRewardByPoolId[poolId]; - const operatorReward = membersStakeInPool.eq(0) ? - totalReward : - totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN); + const operatorReward = membersStakeInPool.eq(0) + ? totalReward + : totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN); const membersTotalReward = totalReward.minus(operatorReward); for (const delegator of delegatorsByPoolId[poolId]) { @@ -133,10 +123,8 @@ export class FinalizerActor extends BaseActor { private async _getDelegatorBalancesByPoolIdAsync( delegatorsByPoolId: DelegatorsByPoolId, ): Promise { - const computeRewardBalanceOfDelegator = - this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; - const rewardVaultBalanceOfOperator = - this._stakingApiWrapper.rewardVaultContract.balanceOfOperator; + const computeRewardBalanceOfDelegator = this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; + const rewardVaultBalanceOfOperator = this._stakingApiWrapper.rewardVaultContract.balanceOfOperator; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { @@ -144,19 +132,11 @@ export class FinalizerActor extends BaseActor { const delegators = delegatorsByPoolId[poolId]; delegatorBalancesByPoolId[poolId] = {}; for (const delegator of delegators) { - let balance = - new BigNumber(delegatorBalancesByPoolId[poolId][delegator] || 0); + let balance = new BigNumber(delegatorBalancesByPoolId[poolId][delegator] || 0); if (delegator === operator) { - balance = balance.plus( - await rewardVaultBalanceOfOperator.callAsync(poolId), - ); + balance = balance.plus(await rewardVaultBalanceOfOperator.callAsync(poolId)); } else { - balance = balance.plus( - await computeRewardBalanceOfDelegator.callAsync( - poolId, - delegator, - ), - ); + balance = balance.plus(await computeRewardBalanceOfDelegator.callAsync(poolId, delegator)); } delegatorBalancesByPoolId[poolId][delegator] = balance; } @@ -167,16 +147,13 @@ 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]; delegatorBalancesByPoolId[poolId] = {}; for (const delegator of delegators) { - delegatorBalancesByPoolId[poolId][ - delegator - ] = (await getStakeDelegatedToPoolByOwner.callAsync( + delegatorBalancesByPoolId[poolId][delegator] = (await getStakeDelegatedToPoolByOwner.callAsync( delegator, poolId, )).currentEpochBalance; @@ -195,13 +172,12 @@ export class FinalizerActor extends BaseActor { const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId); for (const reward of rewards) { const operatorShare = operatorShareByPoolId[reward.poolId]; - expectedRewardVaultBalanceByPoolId[reward.poolId] = - await this._computeExpectedRewardVaultBalanceAsync( - reward.poolId, - reward.reward, - expectedRewardVaultBalanceByPoolId[reward.poolId], - operatorShare, - ); + expectedRewardVaultBalanceByPoolId[reward.poolId] = await this._computeExpectedRewardVaultBalanceAsync( + reward.poolId, + reward.reward, + expectedRewardVaultBalanceByPoolId[reward.poolId], + operatorShare, + ); } return [expectedOperatorBalanceByPoolId, expectedRewardVaultBalanceByPoolId]; } @@ -233,18 +209,13 @@ export class FinalizerActor extends BaseActor { return operatorBalanceByPoolId; } - private async _getOperatorAndDelegatorsStakeInPoolAsync( - poolId: string, - ): Promise<[BigNumber, BigNumber]> { + private async _getOperatorAndDelegatorsStakeInPoolAsync(poolId: string): Promise<[BigNumber, BigNumber]> { const stakingContract = this._stakingApiWrapper.stakingContract; const operator = await stakingContract.getPoolOperator.callAsync(poolId); - const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync( - poolId, - )).currentEpochBalance; - const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync( - operator, - poolId, - )).currentEpochBalance; + const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync(poolId)) + .currentEpochBalance; + const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync(operator, poolId)) + .currentEpochBalance; const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool); return [operatorStakeInPool, membersStakeInPool]; } diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 75f6fa8daf..7293152b62 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -74,7 +74,7 @@ blockchainTests.resets('Testing Rewards', env => { // Skip to next epoch so operator stake is realized. await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); }); - describe.skip('Reward Simulation', () => { + describe('Reward Simulation', () => { interface EndBalances { // staker 1 stakerRewardVaultBalance_1?: BigNumber; @@ -494,7 +494,7 @@ blockchainTests.resets('Testing Rewards', env => { operatorEthVaultBalance: totalRewardsNotForDelegator, }); }); - it.only('Should collect fees correctly when leaving and returning to a pool', async () => { + it('Should collect fees correctly when leaving and returning to a pool', async () => { // first staker delegates (epoch 0) const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)]; const rewardNotForDelegator = toBaseUnitAmount(7); @@ -628,13 +628,13 @@ blockchainTests.resets('Testing Rewards', env => { for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeWithPoolAsync(poolId, stakeAmount); } - // skip epoch, so staker can start earning rewards + // skip epoch, so stakers can start earning rewards await payProtocolFeeAndFinalize(); // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); // Undelegate 0 stake to move rewards from RewardVault into the EthVault. - for (const [staker] of stakersAndStake) { + for (const [staker] of _.reverse(stakersAndStake)) { await staker.moveStakeAsync( new StakeInfo(StakeStatus.Delegated, poolId), new StakeInfo(StakeStatus.Active), diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts index 6c91cd1949..f0e3377023 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts @@ -36,9 +36,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { stake: Numberish; } - async function rewardPoolMembersAsync( - opts?: Partial, - ): Promise { + async function rewardPoolMembersAsync(opts?: Partial): Promise { const _opts = { poolId: hexRandom(), reward: getRandomInteger(1, toBaseUnitAmount(100)), @@ -103,11 +101,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { ...opts, }; const fn = now ? testContract.delegateStakeNow : testContract.delegateStake; - const receipt = await fn.awaitTransactionSuccessAsync( - _opts.delegator, - poolId, - new BigNumber(_opts.stake), - ); + const receipt = await fn.awaitTransactionSuccessAsync(_opts.delegator, poolId, new BigNumber(_opts.stake)); return { ..._opts, deposit: getDepositFromLogs(receipt.logs, poolId, _opts.delegator), @@ -120,17 +114,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { stake?: Numberish, ): Promise> { const _stake = new BigNumber( - stake || (await - testContract - .getStakeDelegatedToPoolByOwner - .callAsync(delegator, poolId) - ).currentEpochBalance, - ); - const receipt = await testContract.undelegateStake.awaitTransactionSuccessAsync( - delegator, - poolId, - _stake, + stake || + (await testContract.getStakeDelegatedToPoolByOwner.callAsync(delegator, poolId)).currentEpochBalance, ); + const receipt = await testContract.undelegateStake.awaitTransactionSuccessAsync(delegator, poolId, _stake); return { stake: _stake, deposit: getDepositFromLogs(receipt.logs, poolId, delegator), @@ -138,11 +125,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { } function getDepositFromLogs(logs: LogEntry[], poolId: string, delegator?: string): BigNumber { - const events = - filterLogsToArguments( - logs, - TestDelegatorRewardsEvents.Deposit, - ); + const events = filterLogsToArguments( + logs, + TestDelegatorRewardsEvents.Deposit, + ); if (events.length > 0) { expect(events.length).to.eq(1); expect(events[0].poolId).to.eq(poolId); @@ -160,11 +146,8 @@ blockchainTests.resets.only('delegator unit rewards', env => { return epoch.toNumber(); } - async function getDelegatorRewardAsync(poolId: string, delegator: string): Promise { - return testContract.computeRewardBalanceOfDelegator.callAsync( - poolId, - delegator, - ); + async function getDelegatorRewardBalanceAsync(poolId: string, delegator: string): Promise { + return testContract.computeRewardBalanceOfDelegator.callAsync(poolId, delegator); } async function touchStakeAsync(poolId: string, delegator: string): Promise> { @@ -197,7 +180,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { it('nothing in epoch 0 for delegator with no stake', async () => { const { poolId } = await rewardPoolMembersAsync(); const delegator = randomAddress(); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -205,7 +188,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 const { poolId } = await rewardPoolMembersAsync(); const delegator = randomAddress(); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -214,7 +197,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { // Assign active stake to pool in epoch 0, which is usuaslly not // possible due to delegating delays. const { delegator } = await delegateStakeNowAsync(poolId); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -222,7 +205,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 const { poolId } = await rewardPoolMembersAsync(); const { delegator } = await delegateStakeAsync(poolId); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -232,7 +215,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) // rewards paid for stake in epoch 0. await rewardPoolMembersAsync({ poolId, stake }); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -243,7 +226,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. const { reward } = await rewardPoolMembersAsync({ poolId, stake }); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -255,7 +238,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake }); await advanceEpochAsync(); // epoch 3 const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake }); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(BigNumber.sum(reward1, reward2)); }); @@ -265,10 +248,11 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward, stake: rewardStake } = await rewardPoolMembersAsync( - { poolId, stake: new BigNumber(delegatorStake).times(2) }, - ); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const { reward, stake: rewardStake } = await rewardPoolMembersAsync({ + poolId, + stake: new BigNumber(delegatorStake).times(2), + }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorRewards = computeDelegatorRewards(reward, delegatorStake, rewardStake); assertRoughlyEquals(delegatorReward, expectedDelegatorRewards); }); @@ -279,12 +263,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward } = await rewardPoolMembersAsync( - { poolId, stake }, - ); + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); const { deposit } = await undelegateStakeAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -294,13 +276,11 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward } = await rewardPoolMembersAsync( - { poolId, stake }, - ); + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); const { deposit } = await undelegateStakeAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); await delegateStakeAsync(poolId, { delegator, stake }); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -316,14 +296,50 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3. - const { reward } = await rewardPoolMembersAsync( - { poolId, stake }, - ); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); + expect(delegatorReward).to.bignumber.eq(reward); + }); + + it('ignores rewards paid in the same epoch the stake was first active in', async () => { + const poolId = hexRandom(); + // stake at 0 + const { delegator, stake } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake now active) + // Pay rewards for epoch 0. + await advanceEpochAsync(); // epoch 2 + // Pay rewards for epoch 1. + const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); - it.only('computes correct rewards for 2 staggered delegators', async () => { + it('uses old stake for rewards paid in the same epoch EXTRA stake was first active in', async () => { + const poolId = hexRandom(); + // stake at 0 + const { delegator, stake: stake1 } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake1 now active) + // add extra stake + const { stake: stake2 } = await delegateStakeAsync(poolId, { delegator }); + const totalStake = BigNumber.sum(stake1, stake2); + // Make the total stake in rewards > totalStake so delegator never + // receives 100% of rewards. + const rewardStake = totalStake.times(2); + await advanceEpochAsync(); // epoch 2 (stake2 now active) + // Pay rewards for epoch 1. + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + await advanceEpochAsync(); // epoch 3 + // Pay rewards for epoch 2. + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); + const expectedDelegatorReward = BigNumber.sum( + computeDelegatorRewards(reward1, stake1, rewardStake), + computeDelegatorRewards(reward2, totalStake, rewardStake), + ); + expect(delegatorReward).to.bignumber.eq(expectedDelegatorReward); + }); + + it('computes correct rewards for 2 staggered delegators', async () => { const poolId = hexRandom(); const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake A now active) @@ -331,24 +347,18 @@ blockchainTests.resets.only('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { reward: reward1 } = await rewardPoolMembersAsync( - { poolId, stake: stakeA }, - ); + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: stakeA }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 (delegator A and B) - const { reward: reward2 } = await rewardPoolMembersAsync( - { poolId, stake: totalStake }, - ); - const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA); + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), computeDelegatorRewards(reward2, stakeA, totalStake), ); assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA); - const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB); - const expectedDelegatorRewardB = BigNumber.sum( - computeDelegatorRewards(BigNumber.sum(reward1, reward2), stakeB, totalStake), - ); + const delegatorRewardB = await getDelegatorRewardBalanceAsync(poolId, delegatorB); + const expectedDelegatorRewardB = BigNumber.sum(computeDelegatorRewards(reward2, stakeB, totalStake)); assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB); }); @@ -360,25 +370,19 @@ blockchainTests.resets.only('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { reward: reward1 } = await rewardPoolMembersAsync( - { poolId, stake: stakeA }, - ); + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: stakeA }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3 (delegator A and B) - const { reward: reward2 } = await rewardPoolMembersAsync( - { poolId, stake: totalStake }, - ); - const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA); + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), computeDelegatorRewards(reward2, stakeA, totalStake), ); assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA); - const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB); - const expectedDelegatorRewardB = BigNumber.sum( - computeDelegatorRewards(reward2, stakeB, totalStake), - ); + const delegatorRewardB = await getDelegatorRewardBalanceAsync(poolId, delegatorB); + const expectedDelegatorRewardB = BigNumber.sum(computeDelegatorRewards(reward2, stakeB, totalStake)); assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB); }); @@ -388,15 +392,17 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward: reward1, stake: rewardStake1 } = await rewardPoolMembersAsync( - { poolId, stake: new BigNumber(delegatorStake).times(2) }, - ); + const { reward: reward1, stake: rewardStake1 } = await rewardPoolMembersAsync({ + poolId, + stake: new BigNumber(delegatorStake).times(2), + }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 - const { reward: reward2, stake: rewardStake2 } = await rewardPoolMembersAsync( - { poolId, stake: new BigNumber(delegatorStake).times(3) }, - ); - const delegatorReward = await getDelegatorRewardAsync(poolId, delegator); + const { reward: reward2, stake: rewardStake2 } = await rewardPoolMembersAsync({ + poolId, + stake: new BigNumber(delegatorStake).times(3), + }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( computeDelegatorRewards(reward1, delegatorStake, rewardStake1), computeDelegatorRewards(reward2, delegatorStake, rewardStake2), @@ -410,7 +416,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId, { stake: 0 }); await advanceEpochAsync(); // epoch 1 await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(0); }); @@ -419,32 +425,32 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(0); }); - it('returns only unfinalized rewards from epoch 2 for delegator delegating in epoch 1', async () => { + it('returns unfinalized rewards from epoch 2 for delegator delegating in epoch 0', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(unfinalizedReward); }); - it('returns only unfinalized rewards from epoch 3 for delegator delegating in epoch 1', async () => { + it('returns unfinalized rewards from epoch 3 for delegator delegating in epoch 0', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 await advanceEpochAsync(); // epoch 3 const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(unfinalizedReward); }); - it('returns unfinalized rewards from epoch 3 + rewards from epoch 2 for delegator delegating in epoch 1', async () => { + it('returns unfinalized rewards from epoch 3 + rewards from epoch 2 for delegator delegating in epoch 0', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 @@ -452,7 +458,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake }); await advanceEpochAsync(); // epoch 3 const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); expect(reward).to.bignumber.eq(expectedReward); }); @@ -466,7 +472,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum(prevReward, unfinalizedReward); expect(reward).to.bignumber.eq(expectedReward); }); @@ -476,16 +482,17 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { reward: prevReward, stake: prevStake } = await rewardPoolMembersAsync( - { poolId, stake: new BigNumber(stake).times(2) }, - ); + const { reward: prevReward, stake: prevStake } = await rewardPoolMembersAsync({ + poolId, + stake: new BigNumber(stake).times(2), + }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 - const { reward: unfinalizedReward, stake: unfinalizedStake } = - await setUnfinalizedMembersRewardsAsync( - { poolId, stake: new BigNumber(stake).times(5) }, - ); - const reward = await getDelegatorRewardAsync(poolId, delegator); + const { reward: unfinalizedReward, stake: unfinalizedStake } = await setUnfinalizedMembersRewardsAsync({ + poolId, + stake: new BigNumber(stake).times(5), + }); + const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedReward = BigNumber.sum( computeDelegatorRewards(prevReward, stake, prevStake), computeDelegatorRewards(unfinalizedReward, stake, unfinalizedStake), @@ -504,7 +511,9 @@ blockchainTests.resets.only('delegator unit rewards', env => { // rewards paid for stake in epoch 1 const { reward } = await rewardPoolMembersAsync({ poolId, stake }); const { deposit } = await touchStakeAsync(poolId, delegator); + const finalRewardBalance = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); + expect(finalRewardBalance).to.bignumber.eq(0); }); it('does not collect extra rewards from delegating more stake in the reward epoch', async () => { @@ -513,21 +522,21 @@ blockchainTests.resets.only('delegator unit rewards', env => { // stake stakeResults.push(await delegateStakeAsync(poolId)); const { delegator, stake } = stakeResults[0]; - const totalStake = new BigNumber(stake).times(2); + const rewardStake = new BigNumber(stake).times(2); await advanceEpochAsync(); // epoch 1 (stake now active) // add more stake. stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake })); await advanceEpochAsync(); // epoch 1 (2 * stake now active) // reward for epoch 1, using 2 * stake so delegator should // only be entitled to a fraction of the rewards. - const { reward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { reward } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); await advanceEpochAsync(); // epoch 2 // touch the stake one last time stakeResults.push(await touchStakeAsync(poolId, delegator)); // Should only see deposits for epoch 2. - const expectedDeposit = computeDelegatorRewards(reward, stake, totalStake); const allDeposits = stakeResults.map(r => r.deposit); - assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedDeposit); + const expectedReward = computeDelegatorRewards(reward, stake, rewardStake); + assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedReward); }); it('only collects rewards from staked epochs', async () => { @@ -536,27 +545,46 @@ blockchainTests.resets.only('delegator unit rewards', env => { // stake stakeResults.push(await delegateStakeAsync(poolId)); const { delegator, stake } = stakeResults[0]; - await advanceEpochAsync(); // epoch 1 (stake now active) - // unstake before and after reward payout, to be extra sneaky. - const unstake1 = new BigNumber(stake).dividedToIntegerBy(2); - stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake1)); + const rewardStake = new BigNumber(stake).times(2); + await advanceEpochAsync(); // epoch 1 (full stake now active) // reward for epoch 0 - await rewardPoolMembersAsync({ poolId, stake }); - const unstake2 = new BigNumber(stake).minus(unstake1); - stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake2)); - await advanceEpochAsync(); // epoch 2 (no active stake) + await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + // unstake some + const unstake = new BigNumber(stake).dividedToIntegerBy(2); + stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake)); + await advanceEpochAsync(); // epoch 2 (half active stake) // reward for epoch 1 - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); // re-stake - stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake })); - await advanceEpochAsync(); // epoch 3 (stake now active) + stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake: unstake })); + await advanceEpochAsync(); // epoch 3 (full stake now active) // reward for epoch 2 - await rewardPoolMembersAsync({ poolId, stake }); - // touch the stake one last time + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + // touch the stake to claim rewards stakeResults.push(await touchStakeAsync(poolId, delegator)); - // Should only see deposits for epoch 2. const allDeposits = stakeResults.map(r => r.deposit); - assertRoughlyEquals(BigNumber.sum(...allDeposits), reward); + const expectedReward = BigNumber.sum( + computeDelegatorRewards(reward1, stake, rewardStake), + computeDelegatorRewards(reward2, new BigNumber(stake).minus(unstake), rewardStake), + ); + assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedReward); + }); + + it('two delegators can collect split rewards as soon as available', async () => { + const poolId = hexRandom(); + const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId); + const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId); + const totalStake = BigNumber.sum(stakeA, stakeB); + await advanceEpochAsync(); // epoch 1 (stakes now active) + await advanceEpochAsync(); // epoch 2 + // rewards paid for stake in epoch 1 + const { reward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + // delegator A will finalize and collect rewards by touching stake. + const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + assertRoughlyEquals(depositA, computeDelegatorRewards(reward, stakeA, totalStake)); + // delegator B will collect rewards by touching stake + const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + assertRoughlyEquals(depositB, computeDelegatorRewards(reward, stakeB, totalStake)); }); it('delegator B collects correct rewards after delegator A finalizes', async () => { @@ -570,7 +598,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake }); + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ + poolId, + stake: totalStake, + }); const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); // delegator A will finalize and collect rewards by touching stake. const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); @@ -591,7 +622,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake }); + const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ + poolId, + stake: totalStake, + }); const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); // finalize await finalizePoolAsync(poolId); diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 11a8a15056..48e3ebcd41 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -255,7 +255,7 @@ blockchainTests.resets('finalizer unit tests', env => { // Add a pool so there is state to clear. await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - assertFinalizationStateAsync({ + return assertFinalizationStateAsync({ currentEpoch: INITIAL_EPOCH + 1, closingEpoch: INITIAL_EPOCH, numActivePoolsThisEpoch: 0, @@ -268,7 +268,7 @@ blockchainTests.resets('finalizer unit tests', env => { // Add a pool so there is state to clear. const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - assertFinalizationStateAsync({ + return assertFinalizationStateAsync({ unfinalizedPoolsRemaining: 1, unfinalizedRewardsAvailable: INITIAL_BALANCE, unfinalizedTotalFeesCollected: pool.feesCollected, @@ -318,7 +318,7 @@ blockchainTests.resets('finalizer unit tests', env => { it('can finalize multiple pools', async () => { const nextEpoch = INITIAL_EPOCH + 1; - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); @@ -338,7 +338,7 @@ blockchainTests.resets('finalizer unit tests', env => { it('can finalize multiple pools over multiple transactions', async () => { const nextEpoch = INITIAL_EPOCH + 1; - const pools = await Promise.all(_.times(2, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipts = await Promise.all( pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), @@ -358,7 +358,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('ignores a non-active pool', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const nonActivePoolId = hexRandom(); const poolIds = _.shuffle([...pools.map(p => p.poolId), nonActivePoolId]); await testContract.endEpoch.awaitTransactionSuccessAsync(); @@ -371,7 +371,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('ignores a finalized pool', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); await testContract.endEpoch.awaitTransactionSuccessAsync(); const [finalizedPool] = _.sampleSize(pools, 1); @@ -385,7 +385,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('resets pool state after finalizing it', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); @@ -399,7 +399,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('`rewardsPaid` is the sum of all pool rewards', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); @@ -412,7 +412,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; @@ -438,7 +438,7 @@ blockchainTests.resets('finalizer unit tests', env => { for (const i of _.times(numTests)) { const numPools = _.random(1, 32); it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => { - const pools = await Promise.all(_.times(numPools, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; @@ -477,7 +477,7 @@ blockchainTests.resets('finalizer unit tests', env => { it('can finalize multiple pools over multiple transactions', async () => { const nextEpoch = INITIAL_EPOCH + 1; - const pools = await Promise.all(_.times(2, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipts = await Promise.all( pools.map(pool => testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId)), @@ -497,7 +497,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('ignores a finalized pool', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const [finalizedPool] = _.sampleSize(pools, 1); await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); @@ -507,7 +507,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('resets pool state after finalizing it', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId); @@ -521,7 +521,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); @@ -551,7 +551,7 @@ blockchainTests.resets('finalizer unit tests', env => { for (const i of _.times(numTests)) { const numPools = _.random(1, 32); it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => { - const pools = await Promise.all(_.times(numPools, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync())); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const receipts = await Promise.all( @@ -601,11 +601,11 @@ blockchainTests.resets('finalizer unit tests', env => { it('rolls over leftover rewards into th next epoch', async () => { const poolIds = _.times(3, () => hexRandom()); - await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id }))); + await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); await testContract.endEpoch.awaitTransactionSuccessAsync(); let receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(receipt.logs)[0]; - await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id }))); + await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); @@ -690,7 +690,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('returns empty if pool was already finalized', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const [pool] = _.sampleSize(pools, 1); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); @@ -712,7 +712,7 @@ blockchainTests.resets('finalizer unit tests', env => { }); it('computes one reward among multiple pools', async () => { - const pools = await Promise.all(_.times(3, () => addActivePoolAsync())); + const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const [pool] = _.sampleSize(pools, 1); const totalFeesCollected = BigNumber.sum(...pools.map(p => p.feesCollected)); diff --git a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts index 8e76add5b8..34e425c1da 100644 --- a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts +++ b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts @@ -1,12 +1,4 @@ -import { - blockchainTests, - constants, - expect, - hexConcat, - hexRandom, - hexSlice, - testCombinatoriallyWithReferenceFunc, -} from '@0x/contracts-test-utils'; +import { blockchainTests, constants, expect, hexConcat, hexRandom, hexSlice } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; import { cartesianProduct } from 'js-combinatorics'; From 52b0ba5b056ca82f478137a6e45e41f6b6866bc0 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 17 Sep 2019 14:29:32 -0400 Subject: [PATCH 28/52] `@0x/contracts-staking`: Fix linter errors. --- .../staking/contracts/test/TestFinalizer.sol | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index ebd530fdd3..8a5305c30f 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -53,6 +53,40 @@ contract TestFinalizer is init(); } + /// @dev Activate a pool in the current epoch. + function addActivePool( + bytes32 poolId, + uint32 operatorShare, + uint256 feesCollected, + uint256 membersStake, + uint256 weightedStake + ) + external + { + require(feesCollected > 0, "FEES_MUST_BE_NONZERO"); + mapping (bytes32 => IStructs.ActivePool) storage activePools = + _getActivePoolsFromEpoch(currentEpoch); + IStructs.ActivePool memory pool = activePools[poolId]; + require(pool.feesCollected == 0, "POOL_ALREADY_ADDED"); + _operatorSharesByPool[poolId] = operatorShare; + activePools[poolId] = IStructs.ActivePool({ + feesCollected: feesCollected, + membersStake: membersStake, + weightedStake: weightedStake + }); + totalFeesCollectedThisEpoch += feesCollected; + totalWeightedStakeThisEpoch += weightedStake; + numActivePoolsThisEpoch += 1; + } + + /// @dev Expose `_finalizePool()` + function internalFinalizePool(bytes32 poolId) + external + returns (IStructs.PoolRewards memory rewards) + { + rewards = _finalizePool(poolId); + } + /// @dev Get finalization-related state variables. function getFinalizationState() external @@ -82,32 +116,6 @@ contract TestFinalizer is _unfinalizedTotalWeightedStake = unfinalizedTotalWeightedStake; } - /// @dev Activate a pool in the current epoch. - function addActivePool( - bytes32 poolId, - uint32 operatorShare, - uint256 feesCollected, - uint256 membersStake, - uint256 weightedStake - ) - external - { - require(feesCollected > 0, "FEES_MUST_BE_NONZERO"); - mapping (bytes32 => IStructs.ActivePool) storage activePools = - _getActivePoolsFromEpoch(currentEpoch); - IStructs.ActivePool memory pool = activePools[poolId]; - require(pool.feesCollected == 0, "POOL_ALREADY_ADDED"); - _operatorSharesByPool[poolId] = operatorShare; - activePools[poolId] = IStructs.ActivePool({ - feesCollected: feesCollected, - membersStake: membersStake, - weightedStake: weightedStake - }); - totalFeesCollectedThisEpoch += feesCollected; - totalWeightedStakeThisEpoch += weightedStake; - numActivePoolsThisEpoch += 1; - } - /// @dev Compute Cobb-Douglas. function cobbDouglas( uint256 totalRewards, @@ -140,7 +148,6 @@ contract TestFinalizer is rewards = _getUnfinalizedPoolRewards(poolId); } - /// @dev Expose `_getActivePoolFromEpoch`. function internalGetActivePoolFromEpoch(uint256 epoch, bytes32 poolId) external @@ -150,15 +157,6 @@ contract TestFinalizer is pool = _getActivePoolFromEpoch(epoch, poolId); } - - /// @dev Expose `_finalizePool()` - function internalFinalizePool(bytes32 poolId) - external - returns (IStructs.PoolRewards memory rewards) - { - rewards = _finalizePool(poolId); - } - /// @dev Overridden to just store inputs. function _recordRewardForDelegators( bytes32 poolId, @@ -228,8 +226,7 @@ contract TestFinalizer is currentEpoch += 1; } + // solhint-disable no-empty-blocks /// @dev Overridden to do nothing. - function _unwrapWETH() internal { - // NOOP - } + function _unwrapWETH() internal {} } From 7fb5ed0b429a0fd39b817cc315c0d4f6e0360b1b Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 17 Sep 2019 22:12:04 -0400 Subject: [PATCH 29/52] `@0x/contracts-staking`: Add another test case to delegator rewards tests. `@0x/contracts-staking`: Rename `delegator_reward_balance_test.ts` -> `delegator_reward_test.ts`. `@0x/contracts-staking`: Last call before rebasing against 3.0. --- ...lance_test.ts => delegator_reward_test.ts} | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) rename contracts/staking/test/unit_tests/{delegator_reward_balance_test.ts => delegator_reward_test.ts} (94%) diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts similarity index 94% rename from contracts/staking/test/unit_tests/delegator_reward_balance_test.ts rename to contracts/staking/test/unit_tests/delegator_reward_test.ts index f0e3377023..9762eee537 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_balance_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -18,7 +18,7 @@ import { import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; -blockchainTests.resets.only('delegator unit rewards', env => { +blockchainTests.resets('delegator unit rewards', env => { let testContract: TestDelegatorRewardsContract; before(async () => { @@ -239,7 +239,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 3 const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); - expect(delegatorReward).to.bignumber.eq(BigNumber.sum(reward1, reward2)); + assertRoughlyEquals(delegatorReward, BigNumber.sum(reward1, reward2)); }); it('partial rewards from epoch 2 and 3 for delegator partially delegating in epoch 0', async () => { @@ -314,7 +314,35 @@ blockchainTests.resets.only('delegator unit rewards', env => { expect(delegatorReward).to.bignumber.eq(reward); }); - it('uses old stake for rewards paid in the same epoch EXTRA stake was first active in', async () => { + it('uses old stake for rewards paid in the same epoch extra stake is added', async () => { + const poolId = hexRandom(); + // stake at 0 + const { delegator, stake: stake1 } = await delegateStakeAsync(poolId); + await advanceEpochAsync(); // epoch 1 (stake1 now active) + await advanceEpochAsync(); // epoch 2 + const stake2 = getRandomInteger(0, stake1); + const totalStake = BigNumber.sum(stake1, stake2); + // Make the total stake in rewards > totalStake so delegator never + // receives 100% of rewards. + const rewardStake = totalStake.times(2); + // Pay rewards for epoch 1. + const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + // add extra stake + const { deposit } = await delegateStakeAsync(poolId, { delegator, stake: stake2 }); + await advanceEpochAsync(); // epoch 3 (stake2 now active) + // Pay rewards for epoch 2. + await advanceEpochAsync(); // epoch 4 + // Pay rewards for epoch 3. + const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); + const expectedDelegatorReward = BigNumber.sum( + computeDelegatorRewards(reward1, stake1, rewardStake), + computeDelegatorRewards(reward2, totalStake, rewardStake), + ); + assertRoughlyEquals(BigNumber.sum(deposit, delegatorReward), expectedDelegatorReward); + }); + + it('uses old stake for rewards paid in the epoch right after extra stake is added', async () => { const poolId = hexRandom(); // stake at 0 const { delegator, stake: stake1 } = await delegateStakeAsync(poolId); @@ -322,10 +350,10 @@ blockchainTests.resets.only('delegator unit rewards', env => { // add extra stake const { stake: stake2 } = await delegateStakeAsync(poolId, { delegator }); const totalStake = BigNumber.sum(stake1, stake2); + await advanceEpochAsync(); // epoch 2 (stake2 now active) // Make the total stake in rewards > totalStake so delegator never // receives 100% of rewards. const rewardStake = totalStake.times(2); - await advanceEpochAsync(); // epoch 2 (stake2 now active) // Pay rewards for epoch 1. const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); await advanceEpochAsync(); // epoch 3 @@ -336,7 +364,7 @@ blockchainTests.resets.only('delegator unit rewards', env => { computeDelegatorRewards(reward1, stake1, rewardStake), computeDelegatorRewards(reward2, totalStake, rewardStake), ); - expect(delegatorReward).to.bignumber.eq(expectedDelegatorReward); + assertRoughlyEquals(delegatorReward, expectedDelegatorReward); }); it('computes correct rewards for 2 staggered delegators', async () => { From ac7f6aef9ecc7005a4d6da48377f40985d12ed6b Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Sep 2019 12:13:45 -0400 Subject: [PATCH 30/52] `@0x/contracts-staking`: It compiles! --- contracts/staking/contracts/src/Staking.sol | 16 +- .../staking/contracts/src/StakingProxy.sol | 2 + .../contracts/src/fees/MixinExchangeFees.sol | 17 +- .../src/fees/MixinExchangeManager.sol | 2 + .../contracts/src/immutable/MixinStorage.sol | 22 +- .../contracts/src/interfaces/IEthVault.sol | 15 +- .../src/interfaces/IStakingEvents.sol | 12 +- .../interfaces/IStakingPoolRewardVault.sol | 43 ++- .../contracts/src/interfaces/IStructs.sol | 10 - .../contracts/src/stake/MixinStake.sol | 118 ++++-- .../src/stake/MixinStakeBalances.sol | 5 + .../contracts/src/stake/MixinStakeStorage.sol | 4 + .../staking_pools/MixinCumulativeRewards.sol | 212 +++++----- .../src/staking_pools/MixinStakingPool.sol | 20 +- .../staking_pools/MixinStakingPoolMakers.sol | 2 + .../MixinStakingPoolModifiers.sol | 2 + .../staking_pools/MixinStakingPoolRewards.sol | 362 +++++++++++------- .../contracts/src/sys/MixinAbstract.sol | 26 +- .../contracts/src/sys/MixinFinalizer.sol | 211 +++++----- .../staking/contracts/src/sys/MixinParams.sol | 2 + .../contracts/src/sys/MixinScheduler.sol | 2 + .../staking/contracts/src/vaults/EthVault.sol | 26 +- .../src/vaults/StakingPoolRewardVault.sol | 45 ++- .../staking/contracts/src/vaults/ZrxVault.sol | 2 + .../contracts/test/TestDelegatorRewards.sol | 185 ++++++--- .../staking/contracts/test/TestFinalizer.sol | 135 +++---- .../contracts/test/TestProtocolFees.sol | 10 - .../contracts/test/TestStorageLayout.sol | 2 +- contracts/staking/src/artifacts.ts | 6 +- contracts/staking/src/wrappers.ts | 2 + contracts/staking/tsconfig.json | 2 + .../utils/contracts/src/LibFractions.sol | 8 +- .../order-utils/src/staking_revert_errors.ts | 4 + 33 files changed, 889 insertions(+), 643 deletions(-) diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index 18f0469eca..9dffb115b6 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -29,10 +29,24 @@ import "./fees/MixinExchangeFees.sol"; contract Staking is IStaking, + IStakingEvents, + MixinAbstract, + MixinConstants, + MixinDeploymentConstants, + Ownable, + MixinStorage, + MixinStakingPoolModifiers, + MixinExchangeManager, MixinParams, + MixinScheduler, + MixinStakeStorage, + MixinStakingPoolMakers, + MixinStakeBalances, + MixinCumulativeRewards, + MixinStakingPoolRewards, MixinStakingPool, MixinStake, - MixinExchangeFees, + MixinExchangeFees { // this contract can receive ETH // solhint-disable no-empty-blocks diff --git a/contracts/staking/contracts/src/StakingProxy.sol b/contracts/staking/contracts/src/StakingProxy.sol index 07f7271c77..09ad728a3a 100644 --- a/contracts/staking/contracts/src/StakingProxy.sol +++ b/contracts/staking/contracts/src/StakingProxy.sol @@ -27,6 +27,8 @@ import "./interfaces/IStakingProxy.sol"; contract StakingProxy is IStakingProxy, + MixinConstants, + Ownable, MixinStorage { using LibProxy for address; diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 9e61dcbb69..992a38c5aa 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -44,10 +44,19 @@ import "./MixinExchangeManager.sol"; /// disincentive for market makers to monopolize a single pool that they /// all delegate to. contract MixinExchangeFees is + IStakingEvents, + MixinAbstract, + MixinConstants, MixinDeploymentConstants, + Ownable, + MixinStorage, + MixinStakingPoolModifiers, MixinExchangeManager, - MixinAbstract, + MixinScheduler, + MixinStakeStorage, + MixinStakingPoolMakers, MixinStakeBalances, + MixinCumulativeRewards, MixinStakingPoolRewards, MixinStakingPool { @@ -99,7 +108,7 @@ contract MixinExchangeFees is } // Look up the pool for this epoch. - uint256 currentEpoch = getCurrentEpoch(); + uint256 currentEpoch = currentEpoch; mapping (bytes32 => IStructs.ActivePool) storage activePoolsThisEpoch = _getActivePoolsFromEpoch(currentEpoch); IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId]; @@ -169,7 +178,7 @@ contract MixinExchangeFees is // because we only need to remember state in the current epoch and the // epoch prior. IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(getCurrentEpoch(), poolId); + _getActivePoolFromEpoch(currentEpoch, poolId); feesCollected = pool.feesCollected; } @@ -188,7 +197,7 @@ contract MixinExchangeFees is returns (uint256 membersStake, uint256 weightedStake) { uint256 operatorStake = getStakeDelegatedToPoolByOwner( - getPoolOperator(poolId), + poolById[poolId].operator, poolId ).currentEpochBalance; membersStake = totalStake.safeSub(operatorStake); diff --git a/contracts/staking/contracts/src/fees/MixinExchangeManager.sol b/contracts/staking/contracts/src/fees/MixinExchangeManager.sol index c174ede72a..04e3713aa4 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeManager.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeManager.sol @@ -30,6 +30,8 @@ import "../immutable/MixinStorage.sol"; /// then it should be removed. contract MixinExchangeManager is IStakingEvents, + MixinConstants, + Ownable, MixinStorage { /// @dev Asserts that the call is coming from a valid exchange. diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 95d9978521..11433d9b55 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -74,12 +74,6 @@ contract MixinStorage is // mapping from Owner to Amount of Withdrawable Stake mapping (address => uint256) internal _withdrawableStakeByOwner; - // Mapping from Owner to Pool Id to epoch of the last rewards collected. - // This is the last reward epoch for a pool that a delegator collected - // rewards from. This is different from the epoch when the rewards were - // collected This will always be `<= currentEpoch`. - mapping (address => mapping (bytes32 => uint256)) internal lastCollectedRewardsEpochToPoolByOwner; - // tracking Pool Id bytes32 public nextPoolId = INITIAL_POOL_ID; @@ -141,35 +135,35 @@ contract MixinStorage is /// @dev The total fees collected in the current epoch, built up iteratively /// in `payProtocolFee()`. - uint256 internal totalFeesCollectedThisEpoch; + uint256 public totalFeesCollectedThisEpoch; /// @dev The total weighted stake in the current epoch, built up iteratively /// in `payProtocolFee()`. - uint256 internal totalWeightedStakeThisEpoch; + uint256 public totalWeightedStakeThisEpoch; /// @dev State information for each active pool in an epoch. /// In practice, we only store state for `currentEpoch % 2`. mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) internal - activePoolsByEpoch; + _activePoolsByEpoch; /// @dev Number of pools activated in the current epoch. - uint256 internal numActivePoolsThisEpoch; + uint256 public numActivePoolsThisEpoch; /// @dev Rewards (ETH) available to the epoch being finalized (the previous /// epoch). This is simply the balance of the contract at the end of /// the epoch. - uint256 internal unfinalizedRewardsAvailable; + uint256 public unfinalizedRewardsAvailable; /// @dev The number of active pools in the last epoch that have yet to be /// finalized through `finalizePools()`. - uint256 internal unfinalizedPoolsRemaining; + uint256 public unfinalizedPoolsRemaining; /// @dev The total fees collected for the epoch being finalized. - uint256 internal unfinalizedTotalFeesCollected; + uint256 public unfinalizedTotalFeesCollected; /// @dev The total fees collected for the epoch being finalized. - uint256 internal unfinalizedTotalWeightedStake; + uint256 public unfinalizedTotalWeightedStake; /// @dev How many rewards were paid at the end of finalization. uint256 totalRewardsPaidLastEpoch; diff --git a/contracts/staking/contracts/src/interfaces/IEthVault.sol b/contracts/staking/contracts/src/interfaces/IEthVault.sol index 8867557b65..225658768f 100644 --- a/contracts/staking/contracts/src/interfaces/IEthVault.sol +++ b/contracts/staking/contracts/src/interfaces/IEthVault.sol @@ -42,13 +42,14 @@ interface IEthVault { uint256 amount ); - /// @dev Deposit an `amount` of ETH from `owner` into the vault. - /// Note that only the Staking contract can call this. - /// Note that this can only be called when *not* in Catostrophic Failure mode. - /// @param owner of ETH Tokens. - function depositFor(address owner) - external - payable; + /// @dev 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 + /// same transaction. + /// Note that this is only callable by the staking contract. + /// @param owner Owner of the ETH. + /// @param amount Amount of deposit. + function recordDepositFor(address owner, uint256 amount) + external; /// @dev Withdraw an `amount` of ETH to `msg.sender` from the vault. /// Note that only the Staking contract can call this. diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index fe5a3a6f27..2084d222d5 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -48,8 +48,8 @@ interface IStakingEvents { /// @param epoch The epoch in which the pool was activated. /// @param poolId The ID of the pool. event StakingPoolActivated( - uint256 epoch, - bytes32 poolId + uint256 indexed epoch, + bytes32 indexed poolId ); /// @dev Emitted by MixinFinalizer when an epoch has ended. @@ -59,7 +59,7 @@ interface IStakingEvents { /// @param totalWeightedStake Total weighted stake across all active pools. /// @param totalFeesCollected Total fees collected across all active pools. event EpochEnded( - uint256 epoch, + uint256 indexed epoch, uint256 numActivePools, uint256 rewardsAvailable, uint256 totalFeesCollected, @@ -71,7 +71,7 @@ interface IStakingEvents { /// @param rewardsPaid Total amount of rewards paid out. /// @param rewardsRemaining Rewards left over. event EpochFinalized( - uint256 epoch, + uint256 indexed epoch, uint256 rewardsPaid, uint256 rewardsRemaining ); @@ -82,8 +82,8 @@ interface IStakingEvents { /// @param operatorReward Amount of reward paid to pool operator. /// @param membersReward Amount of reward paid to pool members. event RewardsPaid( - uint256 epoch, - bytes32 poolId, + uint256 indexed epoch, + bytes32 indexed poolId, uint256 operatorReward, uint256 membersReward ); diff --git a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol index 2653ce4154..d536529f22 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol @@ -33,34 +33,35 @@ interface IStakingPoolRewardVault { uint256 amount ); - /// @dev Emitted when a reward is transferred to the ETH vault. - /// @param amount The amount in ETH withdrawn. - /// @param member of the pool. - /// @param poolId The pool the reward was deposited for. - event PoolRewardTransferredToEthVault( + /// @dev Emitted when rewards are transferred out fo the vault. + /// @param poolId Unique Id of pool. + /// @param to Address to send funds to. + /// @param amount Amount of ETH to transfer. + event PoolRewardTransferred( bytes32 indexed poolId, - address indexed member, + address to, uint256 amount ); - /// @dev Deposit an amount of ETH (`msg.value`) for `poolId` into the vault. - /// Note that this is only callable by the staking contract. - /// @param poolId that owns the ETH. - function depositFor(bytes32 poolId) - external - payable; - /// @dev Withdraw some amount in ETH of a pool member. - /// Note that this is only callable by the staking contract. + /// @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. + /// Note that this is only callable by the staking contract. + /// @param poolId Pool that holds the ETH. + /// @param amount Amount of deposit. + function recordDepositFor(bytes32 poolId, uint256 amount) + external; + + /// @dev Withdraw some amount in ETH from a pool. + /// Note that this is only callable by the staking contract. /// @param poolId Unique Id of pool. - /// @param member of pool to transfer funds to. - /// @param amount Amount in ETH to transfer. - /// @param ethVaultAddress address of Eth Vault to send rewards to. - function transferToEthVault( + /// @param to Address to send funds to. + /// @param amount Amount of ETH to transfer. + function transfer( bytes32 poolId, - address member, - uint256 amount, - address ethVaultAddress + address payable to, + uint256 amount ) external; diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index 790c0c2bcf..4d738ea3dd 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -32,16 +32,6 @@ interface IStructs { uint256 membersStake; } - /// @dev Rewards credited to a pool during finalization. - /// @param operatorReward The amount of reward credited to the pool operator. - /// @param membersReward The amount of reward credited to the pool members. - /// @param membersStake The amount of members/delegated stake in the pool. - struct PoolRewards { - uint256 operatorReward; - uint256 membersReward; - uint256 membersStake; - } - /// @dev Encapsulates a balance for the current and next epochs. /// Note that these balances may be stale if the current epoch /// is greater than `currentEpoch`. diff --git a/contracts/staking/contracts/src/stake/MixinStake.sol b/contracts/staking/contracts/src/stake/MixinStake.sol index 0b354196c7..0ef2ef8801 100644 --- a/contracts/staking/contracts/src/stake/MixinStake.sol +++ b/contracts/staking/contracts/src/stake/MixinStake.sol @@ -27,16 +27,24 @@ import "../libs/LibStakingRichErrors.sol"; /// @dev This mixin contains logic for managing ZRX tokens and Stake. contract MixinStake is + IStakingEvents, + MixinAbstract, + MixinConstants, + Ownable, MixinStorage, + MixinStakingPoolModifiers, + MixinScheduler, + MixinStakeStorage, MixinStakingPoolMakers, + MixinStakeBalances, + MixinCumulativeRewards, MixinStakingPoolRewards, MixinStakingPool - { using LibSafeMath for uint256; - /// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. Unstake to retrieve the ZRX. - /// Stake is in the 'Active' status. + /// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. + /// Unstake to retrieve the ZRX. Stake is in the 'Active' status. /// @param amount of ZRX to stake. function stake(uint256 amount) external @@ -59,8 +67,9 @@ contract MixinStake is ); } - /// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to the owner. - /// Stake must be in the 'inactive' status for at least one full epoch to unstake. + /// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to + /// the owner. Stake must be in the 'inactive' status for at least + /// one full epoch to unstake. /// @param amount of ZRX to unstake. function unstake(uint256 amount) external @@ -85,7 +94,8 @@ contract MixinStake is _decrementCurrentAndNextBalance(globalStakeByStatus[uint8(IStructs.StakeStatus.INACTIVE)], amount); // update withdrawable field - _withdrawableStakeByOwner[owner] = currentWithdrawableStake.safeSub(amount); + _withdrawableStakeByOwner[owner] = + currentWithdrawableStake.safeSub(amount); // withdraw equivalent amount of ZRX from vault zrxVault.withdrawFrom(owner, amount); @@ -110,16 +120,20 @@ contract MixinStake is external { // sanity check - do nothing if moving stake between the same status - if (from.status != IStructs.StakeStatus.DELEGATED && from.status == to.status) { + if (from.status != IStructs.StakeStatus.DELEGATED + && from.status == to.status) + { return; - } else if (from.status == IStructs.StakeStatus.DELEGATED && from.poolId == to.poolId) { + } else if (from.status == IStructs.StakeStatus.DELEGATED + && from.poolId == to.poolId) + { return; } address payable owner = msg.sender; - // handle delegation; this must be done before moving stake as the current - // (out-of-sync) status is used during delegation. + // handle delegation; this must be done before moving stake as the + // current (out-of-sync) status is used during delegation. if (from.status == IStructs.StakeStatus.DELEGATED) { _undelegateStake( from.poolId, @@ -136,14 +150,18 @@ contract MixinStake is ); } - // cache the current withdrawal amount, which may change if we're moving out of the inactive status. - uint256 withdrawableStake = (from.status == IStructs.StakeStatus.INACTIVE) + // cache the current withdrawal amount, which may change if we're + // moving out of the inactive status. + uint256 withdrawableStake = + (from.status == IStructs.StakeStatus.INACTIVE) ? getWithdrawableStake(owner) : 0; // execute move - IStructs.StoredBalance storage fromPtr = _getBalancePtrFromStatus(owner, from.status); - IStructs.StoredBalance storage toPtr = _getBalancePtrFromStatus(owner, to.status); + IStructs.StoredBalance storage fromPtr = + _getBalancePtrFromStatus(owner, from.status); + IStructs.StoredBalance storage toPtr = + _getBalancePtrFromStatus(owner, to.status); _moveStake(fromPtr, toPtr, amount); // update global total of stake in the statuses being moved between @@ -155,7 +173,8 @@ contract MixinStake is // update withdrawable field, if necessary if (from.status == IStructs.StakeStatus.INACTIVE) { - _withdrawableStakeByOwner[owner] = _computeWithdrawableStake(owner, withdrawableStake); + _withdrawableStakeByOwner[owner] = + _computeWithdrawableStake(owner, withdrawableStake); } // notify @@ -171,8 +190,8 @@ contract MixinStake is /// @dev Delegates a owners stake to a staking pool. /// @param poolId Id of pool to delegate to. - /// @param owner who wants to delegate. - /// @param amount of stake to delegate. + /// @param owner Owner who wants to delegate. + /// @param amount Amount of stake to delegate. function _delegateStake( bytes32 poolId, address payable owner, @@ -180,27 +199,38 @@ contract MixinStake is ) private { - // sanity check the pool we're delegating to exists + // Sanity check the pool we're delegating to exists. _assertStakingPoolExists(poolId); - // cache amount delegated to pool by owner - IStructs.StoredBalance memory initDelegatedStakeToPoolByOwner = _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[owner][poolId]); + // 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 the owner has delegated to the input pool. + _incrementNextBalance( + _delegatedStakeToPoolByOwner[owner][poolId], + amount + ); - // increment how much stake has been delegated to pool + // 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); + // 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. /// @param poolId Id of pool to un-delegate from. - /// @param owner who wants to un-delegate. - /// @param amount of stake to un-delegate. + /// @param owner Owner who wants to un-delegate. + /// @param amount Amount of stake to un-delegate. function _undelegateStake( bytes32 poolId, address payable owner, @@ -212,24 +242,38 @@ contract MixinStake is _assertStakingPoolExists(poolId); // cache amount delegated to pool by owner - IStructs.StoredBalance memory initDelegatedStakeToPoolByOwner = _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[owner][poolId]); + 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); + _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); + // 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. - /// @param owner of stake to query. - /// @param status of user's stake to lookup. - /// @return a storage pointer to the corresponding stake stake - function _getBalancePtrFromStatus(address owner, IStructs.StakeStatus status) + /// @param owner Owner of stake to query. + /// @param status Status of user's stake to lookup. + /// @return storage A storage pointer to the corresponding stake stake + function _getBalancePtrFromStatus( + address owner, + IStructs.StakeStatus status + ) private view returns (IStructs.StoredBalance storage) diff --git a/contracts/staking/contracts/src/stake/MixinStakeBalances.sol b/contracts/staking/contracts/src/stake/MixinStakeBalances.sol index d8003977e6..21350423b1 100644 --- a/contracts/staking/contracts/src/stake/MixinStakeBalances.sol +++ b/contracts/staking/contracts/src/stake/MixinStakeBalances.sol @@ -27,6 +27,11 @@ import "./MixinStakeStorage.sol"; /// @dev This mixin contains logic for querying stake balances. /// **** Read MixinStake before continuing **** contract MixinStakeBalances is + IStakingEvents, + MixinConstants, + Ownable, + MixinStorage, + MixinScheduler, MixinStakeStorage { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/stake/MixinStakeStorage.sol b/contracts/staking/contracts/src/stake/MixinStakeStorage.sol index 0c202aeabf..87539f45a0 100644 --- a/contracts/staking/contracts/src/stake/MixinStakeStorage.sol +++ b/contracts/staking/contracts/src/stake/MixinStakeStorage.sol @@ -26,6 +26,10 @@ import "../sys/MixinScheduler.sol"; /// @dev This mixin contains logic for managing stake storage. contract MixinStakeStorage is + IStakingEvents, + MixinConstants, + Ownable, + MixinStorage, MixinScheduler { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol index 53dfb4b88e..f61142afe3 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol @@ -25,6 +25,12 @@ import "../stake/MixinStakeBalances.sol"; contract MixinCumulativeRewards is + IStakingEvents, + MixinConstants, + Ownable, + MixinStorage, + MixinScheduler, + MixinStakeStorage, MixinStakeBalances { using LibSafeMath for uint256; @@ -34,7 +40,7 @@ contract MixinCumulativeRewards is function _initializeCumulativeRewards(bytes32 poolId) internal { - // sets the default cumulative reward + // Sets the default cumulative reward _forceSetCumulativeReward( poolId, currentEpoch, @@ -51,31 +57,35 @@ contract MixinCumulativeRewards is pure returns (bool) { - // we use the denominator as a proxy for whether the cumulative + // 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. + /// @dev 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. + /// @param epoch 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 + // 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 ); } /// @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. + /// @param epoch Epoch of cumulative reward. + /// @param value Value of cumulative reward. function _trySetCumulativeReward( bytes32 poolId, uint256 epoch, @@ -84,7 +94,7 @@ contract MixinCumulativeRewards is internal { if (_isCumulativeRewardSet(_cumulativeRewardsByPool[poolId][epoch])) { - // do nothing; we don't want to override the current value + // Do nothing; we don't want to override the current value return; } _forceSetCumulativeReward(poolId, epoch, value); @@ -93,8 +103,8 @@ contract MixinCumulativeRewards is /// @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. + /// @param epoch Epoch of cumulative reward. + /// @param value Value of cumulative reward. function _forceSetCumulativeReward( bytes32 poolId, uint256 epoch, @@ -108,7 +118,7 @@ contract MixinCumulativeRewards is /// @dev Tries to unset the cumulative reward for `poolId` at `epoch`. /// @param poolId Unique id of pool. - /// @param epoch of cumulative reward to unset. + /// @param epoch Epoch of cumulative reward to unset. function _tryUnsetCumulativeReward(bytes32 poolId, uint256 epoch) internal { @@ -120,11 +130,11 @@ contract MixinCumulativeRewards is /// @dev Unsets the cumulative reward for `poolId` at `epoch`. /// @param poolId Unique id of pool. - /// @param epoch of cumulative reward to unset. + /// @param epoch Epoch of cumulative reward to unset. function _forceUnsetCumulativeReward(bytes32 poolId, uint256 epoch) internal { - _cumulativeRewardsByPool[poolId][epoch] = IStructs.Fraction({numerator: 0, denominator: 0}); + delete _cumulativeRewardsByPool[poolId][epoch]; } /// @dev Returns info on most recent cumulative reward. @@ -133,31 +143,38 @@ contract MixinCumulativeRewards is view returns (IStructs.CumulativeRewardInfo memory) { - // fetch the last epoch at which we stored a cumulative reward for this pool - uint256 cumulativeRewardsLastStored = _cumulativeRewardsByPoolLastStored[poolId]; + // 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 + // Query and return cumulative reward info for this pool return IStructs.CumulativeRewardInfo({ - cumulativeReward: _cumulativeRewardsByPool[poolId][cumulativeRewardsLastStored], + 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. + /// 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) + /// @param epoch 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]; + // Check if we should do any work + uint256 currentMostRecentEpoch = + _cumulativeRewardsByPoolLastStored[poolId]; if (epoch == currentMostRecentEpoch) { return; } - // update state to reflect the most recent cumulative reward + // Update state to reflect the most recent cumulative reward _forceSetMostRecentCumulativeRewardEpoch( poolId, currentMostRecentEpoch, @@ -167,8 +184,10 @@ contract MixinCumulativeRewards is /// @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. + /// @param currentMostRecentEpoch Epoch of the most recent cumulative + /// reward. + /// @param newMostRecentEpoch Epoch of the new most recent cumulative + /// reward. function _forceSetMostRecentCumulativeRewardEpoch( bytes32 poolId, uint256 currentMostRecentEpoch, @@ -176,19 +195,21 @@ contract MixinCumulativeRewards is ) internal { - // sanity check that we're not trying to go back in time + // 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 + // 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` + /// @param epoch 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, @@ -211,51 +232,13 @@ contract MixinCumulativeRewards is } } - /// @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] + /// @param memberStakeOverInterval Stake delegated to pool by member over + /// the interval. + /// @param beginEpoch Beginning of interval. + /// @param endEpoch End of interval. + /// @return rewards Reward accumulated over interval [beginEpoch, endEpoch] function _computeMemberRewardOverInterval( bytes32 poolId, uint256 memberStakeOverInterval, @@ -264,18 +247,19 @@ contract MixinCumulativeRewards is ) internal view - returns (uint256) + returns (uint256 reward) { - // sanity check inputs if (memberStakeOverInterval == 0) { return 0; } - // sanity check interval - if (beginEpoch >= endEpoch) { + // Sanity check interval + if (beginEpoch > endEpoch) { LibRichErrors.rrevert( LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors.CumulativeRewardIntervalErrorCode.BeginEpochMustBeLessThanEndEpoch, + LibStakingRichErrors + .CumulativeRewardIntervalErrorCode + .BeginEpochMustBeLessThanEndEpoch, poolId, beginEpoch, endEpoch @@ -283,12 +267,15 @@ contract MixinCumulativeRewards is ); } - // sanity check begin reward - IStructs.Fraction memory beginReward = _cumulativeRewardsByPool[poolId][beginEpoch]; + // Sanity check begin reward + IStructs.Fraction memory beginReward = + _cumulativeRewardsByPool[poolId][beginEpoch]; if (!_isCumulativeRewardSet(beginReward)) { LibRichErrors.rrevert( LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors.CumulativeRewardIntervalErrorCode.BeginEpochDoesNotHaveReward, + LibStakingRichErrors + .CumulativeRewardIntervalErrorCode + .BeginEpochDoesNotHaveReward, poolId, beginEpoch, endEpoch @@ -296,12 +283,15 @@ contract MixinCumulativeRewards is ); } - // sanity check end reward - IStructs.Fraction memory endReward = _cumulativeRewardsByPool[poolId][endEpoch]; + // Sanity check end reward + IStructs.Fraction memory endReward = + _cumulativeRewardsByPool[poolId][endEpoch]; if (!_isCumulativeRewardSet(endReward)) { LibRichErrors.rrevert( LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors.CumulativeRewardIntervalErrorCode.EndEpochDoesNotHaveReward, + LibStakingRichErrors + .CumulativeRewardIntervalErrorCode + .EndEpochDoesNotHaveReward, poolId, beginEpoch, endEpoch @@ -309,14 +299,56 @@ contract MixinCumulativeRewards is ); } - // compute reward - uint256 reward = LibFractions.scaleFractionalDifference( + // Compute reward + reward = LibFractions.scaleFractionalDifference( endReward.numerator, endReward.denominator, beginReward.numerator, beginReward.denominator, memberStakeOverInterval ); - return reward; + } + + /// @dev Adds a dependency on a cumulative reward for a given epoch. + /// @param poolId Unique Id of pool. + /// @param epoch Epoch to remove dependency from. + /// @param mostRecentCumulativeRewardInfo Info on the most recent cumulative + /// reward. + function _addDependencyOnCumulativeReward( + bytes32 poolId, + uint256 epoch, + IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo + ) + private + { + // 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 Epoch to remove dependency from. + function _removeDependencyOnCumulativeReward( + bytes32 poolId, + uint256 epoch + ) + private + { + // 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); } } diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol index 5b599350d7..86ff675de9 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol @@ -28,8 +28,17 @@ import "./MixinStakingPoolMakers.sol"; contract MixinStakingPool is + IStakingEvents, + MixinAbstract, + MixinConstants, + Ownable, MixinStorage, + MixinStakingPoolModifiers, + MixinScheduler, + MixinStakeStorage, MixinStakingPoolMakers, + MixinStakeBalances, + MixinCumulativeRewards, MixinStakingPoolRewards { using LibSafeMath for uint256; @@ -113,17 +122,6 @@ contract MixinStakingPool is return poolById[poolId]; } - /// @dev Look up the operator of a pool. - /// @param poolId The ID of the pool. - /// @return operatorAddress The pool operator. - function getPoolOperator(bytes32 poolId) - public - view - returns (address operatorAddress) - { - return rewardVault.operatorOf(poolId); - } - /// @dev Computes the unique id that comes after the input pool id. /// @param poolId Unique id of pool. /// @return Next pool id after input pool. diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol index c09991e537..c96144cdb1 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol @@ -31,6 +31,8 @@ import "./MixinStakingPoolModifiers.sol"; /// @dev This mixin contains logic for staking pools. contract MixinStakingPoolMakers is IStakingEvents, + MixinConstants, + Ownable, MixinStorage, MixinStakingPoolModifiers { diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol index 7892f0c9af..4e01992f25 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol @@ -23,6 +23,8 @@ import "../immutable/MixinStorage.sol"; contract MixinStakingPoolModifiers is + MixinConstants, + Ownable, MixinStorage { diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 92906d0a33..d7e1994e58 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -27,31 +27,53 @@ import "../sys/MixinAbstract.sol"; contract MixinStakingPoolRewards is - MixinCumulativeRewards, - MixinAbstract + IStakingEvents, + MixinAbstract, + MixinConstants, + Ownable, + MixinStorage, + MixinScheduler, + MixinStakeStorage, + MixinStakeBalances, + MixinCumulativeRewards { using LibSafeMath for uint256; - /// @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. - /// This is used by a delegator when they want to sync their rewards without delegating/undelegating. - /// It's effectively the same as delegating zero stake. + function computeRewardBalanceOfOperator(bytes32 poolId, address operator) + public + view + returns (uint256 reward) + { + // TODO. + // unfinalizedStake + + } + + /// @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. + /// This is used by a delegator when they want to sync their rewards + /// without delegating/undelegating. It's effectively the same as + /// delegating zero stake. /// @param poolId Unique id of pool. function syncDelegatorRewards(bytes32 poolId) external { address member = msg.sender; - IStructs.StoredBalance memory finalDelegatedStakeToPoolByOwner = _loadAndSyncBalance(_delegatedStakeToPoolByOwner[member][poolId]); + IStructs.StoredBalance memory finalDelegatedStakeToPoolByOwner = + _loadAndSyncBalance(_delegatedStakeToPoolByOwner[member][poolId]); _syncRewardsForDelegator( poolId, member, - _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[member][poolId]), // initial balance + // Initial balance + _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[member][poolId]), finalDelegatedStakeToPoolByOwner ); - // update stored balance with synchronized version; this prevents redundant withdrawals. - _delegatedStakeToPoolByOwner[member][poolId] = finalDelegatedStakeToPoolByOwner; + // Update stored balance with synchronized version; this prevents + // redundant withdrawals. + _delegatedStakeToPoolByOwner[member][poolId] = + finalDelegatedStakeToPoolByOwner; } /// @dev Computes the reward balance in ETH of a specific member of a pool. @@ -59,51 +81,38 @@ contract MixinStakingPoolRewards is /// @param member The member of the pool. /// @return totalReward Balance in ETH. function computeRewardBalanceOfDelegator(bytes32 poolId, address member) - public + external view returns (uint256 reward) { - IStructs.PoolRewards memory unfinalizedPoolRewards = + IStructs.Pool memory pool = poolById[poolId]; + // Get any unfinalized rewards. + (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); - reward = _computeRewardBalanceOfDelegator( - poolId, - member, - unfinalizedPoolRewards.membersReward, - unfinalizedPoolRewards.membersStake + // Get the members' portion. + (, uint256 unfinalizedMembersReward) = _splitStakingPoolRewards( + pool.operatorShare, + unfinalizedTotalRewards, + unfinalizedMembersStake ); - } - - /// @dev Computes the reward balance in ETH of a specific member of a pool. - /// @param poolId Unique id of pool. - /// @param member The member of the pool. - /// @param unfinalizedMembersReward Unfinalized memeber reward for - /// this pool in the current epoch. - /// @param unfinalizedDelegatedStake Unfinalized total delegated stake for - /// this pool in the current epoch. - /// @return totalReward Balance in ETH. - function _computeRewardBalanceOfDelegator( - bytes32 poolId, - address member, - uint256 unfinalizedMembersReward, - uint256 unfinalizedDelegatedStake - ) - internal - view - returns (uint256 reward) - { - return _computeRewardBalanceOfDelegator( + reward = _computeRewardBalanceOfDelegator( poolId, _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[member][poolId]), - currentEpoch + currentEpoch, + unfinalizedMembersReward, + unfinalizedMembersStake ); } - /// @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. + /// @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 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. + /// @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, @@ -112,8 +121,9 @@ contract MixinStakingPoolRewards is ) internal { - // 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. + // Rransfer 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, @@ -121,14 +131,16 @@ contract MixinStakingPoolRewards is currentEpoch ); - // add dependencies on cumulative rewards for this epoch and the previous epoch, if necessary. + // Add dependencies on cumulative rewards for this epoch and the next + // epoch, if necessary. _setCumulativeRewardDependenciesForDelegator( poolId, finalDelegatedStakeToPoolByOwner, true ); - // remove dependencies on previous cumulative rewards, if they are no longer needed. + // Remove dependencies on previous cumulative rewards, if they are no + // longer needed. _setCumulativeRewardDependenciesForDelegator( poolId, initialDelegatedStakeToPoolByOwner, @@ -136,193 +148,241 @@ contract MixinStakingPoolRewards is ); } - /// @dev Handles a pool's reward. This will deposit the operator's reward into the Eth Vault and - /// the members' reward into the Staking Pool Vault. It also records the cumulative reward, which - /// is used to compute each delegator's portion of the members' reward. + /// @dev Handles a pool's reward at the current epoch. + /// This will compute the reward split and record the cumulative + /// reward, which is used to compute each delegator's portion of the + /// members' reward. It will NOT make any transfers to the eth or + /// reward vaults. That should be done with a separate call to + /// `_depositStakingPoolRewards()``. /// @param poolId Unique Id of pool. /// @param reward received by the pool. - /// @param amountOfDelegatedStake the amount of delegated stake that will split the reward. - /// @param epoch at which this was earned. - function _handleStakingPoolReward( + /// @param membersStake the amount of non-operator delegated stake that + /// will split the reward. + /// @return operatorReward + function _recordStakingPoolRewards( bytes32 poolId, uint256 reward, - uint256 amountOfDelegatedStake + uint256 membersStake ) internal + returns (uint256 operatorReward, uint256 membersReward) { IStructs.Pool memory pool = poolById[poolId]; - // compute the operator's portion of the reward and transfer it to the ETH vault (we round in favor of the operator). - uint256 operatorPortion = amountOfDelegatedStake == 0 - ? reward - : LibMath.getPartialAmountCeil( - uint256(pool.operatorShare), - PPM_DENOMINATOR, - reward - ); + // Split the reward between operator and members + (operatorReward, membersReward) = + _splitStakingPoolRewards(pool.operatorShare, reward, membersStake); - ethVault.depositFor.value(operatorPortion)(pool.operator); + // Record the operator's reward in the eth vault. + ethVault.recordDepositFor(pool.operator, operatorReward); - // compute the reward portion for the pool members and transfer it to the Reward Vault. - uint256 membersPortion = reward.safeSub(operatorPortion); - if (membersPortion == 0) { - return; + if (membersReward == 0) { + return (operatorReward, membersReward); } + // Record the members reward in the reward vault. + rewardVault.recordDepositFor(poolId, membersReward); - rewardVault.depositFor.value(membersPortion)(poolId); - - // cache a storage pointer to the cumulative rewards for `poolId` indexed by epoch. - mapping (uint256 => IStructs.Fraction) storage _cumulativeRewardsByPoolPtr = _cumulativeRewardsByPool[poolId]; + // Cache a storage pointer to the cumulative rewards for `poolId` + // indexed by epoch. + mapping (uint256 => IStructs.Fraction) + storage + _cumulativeRewardsByPoolPtr = _cumulativeRewardsByPool[poolId]; // Fetch the last epoch at which we stored an entry for this pool; // this is the most up-to-date cumulative rewards for this pool. - uint256 cumulativeRewardsLastStored = _cumulativeRewardsByPoolLastStored[poolId]; - IStructs.Fraction memory mostRecentCumulativeRewards = _cumulativeRewardsByPoolPtr[cumulativeRewardsLastStored]; + uint256 cumulativeRewardsLastStored = + _cumulativeRewardsByPoolLastStored[poolId]; + IStructs.Fraction memory mostRecentCumulativeRewards = + _cumulativeRewardsByPoolPtr[cumulativeRewardsLastStored]; // Compute new cumulative reward (uint256 numerator, uint256 denominator) = LibFractions.addFractions( mostRecentCumulativeRewards.numerator, mostRecentCumulativeRewards.denominator, - membersPortion, - amountOfDelegatedStake - ); - - // Normalize fraction components by dividing by the minimum denominator. - uint256 minDenominator = - mostRecentCumulativeRewards.denominator <= amountOfDelegatedStake ? - mostRecentCumulativeRewards.denominator : - amountOfDelegatedStake; - minDenominator = minDenominator == 0 ? 1 : minDenominator; - (uint256 numeratorNormalized, uint256 denominatorNormalized) = ( - numerator.safeDiv(minDenominator), - denominator.safeDiv(minDenominator) + membersReward, + membersStake ); // store cumulative rewards and set most recent _forceSetCumulativeReward( poolId, - epoch, - IStructs.Fraction({ - numerator: numeratorNormalized, - denominator: denominatorNormalized - }) + currentEpoch, + IStructs.Fraction(numerator, denominator) ); } - /// @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 Deposit rewards into the eth vault and reward vault for pool + /// operators and members rewards, respectively. This should be called + /// in tandem with `_recordStakingPoolRewards()`. We separate them + /// so we can bath deposits, because ETH transfers are expensive. + /// @param operatorReward Operator rewards. + /// @param membersReward Operator rewards. + function _depositStakingPoolRewards( + uint256 operatorReward, + uint256 membersReward + ) + internal + { + address(uint160(address(ethVault))).transfer(operatorReward); + address(uint160(address(rewardVault))).transfer(membersReward); + } + + /// @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. + /// @param unsyncedStake Unsynced stake of the delegator to the pool. function _transferDelegatorRewardsToEthVault( bytes32 poolId, address member, - IStructs.StoredBalance memory unsyncedDelegatedStakeToPoolByOwner, + IStructs.StoredBalance memory unsyncedStake, uint256 currentEpoch ) private { - // compute balance owed to delegator + // Ensure the pool is finalized. + _finalizePool(poolId); + + // Compute balance owed to delegator uint256 balance = _computeRewardBalanceOfDelegator( poolId, - unsyncedDelegatedStakeToPoolByOwner, - currentEpoch + unsyncedStake, + currentEpoch, + // No unfinalized values because we ensured the pool is already + // finalized. + 0, + 0 ); if (balance == 0) { return; } - // transfer from transient Reward Pool vault to ETH Vault - rewardVault.transferToEthVault( + // Transfer from transient Reward Pool vault to ETH Vault + ethVault.recordDepositFor(member, balance); + rewardVault.transfer( poolId, - member, - balance, - address(ethVault) + address(uint160(address(ethVault))), + balance ); } + /// @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 unsyncedDelegatedStakeToPoolByOwner Unsynced delegated stake to pool by owner + /// @param unsyncedStake Unsynced delegated stake to pool by owner /// @param currentEpoch The epoch in which this call is executing + /// @param unfinalizedMembersReward Unfinalized total members reward + /// (if any). + /// @param unfinalizedMembersStake Unfinalized total members stake (if any). /// @return totalReward Balance in ETH. function _computeRewardBalanceOfDelegator( bytes32 poolId, - IStructs.StoredBalance memory unsyncedDelegatedStakeToPoolByOwner, - uint256 currentEpoch + IStructs.StoredBalance memory unsyncedStake, + uint256 currentEpoch, + uint256 unfinalizedMembersReward, + uint256 unfinalizedMembersStake ) private view - returns (uint256 totalReward) + returns (uint256 reward) { - uint256 currentEpoch = getCurrentEpoch(); // There can be no rewards in epoch 0 because there is no delegated // stake. if (currentEpoch == 0) { return reward = 0; } - IStructs.StoredBalance memory stake = - _loadUnsyncedBalance(delegatedStakeToPoolByOwner[member][poolId]); // There can be no rewards if the last epoch when stake was synced is // equal to the current epoch, because all prior rewards, including // rewards finalized this epoch have been claimed. - if (stake.currentEpoch == currentEpoch) { + if (unsyncedStake.currentEpoch == currentEpoch) { return reward = 0; } // If there are unfinalized rewards this epoch, compute the member's // share. - if (unfinalizedMembersReward != 0 && unfinalizedDelegatedStake != 0) { + if (unfinalizedMembersReward != 0 && unfinalizedMembersStake != 0) { // Unfinalized rewards are always earned from stake in // the prior epoch so we want the stake at `currentEpoch-1`. - uint256 _stake = stake.currentEpoch >= currentEpoch - 1 ? - stake.currentEpochBalance : - stake.nextEpochBalance; + uint256 _stake = unsyncedStake.currentEpoch >= currentEpoch - 1 ? + unsyncedStake.currentEpochBalance : + unsyncedStake.nextEpochBalance; if (_stake != 0) { reward = _stake .safeMul(unfinalizedMembersReward) - .safeDiv(unfinalizedDelegatedStake); + .safeDiv(unfinalizedMembersStake); } } - // Get the last epoch where a reward was credited to this pool. - uint256 lastRewardEpoch = lastPoolRewardEpoch[poolId]; + // Get the last epoch where a reward was credited to this pool, which + // also happens to be when we last created a cumulative reward entry. + uint256 lastRewardEpoch = _cumulativeRewardsByPoolLastStored[poolId]; // If the stake has been touched since the last reward epoch, // it has already been claimed. - if (stake.currentEpoch >= lastRewardEpoch) { + if (unsyncedStake.currentEpoch >= lastRewardEpoch) { return reward; } - // From here we know: `stake.currentEpoch < currentEpoch > 0`. + // From here we know: `unsyncedStake.currentEpoch < currentEpoch > 0`. - if (stake.currentEpoch < lastRewardEpoch) { + if (unsyncedStake.currentEpoch >= lastRewardEpoch) { + return reward; + } + + reward = reward.safeAdd( + _computeMemberRewardOverInterval( + poolId, + unsyncedStake.currentEpochBalance, + unsyncedStake.currentEpoch, + unsyncedStake.currentEpoch + 1 + ) + ); + if (unsyncedStake.currentEpoch + 1 < lastRewardEpoch) { reward = reward.safeAdd( _computeMemberRewardOverInterval( poolId, - stake, - stake.currentEpoch, - stake.currentEpoch + 1 + unsyncedStake.nextEpochBalance, + unsyncedStake.currentEpoch + 1, + lastRewardEpoch ) ); - if (stake.currentEpoch + 1 < lastRewardEpoch) { - reward = reward.safeAdd( - _computeMemberRewardOverInterval( - poolId, - stake, - stake.currentEpoch + 1, - lastRewardEpoch - ) - ); - } } } /// @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. + /// A delegator always depends on the cumulative reward for the current + /// and next epoch, if they would still have stake in the next epoch. /// @param poolId Unique id of pool. - /// @param _delegatedStakeToPoolByOwner Amount of stake the member has delegated to the 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, @@ -331,26 +391,34 @@ contract MixinStakingPoolRewards is ) private { - // if this delegator is not yet initialized then there's no dependency to unset. + // 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); + // 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) { + // Record dependency on the next epoch + uint256 nextEpoch = currentEpoch.safeAdd(1); + if (_delegatedStakeToPoolByOwner.currentEpoch > 0 + && _delegatedStakeToPoolByOwner.nextEpochBalance != 0) + { _addOrRemoveDependencyOnCumulativeReward( poolId, - uint256(_delegatedStakeToPoolByOwner.currentEpoch).safeSub(1), + nextEpoch, mostRecentCumulativeRewardInfo, isDependent ); } - // record dependency on current epoch. - if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 || _delegatedStakeToPoolByOwner.nextEpochBalance != 0) { + // Record dependency on current epoch. + if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 + || _delegatedStakeToPoolByOwner.nextEpochBalance != 0) + { _addOrRemoveDependencyOnCumulativeReward( poolId, _delegatedStakeToPoolByOwner.currentEpoch, @@ -358,9 +426,5 @@ contract MixinStakingPoolRewards is isDependent ); } - uint256 nextEpoch = epoch.safeAdd(1); - if (!_isCumulativeRewardSet(cumulativeRewardsByPoolPtr[nextEpoch])) { - cumulativeRewardsByPoolPtr[nextEpoch] = mostRecentCumulativeRewards; - } } } diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol index d2be6624e0..987b695b74 100644 --- a/contracts/staking/contracts/src/sys/MixinAbstract.sol +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -29,11 +29,16 @@ contract MixinAbstract { /// @dev Computes the reward owed to a pool during finalization. /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. - /// @return rewards Amount of rewards for this pool. + /// @return operatorReward The reward owed to the pool operator. + /// @return membersStake The total stake for all non-operator members in + /// this pool. function _getUnfinalizedPoolRewards(bytes32 poolId) internal view - returns (IStructs.PoolRewards memory rewards); + returns ( + uint256 reward, + uint256 membersStake + ); /// @dev Get an active pool from an epoch by its ID. /// @param epoch The epoch the pool was/will be active in. @@ -61,13 +66,20 @@ contract MixinAbstract { /// @dev Instantly finalizes a single pool that was active in the previous /// epoch, crediting it rewards and sending those rewards to the reward - /// vault. This can be called by internal functions that need to - /// finalize a pool immediately. Does nothing if the pool is already + /// and eth vault. This can be called by internal functions that need + /// to finalize a pool immediately. Does nothing if the pool is already + /// finalized. Does nothing if the pool was not active or was already /// finalized. /// @param poolId The pool ID to finalize. - /// @return rewards Rewards. - /// @return rewards The rewards credited to the pool. + /// @return operatorReward The reward credited to the pool operator. + /// @return membersReward The reward credited to the pool members. + /// @return membersStake The total stake for all non-operator members in + /// this pool. function _finalizePool(bytes32 poolId) internal - returns (IStructs.PoolRewards memory rewards); + returns ( + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake + ); } diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 77e042564c..b58fccf14a 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -31,7 +31,6 @@ import "../interfaces/IStakingEvents.sol"; import "../interfaces/IStructs.sol"; import "../stake/MixinStakeBalances.sol"; import "../staking_pools/MixinStakingPool.sol"; -import "../staking_pools/MixinStakingPoolRewardVault.sol"; import "./MixinScheduler.sol"; @@ -47,11 +46,10 @@ contract MixinFinalizer is MixinDeploymentConstants, Ownable, MixinStorage, - MixinZrxVault, MixinScheduler, - MixinStakingPoolRewardVault, MixinStakeStorage, MixinStakeBalances, + MixinCumulativeRewards, MixinStakingPoolRewards { using LibSafeMath for uint256; @@ -67,7 +65,7 @@ contract MixinFinalizer is external returns (uint256 _unfinalizedPoolsRemaining) { - uint256 closingEpoch = getCurrentEpoch(); + uint256 closingEpoch = currentEpoch; // Make sure the previous epoch has been fully finalized. if (unfinalizedPoolsRemaining != 0) { @@ -114,9 +112,9 @@ contract MixinFinalizer is } /// @dev Finalizes pools that were active in the previous epoch, paying out - /// rewards to the reward vault. Keepers should call this function - /// repeatedly until all active pools that were emitted in in a - /// `StakingPoolActivated` in the prior epoch have been finalized. + /// rewards to the reward and eth vault. Keepers should call this + /// function repeatedly until all active pools that were emitted in in + /// a `StakingPoolActivated` in the prior epoch have been finalized. /// Pools that have already been finalized will be silently ignored. /// We deliberately try not to revert here in case multiple parties /// are finalizing pools. @@ -126,7 +124,7 @@ contract MixinFinalizer is external returns (uint256 _unfinalizedPoolsRemaining) { - uint256 epoch = getCurrentEpoch(); + uint256 epoch = currentEpoch; // There are no pools to finalize at epoch 0. if (epoch == 0) { return _unfinalizedPoolsRemaining = 0; @@ -144,8 +142,11 @@ contract MixinFinalizer is _getActivePoolsFromEpoch(epoch - 1); uint256 numPoolIds = poolIds.length; uint256 rewardsPaid = 0; + uint256 totalOperatorRewardsPaid = 0; + uint256 totalMembersRewardsPaid = 0; - for (uint256 i = 0; i != numPoolIds && poolsRemaining != 0; ++i) { + for (uint256 i = 0; i != numPoolIds && poolsRemaining != 0; ++i) + { bytes32 poolId = poolIds[i]; IStructs.ActivePool memory pool = activePools[poolId]; @@ -154,12 +155,17 @@ contract MixinFinalizer is continue; } - IStructs.PoolRewards memory poolRewards = - _creditRewardsToPool(epoch, poolId, pool); + (uint256 operatorReward, uint256 membersReward) = + _creditRewardsToPool(epoch, poolId, pool, rewardsPaid); - rewardsPaid = rewardsPaid.safeAdd( - poolRewards.operatorReward + poolRewards.membersReward - ); + totalOperatorRewardsPaid = + totalOperatorRewardsPaid.safeAdd(operatorReward); + totalMembersRewardsPaid = + totalMembersRewardsPaid.safeAdd(membersReward); + + rewardsPaid = rewardsPaid + .safeAdd(operatorReward) + .safeAdd(membersReward); // Decrease the number of unfinalized pools left. poolsRemaining = poolsRemaining.safeSub(1); @@ -182,47 +188,52 @@ contract MixinFinalizer is ); } - // Deposit all the rewards at once into the RewardVault. + // Deposit all the rewards at once. if (rewardsPaid != 0) { - _depositIntoStakingPoolRewardVault(rewardsPaid); + _depositStakingPoolRewards(totalOperatorRewardsPaid, totalMembersRewardsPaid); } - } /// @dev Instantly finalizes a single pool that was active in the previous /// epoch, crediting it rewards and sending those rewards to the reward - /// vault. This can be called by internal functions that need to - /// finalize a pool immediately. Does nothing if the pool is already + /// and eth vault. This can be called by internal functions that need + /// to finalize a pool immediately. Does nothing if the pool is already /// finalized. Does nothing if the pool was not active or was already /// finalized. /// @param poolId The pool ID to finalize. - /// @return rewards Rewards. - /// @return rewards The rewards credited to the pool. + /// @return operatorReward The reward credited to the pool operator. + /// @return membersReward The reward credited to the pool members. + /// @return membersStake The total stake for all non-operator members in + /// this pool. function _finalizePool(bytes32 poolId) internal - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake + ) { - uint256 epoch = getCurrentEpoch(); + uint256 epoch = currentEpoch; // There are no pools to finalize at epoch 0. if (epoch == 0) { - return rewards; + return (operatorReward, membersReward, membersStake); } IStructs.ActivePool memory pool = _getActivePoolFromEpoch(epoch - 1, poolId); // Do nothing if the pool was not active (has no fees). if (pool.feesCollected == 0) { - return rewards; + return (operatorReward, membersReward, membersStake); } - rewards = _creditRewardsToPool(epoch, poolId, pool); - uint256 totalReward = - rewards.membersReward.safeAdd(rewards.operatorReward); + (operatorReward, membersReward) = + _creditRewardsToPool(epoch, poolId, pool, 0); + uint256 totalReward = operatorReward.safeAdd(membersReward); if (totalReward > 0) { totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch.safeAdd(totalReward); - _depositIntoStakingPoolRewardVault(totalReward); + _depositStakingPoolRewards(operatorReward, membersReward); } // Decrease the number of unfinalized pools left. @@ -238,26 +249,32 @@ contract MixinFinalizer is unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) ); } + membersStake = pool.membersStake; } /// @dev Computes the reward owed to a pool during finalization. /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. - /// @return rewards Amount of rewards for this pool. + /// @return operatorReward The reward owed to the pool operator. + /// @return membersStake The total stake for all non-operator members in + /// this pool. function _getUnfinalizedPoolRewards(bytes32 poolId) internal view - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 reward, + uint256 membersStake + ) { - uint256 epoch = getCurrentEpoch(); + uint256 epoch = currentEpoch; // There are no pools to finalize at epoch 0. if (epoch == 0) { - return rewards; + return (reward, membersStake); } - rewards = _getUnfinalizedPoolRewards( - poolId, - _getActivePoolFromEpoch(epoch - 1, poolId) - ); + IStructs.ActivePool memory pool = + _getActivePoolFromEpoch(epoch - 1, poolId); + reward = _getUnfinalizedPoolRewards(pool, 0); + membersStake = pool.membersStake; } /// @dev Get an active pool from an epoch by its ID. @@ -287,11 +304,13 @@ contract MixinFinalizer is view returns (mapping (bytes32 => IStructs.ActivePool) storage activePools) { - activePools = activePoolsByEpoch[epoch % 2]; + activePools = _activePoolsByEpoch[epoch % 2]; } /// @dev Converts the entire WETH balance of the contract into ETH. - function _unwrapWETH() internal { + function _unwrapWETH() + internal + { uint256 wethBalance = IEtherToken(WETH_ADDRESS) .balanceOf(address(this)); if (wethBalance != 0) { @@ -299,69 +318,28 @@ contract MixinFinalizer is } } - /// @dev Splits an amount between the pool operator and members of the - /// pool based on the pool operator's share. - /// @param poolId The ID of the pool. - /// @param amount Amount to to split. - /// @return operatorPortion Portion of `amount` attributed to the operator. - /// @return membersPortion Portion of `amount` attributed to the pool. - function _splitRewardAmountBetweenOperatorAndMembers( - bytes32 poolId, - uint256 amount - ) - internal - view - returns (uint256 operatorReward, uint256 membersReward) - { - (operatorReward, membersReward) = - rewardVault.splitAmountBetweenOperatorAndMembers(poolId, amount); - } - - /// @dev Record a deposit for a pool in the RewardVault. - /// @param poolId ID of the pool. - /// @param amount Amount in ETH to record. - /// @param operatorOnly Only attribute amount to operator. - /// @return operatorPortion Portion of `amount` attributed to the operator. - /// @return membersPortion Portion of `amount` attributed to the pool. - function _recordDepositInRewardVaultFor( - bytes32 poolId, - uint256 amount, - bool operatorOnly - ) - internal - returns ( - uint256 operatorPortion, - uint256 membersPortion - ) - { - (operatorPortion, membersPortion) = rewardVault.recordDepositFor( - poolId, - amount, - operatorOnly - ); - } - /// @dev Computes the reward owed to a pool during finalization. - /// @param poolId The pool's ID. /// @param pool The active pool. - /// @return rewards Amount of rewards for this pool. + /// @param unpaidRewards Rewards that have been credited but not finalized. + /// @return rewards Unfinalized rewards for this pool. function _getUnfinalizedPoolRewards( - bytes32 poolId, - IStructs.ActivePool memory pool + IStructs.ActivePool memory pool, + uint256 unpaidRewards ) private view - returns (IStructs.PoolRewards memory rewards) + returns (uint256 rewards) { // There can't be any rewards if the pool was active or if it has // no stake. if (pool.feesCollected == 0) { - return rewards; + return rewards = 0; } + uint256 unfinalizedRewardsAvailable_ = unfinalizedRewardsAvailable; // Use the cobb-douglas function to compute the total reward. - uint256 totalReward = LibCobbDouglas._cobbDouglas( - unfinalizedRewardsAvailable, + rewards = LibCobbDouglas._cobbDouglas( + unfinalizedRewardsAvailable_, pool.feesCollected, unfinalizedTotalFeesCollected, pool.weightedStake, @@ -370,17 +348,15 @@ contract MixinFinalizer is cobbDouglasAlphaDenominator ); - // Split the reward between the operator and delegators. - if (pool.membersStake == 0) { - rewards.operatorReward = totalReward; - } else { - (rewards.operatorReward, rewards.membersReward) = - _splitRewardAmountBetweenOperatorAndMembers( - poolId, - totalReward - ); + // Clip the reward to always be under + // `unfinalizedRewardsAvailable - totalRewardsPaid - unpaidRewards`, + // in case cobb-douglas overflows, which should be unlikely. + uint256 rewardsRemaining = unfinalizedRewardsAvailable_ + .safeSub(totalRewardsPaidLastEpoch) + .safeSub(unpaidRewards); + if (rewardsRemaining < rewards) { + rewards = rewardsRemaining; } - rewards.membersStake = pool.membersStake; } /// @dev Credits finalization rewards to a pool that was active in the @@ -388,48 +364,41 @@ contract MixinFinalizer is /// @param epoch The current epoch. /// @param poolId The pool ID to finalize. /// @param pool The active pool to finalize. + /// @param unpaidRewards Rewards that have been credited but not finalized. /// @return rewards Rewards. - /// @return rewards The rewards credited to the pool. + /// @return operatorReward The reward credited to the pool operator. + /// @return membersReward The reward credited to the pool members. function _creditRewardsToPool( uint256 epoch, bytes32 poolId, - IStructs.ActivePool memory pool + IStructs.ActivePool memory pool, + uint256 unpaidRewards ) private - returns (IStructs.PoolRewards memory rewards) + returns (uint256 operatorReward, uint256 membersReward) { // Clear the pool state so we don't finalize it again, and to recoup // some gas. delete _getActivePoolsFromEpoch(epoch - 1)[poolId]; // Compute the rewards. - rewards = _getUnfinalizedPoolRewards(poolId, pool); - uint256 totalReward = - rewards.membersReward.safeAdd(rewards.operatorReward); + uint256 rewards = _getUnfinalizedPoolRewards(pool, unpaidRewards); - // Credit the pool the rewards in the RewardVault. - _recordDepositInRewardVaultFor( + // Credit the pool. + // Note that we credit at the CURRENT epoch even though these rewards + // were earned in the previous epoch. + (operatorReward, membersReward) = _recordStakingPoolRewards( poolId, - totalReward, - // If no delegated stake, all rewards go to the operator. - pool.membersStake == 0 + rewards, + pool.membersStake ); - // Sync delegator rewards. - if (rewards.membersReward != 0) { - _recordRewardForDelegators( - poolId, - rewards.membersReward, - pool.membersStake - ); - } - // Emit an event. emit RewardsPaid( epoch, poolId, - rewards.operatorReward, - rewards.membersReward + operatorReward, + membersReward ); } } diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index 64644dd49a..cf8c14ab51 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -30,6 +30,8 @@ import "../libs/LibStakingRichErrors.sol"; contract MixinParams is IStakingEvents, + MixinConstants, + Ownable, MixinStorage { /// @dev Set all configurable parameters at once. diff --git a/contracts/staking/contracts/src/sys/MixinScheduler.sol b/contracts/staking/contracts/src/sys/MixinScheduler.sol index a512fc8527..452bfe8a2b 100644 --- a/contracts/staking/contracts/src/sys/MixinScheduler.sol +++ b/contracts/staking/contracts/src/sys/MixinScheduler.sol @@ -33,6 +33,8 @@ import "../interfaces/IStakingEvents.sol"; /// and consistent scheduling metric than time. TimeLocks, for example, are measured in epochs. contract MixinScheduler is IStakingEvents, + MixinConstants, + Ownable, MixinStorage { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/vaults/EthVault.sol b/contracts/staking/contracts/src/vaults/EthVault.sol index 8627feb626..c3c7510c4b 100644 --- a/contracts/staking/contracts/src/vaults/EthVault.sol +++ b/contracts/staking/contracts/src/vaults/EthVault.sol @@ -26,6 +26,8 @@ import "./MixinVaultCore.sol"; /// @dev This vault manages ETH. contract EthVault is IEthVault, + IVaultCore, + Ownable, MixinVaultCore { using LibSafeMath for uint256; @@ -33,19 +35,21 @@ contract EthVault is // mapping from Owner to ETH balance mapping (address => uint256) internal _balances; - /// @dev Deposit an `amount` of ETH from `owner` into the vault. - /// Note that only the Staking contract can call this. - /// Note that this can only be called when *not* in Catostrophic Failure mode. - /// @param owner of ETH Tokens. - function depositFor(address owner) + // solhint-disable no-empty-blocks + /// @dev Payable fallback for bulk-deposits. + function () payable external {} + + /// @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 + /// same transaction. + /// Note that this is only callable by the staking contract. + /// @param owner Owner of the ETH. + /// @param amount Amount of deposit. + function recordDepositFor(address owner, uint256 amount) external - payable + onlyStakingProxy { - // update balance - uint256 amount = msg.value; - _balances[owner] = _balances[owner].safeAdd(msg.value); - - // notify + _balances[owner] = _balances[owner].safeAdd(amount); emit EthDepositedIntoVault(msg.sender, owner, amount); } diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 7eaf8cb9d2..12c56c2e06 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -25,14 +25,15 @@ import "../libs/LibStakingRichErrors.sol"; import "../libs/LibSafeDowncast.sol"; import "./MixinVaultCore.sol"; import "../interfaces/IStakingPoolRewardVault.sol"; -import "../interfaces/IEthVault.sol"; import "../immutable/MixinConstants.sol"; /// @dev This vault manages staking pool rewards. contract StakingPoolRewardVault is IStakingPoolRewardVault, + IVaultCore, MixinConstants, + Ownable, MixinVaultCore { using LibSafeMath for uint256; @@ -40,38 +41,42 @@ contract StakingPoolRewardVault is // mapping from poolId to Pool metadata mapping (bytes32 => uint256) internal _balanceByPoolId; - /// @dev Deposit an amount of ETH (`msg.value`) for `poolId` into the vault. - /// Note that this is only callable by the staking contract. - /// @param poolId that holds the ETH. - function depositFor(bytes32 poolId) + // solhint-disable no-empty-blocks + /// @dev Payable fallback for bulk-deposits. + function () payable external {} + + /// @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. + /// Note that this is only callable by the staking contract. + /// @param poolId Pool that holds the ETH. + /// @param amount Amount of deposit. + function recordDepositFor(bytes32 poolId, uint256 amount) external - payable onlyStakingProxy { - _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(msg.value); - emit EthDepositedIntoVault(msg.sender, poolId, msg.value); + _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(amount); + emit EthDepositedIntoVault(msg.sender, poolId, amount); } - /// @dev Withdraw some amount in ETH of a pool member. - /// Note that this is only callable by the staking contract. + /// @dev Withdraw some amount in ETH from a pool. + /// Note that this is only callable by the staking contract. /// @param poolId Unique Id of pool. - /// @param member of pool to transfer funds to. - /// @param amount Amount in ETH to transfer. - /// @param ethVaultAddress address of Eth Vault to send rewards to. - function transferToEthVault( + /// @param to Address to send funds to. + /// @param amount Amount of ETH to transfer. + function transfer( bytes32 poolId, - address member, - uint256 amount, - address ethVaultAddress + address payable to, + uint256 amount ) external onlyStakingProxy { _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeSub(amount); - IEthVault(ethVaultAddress).depositFor.value(amount)(member); - emit PoolRewardTransferredToEthVault( + to.transfer(amount); + emit PoolRewardTransferred( poolId, - member, + to, amount ); } diff --git a/contracts/staking/contracts/src/vaults/ZrxVault.sol b/contracts/staking/contracts/src/vaults/ZrxVault.sol index 486a92d048..91f2f4bfcd 100644 --- a/contracts/staking/contracts/src/vaults/ZrxVault.sol +++ b/contracts/staking/contracts/src/vaults/ZrxVault.sol @@ -34,7 +34,9 @@ import "./MixinVaultCore.sol"; /// failure mode, it cannot be returned to normal mode; this prevents /// corruption of related state in the staking contract. contract ZrxVault is + IVaultCore, IZrxVault, + Ownable, MixinVaultCore { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 962a4effea..6cbe5ffbd7 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -20,35 +20,52 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "../src/interfaces/IStructs.sol"; +import "../src/interfaces/IStakingPoolRewardVault.sol"; +import "../src/interfaces/IEthVault.sol"; import "./TestStaking.sol"; contract TestDelegatorRewards is TestStaking { - event Deposit( + event RecordDepositToEthVault( + address owner, + uint256 amount + ); + + event RecordDepositToRewardVault( bytes32 poolId, - address member, - uint256 balance + uint256 membersReward ); event FinalizePool( bytes32 poolId, - uint256 reward, - uint256 stake + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake ); struct UnfinalizedMembersReward { - uint256 reward; - uint256 stake; + uint256 operatorReward; + uint256 membersReward; + uint256 membersStake; } constructor() public { - init(); + init( + address(1), + address(1), + address(1), + address(1) + ); + // Set this contract up as the eth and reward vault to intercept + // deposits. + ethVault = IEthVault(address(this)); + rewardVault = IStakingPoolRewardVault(address(this)); } mapping (uint256 => mapping (bytes32 => UnfinalizedMembersReward)) private - unfinalizedMembersRewardByPoolByEpoch; + unfinalizedPoolRewardsByEpoch; /// @dev Expose _finalizePool function internalFinalizePool(bytes32 poolId) external { @@ -58,15 +75,17 @@ contract TestDelegatorRewards is /// @dev Set unfinalized members reward for a pool in the current epoch. function setUnfinalizedMembersRewards( bytes32 poolId, + uint256 operatorReward, uint256 membersReward, uint256 membersStake ) external { - unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId] = + unfinalizedPoolRewardsByEpoch[currentEpoch][poolId] = UnfinalizedMembersReward({ - reward: membersReward, - stake: membersStake + operatorReward: operatorReward, + membersReward: membersReward, + membersStake: membersStake }); } @@ -85,13 +104,20 @@ contract TestDelegatorRewards is ) external { - _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); - _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance memory initialStake = + _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = - delegatedStakeToPoolByOwner[delegator][poolId]; + _delegatedStakeToPoolByOwner[delegator][poolId]; + _stake.isInitialized = true; _stake.currentEpochBalance += uint96(stake); _stake.nextEpochBalance += uint96(stake); - _stake.currentEpoch = uint64(currentEpoch); + _stake.currentEpoch = uint32(currentEpoch); + _syncRewardsForDelegator( + poolId, + delegator, + initialStake, + _stake + ); } /// @dev Create and delegate stake that will occur in the next epoch @@ -104,15 +130,22 @@ contract TestDelegatorRewards is ) external { - _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); - _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance memory initialStake = + _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = - delegatedStakeToPoolByOwner[delegator][poolId]; + _delegatedStakeToPoolByOwner[delegator][poolId]; if (_stake.currentEpoch < currentEpoch) { _stake.currentEpochBalance = _stake.nextEpochBalance; } + _stake.isInitialized = true; _stake.nextEpochBalance += uint96(stake); - _stake.currentEpoch = uint64(currentEpoch); + _stake.currentEpoch = uint32(currentEpoch); + _syncRewardsForDelegator( + poolId, + delegator, + initialStake, + _stake + ); } /// @dev Clear stake that will occur in the next epoch @@ -125,67 +158,115 @@ contract TestDelegatorRewards is ) external { - _transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator); - _syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch); + IStructs.StoredBalance memory initialStake = + _delegatedStakeToPoolByOwner[delegator][poolId]; IStructs.StoredBalance storage _stake = - delegatedStakeToPoolByOwner[delegator][poolId]; + _delegatedStakeToPoolByOwner[delegator][poolId]; if (_stake.currentEpoch < currentEpoch) { _stake.currentEpochBalance = _stake.nextEpochBalance; } + _stake.isInitialized = true; _stake.nextEpochBalance -= uint96(stake); - _stake.currentEpoch = uint64(currentEpoch); + _stake.currentEpoch = uint32(currentEpoch); + _syncRewardsForDelegator( + poolId, + delegator, + initialStake, + _stake + ); } - /// @dev Expose `_recordDepositInRewardVaultFor`. - function recordRewardForDelegators( - bytes32 poolId, - uint256 reward, - uint256 amountOfDelegatedStake + /// @dev `IEthVault.recordDepositFor()`,` overridden to just emit events. + function recordDepositFor( + address owner, + uint256 amount ) external { - _recordRewardForDelegators(poolId, reward, amountOfDelegatedStake); + emit RecordDepositToEthVault( + owner, + amount + ); } - /// @dev Overridden to just emit events. - function _transferMemberBalanceToEthVault( + /// @dev `IStakingPoolRewardVault.recordDepositFor()`,` + /// overridden to just emit events. + function recordDepositFor( bytes32 poolId, - address member, - uint256 balance + uint256 membersReward ) - internal + external { - emit Deposit( + emit RecordDepositToRewardVault( poolId, - member, - balance + membersReward ); } - /// @dev Overridden to realize unfinalizedMembersRewardByPoolByEpoch in - /// the current epoch and eit a event, + /// @dev Expose `_recordStakingPoolRewards`. + function recordStakingPoolRewards( + bytes32 poolId, + uint256 operatorReward, + uint256 membersReward, + uint256 rewards, + uint256 amountOfDelegatedStake + ) + public + { + _setOperatorShare(poolId, operatorReward, membersReward); + _recordStakingPoolRewards(poolId, rewards, amountOfDelegatedStake); + } + + /// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in + /// the current epoch and emit a event, function _finalizePool(bytes32 poolId) internal - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake + ) { UnfinalizedMembersReward memory reward = - unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; - delete unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; - rewards.membersReward = reward.reward; - rewards.membersStake = reward.stake; - _recordRewardForDelegators(poolId, reward.reward, reward.stake); - emit FinalizePool(poolId, reward.reward, reward.stake); + unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; + delete unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; + + _setOperatorShare(poolId, operatorReward, membersReward); + + uint256 totalRewards = reward.operatorReward + reward.membersReward; + membersStake = reward.membersStake; + (operatorReward, membersReward) = + _recordStakingPoolRewards(poolId, totalRewards, membersStake); + emit FinalizePool(poolId, operatorReward, membersReward, membersStake); } - /// @dev Overridden to use unfinalizedMembersRewardByPoolByEpoch. + /// @dev Overridden to use unfinalizedPoolRewardsByEpoch. function _getUnfinalizedPoolRewards(bytes32 poolId) internal view - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 totalReward, + uint256 membersStake + ) { UnfinalizedMembersReward storage reward = - unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId]; - rewards.membersReward = reward.reward; - rewards.membersStake = reward.stake; + unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; + totalReward = reward.operatorReward + reward.membersReward; + membersStake = reward.membersStake; + } + + /// @dev Set the operator share of a pool based on reward ratios. + function _setOperatorShare( + bytes32 poolId, + uint256 operatorReward, + uint256 membersReward + ) + private + { + uint32 operatorShare = uint32( + operatorReward * PPM_DENOMINATOR / (operatorReward + membersReward) + ); + poolById[poolId].operatorShare = operatorShare; } + } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 8a5305c30f..c7feac5dd2 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -27,30 +27,37 @@ import "./TestStaking.sol"; contract TestFinalizer is TestStaking { - event RecordRewardForDelegatorsCall( + event RecordStakingPoolRewards( bytes32 poolId, uint256 membersReward, uint256 membersStake ); - event RecordDepositInRewardVaultForCall( - bytes32 poolId, - uint256 totalReward, - bool operatorOnly + event DepositStakingPoolRewards( + uint256 operatorReward, + uint256 membersReward ); - event DepositIntoStakingPoolRewardVaultCall( - uint256 amount - ); - - address payable private _rewardReceiver; + address payable private _operatorRewardsReceiver; + address payable private _membersRewardsReceiver; mapping (bytes32 => uint32) private _operatorSharesByPool; - /// @param rewardReceiver The address to transfer rewards into when + /// @param operatorRewardsReceiver The address to transfer rewards into when /// a pool is finalized. - constructor(address payable rewardReceiver) public { - _rewardReceiver = rewardReceiver; - init(); + constructor( + address payable operatorRewardsReceiver, + address payable membersRewardsReceiver + ) + public + { + init( + address(1), + address(1), + address(1), + address(1) + ); + _operatorRewardsReceiver = operatorRewardsReceiver; + _membersRewardsReceiver = membersRewardsReceiver; } /// @dev Activate a pool in the current epoch. @@ -82,9 +89,15 @@ contract TestFinalizer is /// @dev Expose `_finalizePool()` function internalFinalizePool(bytes32 poolId) external - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake + ) { - rewards = _finalizePool(poolId); + (operatorReward, + membersReward, + membersStake) = _finalizePool(poolId); } /// @dev Get finalization-related state variables. @@ -143,9 +156,12 @@ contract TestFinalizer is function internalGetUnfinalizedPoolRewards(bytes32 poolId) external view - returns (IStructs.PoolRewards memory rewards) + returns ( + uint256 totalReward, + uint256 membersStake + ) { - rewards = _getUnfinalizedPoolRewards(poolId); + (totalReward, membersStake) = _getUnfinalizedPoolRewards(poolId); } /// @dev Expose `_getActivePoolFromEpoch`. @@ -157,76 +173,63 @@ contract TestFinalizer is pool = _getActivePoolFromEpoch(epoch, poolId); } - /// @dev Overridden to just store inputs. - function _recordRewardForDelegators( + /// @dev Overridden to log and do some basic math. + function _recordStakingPoolRewards( bytes32 poolId, - uint256 membersReward, + uint256 reward, uint256 membersStake ) internal + returns (uint256 operatorReward, uint256 membersReward) { - emit RecordRewardForDelegatorsCall( + (operatorReward, membersReward) = _splitReward(poolId, reward); + emit RecordStakingPoolRewards( poolId, - membersReward, + reward, membersStake ); } - /// @dev Overridden to store inputs and do some really basic math. - function _depositIntoStakingPoolRewardVault(uint256 amount) internal { - emit DepositIntoStakingPoolRewardVaultCall(amount); - _rewardReceiver.transfer(amount); - } - - /// @dev Overridden to store inputs and do some really basic math. - function _recordDepositInRewardVaultFor( - bytes32 poolId, - uint256 totalReward, - bool operatorOnly + /// @dev Overridden to log and transfer to receivers. + function _depositStakingPoolRewards( + uint256 operatorReward, + uint256 membersReward ) internal - returns ( - uint256 operatorPortion, - uint256 membersPortion - ) { - emit RecordDepositInRewardVaultForCall( - poolId, - totalReward, - operatorOnly - ); + emit DepositStakingPoolRewards(operatorReward, membersReward); + address(_operatorRewardsReceiver).transfer(operatorReward); + address(_membersRewardsReceiver).transfer(operatorReward); + } - if (operatorOnly) { - operatorPortion = totalReward; - } else { - (operatorPortion, membersPortion) = - _splitRewardAmountBetweenOperatorAndMembers( - poolId, - totalReward - ); - } + /// @dev Overriden to just increase the epoch counter. + function _goToNextEpoch() internal { + currentEpoch += 1; } - /// @dev Overridden to do some really basic math. - function _splitRewardAmountBetweenOperatorAndMembers( + // solhint-disable no-empty-blocks + /// @dev Overridden to do nothing. + function _unwrapWETH() internal {} + + /// @dev Split a pool's total reward between the operator and members. + function _splitReward( bytes32 poolId, uint256 amount ) - internal + private view - returns (uint256 operatorPortion, uint256 membersPortion) + returns (uint256 operatorReward, uint256 membersReward) { + IStructs.ActivePool memory pool = _getActivePoolFromEpoch( + currentEpoch - 1, + poolId + ); uint32 operatorShare = _operatorSharesByPool[poolId]; - operatorPortion = operatorShare * amount / PPM_DENOMINATOR; - membersPortion = amount - operatorPortion; - } - - /// @dev Overriden to just increase the epoch counter. - function _goToNextEpoch() internal { - currentEpoch += 1; + (operatorReward, membersReward) = _splitStakingPoolRewards( + operatorShare, + amount, + pool.membersStake + ); } - // solhint-disable no-empty-blocks - /// @dev Overridden to do nothing. - function _unwrapWETH() internal {} } diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index 1df9efbf53..ed5b182502 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -110,14 +110,4 @@ contract TestProtocolFees is nextEpochBalance: pool.operatorStake }); } - - /// @dev Overridden to use test pools. - function getPoolOperator(bytes32) - public - view - returns (address operatorAddress) - { - // Just return nil, we won't use it. - return address(0); - } } diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index 27133485a2..c4658b4d54 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -129,7 +129,7 @@ contract TestStorageLayout is if sub(totalWeightedStakeThisEpoch_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) - if sub(activePoolsByEpoch_slot, slot) { revertIncorrectStorageSlot() } + if sub(_activePoolsByEpoch_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) if sub(numActivePoolsThisEpoch_slot, slot) { revertIncorrectStorageSlot() } diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 2919f41aae..ee324dbada 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -3,7 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -import { ContractArtifact } from "ethereum-types"; +import { ContractArtifact } from 'ethereum-types'; import * as EthVault from '../generated-artifacts/EthVault.json'; import * as IEthVault from '../generated-artifacts/IEthVault.json'; @@ -47,6 +47,8 @@ import * as StakingProxy from '../generated-artifacts/StakingProxy.json'; import * as TestAssertStorageParams from '../generated-artifacts/TestAssertStorageParams.json'; import * as TestCobbDouglas from '../generated-artifacts/TestCobbDouglas.json'; import * as TestCumulativeRewardTracking from '../generated-artifacts/TestCumulativeRewardTracking.json'; +import * as TestDelegatorRewards from '../generated-artifacts/TestDelegatorRewards.json'; +import * as TestFinalizer from '../generated-artifacts/TestFinalizer.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'; @@ -103,6 +105,8 @@ export const artifacts = { TestAssertStorageParams: TestAssertStorageParams as ContractArtifact, TestCobbDouglas: TestCobbDouglas as ContractArtifact, TestCumulativeRewardTracking: TestCumulativeRewardTracking as ContractArtifact, + TestDelegatorRewards: TestDelegatorRewards as ContractArtifact, + TestFinalizer: TestFinalizer as ContractArtifact, TestInitTarget: TestInitTarget as ContractArtifact, TestLibFixedMath: TestLibFixedMath as ContractArtifact, TestLibProxy: TestLibProxy as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 59a9e71b8f..916c8f8e69 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -45,6 +45,8 @@ export * from '../generated-wrappers/staking_proxy'; export * from '../generated-wrappers/test_assert_storage_params'; export * from '../generated-wrappers/test_cobb_douglas'; export * from '../generated-wrappers/test_cumulative_reward_tracking'; +export * from '../generated-wrappers/test_delegator_rewards'; +export * from '../generated-wrappers/test_finalizer'; export * from '../generated-wrappers/test_init_target'; export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_lib_proxy'; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 78deff5623..9407f7f7b2 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -45,6 +45,8 @@ "generated-artifacts/TestAssertStorageParams.json", "generated-artifacts/TestCobbDouglas.json", "generated-artifacts/TestCumulativeRewardTracking.json", + "generated-artifacts/TestDelegatorRewards.json", + "generated-artifacts/TestFinalizer.json", "generated-artifacts/TestInitTarget.json", "generated-artifacts/TestLibFixedMath.json", "generated-artifacts/TestLibProxy.json", diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index 13e0c80370..39dadf1aca 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -9,8 +9,6 @@ library LibFractions { /// @dev Maximum value for addition result components. uint256 constant internal RESCALE_THRESHOLD = 10 ** 27; - /// @dev Rescale factor for addition. - uint256 constant internal RESCALE_BASE = 10 ** 9; /// @dev Safely adds two fractions `n1/d1 + n2/d2` /// @param n1 numerator of `1` @@ -46,8 +44,10 @@ library LibFractions { // If either the numerator or the denominator are > RESCALE_THRESHOLD, // re-scale them to prevent overflows in future operations. if (numerator > RESCALE_THRESHOLD || denominator > RESCALE_THRESHOLD) { - numerator = numerator.safeDiv(RESCALE_BASE); - denominator = denominator.safeDiv(RESCALE_BASE); + uint256 rescaleBase = numerator >= denominator ? numerator : denominator; + rescaleBase /= RESCALE_THRESHOLD; + numerator = numerator.safeDiv(rescaleBase); + denominator = denominator.safeDiv(rescaleBase); } } diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index c0346e663b..0d0f53dff4 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -29,6 +29,10 @@ export enum InvalidParamValueErrorCode { InvalidZrxVaultAddress, InvalidEpochDuration, InvalidMinimumPoolStake, + InvalidWethProxyAddress, + InvalidEthVaultAddress, + InvalidRewardVaultAddress, + InvalidZrxVaultAddress, } export enum InitializationErrorCode { From 993f05d5acbd5243d2b6017d8078adc8c36a9f90 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Sep 2019 14:02:52 -0400 Subject: [PATCH 31/52] `@0x/contracts-staking`: Fixing tests. --- contracts/staking/contracts/src/Staking.sol | 3 +- .../contracts/src/fees/MixinExchangeFees.sol | 2 +- .../contracts/src/immutable/MixinStorage.sol | 2 +- .../src/staking_pools/MixinStakingPool.sol | 10 +- .../staking_pools/MixinStakingPoolMakers.sol | 6 +- .../MixinStakingPoolModifiers.sol | 4 +- .../staking_pools/MixinStakingPoolRewards.sol | 33 ++- .../contracts/test/TestDelegatorRewards.sol | 27 ++- .../contracts/test/TestStorageLayout.sol | 2 +- .../staking/test/actors/finalizer_actor.ts | 51 +++- contracts/staking/test/protocol_fees.ts | 30 ++- contracts/staking/test/rewards_test.ts | 2 +- .../test/unit_tests/delegator_reward_test.ts | 223 ++++++++++-------- contracts/staking/test/utils/api_wrapper.ts | 14 +- .../cumulative_reward_tracking_simulation.ts | 2 +- contracts/staking/test/utils/types.ts | 13 +- 16 files changed, 246 insertions(+), 178 deletions(-) diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index 9dffb115b6..d0d4dd2945 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -46,7 +46,8 @@ contract Staking is MixinStakingPoolRewards, MixinStakingPool, MixinStake, - MixinExchangeFees + MixinExchangeFees, + MixinFinalizer { // this contract can receive ETH // solhint-disable no-empty-blocks diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 992a38c5aa..66419d8949 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -197,7 +197,7 @@ contract MixinExchangeFees is returns (uint256 membersStake, uint256 weightedStake) { uint256 operatorStake = getStakeDelegatedToPoolByOwner( - poolById[poolId].operator, + _poolById[poolId].operator, poolId ).currentEpochBalance; membersStake = totalStake.safeSub(operatorStake); diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 11433d9b55..2e247d1a1b 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -82,7 +82,7 @@ contract MixinStorage is mapping (address => IStructs.MakerPoolJoinStatus) public poolJoinedByMakerAddress; // mapping from Pool Id to Pool - mapping (bytes32 => IStructs.Pool) public poolById; + mapping (bytes32 => IStructs.Pool) internal _poolById; // current epoch uint256 public currentEpoch = INITIAL_EPOCH; diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol index 86ff675de9..df00a9f000 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol @@ -73,7 +73,7 @@ contract MixinStakingPool is operatorShare: operatorShare, numberOfMakers: 0 }); - poolById[poolId] = pool; + _poolById[poolId] = pool; // initialize cumulative rewards for this pool; // this is used to track rewards earned by delegators. @@ -96,7 +96,7 @@ contract MixinStakingPool is external { // load pool and assert that we can decrease - uint32 currentOperatorShare = poolById[poolId].operatorShare; + uint32 currentOperatorShare = _poolById[poolId].operatorShare; _assertNewOperatorShare( poolId, currentOperatorShare, @@ -104,7 +104,7 @@ contract MixinStakingPool is ); // decrease operator share - poolById[poolId].operatorShare = newOperatorShare; + _poolById[poolId].operatorShare = newOperatorShare; emit OperatorShareDecreased( poolId, currentOperatorShare, @@ -119,7 +119,7 @@ contract MixinStakingPool is view returns (IStructs.Pool memory) { - return poolById[poolId]; + return _poolById[poolId]; } /// @dev Computes the unique id that comes after the input pool id. @@ -140,7 +140,7 @@ contract MixinStakingPool is view returns (bool) { - if (poolById[poolId].operator == NIL_ADDRESS) { + if (_poolById[poolId].operator == NIL_ADDRESS) { // we use the pool's operator as a proxy for its existence LibRichErrors.rrevert( LibStakingRichErrors.PoolExistenceError( diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol index c96144cdb1..287331c2c9 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolMakers.sol @@ -111,7 +111,7 @@ contract MixinStakingPoolMakers is confirmed: false }); poolJoinedByMakerAddress[makerAddress] = poolJoinStatus; - poolById[poolId].numberOfMakers = uint256(poolById[poolId].numberOfMakers).safeSub(1).downcastToUint32(); + _poolById[poolId].numberOfMakers = uint256(_poolById[poolId].numberOfMakers).safeSub(1).downcastToUint32(); // Maker has been removed from the pool` emit MakerRemovedFromStakingPool( @@ -157,7 +157,7 @@ contract MixinStakingPoolMakers is internal { // cache pool for use throughout this function - IStructs.Pool memory pool = poolById[poolId]; + IStructs.Pool memory pool = _poolById[poolId]; // Is the maker already in a pool? if (isMakerAssignedToStakingPool(makerAddress)) { @@ -195,7 +195,7 @@ contract MixinStakingPoolMakers is confirmed: true }); poolJoinedByMakerAddress[makerAddress] = poolJoinStatus; - poolById[poolId].numberOfMakers = uint256(pool.numberOfMakers).safeAdd(1).downcastToUint32(); + _poolById[poolId].numberOfMakers = uint256(pool.numberOfMakers).safeAdd(1).downcastToUint32(); // Maker has been added to the pool emit MakerAddedToStakingPool( diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol index 4e01992f25..52ac7b1829 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolModifiers.sol @@ -31,7 +31,7 @@ contract MixinStakingPoolModifiers is /// @dev Asserts that the sender is the operator of the input pool. /// @param poolId Pool sender must be operator of. modifier onlyStakingPoolOperator(bytes32 poolId) { - address operator = poolById[poolId].operator; + address operator = _poolById[poolId].operator; if (msg.sender != operator) { LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableByPoolOperatorError( msg.sender, @@ -46,7 +46,7 @@ contract MixinStakingPoolModifiers is /// @param poolId Pool sender must be operator of. /// @param makerAddress Address of a maker in the pool. modifier onlyStakingPoolOperatorOrMaker(bytes32 poolId, address makerAddress) { - address operator = poolById[poolId].operator; + address operator = _poolById[poolId].operator; if ( msg.sender != operator && msg.sender != makerAddress diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index d7e1994e58..991e8bfeb5 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -39,15 +39,6 @@ contract MixinStakingPoolRewards is { using LibSafeMath for uint256; - function computeRewardBalanceOfOperator(bytes32 poolId, address operator) - public - view - returns (uint256 reward) - { - // TODO. - // unfinalizedStake + - } - /// @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. @@ -76,6 +67,26 @@ contract MixinStakingPoolRewards is finalDelegatedStakeToPoolByOwner; } + /// @dev Computes the reward balance in ETH of the operator of a pool. + /// @param poolId Unique id of pool. + /// @return totalReward Balance in ETH. + function computeRewardBalanceOfOperator(bytes32 poolId) + external + view + returns (uint256 reward) + { + IStructs.Pool memory pool = _poolById[poolId]; + // Get any unfinalized rewards. + (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = + _getUnfinalizedPoolRewards(poolId); + // Get the operators' portion. + (reward,) = _splitStakingPoolRewards( + pool.operatorShare, + unfinalizedTotalRewards, + unfinalizedMembersStake + ); + } + /// @dev Computes the reward balance in ETH of a specific member of a pool. /// @param poolId Unique id of pool. /// @param member The member of the pool. @@ -85,7 +96,7 @@ contract MixinStakingPoolRewards is view returns (uint256 reward) { - IStructs.Pool memory pool = poolById[poolId]; + IStructs.Pool memory pool = _poolById[poolId]; // Get any unfinalized rewards. (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); @@ -167,7 +178,7 @@ contract MixinStakingPoolRewards is internal returns (uint256 operatorReward, uint256 membersReward) { - IStructs.Pool memory pool = poolById[poolId]; + IStructs.Pool memory pool = _poolById[poolId]; // Split the reward between operator and members (operatorReward, membersReward) = diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 6cbe5ffbd7..0fc807e617 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -45,7 +45,7 @@ contract TestDelegatorRewards is uint256 membersStake ); - struct UnfinalizedMembersReward { + struct UnfinalizedPoolReward { uint256 operatorReward; uint256 membersReward; uint256 membersStake; @@ -64,7 +64,7 @@ contract TestDelegatorRewards is rewardVault = IStakingPoolRewardVault(address(this)); } - mapping (uint256 => mapping (bytes32 => UnfinalizedMembersReward)) private + mapping (uint256 => mapping (bytes32 => UnfinalizedPoolReward)) private unfinalizedPoolRewardsByEpoch; /// @dev Expose _finalizePool @@ -72,8 +72,8 @@ contract TestDelegatorRewards is _finalizePool(poolId); } - /// @dev Set unfinalized members reward for a pool in the current epoch. - function setUnfinalizedMembersRewards( + /// @dev Set unfinalized rewards for a pool in the current epoch. + function setUnfinalizedPoolReward( bytes32 poolId, uint256 operatorReward, uint256 membersReward, @@ -82,7 +82,7 @@ contract TestDelegatorRewards is external { unfinalizedPoolRewardsByEpoch[currentEpoch][poolId] = - UnfinalizedMembersReward({ + UnfinalizedPoolReward({ operatorReward: operatorReward, membersReward: membersReward, membersStake: membersStake @@ -208,13 +208,16 @@ contract TestDelegatorRewards is bytes32 poolId, uint256 operatorReward, uint256 membersReward, - uint256 rewards, - uint256 amountOfDelegatedStake + uint256 membersStake ) - public + external { _setOperatorShare(poolId, operatorReward, membersReward); - _recordStakingPoolRewards(poolId, rewards, amountOfDelegatedStake); + _recordStakingPoolRewards( + poolId, + operatorReward + membersReward, + membersStake + ); } /// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in @@ -227,7 +230,7 @@ contract TestDelegatorRewards is uint256 membersStake ) { - UnfinalizedMembersReward memory reward = + UnfinalizedPoolReward memory reward = unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; delete unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; @@ -249,7 +252,7 @@ contract TestDelegatorRewards is uint256 membersStake ) { - UnfinalizedMembersReward storage reward = + UnfinalizedPoolReward storage reward = unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; totalReward = reward.operatorReward + reward.membersReward; membersStake = reward.membersStake; @@ -266,7 +269,7 @@ contract TestDelegatorRewards is uint32 operatorShare = uint32( operatorReward * PPM_DENOMINATOR / (operatorReward + membersReward) ); - poolById[poolId].operatorShare = operatorShare; + _poolById[poolId].operatorShare = operatorShare; } } diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index c4658b4d54..1cb748fe18 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -78,7 +78,7 @@ contract TestStorageLayout is if sub(poolJoinedByMakerAddress_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) - if sub(poolById_slot, slot) { revertIncorrectStorageSlot() } + if sub(_poolById_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) if sub(currentEpoch_slot, slot) { revertIncorrectStorageSlot() } diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 6a5aa00953..4697cbf197 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -6,10 +6,10 @@ import { StakingApiWrapper } from '../utils/api_wrapper'; import { DelegatorBalancesByPoolId, DelegatorsByPoolId, + OperatorBalanceByPoolId, OperatorByPoolId, OperatorShareByPoolId, RewardByPoolId, - RewardVaultBalance, RewardVaultBalanceByPoolId, } from '../utils/types'; @@ -47,9 +47,14 @@ export class FinalizerActor extends BaseActor { const rewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); const delegatorBalancesByPoolId = await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); const delegatorStakesByPoolId = await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId); + const operatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId); // compute expected changes - const expectedRewardVaultBalanceByPoolId = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( + const [ + expectedOperatorBalanceByPoolId, + expectedRewardVaultBalanceByPoolId, + ] = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( rewards, + operatorBalanceByPoolId, rewardVaultBalanceByPoolId, operatorShareByPoolId, ); @@ -123,8 +128,10 @@ export class FinalizerActor extends BaseActor { private async _getDelegatorBalancesByPoolIdAsync( delegatorsByPoolId: DelegatorsByPoolId, ): Promise { - const computeRewardBalanceOfDelegator = this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; - const rewardVaultBalanceOfOperator = this._stakingApiWrapper.rewardVaultContract.balanceOfOperator; + const computeRewardBalanceOfDelegator = + this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator; + const computeRewardBalanceOfOperator = + this._stakingApiWrapper.stakingContract.computeRewardBalanceOfOperator; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { @@ -134,7 +141,7 @@ export class FinalizerActor extends BaseActor { for (const delegator of delegators) { let balance = new BigNumber(delegatorBalancesByPoolId[poolId][delegator] || 0); if (delegator === operator) { - balance = balance.plus(await rewardVaultBalanceOfOperator.callAsync(poolId)); + balance = balance.plus(await computeRewardBalanceOfOperator.callAsync(poolId)); } else { balance = balance.plus(await computeRewardBalanceOfDelegator.callAsync(poolId, delegator)); } @@ -147,7 +154,8 @@ 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]; @@ -172,9 +180,13 @@ export class FinalizerActor extends BaseActor { const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId); for (const reward of rewards) { const operatorShare = operatorShareByPoolId[reward.poolId]; - expectedRewardVaultBalanceByPoolId[reward.poolId] = await this._computeExpectedRewardVaultBalanceAsync( + [ + expectedOperatorBalanceByPoolId[reward.poolId], + expectedRewardVaultBalanceByPoolId[reward.poolId], + ] = await this._computeExpectedRewardVaultBalanceAsync( reward.poolId, reward.reward, + expectedOperatorBalanceByPoolId[reward.poolId], expectedRewardVaultBalanceByPoolId[reward.poolId], operatorShare, ); @@ -188,9 +200,16 @@ export class FinalizerActor extends BaseActor { operatorBalance: BigNumber, rewardVaultBalance: BigNumber, operatorShare: BigNumber, - ): Promise { - const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); - const operatorPortion = membersStakeInPool.eq(0) + ): Promise<[BigNumber, BigNumber]> { + const totalStakeDelegatedToPool = (await + this._stakingApiWrapper + .stakingContract + .getTotalStakeDelegatedToPool + .callAsync( + poolId, + ) + ).currentEpochBalance; + const operatorPortion = totalStakeDelegatedToPool.eq(0) ? reward : reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT); const membersPortion = reward.minus(operatorPortion); @@ -211,7 +230,7 @@ export class FinalizerActor extends BaseActor { private async _getOperatorAndDelegatorsStakeInPoolAsync(poolId: string): Promise<[BigNumber, BigNumber]> { const stakingContract = this._stakingApiWrapper.stakingContract; - const operator = await stakingContract.getPoolOperator.callAsync(poolId); + const operator = (await stakingContract.getStakingPool.callAsync(poolId)).operator; const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync(poolId)) .currentEpochBalance; const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync(operator, poolId)) @@ -223,8 +242,14 @@ export class FinalizerActor extends BaseActor { private async _getOperatorShareByPoolIdAsync(poolIds: string[]): Promise { const operatorShareByPoolId: OperatorShareByPoolId = {}; for (const poolId of poolIds) { - const pool = await this._stakingApiWrapper.stakingContract.getStakingPool.callAsync(poolId); - operatorShareByPoolId[poolId] = new BigNumber(pool.operatorShare); + operatorShareByPoolId[poolId] = new BigNumber( + (await this + ._stakingApiWrapper + .stakingContract + .getStakingPool + .callAsync(poolId) + ).operatorShare, + ); } return operatorShareByPoolId; } diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index d96d9653f1..010115c6cf 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -162,6 +162,10 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); }); + async function getProtocolFeesAsync(poolId: string): Promise { + return testContract.getProtocolFeesThisEpochByPool.callAsync(poolId); + } + describe('ETH fees', () => { function assertNoWETHTransferLogs(logs: LogEntry[]): void { const logsArgs = filterLogsToArguments( @@ -191,7 +195,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); assertNoWETHTransferLogs(receipt.logs); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -204,7 +208,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); assertNoWETHTransferLogs(receipt.logs); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(ZERO_AMOUNT); }); @@ -222,7 +226,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(); await payAsync(); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); }); @@ -262,7 +266,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: ZERO_AMOUNT }, ); assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -275,7 +279,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { { from: exchangeAddress, value: ZERO_AMOUNT }, ); assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(ZERO_AMOUNT); }); @@ -293,7 +297,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(); await payAsync(); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); @@ -313,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 = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); }); @@ -333,7 +337,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(makerAddress); await payAsync(otherMakerAddress); const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const poolFees = getProtocolFeesAsync(poolId); expect(poolFees).to.bignumber.eq(expectedTotalFees); }); @@ -354,8 +358,8 @@ blockchainTests('Protocol Fee Unit Tests', env => { await payAsync(poolId, makerAddress, fee); await payAsync(otherPoolId, otherMakerAddress, otherFee); const [poolFees, otherPoolFees] = await Promise.all([ - testContract.protocolFeesThisEpochByPool.callAsync(poolId), - testContract.protocolFeesThisEpochByPool.callAsync(otherPoolId), + getProtocolFeesAsync(poolId), + getProtocolFeesAsync(otherPoolId), ]); expect(poolFees).to.bignumber.eq(fee); expect(otherPoolFees).to.bignumber.eq(otherFee); @@ -371,7 +375,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const feesCredited = getProtocolFeesAsync(poolId); expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -383,7 +387,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const feesCredited = getProtocolFeesAsync(poolId); expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); }); @@ -395,7 +399,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { DEFAULT_PROTOCOL_FEE_PAID, { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, ); - const feesCredited = await testContract.protocolFeesThisEpochByPool.callAsync(poolId); + const feesCredited = 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 7293152b62..a931373492 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -133,7 +133,7 @@ blockchainTests.resets('Testing Rewards', env => { ), stakingApiWrapper.ethVaultContract.balanceOf.callAsync(stakers[1].getOwner()), // operator - stakingApiWrapper.ethVaultContract.balanceOf.callAsync(poolOperator), + stakingApiWrapper.ethVaultContract.balanceOf.callAsync(poolOperator.getOwner()), // undivided balance in reward pool stakingApiWrapper.rewardVaultContract.balanceOf.callAsync(poolId), ]); diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index 9762eee537..a6901a7b83 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -12,8 +12,9 @@ import { LogEntry } from 'ethereum-types'; import { artifacts, TestDelegatorRewardsContract, - TestDelegatorRewardsDepositEventArgs, TestDelegatorRewardsEvents, + TestDelegatorRewardsRecordDepositToEthVaultEventArgs as EthVaultDepositEventArgs, + TestDelegatorRewardsRecordDepositToRewardVaultEventArgs as RewardVaultDepositEventArgs, } from '../../src'; import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; @@ -32,50 +33,52 @@ blockchainTests.resets('delegator unit rewards', env => { interface RewardPoolMembersOpts { poolId: string; - reward: Numberish; - stake: Numberish; + membersReward: Numberish; + operatorReward: Numberish; + membersStake: Numberish; } async function rewardPoolMembersAsync(opts?: Partial): Promise { const _opts = { poolId: hexRandom(), - reward: getRandomInteger(1, toBaseUnitAmount(100)), - stake: getRandomInteger(1, toBaseUnitAmount(10)), + membersReward: getRandomInteger(1, toBaseUnitAmount(100)), + operatorReward: getRandomInteger(1, toBaseUnitAmount(100)), + membersStake: getRandomInteger(1, toBaseUnitAmount(10)), ...opts, }; - await testContract.recordRewardForDelegators.awaitTransactionSuccessAsync( + await testContract.recordStakingPoolRewards.awaitTransactionSuccessAsync( _opts.poolId, - new BigNumber(_opts.reward), - new BigNumber(_opts.stake), + new BigNumber(_opts.operatorReward), + new BigNumber(_opts.membersReward), + new BigNumber(_opts.membersStake), ); return _opts; } - interface SetUnfinalizedMembersRewardsOpts { - poolId: string; - reward: Numberish; - stake: Numberish; - } + interface SetUnfinalizedMembersRewardsOpts extends RewardPoolMembersOpts {} - async function setUnfinalizedMembersRewardsAsync( + async function setUnfinalizedPoolRewardAsync( opts?: Partial, ): Promise { const _opts = { poolId: hexRandom(), - reward: getRandomInteger(1, toBaseUnitAmount(100)), - stake: getRandomInteger(1, toBaseUnitAmount(10)), + membersReward: getRandomInteger(1, toBaseUnitAmount(100)), + operatorReward: getRandomInteger(1, toBaseUnitAmount(100)), + membersStake: getRandomInteger(1, toBaseUnitAmount(10)), ...opts, }; - await testContract.setUnfinalizedMembersRewards.awaitTransactionSuccessAsync( + await testContract.setUnfinalizedPoolReward.awaitTransactionSuccessAsync( _opts.poolId, - new BigNumber(_opts.reward), - new BigNumber(_opts.stake), + new BigNumber(_opts.operatorReward), + new BigNumber(_opts.membersReward), + new BigNumber(_opts.membersStake), ); return _opts; } - type ResultWithDeposit = T & { - deposit: BigNumber; + type ResultWithDeposits = T & { + ethVaultDeposit: BigNumber; + rewardVaultDeposit: BigNumber; }; interface DelegateStakeOpts { @@ -86,7 +89,7 @@ blockchainTests.resets('delegator unit rewards', env => { async function delegateStakeNowAsync( poolId: string, opts?: Partial, - ): Promise> { + ): Promise> { return delegateStakeAsync(poolId, opts, true); } @@ -94,7 +97,7 @@ blockchainTests.resets('delegator unit rewards', env => { poolId: string, opts?: Partial, now?: boolean, - ): Promise> { + ): Promise> { const _opts = { delegator: randomAddress(), stake: getRandomInteger(1, toBaseUnitAmount(10)), @@ -102,9 +105,11 @@ blockchainTests.resets('delegator unit rewards', env => { }; const fn = now ? testContract.delegateStakeNow : testContract.delegateStake; const receipt = await fn.awaitTransactionSuccessAsync(_opts.delegator, poolId, new BigNumber(_opts.stake)); + const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId, _opts.delegator); return { ..._opts, - deposit: getDepositFromLogs(receipt.logs, poolId, _opts.delegator), + ethVaultDeposit, + rewardVaultDeposit, }; } @@ -112,37 +117,49 @@ blockchainTests.resets('delegator unit rewards', env => { poolId: string, delegator: string, stake?: Numberish, - ): Promise> { + ): Promise> { const _stake = new BigNumber( stake || (await testContract.getStakeDelegatedToPoolByOwner.callAsync(delegator, poolId)).currentEpochBalance, ); const receipt = await testContract.undelegateStake.awaitTransactionSuccessAsync(delegator, poolId, _stake); + const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId, delegator); return { stake: _stake, - deposit: getDepositFromLogs(receipt.logs, poolId, delegator), + ethVaultDeposit, + rewardVaultDeposit, }; } - function getDepositFromLogs(logs: LogEntry[], poolId: string, delegator?: string): BigNumber { - const events = filterLogsToArguments( + function getDepositsFromLogs(logs: LogEntry[], poolId: string, delegator?: string): [BigNumber, BigNumber] { + let ethVaultDeposit = constants.ZERO_AMOUNT; + let rewardVaultDeposit = constants.ZERO_AMOUNT; + const ethVaultDepositArgs = filterLogsToArguments( logs, - TestDelegatorRewardsEvents.Deposit, + TestDelegatorRewardsEvents.RecordDepositToEthVault, ); - if (events.length > 0) { - expect(events.length).to.eq(1); - expect(events[0].poolId).to.eq(poolId); + if (ethVaultDepositArgs.length > 0) { + expect(ethVaultDepositArgs.length).to.eq(1); if (delegator !== undefined) { - expect(events[0].member).to.eq(delegator); + expect(ethVaultDepositArgs[0].owner).to.eq(delegator); } - return events[0].balance; + ethVaultDeposit = ethVaultDepositArgs[0].amount; } - return constants.ZERO_AMOUNT; + const rewardVaultDepositArgs = filterLogsToArguments( + logs, + TestDelegatorRewardsEvents.RecordDepositToRewardVault, + ); + if (rewardVaultDepositArgs.length > 0) { + expect(rewardVaultDepositArgs.length).to.eq(1); + expect(rewardVaultDepositArgs[0].poolId).to.eq(poolId); + rewardVaultDeposit = rewardVaultDepositArgs[0].amount; + } + return [ethVaultDeposit, rewardVaultDeposit]; } async function advanceEpochAsync(): Promise { await testContract.advanceEpoch.awaitTransactionSuccessAsync(); - const epoch = await testContract.getCurrentEpoch.callAsync(); + const epoch = await testContract.currentEpoch.callAsync(); return epoch.toNumber(); } @@ -150,14 +167,16 @@ blockchainTests.resets('delegator unit rewards', env => { return testContract.computeRewardBalanceOfDelegator.callAsync(poolId, delegator); } - async function touchStakeAsync(poolId: string, delegator: string): Promise> { + async function touchStakeAsync(poolId: string, delegator: string): Promise> { return undelegateStakeAsync(poolId, delegator, 0); } - async function finalizePoolAsync(poolId: string): Promise> { + async function finalizePoolAsync(poolId: string): Promise> { const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); + const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId); return { - deposit: getDepositFromLogs(receipt.logs, poolId), + ethVaultDeposit, + rewardVaultDeposit, }; } @@ -214,7 +233,7 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) // rewards paid for stake in epoch 0. - await rewardPoolMembersAsync({ poolId, stake }); + await rewardPoolMembersAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -225,7 +244,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -235,9 +254,9 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); assertRoughlyEquals(delegatorReward, BigNumber.sum(reward1, reward2)); }); @@ -248,9 +267,9 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward, stake: rewardStake } = await rewardPoolMembersAsync({ + const { membersReward: reward, membersStake: rewardStake } = await rewardPoolMembersAsync({ poolId, - stake: new BigNumber(delegatorStake).times(2), + membersStake: new BigNumber(delegatorStake).times(2), }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorRewards = computeDelegatorRewards(reward, delegatorStake, rewardStake); @@ -263,8 +282,8 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); - const { deposit } = await undelegateStakeAsync(poolId, delegator); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); @@ -276,8 +295,8 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); - const { deposit } = await undelegateStakeAsync(poolId, delegator); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); await delegateStakeAsync(poolId, { delegator, stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); @@ -290,13 +309,13 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - await rewardPoolMembersAsync({ poolId, stake }); + await rewardPoolMembersAsync({ poolId, membersStake: stake }); await undelegateStakeAsync(poolId, delegator); await delegateStakeAsync(poolId, { delegator, stake }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3. - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -309,7 +328,7 @@ blockchainTests.resets('delegator unit rewards', env => { // Pay rewards for epoch 0. await advanceEpochAsync(); // epoch 2 // Pay rewards for epoch 1. - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -326,14 +345,14 @@ blockchainTests.resets('delegator unit rewards', env => { // receives 100% of rewards. const rewardStake = totalStake.times(2); // Pay rewards for epoch 1. - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); // add extra stake - const { deposit } = await delegateStakeAsync(poolId, { delegator, stake: stake2 }); + const { ethVaultDeposit: deposit } = await delegateStakeAsync(poolId, { delegator, stake: stake2 }); await advanceEpochAsync(); // epoch 3 (stake2 now active) // Pay rewards for epoch 2. await advanceEpochAsync(); // epoch 4 // Pay rewards for epoch 3. - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( computeDelegatorRewards(reward1, stake1, rewardStake), @@ -355,10 +374,10 @@ blockchainTests.resets('delegator unit rewards', env => { // receives 100% of rewards. const rewardStake = totalStake.times(2); // Pay rewards for epoch 1. - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); await advanceEpochAsync(); // epoch 3 // Pay rewards for epoch 2. - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( computeDelegatorRewards(reward1, stake1, rewardStake), @@ -375,10 +394,10 @@ blockchainTests.resets('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: stakeA }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stakeA }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 (delegator A and B) - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), @@ -398,11 +417,11 @@ blockchainTests.resets('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: stakeA }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stakeA }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3 (delegator A and B) - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), @@ -420,15 +439,15 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { reward: reward1, stake: rewardStake1 } = await rewardPoolMembersAsync({ + const { membersReward: reward1, membersStake: rewardStake1 } = await rewardPoolMembersAsync({ poolId, - stake: new BigNumber(delegatorStake).times(2), + membersStake: new BigNumber(delegatorStake).times(2), }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 - const { reward: reward2, stake: rewardStake2 } = await rewardPoolMembersAsync({ + const { membersReward: reward2, membersStake: rewardStake2 } = await rewardPoolMembersAsync({ poolId, - stake: new BigNumber(delegatorStake).times(3), + membersStake: new BigNumber(delegatorStake).times(3), }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( @@ -443,7 +462,7 @@ blockchainTests.resets('delegator unit rewards', env => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId, { stake: 0 }); await advanceEpochAsync(); // epoch 1 - await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(0); }); @@ -452,7 +471,7 @@ blockchainTests.resets('delegator unit rewards', env => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 - await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(0); }); @@ -462,7 +481,8 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const { membersReward: unfinalizedReward } = + await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(unfinalizedReward); }); @@ -473,7 +493,8 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 await advanceEpochAsync(); // epoch 3 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake }); + const { membersReward: unfinalizedReward } = + await setUnfinalizedPoolRewardAsync({ poolId, membersStake: stake }); const reward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(reward).to.bignumber.eq(unfinalizedReward); }); @@ -483,9 +504,9 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, 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); @@ -496,10 +517,11 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake }); + const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, 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); @@ -510,16 +532,17 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { reward: prevReward, stake: prevStake } = await rewardPoolMembersAsync({ + const { membersReward: prevReward, membersStake: prevStake } = await rewardPoolMembersAsync({ poolId, - stake: new BigNumber(stake).times(2), + membersStake: new BigNumber(stake).times(2), }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 - const { reward: unfinalizedReward, stake: unfinalizedStake } = await setUnfinalizedMembersRewardsAsync({ - poolId, - stake: 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), @@ -537,8 +560,8 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { reward } = await rewardPoolMembersAsync({ poolId, stake }); - const { deposit } = await touchStakeAsync(poolId, delegator); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { ethVaultDeposit: deposit } = await touchStakeAsync(poolId, delegator); const finalRewardBalance = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); expect(finalRewardBalance).to.bignumber.eq(0); @@ -557,12 +580,12 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (2 * stake now active) // reward for epoch 1, using 2 * stake so delegator should // only be entitled to a fraction of the rewards. - const { reward } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); await advanceEpochAsync(); // epoch 2 // touch the stake one last time stakeResults.push(await touchStakeAsync(poolId, delegator)); // Should only see deposits for epoch 2. - const allDeposits = stakeResults.map(r => r.deposit); + const allDeposits = stakeResults.map(r => r.ethVaultDeposit); const expectedReward = computeDelegatorRewards(reward, stake, rewardStake); assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedReward); }); @@ -576,21 +599,21 @@ blockchainTests.resets('delegator unit rewards', env => { const rewardStake = new BigNumber(stake).times(2); await advanceEpochAsync(); // epoch 1 (full stake now active) // reward for epoch 0 - await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); // unstake some const unstake = new BigNumber(stake).dividedToIntegerBy(2); stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake)); await advanceEpochAsync(); // epoch 2 (half active stake) // reward for epoch 1 - const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); // re-stake stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake: unstake })); await advanceEpochAsync(); // epoch 3 (full stake now active) // reward for epoch 2 - const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); // touch the stake to claim rewards stakeResults.push(await touchStakeAsync(poolId, delegator)); - const allDeposits = stakeResults.map(r => r.deposit); + const allDeposits = stakeResults.map(r => r.ethVaultDeposit); const expectedReward = BigNumber.sum( computeDelegatorRewards(reward1, stake, rewardStake), computeDelegatorRewards(reward2, new BigNumber(stake).minus(unstake), rewardStake), @@ -606,12 +629,12 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { reward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); // delegator A will finalize and collect rewards by touching stake. - const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + const { ethVaultDeposit: depositA } = await touchStakeAsync(poolId, delegatorA); assertRoughlyEquals(depositA, computeDelegatorRewards(reward, stakeA, totalStake)); // delegator B will collect rewards by touching stake - const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + const { ethVaultDeposit: depositB } = await touchStakeAsync(poolId, delegatorB); assertRoughlyEquals(depositB, computeDelegatorRewards(reward, stakeB, totalStake)); }); @@ -623,19 +646,19 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ poolId, - stake: totalStake, + membersStake: totalStake, }); const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); // delegator A will finalize and collect rewards by touching stake. - const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + const { ethVaultDeposit: depositA } = await touchStakeAsync(poolId, delegatorA); assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake)); // delegator B will collect rewards by touching stake - const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + const { ethVaultDeposit: depositB } = await touchStakeAsync(poolId, delegatorB); assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake)); }); @@ -647,21 +670,21 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake }); + const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 - const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ + const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ poolId, - stake: totalStake, + membersStake: totalStake, }); const totalRewards = BigNumber.sum(prevReward, unfinalizedReward); // finalize await finalizePoolAsync(poolId); // delegator A will collect rewards by touching stake. - const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA); + const { ethVaultDeposit: depositA } = await touchStakeAsync(poolId, delegatorA); assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake)); // delegator B will collect rewards by touching stake - const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB); + const { ethVaultDeposit: depositB } = await touchStakeAsync(poolId, delegatorB); assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake)); }); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index c9806da681..d83122fae1 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -73,7 +73,7 @@ export class StakingApiWrapper { }, findActivePoolIdsAsync: async (epoch?: number): Promise => { - const _epoch = epoch !== undefined ? epoch : await this.stakingContract.getCurrentEpoch.callAsync(); + const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync(); const events = filterLogsToArguments( await this.stakingContract.getLogsAsync( StakingEvents.StakingPoolActivated, @@ -111,12 +111,12 @@ export class StakingApiWrapper { ...params, }; return this.stakingContract.setParams.awaitTransactionSuccessAsync( - _params.epochDurationInSeconds, - _params.rewardDelegatedStakeWeight, - _params.minimumPoolStake, - _params.maximumMakersInPool, - _params.cobbDouglasAlphaNumerator, - _params.cobbDouglasAlphaDenominator, + new BigNumber(_params.epochDurationInSeconds), + new BigNumber(_params.rewardDelegatedStakeWeight), + new BigNumber(_params.minimumPoolStake), + new BigNumber(_params.maximumMakersInPool), + new BigNumber(_params.cobbDouglasAlphaNumerator), + new BigNumber(_params.cobbDouglasAlphaDenominator), _params.wethProxyAddress, _params.ethVaultAddress, _params.rewardVaultAddress, diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index d90f895a70..04be9518e3 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -123,7 +123,7 @@ export class CumulativeRewardTrackingSimulation { let txReceipt: TransactionReceiptWithDecodedLogs; switch (action) { case TestAction.Finalize: - txReceipt = await this._stakingApiWrapper.utils.skipToNextEpochAsync(); + txReceipt = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); break; case TestAction.Delegate: diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index a21918a09b..3235eca5c0 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -1,14 +1,15 @@ +import { Numberish } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { constants } from './constants'; export interface StakingParams { - epochDurationInSeconds: BigNumber; - rewardDelegatedStakeWeight: number | BigNumber; - minimumPoolStake: BigNumber; - maximumMakersInPool: BigNumber; - cobbDouglasAlphaNumerator: number | BigNumber; - cobbDouglasAlphaDenominator: number | BigNumber; + epochDurationInSeconds: Numberish; + rewardDelegatedStakeWeight: Numberish; + minimumPoolStake: Numberish; + maximumMakersInPool: Numberish; + cobbDouglasAlphaNumerator: Numberish; + cobbDouglasAlphaDenominator: Numberish; wethProxyAddress: string; ethVaultAddress: string; rewardVaultAddress: string; From b4b6d4d96988393e646a0d5764fcf30de1a51d43 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Sep 2019 18:33:44 -0400 Subject: [PATCH 32/52] `@0x/contracts-test-utils`: Add `shortZip()`. --- contracts/test-utils/CHANGELOG.json | 6 +++++- contracts/test-utils/src/index.ts | 1 + contracts/test-utils/src/lang_utils.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 contracts/test-utils/src/lang_utils.ts diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index a5fff1105f..875f1fbc30 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -84,7 +84,11 @@ }, { "note": "Tweaks/Upgrades to `hex_utils`, most notably `hexSlice()`", - "pr": "TODO" + "pr": 2155 + }, + { + "note": "Add `shortZip()` to `lang_utils.ts`", + "pr": 2155 } ] }, diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 4156b23991..7df55bf89a 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -50,3 +50,4 @@ export { export { blockchainTests, BlockchainTestsEnvironment, describe } from './mocha_blockchain'; export { chaiSetup, expect } from './chai_setup'; export { getCodesizeFromArtifact } from './codesize'; +export { shortZip } from './lang_utils'; diff --git a/contracts/test-utils/src/lang_utils.ts b/contracts/test-utils/src/lang_utils.ts new file mode 100644 index 0000000000..0a3d923260 --- /dev/null +++ b/contracts/test-utils/src/lang_utils.ts @@ -0,0 +1,9 @@ +import * as _ from 'lodash'; + +/** + * _.zip() that clips to the shortest array. + */ +export function shortZip(a: T1[], b: T2[]): Array<[T1, T2]> { + const minLength = Math.min(a.length, b.length); + return _.zip(a.slice(0, minLength), b.slice(0, minLength)) as Array<[T1, T2]>; +} From d33080cf08a098fdbd193d88b92ddbb9de150f53 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Sep 2019 18:55:03 -0400 Subject: [PATCH 33/52] `@0x/contracts-staking`: Finalizer tests passing. --- .../staking/contracts/test/TestFinalizer.sol | 53 +-- .../staking/test/unit_tests/finalizer_test.ts | 422 ++++++++---------- contracts/staking/test/utils/number_utils.ts | 11 + 3 files changed, 226 insertions(+), 260 deletions(-) diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index c7feac5dd2..ccf038c53c 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -29,7 +29,7 @@ contract TestFinalizer is { event RecordStakingPoolRewards( bytes32 poolId, - uint256 membersReward, + uint256 totalReward, uint256 membersStake ); @@ -38,6 +38,17 @@ contract TestFinalizer is uint256 membersReward ); + struct UnfinalizedPoolReward { + uint256 totalReward; + uint256 membersStake; + } + + struct FinalizedPoolRewards { + uint256 operatorReward; + uint256 membersReward; + uint256 membersStake; + } + address payable private _operatorRewardsReceiver; address payable private _membersRewardsReceiver; mapping (bytes32 => uint32) private _operatorSharesByPool; @@ -87,17 +98,13 @@ contract TestFinalizer is } /// @dev Expose `_finalizePool()` - function internalFinalizePool(bytes32 poolId) + function finalizePool(bytes32 poolId) external - returns ( - uint256 operatorReward, - uint256 membersReward, - uint256 membersStake - ) + returns (FinalizedPoolRewards memory reward) { - (operatorReward, - membersReward, - membersStake) = _finalizePool(poolId); + (reward.operatorReward, + reward.membersReward, + reward.membersStake) = _finalizePool(poolId); } /// @dev Get finalization-related state variables. @@ -153,19 +160,17 @@ contract TestFinalizer is } /// @dev Expose `_getUnfinalizedPoolReward()` - function internalGetUnfinalizedPoolRewards(bytes32 poolId) + function getUnfinalizedPoolRewards(bytes32 poolId) external view - returns ( - uint256 totalReward, - uint256 membersStake - ) + returns (UnfinalizedPoolReward memory reward) { - (totalReward, membersStake) = _getUnfinalizedPoolRewards(poolId); + (reward.totalReward, reward.membersStake) = + _getUnfinalizedPoolRewards(poolId); } /// @dev Expose `_getActivePoolFromEpoch`. - function internalGetActivePoolFromEpoch(uint256 epoch, bytes32 poolId) + function getActivePoolFromEpoch(uint256 epoch, bytes32 poolId) external view returns (IStructs.ActivePool memory pool) @@ -182,7 +187,8 @@ contract TestFinalizer is internal returns (uint256 operatorReward, uint256 membersReward) { - (operatorReward, membersReward) = _splitReward(poolId, reward); + (operatorReward, membersReward) = + _splitReward(poolId, reward, membersStake); emit RecordStakingPoolRewards( poolId, reward, @@ -199,7 +205,7 @@ contract TestFinalizer is { emit DepositStakingPoolRewards(operatorReward, membersReward); address(_operatorRewardsReceiver).transfer(operatorReward); - address(_membersRewardsReceiver).transfer(operatorReward); + address(_membersRewardsReceiver).transfer(membersReward); } /// @dev Overriden to just increase the epoch counter. @@ -214,21 +220,18 @@ contract TestFinalizer is /// @dev Split a pool's total reward between the operator and members. function _splitReward( bytes32 poolId, - uint256 amount + uint256 amount, + uint256 membersStake ) private view returns (uint256 operatorReward, uint256 membersReward) { - IStructs.ActivePool memory pool = _getActivePoolFromEpoch( - currentEpoch - 1, - poolId - ); uint32 operatorShare = _operatorSharesByPool[poolId]; (operatorReward, membersReward) = _splitStakingPoolRewards( operatorShare, amount, - pool.membersStake + membersStake ); } diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 48e3ebcd41..077359b1cc 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -5,6 +5,7 @@ import { filterLogsToArguments, hexRandom, Numberish, + shortZip, } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; import { BigNumber } from '@0x/utils'; @@ -18,26 +19,34 @@ import { IStakingEventsEvents, IStakingEventsRewardsPaidEventArgs, TestFinalizerContract, - TestFinalizerDepositIntoStakingPoolRewardVaultCallEventArgs, + TestFinalizerDepositStakingPoolRewardsEventArgs as DepositStakingPoolRewardsEventArgs, TestFinalizerEvents, + TestFinalizerRecordStakingPoolRewardsEventArgs as RecordStakingPoolRewardsEventArgs, } from '../../src'; -import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; +import { + assertRoughlyEquals as _assertIntegerRoughlyEquals, + getRandomInteger, + toBaseUnitAmount, +} from '../utils/number_utils'; blockchainTests.resets('finalizer unit tests', env => { const { ZERO_AMOUNT } = constants; const INITIAL_EPOCH = 0; const INITIAL_BALANCE = toBaseUnitAmount(32); - let rewardReceiverAddress: string; + let operatorRewardsReceiver: string; + let membersRewardsReceiver: string; let testContract: TestFinalizerContract; before(async () => { - rewardReceiverAddress = hexRandom(constants.ADDRESS_LENGTH); + operatorRewardsReceiver = hexRandom(constants.ADDRESS_LENGTH); + membersRewardsReceiver = hexRandom(constants.ADDRESS_LENGTH); testContract = await TestFinalizerContract.deployFrom0xArtifactAsync( artifacts.TestFinalizer, env.provider, env.txDefaults, artifacts, - rewardReceiverAddress, + operatorRewardsReceiver, + membersRewardsReceiver, ); // Give the contract a balance. await sendEtherAsync(testContract.address, INITIAL_BALANCE); @@ -112,86 +121,146 @@ blockchainTests.resets('finalizer unit tests', env => { async function assertFinalizationStateAsync(expected: Partial): Promise { const actual = await getFinalizationStateAsync(); - if (expected.balance !== undefined) { - expect(actual.balance).to.bignumber.eq(expected.balance); - } - if (expected.currentEpoch !== undefined) { - expect(actual.currentEpoch).to.eq(expected.currentEpoch); - } - if (expected.closingEpoch !== undefined) { - expect(actual.closingEpoch).to.eq(expected.closingEpoch); - } - if (expected.numActivePoolsThisEpoch !== undefined) { - expect(actual.numActivePoolsThisEpoch).to.eq(expected.numActivePoolsThisEpoch); - } - if (expected.totalFeesCollectedThisEpoch !== undefined) { - expect(actual.totalFeesCollectedThisEpoch).to.bignumber.eq(expected.totalFeesCollectedThisEpoch); - } - if (expected.totalWeightedStakeThisEpoch !== undefined) { - expect(actual.totalWeightedStakeThisEpoch).to.bignumber.eq(expected.totalWeightedStakeThisEpoch); - } - if (expected.unfinalizedPoolsRemaining !== undefined) { - expect(actual.unfinalizedPoolsRemaining).to.eq(expected.unfinalizedPoolsRemaining); - } - if (expected.unfinalizedRewardsAvailable !== undefined) { - expect(actual.unfinalizedRewardsAvailable).to.bignumber.eq(expected.unfinalizedRewardsAvailable); - } - if (expected.unfinalizedTotalFeesCollected !== undefined) { - expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected); - } - if (expected.unfinalizedTotalFeesCollected !== undefined) { - expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected); - } + assertEqualNumberFields(actual, expected); } function assertEpochEndedEvent(logs: LogEntry[], args: Partial): void { const events = getEpochEndedEvents(logs); expect(events.length).to.eq(1); - if (args.epoch !== undefined) { - expect(events[0].epoch).to.bignumber.eq(INITIAL_EPOCH); - } - if (args.numActivePools !== undefined) { - expect(events[0].numActivePools).to.bignumber.eq(args.numActivePools); - } - if (args.rewardsAvailable !== undefined) { - expect(events[0].rewardsAvailable).to.bignumber.eq(args.rewardsAvailable); - } - if (args.totalFeesCollected !== undefined) { - expect(events[0].totalFeesCollected).to.bignumber.eq(args.totalFeesCollected); - } - if (args.totalWeightedStake !== undefined) { - expect(events[0].totalWeightedStake).to.bignumber.eq(args.totalWeightedStake); - } + assertEqualNumberFields(events[0], args); } function assertEpochFinalizedEvent(logs: LogEntry[], args: Partial): void { const events = getEpochFinalizedEvents(logs); expect(events.length).to.eq(1); - if (args.epoch !== undefined) { - expect(events[0].epoch).to.bignumber.eq(args.epoch); + assertEqualNumberFields(events[0], args); + } + + function assertRoughlyEquals(actual: Numberish, expected: Numberish): void { + _assertIntegerRoughlyEquals(actual, expected, 5); + } + + function assertEqualNumberFields(actual: T, expected: Partial): void { + for (const key of Object.keys(actual)) { + const a = (actual as any)[key] as BigNumber; + const e = (expected as any)[key] as Numberish; + if (e !== undefined) { + expect(a, key).to.bignumber.eq(e); + } } - if (args.rewardsPaid !== undefined) { - expect(events[0].rewardsPaid).to.bignumber.eq(args.rewardsPaid); + } + + async function assertFinalizationLogsAndBalancesAsync( + rewardsAvailable: Numberish, + activePools: ActivePoolOpts[], + finalizationLogs: LogEntry[], + ): Promise { + const currentEpoch = await getCurrentEpochAsync(); + // Compute the expected rewards for each pool. + const poolRewards = await calculatePoolRewardsAsync(rewardsAvailable, activePools); + const totalRewards = BigNumber.sum(...poolRewards); + const rewardsRemaining = new BigNumber(rewardsAvailable).minus(totalRewards); + const nonZeroPoolRewards = poolRewards.filter(r => !r.isZero()); + const poolsWithNonZeroRewards = _.filter(activePools, (p, i) => !poolRewards[i].isZero()); + const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(activePools, poolRewards); + + // Assert the `RewardsPaid` logs. + const rewardsPaidEvents = getRewardsPaidEvents(finalizationLogs); + expect(rewardsPaidEvents.length).to.eq(poolsWithNonZeroRewards.length); + for (const i of _.times(rewardsPaidEvents.length)) { + const event = rewardsPaidEvents[i]; + const pool = poolsWithNonZeroRewards[i]; + const reward = nonZeroPoolRewards[i]; + const [operatorReward, membersReward] = splitRewards(pool, reward); + expect(event.epoch).to.bignumber.eq(currentEpoch); + assertRoughlyEquals(event.operatorReward, operatorReward); + assertRoughlyEquals(event.membersReward, membersReward); } - if (args.rewardsRemaining !== undefined) { - expect(events[0].rewardsRemaining).to.bignumber.eq(args.rewardsRemaining); + + // Assert the `RecordStakingPoolRewards` logs. + const recordStakingPoolRewardsEvents = getRecordStakingPoolRewardsEvents(finalizationLogs); + for (const i of _.times(recordStakingPoolRewardsEvents.length)) { + const event = recordStakingPoolRewardsEvents[i]; + const pool = poolsWithNonZeroRewards[i]; + const reward = nonZeroPoolRewards[i]; + expect(event.poolId).to.eq(pool.poolId); + assertRoughlyEquals(event.totalReward, reward); + assertRoughlyEquals(event.membersStake, pool.membersStake); + } + + // Assert the `DepositStakingPoolRewards` logs. + // 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)); + assertRoughlyEquals(totalDepositedOperatorRewards, totalOperatorRewards); + assertRoughlyEquals(totalDepositedMembersRewards, totalMembersRewards); } + + // Assert the `EpochFinalized` logs. + const epochFinalizedEvents = getEpochFinalizedEvents(finalizationLogs); + expect(epochFinalizedEvents.length).to.eq(1); + expect(epochFinalizedEvents[0].epoch).to.bignumber.eq(currentEpoch - 1); + assertRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards); + assertRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining); + + // Assert the receiver balances. + await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards); } - function assertDepositIntoStakingPoolRewardVaultCallEvent(logs: LogEntry[], amount?: Numberish): void { - const events = filterLogsToArguments( - logs, - TestFinalizerEvents.DepositIntoStakingPoolRewardVaultCall, - ); - expect(events.length).to.eq(1); - if (amount !== undefined) { - expect(events[0].amount).to.bignumber.eq(amount); + async function assertReceiverBalancesAsync( + operatorRewards: Numberish, + membersRewards: Numberish, + ): Promise { + const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver); + assertRoughlyEquals(operatorRewardsBalance, operatorRewards); + const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver); + assertRoughlyEquals(membersRewardsBalance, membersRewards); + } + + async function calculatePoolRewardsAsync( + rewardsAvailable: Numberish, + activePools: ActivePoolOpts[], + ): Promise { + const totalFees = BigNumber.sum(...activePools.map(p => p.feesCollected)); + const totalStake = BigNumber.sum(...activePools.map(p => p.weightedStake)); + const poolRewards = _.times(activePools.length, () => constants.ZERO_AMOUNT); + for (const i of _.times(activePools.length)) { + const pool = activePools[i]; + const feesCollected = new BigNumber(pool.feesCollected); + if (feesCollected.isZero()) { + continue; + } + poolRewards[i] = await testContract.cobbDouglas.callAsync( + new BigNumber(rewardsAvailable), + new BigNumber(feesCollected), + new BigNumber(totalFees), + new BigNumber(pool.weightedStake), + new BigNumber(totalStake), + ); } + return poolRewards; } - async function assertRewarReceiverBalanceAsync(expectedAmount: Numberish): Promise { - const balance = await getBalanceOfAsync(rewardReceiverAddress); - expect(balance).to.be.bignumber.eq(expectedAmount); + function splitRewards(pool: ActivePoolOpts, totalReward: Numberish): [BigNumber, BigNumber] { + if (new BigNumber(pool.membersStake).isZero()) { + return [new BigNumber(totalReward), ZERO_AMOUNT]; + } + const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); + const membersShare = new BigNumber(totalReward).minus(operatorShare); + return [operatorShare, membersShare]; + } + + // Calculates the split rewards for every pool and returns the operator + // and member sums. + function getTotalSplitRewards(pools: ActivePoolOpts[], rewards: Numberish[]): [BigNumber, BigNumber] { + const _rewards = _.times(pools.length).map(i => splitRewards(pools[i], rewards[i])); + const totalOperatorRewards = BigNumber.sum(..._rewards.map(([o]) => o)); + const totalMembersRewards = BigNumber.sum(..._rewards.map(([, m]) => m)); + return [totalOperatorRewards, totalMembersRewards]; } function getEpochEndedEvents(logs: LogEntry[]): IStakingEventsEpochEndedEventArgs[] { @@ -202,12 +271,20 @@ blockchainTests.resets('finalizer unit tests', env => { return filterLogsToArguments(logs, IStakingEventsEvents.EpochFinalized); } + function getRecordStakingPoolRewardsEvents(logs: LogEntry[]): RecordStakingPoolRewardsEventArgs[] { + return filterLogsToArguments(logs, TestFinalizerEvents.RecordStakingPoolRewards); + } + + function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] { + return filterLogsToArguments(logs, TestFinalizerEvents.DepositStakingPoolRewards); + } + function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] { return filterLogsToArguments(logs, IStakingEventsEvents.RewardsPaid); } async function getCurrentEpochAsync(): Promise { - return (await testContract.getCurrentEpoch.callAsync()).toNumber(); + return (await testContract.currentEpoch.callAsync()).toNumber(); } async function getBalanceOfAsync(whom: string): Promise { @@ -217,7 +294,7 @@ blockchainTests.resets('finalizer unit tests', env => { describe('endEpoch()', () => { it('advances the epoch', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); - const currentEpoch = await testContract.getCurrentEpoch.callAsync(); + const currentEpoch = await testContract.currentEpoch.callAsync(); expect(currentEpoch).to.bignumber.eq(INITIAL_EPOCH + 1); }); @@ -303,58 +380,25 @@ blockchainTests.resets('finalizer unit tests', env => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - expect(rewardsPaidEvents.length).to.eq(1); - expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1); - expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId); - assertEpochFinalizedEvent(receipt.logs, { - epoch: new BigNumber(INITIAL_EPOCH), - rewardsPaid: INITIAL_BALANCE, - }); - assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - await assertRewarReceiverBalanceAsync(totalRewardsPaid); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs); }); it('can finalize multiple pools', async () => { - const nextEpoch = INITIAL_EPOCH + 1; const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const poolIds = pools.map(p => p.poolId); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - expect(rewardsPaidEvents.length).to.eq(pools.length); - for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< - [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] - >) { - expect(event.epoch).to.bignumber.eq(nextEpoch); - expect(event.poolId).to.eq(pool.poolId); - } - assertEpochFinalizedEvent(receipt.logs, { epoch: new BigNumber(INITIAL_EPOCH) }); - assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - await assertRewarReceiverBalanceAsync(totalRewardsPaid); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, receipt.logs); }); it('can finalize multiple pools over multiple transactions', async () => { - const nextEpoch = INITIAL_EPOCH + 1; const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipts = await Promise.all( pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), ); const allLogs = _.flatten(receipts.map(r => r.logs)); - const rewardsPaidEvents = getRewardsPaidEvents(allLogs); - expect(rewardsPaidEvents.length).to.eq(pools.length); - for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< - [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] - >) { - expect(event.epoch).to.bignumber.eq(nextEpoch); - expect(event.poolId).to.eq(pool.poolId); - } - assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) }); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; - await assertRewarReceiverBalanceAsync(totalRewardsPaid); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs); }); it('ignores a non-active pool', async () => { @@ -389,7 +433,7 @@ blockchainTests.resets('finalizer unit tests', env => { const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const poolState = await testContract.internalGetActivePoolFromEpoch.callAsync( + const poolState = await testContract.getActivePoolFromEpoch.callAsync( new BigNumber(INITIAL_EPOCH), pool.poolId, ); @@ -454,54 +498,33 @@ blockchainTests.resets('finalizer unit tests', env => { it('does nothing if there were no active pools', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); const poolId = hexRandom(); - const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); + const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); expect(receipt.logs).to.deep.eq([]); }); it('can finalize a pool', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - expect(rewardsPaidEvents.length).to.eq(1); - expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1); - expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId); - assertEpochFinalizedEvent(receipt.logs, { - epoch: new BigNumber(INITIAL_EPOCH), - rewardsPaid: INITIAL_BALANCE, - }); - assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - await assertRewarReceiverBalanceAsync(totalRewardsPaid); + const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs); }); it('can finalize multiple pools over multiple transactions', async () => { - const nextEpoch = INITIAL_EPOCH + 1; const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipts = await Promise.all( - pools.map(pool => testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId)), + pools.map(pool => testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId)), ); const allLogs = _.flatten(receipts.map(r => r.logs)); - const rewardsPaidEvents = getRewardsPaidEvents(allLogs); - expect(rewardsPaidEvents.length).to.eq(pools.length); - for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array< - [ActivePoolOpts, IStakingEventsRewardsPaidEventArgs] - >) { - expect(event.epoch).to.bignumber.eq(nextEpoch); - expect(event.poolId).to.eq(pool.poolId); - } - assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) }); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; - await assertRewarReceiverBalanceAsync(totalRewardsPaid); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs); }); it('ignores a finalized pool', async () => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const [finalizedPool] = _.sampleSize(pools, 1); - await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); - const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); + await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); + const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); expect(rewardsPaidEvents).to.deep.eq([]); }); @@ -510,8 +533,8 @@ blockchainTests.resets('finalizer unit tests', env => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId); - const poolState = await testContract.internalGetActivePoolFromEpoch.callAsync( + await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); + const poolState = await testContract.getActivePoolFromEpoch.callAsync( new BigNumber(INITIAL_EPOCH), pool.poolId, ); @@ -526,7 +549,7 @@ blockchainTests.resets('finalizer unit tests', env => { const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); const receipts = await Promise.all( - pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), ); const allLogs = _.flatten(receipts.map(r => r.logs)); const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; @@ -539,7 +562,7 @@ blockchainTests.resets('finalizer unit tests', env => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const receipts = await Promise.all( - [pool1, pool2].map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + [pool1, pool2].map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), ); const allLogs = _.flatten(receipts.map(r => r.logs)); const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; @@ -555,7 +578,7 @@ blockchainTests.resets('finalizer unit tests', env => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const receipts = await Promise.all( - pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)), + pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), ); const allLogs = _.flatten(receipts.map(r => r.logs)); const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; @@ -612,81 +635,51 @@ blockchainTests.resets('finalizer unit tests', env => { }); }); - interface PoolRewards { - operatorReward: Numberish; - membersReward: Numberish; + interface FinalizedPoolRewards { + totalReward: Numberish; membersStake: Numberish; } - function assertPoolRewards(actual: PoolRewards, expected: Partial): void { - if (expected.operatorReward !== undefined) { - expect(actual.operatorReward).to.bignumber.eq(expected.operatorReward); - } - if (expected.membersReward !== undefined) { - expect(actual.membersReward).to.bignumber.eq(expected.membersReward); + async function assertUnfinalizedPoolRewardsAsync( + poolId: string, + expected: Partial, + ): Promise { + const actual = await testContract.getUnfinalizedPoolRewards.callAsync(poolId); + if (expected.totalReward !== undefined) { + expect(actual.totalReward).to.bignumber.eq(expected.totalReward); } if (expected.membersStake !== undefined) { expect(actual.membersStake).to.bignumber.eq(expected.membersStake); } } - async function callCobbDouglasAsync( - totalRewards: Numberish, - fees: Numberish, - totalFees: Numberish, - stake: Numberish, - totalStake: Numberish, - ): Promise { - return testContract.cobbDouglas.callAsync( - new BigNumber(totalRewards), - new BigNumber(fees), - new BigNumber(totalFees), - new BigNumber(stake), - new BigNumber(totalStake), - ); - } - - function splitRewards(pool: ActivePoolOpts, totalReward: Numberish): [BigNumber, BigNumber] { - if (new BigNumber(pool.membersStake).isZero()) { - return [new BigNumber(totalReward), ZERO_AMOUNT]; - } - const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); - const membersShare = new BigNumber(totalReward).minus(operatorShare); - return [operatorShare, membersShare]; - } - describe('_getUnfinalizedPoolReward()', () => { const ZERO_REWARDS = { - operatorReward: 0, - membersReward: 0, + totalReward: 0, membersStake: 0, }; it('returns empty if epoch is 0', async () => { const poolId = hexRandom(); - const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId); - assertPoolRewards(rewards, ZERO_REWARDS); + return assertUnfinalizedPoolRewardsAsync(poolId, ZERO_REWARDS); }); it('returns empty if pool was not active', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); const poolId = hexRandom(); - const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId); - assertPoolRewards(rewards, ZERO_REWARDS); + return assertUnfinalizedPoolRewardsAsync(poolId, ZERO_REWARDS); }); it('returns empty if pool is active only in the current epoch', async () => { const pool = await addActivePoolAsync(); - const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(rewards, ZERO_REWARDS); + return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); }); it('returns empty if pool was only active in the 2 epochs ago', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(rewards, ZERO_REWARDS); + return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); }); it('returns empty if pool was already finalized', async () => { @@ -694,19 +687,15 @@ blockchainTests.resets('finalizer unit tests', env => { const [pool] = _.sampleSize(pools, 1); await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(rewards, ZERO_REWARDS); + return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); }); it('computes one reward among one pool', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); const expectedTotalRewards = INITIAL_BALANCE; - const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: expectedOperatorReward, - membersReward: expectedMembersReward, + return assertUnfinalizedPoolRewardsAsync(pool.poolId, { + totalReward: expectedTotalRewards, membersStake: pool.membersStake, }); }); @@ -714,21 +703,10 @@ blockchainTests.resets('finalizer unit tests', env => { it('computes one reward among multiple pools', async () => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const [pool] = _.sampleSize(pools, 1); - const totalFeesCollected = BigNumber.sum(...pools.map(p => p.feesCollected)); - const totalWeightedStake = BigNumber.sum(...pools.map(p => p.weightedStake)); - const expectedTotalRewards = await callCobbDouglasAsync( - INITIAL_BALANCE, - pool.feesCollected, - totalFeesCollected, - pool.weightedStake, - totalWeightedStake, - ); - const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: expectedOperatorReward, - membersReward: expectedMembersReward, + 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, membersStake: pool.membersStake, }); }); @@ -736,10 +714,8 @@ blockchainTests.resets('finalizer unit tests', env => { it('computes a reward with 0% operatorShare', async () => { const pool = await addActivePoolAsync({ operatorShare: 0 }); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: 0, - membersReward: INITIAL_BALANCE, + return assertUnfinalizedPoolRewardsAsync(pool.poolId, { + totalReward: INITIAL_BALANCE, membersStake: pool.membersStake, }); }); @@ -747,32 +723,8 @@ blockchainTests.resets('finalizer unit tests', env => { it('computes a reward with 100% operatorShare', async () => { const pool = await addActivePoolAsync({ operatorShare: 1 }); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: INITIAL_BALANCE, - membersReward: 0, - membersStake: pool.membersStake, - }); - }); - - it('gives all rewards to operator if membersStake is zero', async () => { - const pool = await addActivePoolAsync({ membersStake: 0 }); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: INITIAL_BALANCE, - membersReward: 0, - membersStake: pool.membersStake, - }); - }); - - it('gives all rewards to operator if membersStake is zero, even if operatorShare is zero', async () => { - const pool = await addActivePoolAsync({ membersStake: 0, operatorShare: 0 }); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId); - assertPoolRewards(actualRewards, { - operatorReward: INITIAL_BALANCE, - membersReward: 0, + return assertUnfinalizedPoolRewardsAsync(pool.poolId, { + totalReward: INITIAL_BALANCE, membersStake: pool.membersStake, }); }); diff --git a/contracts/staking/test/utils/number_utils.ts b/contracts/staking/test/utils/number_utils.ts index 3cb2b0733c..aa07ad3d44 100644 --- a/contracts/staking/test/utils/number_utils.ts +++ b/contracts/staking/test/utils/number_utils.ts @@ -90,6 +90,17 @@ export function assertRoughlyEquals(actual: Numberish, expected: Numberish, prec expect(actual).to.bignumber.eq(expected); } +/** + * 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(); + if (diff <= maxError) { + return; + } + expect(actual).to.bignumber.eq(expected); +} + /** * Converts `amount` into a base unit amount with 18 digits. */ From 54ac1c284b7bcfa7bf8efa293d85897bf8eac345 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Sep 2019 22:44:28 -0400 Subject: [PATCH 34/52] `@0x/base-contract`: Properly encode `BigNumber` indexed filter values in `getTopicsForIndexedArgs()`. --- packages/base-contract/CHANGELOG.json | 4 ++++ packages/base-contract/src/utils/filter_utils.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/base-contract/CHANGELOG.json b/packages/base-contract/CHANGELOG.json index 06f210a0ea..e7370d4fb2 100644 --- a/packages/base-contract/CHANGELOG.json +++ b/packages/base-contract/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Make the Promise returned by `awaitTransactionSuccessAsync` compatible with base Promise type", "pr": 1885 + }, + { + "note": "Properly encode `BigNumber` indexed filter values in `getTopicsForIndexedArgs()`", + "pr": 2155 } ] }, diff --git a/packages/base-contract/src/utils/filter_utils.ts b/packages/base-contract/src/utils/filter_utils.ts index f2b7fc7ad1..fce4579726 100644 --- a/packages/base-contract/src/utils/filter_utils.ts +++ b/packages/base-contract/src/utils/filter_utils.ts @@ -1,3 +1,4 @@ +import { BigNumber } from '@0x/utils'; import { BlockRange, ContractAbi, EventAbi, FilterObject, LogEntry } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as jsSHA3 from 'js-sha3'; @@ -51,7 +52,11 @@ export const filterUtils = { // Null is a wildcard topic in a JSON-RPC call topics.push(null); } else { - const value = indexFilterValues[eventInput.name] as string; + let value = indexFilterValues[eventInput.name] as any; + if (BigNumber.isBigNumber(value)) { + // tslint:disable-next-line custom-no-magic-numbers + value = ethUtil.fromSigned(value.toString(10) as any); + } const buffer = ethUtil.toBuffer(value); const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); const topic = ethUtil.bufferToHex(paddedBuffer); From b43fa88606c245bfa6e1e7ddfa506179b7f25ad5 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 03:05:06 -0400 Subject: [PATCH 35/52] `@0x/contracts-staking`: All tests but `rewards_test` working. --- .../interfaces/IStakingPoolRewardVault.sol | 1 - .../contracts/src/stake/MixinStake.sol | 2 +- .../staking_pools/MixinCumulativeRewards.sol | 35 +- .../staking_pools/MixinStakingPoolRewards.sol | 73 ++- .../staking/contracts/src/vaults/EthVault.sol | 2 +- .../src/vaults/StakingPoolRewardVault.sol | 2 +- .../test/TestCumulativeRewardTracking.sol | 14 +- .../contracts/test/TestDelegatorRewards.sol | 29 +- .../staking/contracts/test/TestFinalizer.sol | 4 +- .../contracts/test/TestProtocolFees.sol | 4 +- contracts/staking/package.json | 1 + .../staking/test/actors/finalizer_actor.ts | 27 +- .../test/cumulative_reward_tracking_test.ts | 540 +++++++++++------- contracts/staking/test/protocol_fees.ts | 42 +- contracts/staking/test/rewards_test.ts | 2 +- .../test/unit_tests/delegator_reward_test.ts | 93 ++- .../staking/test/unit_tests/finalizer_test.ts | 27 +- contracts/staking/test/utils/api_wrapper.ts | 4 +- .../cumulative_reward_tracking_simulation.ts | 1 + contracts/staking/test/utils/number_utils.ts | 5 +- 20 files changed, 561 insertions(+), 347 deletions(-) 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,26 +413,23 @@ 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) + // Record dependency on current epoch. + if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 + || _delegatedStakeToPoolByOwner.nextEpochBalance != 0) { _addOrRemoveDependencyOnCumulativeReward( poolId, - nextEpoch, + _delegatedStakeToPoolByOwner.currentEpoch, mostRecentCumulativeRewardInfo, isDependent ); } - // Record dependency on current epoch. - if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 - || _delegatedStakeToPoolByOwner.nextEpochBalance != 0) - { + // Record dependency on the next epoch + if (_delegatedStakeToPoolByOwner.nextEpochBalance != 0) { _addOrRemoveDependencyOnCumulativeReward( poolId, - _delegatedStakeToPoolByOwner.currentEpoch, + 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], [ - { event: 'SetCumulativeReward', epoch: 1 }, - { event: 'SetMostRecentCumulativeReward', epoch: 1 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, + // Creates CR for epoch 0 + TestAction.CreatePool, + // Moves to epoch 1 + TestAction.Finalize, ], - ); - }); - 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 + // 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, ], [ - 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 }, + { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 2 }, ], - [{ 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. + it('re-delegating in a new epoch', 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.Undelegate, // does nothing. This delegator no longer has dependency, but the most recent CR is 1 so we don't remove. + // Creates CR in epoch 0 + TestAction.CreatePool, + // Updates CR for epoch 0 + // Creates CR for epoch 1 + TestAction.Delegate, + // Moves to epoch 1 + TestAction.Finalize, ], - [], - ); - }); - 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 + // Updates CR for epoch 1 + // Sets MRCR to epoch 1 + // Creates CR for epoch 2 + // Clears CR for epoch 0 + TestAction.Delegate, ], [ - 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: 'SetCumulativeReward', epoch: 2 }, + { event: 'UnsetCumulativeReward', epoch: 0 }, ], - [{ event: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }], ); }); - it('should set CR and update Most Recent CR when undelegating', async () => { + it('delegate then undelegate to remove all dependencies', 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 + // 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.Undelegate, // 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: 'SetCumulativeReward', epoch: 2 }, { event: 'SetMostRecentCumulativeReward', epoch: 2 }], + [{ event: 'UnsetCumulativeReward', epoch: 2 }], ); }); - it('should set CR and update Most Recent CR when undelegating, plus remove the CR that is no longer depends on.', async () => { + it('delegating in epoch 1 then again in epoch 2', async () => { await simulation.runTestAsync( [ - TestAction.CreatePool, // creates CR in epoch 0 + // 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.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 + // Move to epoch 2 + TestAction.Finalize, ], [ - TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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 update Most Recent CR when redelegating, plus remove the CR that it no longer depends on.', 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 - 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 0 + // Clears CR for epoch 0 + // Creates CR for epoch 2 + TestAction.Delegate, + // Moves to epoch 2 + TestAction.Finalize, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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: 0 }, + { event: 'UnsetCumulativeReward', epoch: 1 }, ], ); }); - it('should set CR and Most Recent CR when a reward is earned', async () => { + it('delegate in epoch 0 and epoch 1, then undelegate half 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 - TestAction.PayProtocolFee, + // 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, ], [ - TestAction.Finalize, // adds a CR for epoch 1, plus updates most recent CR + // 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 }, ], - [{ 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 () => { + it('delegate in epoch 1 and 2 then again in 3', 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 + // 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, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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: '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 () => { + it('delegate in epoch 0, earn reward in epoch 1', 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, + // Credits pool with rewards + TestAction.PayProtocolFee, ], [ - 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 }, + // 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 redelegating, one full epoch after a reward was earned', 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.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 + // 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 2. Sets most recent CR to epoch 2. + // 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: 'SetMostRecentCumulativeReward', epoch: 3 }, - { event: 'UnsetCumulativeReward', epoch: 2 }, { event: 'SetCumulativeReward', epoch: 4 }, - { event: 'SetMostRecentCumulativeReward', epoch: 4 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, { event: 'UnsetCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 2 }, ], ); }); - it('should set/unset CR and update Most Recent CR when redelegating, one full epoch after a reward was earned', 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.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, ], [ - TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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: 3 }, - { event: 'SetMostRecentCumulativeReward', epoch: 3 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, + { event: 'SetCumulativeReward', epoch: 4 }, { event: 'UnsetCumulativeReward', epoch: 1 }, + { event: 'UnsetCumulativeReward', epoch: 2 }, ], ); }); - it('should set/unset CR and update Most Recent CR when delegating for the first time in an epoch with no CR', 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.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. + // 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, + ], + [ + // 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: 1 }, - { event: 'SetMostRecentCumulativeReward', epoch: 1 }, - { event: 'UnsetCumulativeReward', epoch: 0 }, + { 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('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 () => { + it('earn reward in epoch 1 with no stake, then delegate', async () => { await simulation.runTestAsync( [ - TestAction.CreatePool, // creates CR in epoch 0 - TestAction.Finalize, // creates new CR for epoch 0; moves to epoch 1 + // 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, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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('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 () => { + it('delegate in epoch 1, 3, then delegate in epoch 4', 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 + // 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, ], [ - TestAction.Delegate, // copies CR from epoch 1 to epoch 3. Sets most recent CR to epoch 3. + // 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: '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 }, + { event: 'SetCumulativeReward', epoch: 5 }, + { event: 'UnsetCumulativeReward', epoch: 3 }, ], ); }); - it('should set/unset CR and update Most Recent CR when redelegating after receiving a reward', async () => { + it('delegate in epoch 1, then epoch 3', 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 + // 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, ], [ - TestAction.Undelegate, // copies CR from epoch 1 to epoch 2. Sets most recent CR to epoch 2. + // 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; } From d5bbbe802b06c6124c57f204deddbb71f9381bcf Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 03:21:34 -0400 Subject: [PATCH 36/52] `@0x/base-contract`: Fix linter errors. --- packages/base-contract/src/utils/filter_utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/base-contract/src/utils/filter_utils.ts b/packages/base-contract/src/utils/filter_utils.ts index fce4579726..db6077c5e6 100644 --- a/packages/base-contract/src/utils/filter_utils.ts +++ b/packages/base-contract/src/utils/filter_utils.ts @@ -52,11 +52,13 @@ export const filterUtils = { // Null is a wildcard topic in a JSON-RPC call topics.push(null); } else { + // tslint:disable: no-unnecessary-type-assertion let value = indexFilterValues[eventInput.name] as any; if (BigNumber.isBigNumber(value)) { // tslint:disable-next-line custom-no-magic-numbers value = ethUtil.fromSigned(value.toString(10) as any); } + // tslint:enable: no-unnecessary-type-assertion const buffer = ethUtil.toBuffer(value); const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); const topic = ethUtil.bufferToHex(paddedBuffer); From 86106713dd76de8ce3c022629f030c2391ff57cf Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 05:25:46 -0400 Subject: [PATCH 37/52] `@0x/contracts-staking`: Gag! All tests passing? --- .../contracts/src/fees/MixinExchangeFees.sol | 21 ++-- .../staking/contracts/test/TestStaking.sol | 9 ++ .../staking/test/actors/finalizer_actor.ts | 106 ++++++++++-------- contracts/staking/test/protocol_fees.ts | 2 +- contracts/staking/test/rewards_test.ts | 4 +- contracts/staking/test/utils/api_wrapper.ts | 31 +++++ 6 files changed, 108 insertions(+), 65 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 66419d8949..ffbf767363 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -161,25 +161,20 @@ contract MixinExchangeFees is view returns (uint256 totalBalance) { - totalBalance = address(this).balance + - IEtherToken(WETH_ADDRESS).balanceOf(address(this)); + totalBalance = address(this).balance.safeAdd( + IEtherToken(WETH_ADDRESS).balanceOf(address(this)) + ); } - /// @dev Returns the amount of fees attributed to the input pool this epoch. + /// @dev Get information on an active staking pool in this epoch. /// @param poolId Pool Id to query. - /// @return feesCollectedByPool Amount of fees collected by the pool this - /// epoch. - function getProtocolFeesThisEpochByPool(bytes32 poolId) + /// @return pool ActivePool struct. + function getActiveStakingPoolThisEpoch(bytes32 poolId) external view - returns (uint256 feesCollected) + returns (IStructs.ActivePool memory pool) { - // Look up the pool for this epoch. The epoch index is `currentEpoch % 2` - // because we only need to remember state in the current epoch and the - // epoch prior. - IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(currentEpoch, poolId); - feesCollected = pool.feesCollected; + pool = _getActivePoolFromEpoch(currentEpoch, poolId); } /// @dev Computes the members and weighted stake for a pool at the current diff --git a/contracts/staking/contracts/test/TestStaking.sol b/contracts/staking/contracts/test/TestStaking.sol index b9ab162866..3241f43e8d 100644 --- a/contracts/staking/contracts/test/TestStaking.sol +++ b/contracts/staking/contracts/test/TestStaking.sol @@ -25,6 +25,15 @@ import "../src/Staking.sol"; contract TestStaking is Staking { + /// @dev Overridden to avoid hard-coded WETH. + function getTotalBalance() + external + view + returns (uint256 totalBalance) + { + totalBalance = address(this).balance; + } + // Stub out `_unwrapWETH` to prevent the calls to `finalizeFees` from failing in tests // that do not relate to protocol fee payments in WETH. function _unwrapWETH() diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index ee737dcaca..74298fd708 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -4,6 +4,7 @@ import * as _ from 'lodash'; import { StakingApiWrapper } from '../utils/api_wrapper'; import { + BalanceByOwner, DelegatorBalancesByPoolId, DelegatorsByPoolId, OperatorBalanceByPoolId, @@ -15,11 +16,6 @@ import { import { BaseActor } from './base_actor'; -interface Reward { - reward: BigNumber; - poolId: string; -} - const { PPM_100_PERCENT } = constants; // tslint:disable: prefer-conditional-expression @@ -41,30 +37,31 @@ export class FinalizerActor extends BaseActor { this._delegatorsByPoolId = _.cloneDeep(delegatorsByPoolId); } - public async finalizeAsync(rewards: Reward[] = []): Promise { + public async finalizeAsync(): Promise { // cache initial info and balances const operatorShareByPoolId = await this._getOperatorShareByPoolIdAsync(this._poolIds); const rewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); const delegatorBalancesByPoolId = await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId); const delegatorStakesByPoolId = await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId); const operatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId); + const rewardByPoolId = await this._getRewardByPoolIdAsync(this._poolIds); // compute expected changes const [ expectedOperatorBalanceByPoolId, expectedRewardVaultBalanceByPoolId, ] = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( - rewards, + rewardByPoolId, operatorBalanceByPoolId, rewardVaultBalanceByPoolId, + delegatorStakesByPoolId, operatorShareByPoolId, ); - const totalRewardsByPoolId = _.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward')); const expectedDelegatorBalancesByPoolId = await this._computeExpectedDelegatorBalancesByPoolIdAsync( this._delegatorsByPoolId, delegatorBalancesByPoolId, delegatorStakesByPoolId, operatorShareByPoolId, - totalRewardsByPoolId, + rewardByPoolId, ); // finalize await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); @@ -90,33 +87,26 @@ export class FinalizerActor extends BaseActor { delegatorBalancesByPoolId: DelegatorBalancesByPoolId, delegatorStakesByPoolId: DelegatorBalancesByPoolId, operatorShareByPoolId: OperatorShareByPoolId, - totalRewardByPoolId: RewardByPoolId, + rewardByPoolId: RewardByPoolId, ): Promise { const expectedDelegatorBalancesByPoolId = _.cloneDeep(delegatorBalancesByPoolId); for (const poolId of Object.keys(delegatorsByPoolId)) { - if (totalRewardByPoolId[poolId] === undefined) { - continue; - } - const operator = this._operatorByPoolId[poolId]; - const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId); - const operatorShare = operatorShareByPoolId[poolId].dividedBy(PPM_100_PERCENT); - const totalReward = totalRewardByPoolId[poolId]; + const totalStakeInPool = BigNumber.sum(...Object.values(delegatorStakesByPoolId[poolId])); + const operatorStakeInPool = delegatorStakesByPoolId[poolId][operator]; + const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool); + const operatorShare = operatorShareByPoolId[poolId]; + const totalReward = rewardByPoolId[poolId]; const operatorReward = membersStakeInPool.eq(0) ? totalReward - : totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN); + : totalReward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT); const membersTotalReward = totalReward.minus(operatorReward); for (const delegator of delegatorsByPoolId[poolId]) { let delegatorReward = new BigNumber(0); - if (delegator === operator) { - delegatorReward = operatorReward; - } else if (membersStakeInPool.gt(0)) { + if (delegator !== operator && membersStakeInPool.gt(0)) { const delegatorStake = delegatorStakesByPoolId[poolId][delegator]; - delegatorReward = delegatorStake - .times(membersTotalReward) - .dividedBy(membersStakeInPool) - .integerValue(BigNumber.ROUND_DOWN); + delegatorReward = delegatorStake.times(membersTotalReward).dividedToIntegerBy(membersStakeInPool); } const currentBalance = expectedDelegatorBalancesByPoolId[poolId][delegator] || 0; expectedDelegatorBalancesByPoolId[poolId][delegator] = delegatorReward.plus(currentBalance); @@ -168,23 +158,25 @@ export class FinalizerActor extends BaseActor { } private async _computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( - rewards: Reward[], + rewardByPoolId: RewardByPoolId, operatorBalanceByPoolId: OperatorBalanceByPoolId, rewardVaultBalanceByPoolId: RewardVaultBalanceByPoolId, + delegatorStakesByPoolId: DelegatorBalancesByPoolId, operatorShareByPoolId: OperatorShareByPoolId, ): Promise<[RewardVaultBalanceByPoolId, OperatorBalanceByPoolId]> { const expectedOperatorBalanceByPoolId = _.cloneDeep(operatorBalanceByPoolId); const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId); - for (const reward of rewards) { - const operatorShare = operatorShareByPoolId[reward.poolId]; + for (const poolId of Object.keys(rewardByPoolId)) { + const operatorShare = operatorShareByPoolId[poolId]; [ - expectedOperatorBalanceByPoolId[reward.poolId], - expectedRewardVaultBalanceByPoolId[reward.poolId], + expectedOperatorBalanceByPoolId[poolId], + expectedRewardVaultBalanceByPoolId[poolId], ] = await this._computeExpectedRewardVaultBalanceAsync( - reward.poolId, - reward.reward, - expectedOperatorBalanceByPoolId[reward.poolId], - expectedRewardVaultBalanceByPoolId[reward.poolId], + poolId, + rewardByPoolId[poolId], + expectedOperatorBalanceByPoolId[poolId], + expectedRewardVaultBalanceByPoolId[poolId], + delegatorStakesByPoolId[poolId], operatorShare, ); } @@ -196,12 +188,13 @@ export class FinalizerActor extends BaseActor { reward: BigNumber, operatorBalance: BigNumber, rewardVaultBalance: BigNumber, + stakeBalances: BalanceByOwner, operatorShare: BigNumber, ): Promise<[BigNumber, BigNumber]> { - const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync( - poolId, - )).currentEpochBalance; - const operatorPortion = totalStakeDelegatedToPool.eq(0) + const totalStakeDelegatedToPool = BigNumber.sum(...Object.values(stakeBalances)); + const stakeDelegatedToPoolByOperator = stakeBalances[this._operatorByPoolId[poolId]]; + const membersStakeDelegatedToPool = totalStakeDelegatedToPool.minus(stakeDelegatedToPoolByOperator); + const operatorPortion = membersStakeDelegatedToPool.eq(0) ? reward : reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT); const membersPortion = reward.minus(operatorPortion); @@ -220,17 +213,6 @@ export class FinalizerActor extends BaseActor { return operatorBalanceByPoolId; } - private async _getOperatorAndDelegatorsStakeInPoolAsync(poolId: string): Promise<[BigNumber, BigNumber]> { - const stakingContract = this._stakingApiWrapper.stakingContract; - const operator = (await stakingContract.getStakingPool.callAsync(poolId)).operator; - const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync(poolId)) - .currentEpochBalance; - const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync(operator, poolId)) - .currentEpochBalance; - const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool); - return [operatorStakeInPool, membersStakeInPool]; - } - private async _getOperatorShareByPoolIdAsync(poolIds: string[]): Promise { const operatorShareByPoolId: OperatorShareByPoolId = {}; for (const poolId of poolIds) { @@ -250,4 +232,30 @@ export class FinalizerActor extends BaseActor { } return rewardVaultBalanceByPoolId; } + + private async _getRewardByPoolIdAsync(poolIds: string[]): Promise { + const activePools = await Promise.all( + poolIds.map(async poolId => + this._stakingApiWrapper.stakingContract.getActiveStakingPoolThisEpoch.callAsync(poolId), + ), + ); + const totalRewards = await this._stakingApiWrapper.stakingContract.getTotalBalance.callAsync(); + const totalFeesCollected = BigNumber.sum(...activePools.map(p => p.feesCollected)); + const totalWeightedStake = BigNumber.sum(...activePools.map(p => p.weightedStake)); + if (totalRewards.eq(0) || totalFeesCollected.eq(0) || totalWeightedStake.eq(0)) { + return _.zipObject(poolIds, _.times(poolIds.length, () => new BigNumber(0))); + } + const rewards = await Promise.all( + activePools.map(async pool => + this._stakingApiWrapper.utils.cobbDouglas( + totalRewards, + pool.feesCollected, + totalFeesCollected, + pool.weightedStake, + totalWeightedStake, + ), + ), + ); + return _.zipObject(poolIds, rewards); + } } diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index 0b0cfce7e4..4a8fefba76 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -163,7 +163,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); async function getProtocolFeesAsync(poolId: string): Promise { - return testContract.getProtocolFeesThisEpochByPool.callAsync(poolId); + return (await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId)).feesCollected; } describe('ETH fees', () => { diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index e44742491f..bc93f968a3 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.skip('Testing Rewards', env => { +blockchainTests.resets('Testing Rewards', env => { // tokens & addresses let accounts: string[]; let owner: string; @@ -166,7 +166,7 @@ blockchainTests.resets.skip('Testing Rewards', env => { { from: exchangeAddress, value: fee }, ); } - await finalizer.finalizeAsync([{ reward: fee, poolId }]); + await finalizer.finalizeAsync(); }; const ZERO = new BigNumber(0); it('Reward balance should be zero if not delegated', async () => { diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 722c569cd0..0a45cc5616 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -16,6 +16,7 @@ import { StakingEvents, StakingPoolRewardVaultContract, StakingProxyContract, + TestCobbDouglasContract, ZrxVaultContract, } from '../../src'; @@ -33,6 +34,7 @@ export class StakingApiWrapper { public ethVaultContract: EthVaultContract; public rewardVaultContract: StakingPoolRewardVaultContract; public zrxTokenContract: DummyERC20TokenContract; + public cobbDouglasContract: TestCobbDouglasContract; public utils = { // Epoch Utils fastForwardToNextEpochAsync: async (): Promise => { @@ -141,6 +143,25 @@ export class StakingApiWrapper { await this.stakingContract.getParams.callAsync(), ) as any) as StakingParams; }, + + cobbDouglas: async ( + totalRewards: BigNumber, + ownerFees: BigNumber, + totalFees: BigNumber, + ownerStake: BigNumber, + totalStake: BigNumber, + ): Promise => { + const { cobbDouglasAlphaNumerator, cobbDouglasAlphaDenominator } = await this.utils.getParamsAsync(); + return this.cobbDouglasContract.cobbDouglas.callAsync( + totalRewards, + ownerFees, + totalFees, + ownerStake, + totalStake, + new BigNumber(cobbDouglasAlphaNumerator), + new BigNumber(cobbDouglasAlphaDenominator), + ); + }, }; private readonly _web3Wrapper: Web3Wrapper; @@ -154,12 +175,14 @@ export class StakingApiWrapper { ethVaultContract: EthVaultContract, rewardVaultContract: StakingPoolRewardVaultContract, zrxTokenContract: DummyERC20TokenContract, + cobbDouglasContract: TestCobbDouglasContract, ) { this._web3Wrapper = env.web3Wrapper; this.zrxVaultContract = zrxVaultContract; this.ethVaultContract = ethVaultContract; this.rewardVaultContract = rewardVaultContract; this.zrxTokenContract = zrxTokenContract; + this.cobbDouglasContract = cobbDouglasContract; this.stakingContractAddress = stakingContract.address; this.stakingProxyContract = stakingProxyContract; @@ -246,6 +269,13 @@ export async function deployAndConfigureContractsAsync( rewardVaultContract.address, zrxVaultContract.address, ); + // deploy cobb douglas contract + const cobbDouglasContract = await TestCobbDouglasContract.deployFrom0xArtifactAsync( + artifacts.TestCobbDouglas, + env.provider, + txDefaults, + artifacts, + ); // configure erc20 proxy to accept calls from zrx vault await erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVaultContract.address); @@ -264,5 +294,6 @@ export async function deployAndConfigureContractsAsync( ethVaultContract, rewardVaultContract, zrxTokenContract, + cobbDouglasContract, ); } From 0270777cfc1979e2b802c21fca7d7333e1d2e34d Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 05:41:00 -0400 Subject: [PATCH 38/52] `@0x/contracts-test-utils`: Add `hexHash()` to `hex_utils`. --- contracts/test-utils/CHANGELOG.json | 4 ++++ contracts/test-utils/src/hex_utils.ts | 7 +++++++ contracts/test-utils/src/index.ts | 11 ++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 875f1fbc30..8496757a6f 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -86,6 +86,10 @@ "note": "Tweaks/Upgrades to `hex_utils`, most notably `hexSlice()`", "pr": 2155 }, + { + "note": "Add `hexHash()` to `hex_utils`", + "pr": 2155 + }, { "note": "Add `shortZip()` to `lang_utils.ts`", "pr": 2155 diff --git a/contracts/test-utils/src/hex_utils.ts b/contracts/test-utils/src/hex_utils.ts index 05b7a9c781..007bbf9bd5 100644 --- a/contracts/test-utils/src/hex_utils.ts +++ b/contracts/test-utils/src/hex_utils.ts @@ -58,6 +58,13 @@ export function hexSlice(n: Numberish, start: number, end?: number): string { return '0x'.concat(hex.substring(sliceStart, sliceEnd)); } +/** + * Get the keccak hash of some data. + */ +export function hexHash(n: Numberish): string { + return ethUtil.bufferToHex(ethUtil.sha3(ethUtil.toBuffer(toHex(n)))); + } + /** * Convert a string, a number, or a BigNumber into a hex string. * Works with negative numbers, as well. diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 7df55bf89a..bdac824e23 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -28,7 +28,16 @@ export { bytes32Values, testCombinatoriallyWithReferenceFunc, uint256Values } fr export { TransactionFactory } from './transaction_factory'; export { MutatorContractFunction, TransactionHelper } from './transaction_helper'; export { testWithReferenceFuncAsync } from './test_with_reference'; -export { hexConcat, hexLeftPad, hexInvert, hexSlice, hexRandom, hexRightPad, toHex } from './hex_utils'; +export { + hexConcat, + hexHash, + hexLeftPad, + hexInvert, + hexSlice, + hexRandom, + hexRightPad, + toHex, +} from './hex_utils'; export { BatchMatchOrder, ContractName, From d064543108317731a1b0ba10f0693304ffba832c Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 06:15:09 -0400 Subject: [PATCH 39/52] `@0x/contracts-staking`: Add some `computeRewardBalanceOfOperator()` tests. --- .../staking_pools/MixinStakingPoolRewards.sol | 5 + .../contracts/test/TestDelegatorRewards.sol | 43 +++-- .../test/unit_tests/delegator_reward_test.ts | 152 +++++++++++++----- 3 files changed, 145 insertions(+), 55 deletions(-) diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 7065e4eb39..b44a10a52b 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -68,6 +68,7 @@ contract MixinStakingPoolRewards is } /// @dev Computes the reward balance in ETH of the operator of a pool. + /// This does not include the balance in the ETH vault. /// @param poolId Unique id of pool. /// @return totalReward Balance in ETH. function computeRewardBalanceOfOperator(bytes32 poolId) @@ -75,6 +76,9 @@ contract MixinStakingPoolRewards is view returns (uint256 reward) { + // Because operator rewards are immediately sent to the ETH vault + // on finalization, the only factor in this function are unfinalized + // rewards. IStructs.Pool memory pool = _poolById[poolId]; // Get any unfinalized rewards. (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = @@ -88,6 +92,7 @@ contract MixinStakingPoolRewards is } /// @dev Computes the reward balance in ETH of a specific member of a pool. + /// This does not include the balance in the ETH vault. /// @param poolId Unique id of pool. /// @param member The member of the pool. /// @return totalReward Balance in ETH. diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 0feb81c1bf..922ff0f5d7 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -75,6 +75,7 @@ contract TestDelegatorRewards is /// @dev Set unfinalized rewards for a pool in the current epoch. function setUnfinalizedPoolReward( bytes32 poolId, + address payable operatorAddress, uint256 operatorReward, uint256 membersReward, uint256 membersStake @@ -87,7 +88,32 @@ contract TestDelegatorRewards is membersReward: membersReward, membersStake: membersStake }); + // Lazily initialize this pool. + _poolById[poolId].operator = operatorAddress; _setOperatorShare(poolId, operatorReward, membersReward); + _initGenesisCumulativeRewards(poolId); + } + + /// @dev Expose/wrap `_recordStakingPoolRewards`. + function recordStakingPoolRewards( + bytes32 poolId, + address payable operatorAddress, + uint256 operatorReward, + uint256 membersReward, + uint256 membersStake + ) + external + { + // Lazily initialize this pool. + _poolById[poolId].operator = operatorAddress; + _setOperatorShare(poolId, operatorReward, membersReward); + _initGenesisCumulativeRewards(poolId); + + _recordStakingPoolRewards( + poolId, + operatorReward + membersReward, + membersStake + ); } /// @dev Advance the epoch. @@ -207,23 +233,6 @@ contract TestDelegatorRewards is ); } - /// @dev Expose `_recordStakingPoolRewards`. - function recordStakingPoolRewards( - bytes32 poolId, - uint256 operatorReward, - uint256 membersReward, - uint256 membersStake - ) - external - { - _setOperatorShare(poolId, operatorReward, membersReward); - _recordStakingPoolRewards( - poolId, - operatorReward + membersReward, - membersStake - ); - } - /// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in /// the current epoch and emit a event, function _finalizePool(bytes32 poolId) diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index 4f6d6c45a7..94fafc67c9 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -3,7 +3,9 @@ import { constants, expect, filterLogsToArguments, + hexHash, hexRandom, + hexSlice, Numberish, } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; @@ -35,23 +37,28 @@ blockchainTests.resets('delegator unit rewards', env => { ); }); - interface RewardPoolMembersOpts { + interface RewardPoolOpts { poolId: string; + operator: string; membersReward: Numberish; operatorReward: Numberish; membersStake: Numberish; } - async function rewardPoolMembersAsync(opts?: Partial): Promise { + async function rewardPoolAsync(opts?: Partial): Promise { const _opts = { poolId: hexRandom(), + operator: constants.NULL_ADDRESS, membersReward: getRandomInteger(1, toBaseUnitAmount(100)), operatorReward: getRandomInteger(1, toBaseUnitAmount(100)), membersStake: getRandomInteger(1, toBaseUnitAmount(10)), ...opts, }; + // Generate a deterministic operator address based on the poolId. + _opts.operator = poolIdToOperator(_opts.poolId); await testContract.recordStakingPoolRewards.awaitTransactionSuccessAsync( _opts.poolId, + _opts.operator, new BigNumber(_opts.operatorReward), new BigNumber(_opts.membersReward), new BigNumber(_opts.membersStake), @@ -64,20 +71,24 @@ blockchainTests.resets('delegator unit rewards', env => { return _opts; } - interface SetUnfinalizedMembersRewardsOpts extends RewardPoolMembersOpts {} + interface SetUnfinalizedMembersRewardsOpts extends RewardPoolOpts {} async function setUnfinalizedPoolRewardAsync( opts?: Partial, ): Promise { const _opts = { poolId: hexRandom(), + operator: constants.NULL_ADDRESS, membersReward: getRandomInteger(1, toBaseUnitAmount(100)), operatorReward: getRandomInteger(1, toBaseUnitAmount(100)), membersStake: getRandomInteger(1, toBaseUnitAmount(10)), ...opts, }; + // Generate a deterministic operator address based on the poolId. + _opts.operator = poolIdToOperator(_opts.poolId); await testContract.setUnfinalizedPoolReward.awaitTransactionSuccessAsync( _opts.poolId, + _opts.operator, new BigNumber(_opts.operatorReward), new BigNumber(_opts.membersReward), new BigNumber(_opts.membersStake), @@ -90,6 +101,11 @@ blockchainTests.resets('delegator unit rewards', env => { return _opts; } + // Generates a deterministic operator address given a pool ID. + function poolIdToOperator(poolId: string): string { + return hexSlice(hexHash(poolId), -20); + } + // 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] { @@ -197,6 +213,10 @@ blockchainTests.resets('delegator unit rewards', env => { return testContract.computeRewardBalanceOfDelegator.callAsync(poolId, delegator); } + async function getOperatorRewardBalanceAsync(poolId: string): Promise { + return testContract.computeRewardBalanceOfOperator.callAsync(poolId); + } + async function touchStakeAsync(poolId: string, delegator: string): Promise> { return undelegateStakeAsync(poolId, delegator, 0); } @@ -225,9 +245,65 @@ blockchainTests.resets('delegator unit rewards', env => { .integerValue(BigNumber.ROUND_DOWN); } + describe('computeRewardBalanceOfOperator()', () => { + it('nothing in epoch 0', async () => { + const { poolId } = await rewardPoolAsync(); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + expect(operatorReward).to.bignumber.eq(0); + }); + + it('nothing in epoch 1', async () => { + await advanceEpochAsync(); + const { poolId } = await rewardPoolAsync(); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + expect(operatorReward).to.bignumber.eq(0); + }); + + it('nothing one epoch after rewards', async () => { + const { poolId } = await rewardPoolAsync(); + await advanceEpochAsync(); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + expect(operatorReward).to.bignumber.eq(0); + }); + + describe('with unfinalized rewards', () => { + it('something with unfinalized rewards', async () => { + const { poolId, operatorReward: expectedOperatorReward } = await setUnfinalizedPoolRewardAsync(); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + assertRoughlyEquals(operatorReward, expectedOperatorReward); + }); + + it('nothing for operator with 0% share', async () => { + // We define operator shares implicitly, so we set the operator + // reward to 0, which kind of makes this silly. + const { poolId } = await setUnfinalizedPoolRewardAsync({ operatorReward: 0 }); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + expect(operatorReward).to.bignumber.eq(0); + }); + + it('everything for operator with 100% share', async () => { + // We define operator shares implicitly, so we set the members + // reward to 0. + const { poolId, operatorReward: expectedOperatorReward } = await setUnfinalizedPoolRewardAsync({ + membersReward: 0, + }); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + assertRoughlyEquals(operatorReward, expectedOperatorReward); + }); + + it('nothing once rewards are finalized', async () => { + const { poolId } = await setUnfinalizedPoolRewardAsync(); + // Delegate some stake to trigger finalization. + await delegateStakeAsync(poolId); + const operatorReward = await getOperatorRewardBalanceAsync(poolId); + expect(operatorReward).to.bignumber.eq(0); + }); + }); + }); + describe('computeRewardBalanceOfDelegator()', () => { it('nothing in epoch 0 for delegator with no stake', async () => { - const { poolId } = await rewardPoolMembersAsync(); + const { poolId } = await rewardPoolAsync(); const delegator = randomAddress(); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); @@ -235,14 +311,14 @@ blockchainTests.resets('delegator unit rewards', env => { it('nothing in epoch 1 for delegator with no stake', async () => { await advanceEpochAsync(); // epoch 1 - const { poolId } = await rewardPoolMembersAsync(); + const { poolId } = await rewardPoolAsync(); const delegator = randomAddress(); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); it('nothing in epoch 0 for delegator staked in epoch 0', async () => { - const { poolId } = await rewardPoolMembersAsync(); + const { poolId } = await rewardPoolAsync(); // Assign active stake to pool in epoch 0, which is usuaslly not // possible due to delegating delays. const { delegator } = await delegateStakeNowAsync(poolId); @@ -252,7 +328,7 @@ blockchainTests.resets('delegator unit rewards', env => { it('nothing in epoch 1 for delegator delegating in epoch 1', async () => { await advanceEpochAsync(); // epoch 1 - const { poolId } = await rewardPoolMembersAsync(); + const { poolId } = await rewardPoolAsync(); const { delegator } = await delegateStakeAsync(poolId); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); @@ -263,7 +339,7 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) // rewards paid for stake in epoch 0. - await rewardPoolMembersAsync({ poolId, membersStake: stake }); + await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(0); }); @@ -274,7 +350,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -284,9 +360,9 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); assertRoughlyEquals(delegatorReward, BigNumber.sum(reward1, reward2)); }); @@ -297,7 +373,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { membersReward: reward, membersStake: rewardStake } = await rewardPoolMembersAsync({ + const { membersReward: reward, membersStake: rewardStake } = await rewardPoolAsync({ poolId, membersStake: new BigNumber(delegatorStake).times(2), }); @@ -312,7 +388,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); assertRoughlyEquals(deposit, reward); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); @@ -325,7 +401,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const { ethVaultDeposit: deposit } = await undelegateStakeAsync(poolId, delegator); assertRoughlyEquals(deposit, reward); await delegateStakeAsync(poolId, { delegator, stake }); @@ -339,13 +415,13 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - await rewardPoolMembersAsync({ poolId, membersStake: stake }); + await rewardPoolAsync({ poolId, membersStake: stake }); await undelegateStakeAsync(poolId, delegator); await delegateStakeAsync(poolId, { delegator, stake }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -358,7 +434,7 @@ blockchainTests.resets('delegator unit rewards', env => { // Pay rewards for epoch 0. await advanceEpochAsync(); // epoch 2 // Pay rewards for epoch 1. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(delegatorReward).to.bignumber.eq(reward); }); @@ -375,14 +451,14 @@ blockchainTests.resets('delegator unit rewards', env => { // receives 100% of rewards. const rewardStake = totalStake.times(2); // Pay rewards for epoch 1. - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); // add extra stake const { ethVaultDeposit: deposit } = await delegateStakeAsync(poolId, { delegator, stake: stake2 }); await advanceEpochAsync(); // epoch 3 (stake2 now active) // Pay rewards for epoch 2. await advanceEpochAsync(); // epoch 4 // Pay rewards for epoch 3. - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( computeDelegatorRewards(reward1, stake1, rewardStake), @@ -404,10 +480,10 @@ blockchainTests.resets('delegator unit rewards', env => { // receives 100% of rewards. const rewardStake = totalStake.times(2); // Pay rewards for epoch 1. - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); await advanceEpochAsync(); // epoch 3 // Pay rewards for epoch 2. - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); const expectedDelegatorReward = BigNumber.sum( computeDelegatorRewards(reward1, stake1, rewardStake), @@ -424,10 +500,10 @@ blockchainTests.resets('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stakeA }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: stakeA }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 (delegator A and B) - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: totalStake }); const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), @@ -447,11 +523,11 @@ blockchainTests.resets('delegator unit rewards', env => { const totalStake = BigNumber.sum(stakeA, stakeB); await advanceEpochAsync(); // epoch 2 (stake B now active) // rewards paid for stake in epoch 1 (delegator A only) - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: stakeA }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: stakeA }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 // rewards paid for stake in epoch 3 (delegator A and B) - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: totalStake }); const delegatorRewardA = await getDelegatorRewardBalanceAsync(poolId, delegatorA); const expectedDelegatorRewardA = BigNumber.sum( computeDelegatorRewards(reward1, stakeA, stakeA), @@ -469,13 +545,13 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1. - const { membersReward: reward1, membersStake: rewardStake1 } = await rewardPoolMembersAsync({ + const { membersReward: reward1, membersStake: rewardStake1 } = await rewardPoolAsync({ poolId, membersStake: new BigNumber(delegatorStake).times(2), }); await advanceEpochAsync(); // epoch 3 // rewards paid for stake in epoch 2 - const { membersReward: reward2, membersStake: rewardStake2 } = await rewardPoolMembersAsync({ + const { membersReward: reward2, membersStake: rewardStake2 } = await rewardPoolAsync({ poolId, membersStake: new BigNumber(delegatorStake).times(3), }); @@ -538,7 +614,7 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: prevReward } = await rewardPoolAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ poolId, @@ -554,7 +630,7 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: prevReward } = await rewardPoolAsync({ poolId, membersStake: stake }); await advanceEpochAsync(); // epoch 3 await advanceEpochAsync(); // epoch 4 const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ @@ -571,7 +647,7 @@ blockchainTests.resets('delegator unit rewards', env => { const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 await advanceEpochAsync(); // epoch 2 - const { membersReward: prevReward, membersStake: prevStake } = await rewardPoolMembersAsync({ + const { membersReward: prevReward, membersStake: prevStake } = await rewardPoolAsync({ poolId, membersStake: new BigNumber(stake).times(2), }); @@ -601,7 +677,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: stake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const { ethVaultDeposit: deposit } = await touchStakeAsync(poolId, delegator); const finalRewardBalance = await getDelegatorRewardBalanceAsync(poolId, delegator); expect(deposit).to.bignumber.eq(reward); @@ -621,7 +697,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (2 * stake now active) // reward for epoch 1, using 2 * stake so delegator should // only be entitled to a fraction of the rewards. - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); await advanceEpochAsync(); // epoch 2 // touch the stake one last time stakeResults.push(await touchStakeAsync(poolId, delegator)); @@ -640,18 +716,18 @@ blockchainTests.resets('delegator unit rewards', env => { const rewardStake = new BigNumber(stake).times(2); await advanceEpochAsync(); // epoch 1 (full stake now active) // reward for epoch 0 - await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + await rewardPoolAsync({ poolId, membersStake: rewardStake }); // unstake some const unstake = new BigNumber(stake).dividedToIntegerBy(2); stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake)); await advanceEpochAsync(); // epoch 2 (half active stake) // reward for epoch 1 - const { membersReward: reward1 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward1 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); // re-stake stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake: unstake })); await advanceEpochAsync(); // epoch 3 (full stake now active) // reward for epoch 2 - const { membersReward: reward2 } = await rewardPoolMembersAsync({ poolId, membersStake: rewardStake }); + const { membersReward: reward2 } = await rewardPoolAsync({ poolId, membersStake: rewardStake }); // touch the stake to claim rewards stakeResults.push(await touchStakeAsync(poolId, delegator)); const allDeposits = stakeResults.map(r => r.ethVaultDeposit); @@ -670,7 +746,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { membersReward: reward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); + const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: totalStake }); // delegator A will finalize and collect rewards by touching stake. const { ethVaultDeposit: depositA } = await touchStakeAsync(poolId, delegatorA); assertRoughlyEquals(depositA, computeDelegatorRewards(reward, stakeA, totalStake)); @@ -687,7 +763,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); + const { membersReward: prevReward } = await rewardPoolAsync({ poolId, membersStake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ @@ -711,7 +787,7 @@ blockchainTests.resets('delegator unit rewards', env => { await advanceEpochAsync(); // epoch 1 (stakes now active) await advanceEpochAsync(); // epoch 2 // rewards paid for stake in epoch 1 - const { membersReward: prevReward } = await rewardPoolMembersAsync({ poolId, membersStake: totalStake }); + const { membersReward: prevReward } = await rewardPoolAsync({ poolId, membersStake: totalStake }); await advanceEpochAsync(); // epoch 3 // unfinalized rewards for stake in epoch 2 const { membersReward: unfinalizedReward } = await setUnfinalizedPoolRewardAsync({ From 2bb9b9a8f7e2f07d3f66db4d9a2ec0f0bc255473 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 06:15:25 -0400 Subject: [PATCH 40/52] `@0x/contracts-test-utils`: Ran prettier. --- contracts/test-utils/src/hex_utils.ts | 2 +- contracts/test-utils/src/index.ts | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/contracts/test-utils/src/hex_utils.ts b/contracts/test-utils/src/hex_utils.ts index 007bbf9bd5..ad78e592e0 100644 --- a/contracts/test-utils/src/hex_utils.ts +++ b/contracts/test-utils/src/hex_utils.ts @@ -63,7 +63,7 @@ export function hexSlice(n: Numberish, start: number, end?: number): string { */ export function hexHash(n: Numberish): string { return ethUtil.bufferToHex(ethUtil.sha3(ethUtil.toBuffer(toHex(n)))); - } +} /** * Convert a string, a number, or a BigNumber into a hex string. diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index bdac824e23..4b13e555e1 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -28,16 +28,7 @@ export { bytes32Values, testCombinatoriallyWithReferenceFunc, uint256Values } fr export { TransactionFactory } from './transaction_factory'; export { MutatorContractFunction, TransactionHelper } from './transaction_helper'; export { testWithReferenceFuncAsync } from './test_with_reference'; -export { - hexConcat, - hexHash, - hexLeftPad, - hexInvert, - hexSlice, - hexRandom, - hexRightPad, - toHex, -} from './hex_utils'; +export { hexConcat, hexHash, hexLeftPad, hexInvert, hexSlice, hexRandom, hexRightPad, toHex } from './hex_utils'; export { BatchMatchOrder, ContractName, From 2eff21384089a65d8db3055b4cb41fe539d3f6ff Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 06:17:49 -0400 Subject: [PATCH 41/52] `@0x/contracts-staking`: Import `randomAddress()` instead of defining our own. --- contracts/staking/test/unit_tests/delegator_reward_test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index 94fafc67c9..712121fe54 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -7,6 +7,7 @@ import { hexRandom, hexSlice, Numberish, + randomAddress, } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { LogEntry } from 'ethereum-types'; @@ -230,10 +231,6 @@ blockchainTests.resets('delegator unit rewards', env => { }; } - function randomAddress(): string { - return hexRandom(constants.ADDRESS_LENGTH); - } - function computeDelegatorRewards( totalRewards: Numberish, delegatorStake: Numberish, From 14c4491b8ccb3ac3273419ce236884c05a4b9e1a Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 19 Sep 2019 15:53:43 -0400 Subject: [PATCH 42/52] `@0x/contracts-staking`: Add some extra finalizer tests and light refactorings. `@0x/contracts-staking`: Add finalization-related protocol fees unit tests. --- .../staking/contracts/test/TestFinalizer.sol | 7 + .../contracts/test/TestProtocolFees.sol | 6 + contracts/staking/test/protocol_fees.ts | 283 +++++++++++++----- .../staking/test/unit_tests/finalizer_test.ts | 29 +- 4 files changed, 242 insertions(+), 83 deletions(-) diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 16a4c9b110..5d8d0eea26 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -107,6 +107,13 @@ contract TestFinalizer is reward.membersStake) = _finalizePool(poolId); } + /// @dev Drain the balance of this contract. + function drainBalance() + external + { + address(0).transfer(address(this).balance); + } + /// @dev Get finalization-related state variables. function getFinalizationState() external diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index 29be592894..88b9baf29e 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -53,6 +53,12 @@ contract TestProtocolFees is poolJoinedByMakerAddress[makerAddress].confirmed = true; } + function advanceEpoch() + external + { + currentEpoch += 1; + } + function getWethAssetData() external pure returns (bytes memory) { return WETH_ASSET_DATA; } diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index 4a8fefba76..6bf9dab613 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -14,12 +14,14 @@ import * as _ from 'lodash'; import { artifacts, + IStakingEventsEvents, + IStakingEventsStakingPoolActivatedEventArgs, TestProtocolFeesContract, TestProtocolFeesERC20ProxyContract, TestProtocolFeesERC20ProxyTransferFromCalledEventArgs, } from '../src'; -import { getRandomPortion } from './utils/number_utils'; +import { getRandomInteger } from './utils/number_utils'; blockchainTests('Protocol Fee Unit Tests', env => { let ownerAddress: string; @@ -27,6 +29,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { let notExchangeAddress: string; let testContract: TestProtocolFeesContract; let wethAssetData: string; + let minimumStake: BigNumber; before(async () => { [ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync(); @@ -53,29 +56,31 @@ blockchainTests('Protocol Fee Unit Tests', env => { ); wethAssetData = await testContract.getWethAssetData.callAsync(); + minimumStake = (await testContract.getParams.callAsync())[2]; }); - interface CreatePoolOpts { + interface CreateTestPoolOpts { + poolId: string; operatorStake: Numberish; membersStake: Numberish; makers: string[]; } - async function createTestPoolAsync(opts: Partial): Promise { + async function createTestPoolAsync(opts?: Partial): Promise { const _opts = { - operatorStake: 0, - membersStake: 0, - makers: [], + poolId: hexRandom(), + operatorStake: getRandomInteger(minimumStake, '100e18'), + membersStake: getRandomInteger(minimumStake, '100e18'), + makers: _.times(2, () => randomAddress()), ...opts, }; - const poolId = hexRandom(); await testContract.createTestPool.awaitTransactionSuccessAsync( - poolId, + _opts.poolId, new BigNumber(_opts.operatorStake), new BigNumber(_opts.membersStake), _opts.makers, ); - return poolId; + return _opts; } blockchainTests.resets('payProtocolFee()', () => { @@ -83,11 +88,6 @@ blockchainTests('Protocol Fee Unit Tests', env => { const { ZERO_AMOUNT } = constants; const makerAddress = randomAddress(); const payerAddress = randomAddress(); - let minimumStake: BigNumber; - - before(async () => { - minimumStake = (await testContract.getParams.callAsync())[2]; - }); describe('forbidden actions', () => { it('should revert if called by a non-exchange', async () => { @@ -187,7 +187,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should credit pool if the maker is in a pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -200,7 +200,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should not credit the pool if maker is not in a pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -213,7 +213,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker should go to the same pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async () => { const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -258,7 +258,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -271,7 +271,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake }); const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -284,7 +284,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker should go to the same pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async () => { const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -302,7 +302,7 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('fees paid to the same maker in WETH then ETH should go to the same pool', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); const payAsync = async (inWETH: boolean) => { await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, @@ -322,60 +322,11 @@ 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 payAsync = async (_makerAddress: string) => { - await testContract.payProtocolFee.awaitTransactionSuccessAsync( - _makerAddress, - payerAddress, - DEFAULT_PROTOCOL_FEE_PAID, - { from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID }, - ); - }; - await payAsync(makerAddress); - await payAsync(otherMakerAddress); - const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2); - const poolFees = await getProtocolFeesAsync(poolId); - expect(poolFees).to.bignumber.eq(expectedTotalFees); - }); - - it('fees paid to makers in different pools go to their respective pools', async () => { - 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 payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => { - // prettier-ignore - await testContract.payProtocolFee.awaitTransactionSuccessAsync( - _makerAddress, - payerAddress, - _fee, - { from: exchangeAddress, value: _fee }, - ); - }; - await payAsync(poolId, makerAddress, fee); - await payAsync(otherPoolId, otherMakerAddress, otherFee); - const [poolFees, otherPoolFees] = await Promise.all([ - getProtocolFeesAsync(poolId), - getProtocolFeesAsync(otherPoolId), - ]); - expect(poolFees).to.bignumber.eq(fee); - expect(otherPoolFees).to.bignumber.eq(otherFee); - }); - }); - describe('Dust stake', () => { it('credits pools with stake > minimum', async () => { - const poolId = await createTestPoolAsync({ + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake.plus(1), + membersStake: 0, makers: [makerAddress], }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( @@ -389,7 +340,11 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('credits pools with stake == minimum', async () => { - const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] }); + const { poolId } = await createTestPoolAsync({ + operatorStake: minimumStake, + membersStake: 0, + makers: [makerAddress], + }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, constants.NULL_ADDRESS, @@ -401,8 +356,9 @@ blockchainTests('Protocol Fee Unit Tests', env => { }); it('does not credit pools with stake < minimum', async () => { - const poolId = await createTestPoolAsync({ + const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake.minus(1), + membersStake: 0, makers: [makerAddress], }); await testContract.payProtocolFee.awaitTransactionSuccessAsync( @@ -415,5 +371,184 @@ blockchainTests('Protocol Fee Unit Tests', env => { expect(feesCredited).to.bignumber.eq(0); }); }); + + blockchainTests.resets('Finalization', () => { + let membersStakeWeight: number; + + before(async () => { + membersStakeWeight = (await testContract.getParams.callAsync())[1]; + }); + + interface FinalizationState { + numActivePools: BigNumber; + totalFeesCollected: BigNumber; + totalWeightedStake: BigNumber; + } + + async function getFinalizationStateAsync(): Promise { + return { + numActivePools: await testContract.numActivePoolsThisEpoch.callAsync(), + totalFeesCollected: await testContract.totalFeesCollectedThisEpoch.callAsync(), + totalWeightedStake: await testContract.totalWeightedStakeThisEpoch.callAsync(), + }; + } + + interface PayToMakerResult { + poolActivatedEvents: IStakingEventsStakingPoolActivatedEventArgs[]; + fee: BigNumber; + } + + async function payToMakerAsync(poolMaker: string, fee?: Numberish): Promise { + const _fee = fee === undefined ? getRandomInteger(1, '1e18') : fee; + const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( + poolMaker, + payerAddress, + new BigNumber(_fee), + { from: exchangeAddress, value: _fee }, + ); + const events = filterLogsToArguments( + receipt.logs, + IStakingEventsEvents.StakingPoolActivated, + ); + return { + fee: new BigNumber(_fee), + poolActivatedEvents: events, + }; + } + + function toWeightedStake(operatorStake: Numberish, membersStake: Numberish): BigNumber { + return new BigNumber(membersStake) + .times(membersStakeWeight) + .dividedToIntegerBy(constants.PPM_DENOMINATOR) + .plus(operatorStake); + } + + it('no active pools to start', async () => { + const state = await getFinalizationStateAsync(); + expect(state.numActivePools).to.bignumber.eq(0); + expect(state.totalFeesCollected).to.bignumber.eq(0); + expect(state.totalWeightedStake).to.bignumber.eq(0); + }); + + it('pool is not registered to start', async () => { + const { poolId } = await createTestPoolAsync(); + const pool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId); + expect(pool.feesCollected).to.bignumber.eq(0); + expect(pool.membersStake).to.bignumber.eq(0); + expect(pool.weightedStake).to.bignumber.eq(0); + }); + + it('activates a active pool the first time it earns a fee', async () => { + const pool = await createTestPoolAsync(); + const { + poolId, + makers: [poolMaker], + } = pool; + const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker); + expect(poolActivatedEvents.length).to.eq(1); + expect(poolActivatedEvents[0].poolId).to.eq(poolId); + const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId); + const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake); + expect(actualPool.feesCollected).to.bignumber.eq(fee); + expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake); + expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake); + const state = await getFinalizationStateAsync(); + expect(state.numActivePools).to.bignumber.eq(1); + expect(state.totalFeesCollected).to.bignumber.eq(fee); + expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake); + }); + + it('only adds to the already activated pool in the same epoch', async () => { + const pool = await createTestPoolAsync(); + const { + poolId, + makers: [poolMaker], + } = pool; + const { fee: fee1 } = await payToMakerAsync(poolMaker); + const { fee: fee2, poolActivatedEvents } = await payToMakerAsync(poolMaker); + expect(poolActivatedEvents).to.deep.eq([]); + const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId); + const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake); + const fees = BigNumber.sum(fee1, fee2); + expect(actualPool.feesCollected).to.bignumber.eq(fees); + expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake); + expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake); + const state = await getFinalizationStateAsync(); + expect(state.numActivePools).to.bignumber.eq(1); + expect(state.totalFeesCollected).to.bignumber.eq(fees); + expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake); + }); + + it('can activate multiple pools in the same epoch', async () => { + const pools = await Promise.all(_.times(3, async () => createTestPoolAsync())); + let totalFees = new BigNumber(0); + let totalWeightedStake = new BigNumber(0); + for (const pool of pools) { + const { + poolId, + makers: [poolMaker], + } = pool; + const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker); + expect(poolActivatedEvents.length).to.eq(1); + expect(poolActivatedEvents[0].poolId).to.eq(poolId); + const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId); + const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake); + expect(actualPool.feesCollected).to.bignumber.eq(fee); + expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake); + expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake); + totalFees = totalFees.plus(fee); + totalWeightedStake = totalWeightedStake.plus(expectedWeightedStake); + } + const state = await getFinalizationStateAsync(); + expect(state.numActivePools).to.bignumber.eq(pools.length); + expect(state.totalFeesCollected).to.bignumber.eq(totalFees); + expect(state.totalWeightedStake).to.bignumber.eq(totalWeightedStake); + }); + + it('resets the pool after the epoch advances', async () => { + const pool = await createTestPoolAsync(); + const { + poolId, + makers: [poolMaker], + } = pool; + await payToMakerAsync(poolMaker); + await testContract.advanceEpoch.awaitTransactionSuccessAsync(); + const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId); + expect(actualPool.feesCollected).to.bignumber.eq(0); + expect(actualPool.membersStake).to.bignumber.eq(0); + expect(actualPool.weightedStake).to.bignumber.eq(0); + }); + + describe('Multiple makers', () => { + it('fees paid to different makers in the same pool go to that pool', async () => { + const { poolId, makers } = await createTestPoolAsync(); + const { fee: fee1 } = await payToMakerAsync(makers[0]); + const { fee: fee2 } = await payToMakerAsync(makers[1]); + const expectedTotalFees = BigNumber.sum(fee1, fee2); + const poolFees = await getProtocolFeesAsync(poolId); + expect(poolFees).to.bignumber.eq(expectedTotalFees); + }); + + it('fees paid to makers in different pools go to their respective pools', async () => { + const { + poolId: poolId1, + makers: [maker1], + } = await createTestPoolAsync(); + const { + poolId: poolId2, + makers: [maker2], + } = await createTestPoolAsync(); + const { fee: fee1 } = await payToMakerAsync(maker1); + const { fee: fee2 } = await payToMakerAsync(maker2); + const [poolFees, otherPoolFees] = await Promise.all([ + getProtocolFeesAsync(poolId1), + getProtocolFeesAsync(poolId2), + ]); + expect(poolFees).to.bignumber.eq(fee1); + expect(otherPoolFees).to.bignumber.eq(fee2); + }); + }); + }); }); }); +// tslint:disable: max-file-line-count diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 1555738e2c..5b551cdc3e 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -157,20 +157,19 @@ blockchainTests.resets('finalizer unit tests', env => { ): Promise { const currentEpoch = await getCurrentEpochAsync(); // Compute the expected rewards for each pool. - const poolRewards = await calculatePoolRewardsAsync(rewardsAvailable, activePools); + const poolsWithStake = activePools.filter(p => !new BigNumber(p.weightedStake).isZero()); + const poolRewards = await calculatePoolRewardsAsync(rewardsAvailable, poolsWithStake); const totalRewards = BigNumber.sum(...poolRewards); const rewardsRemaining = new BigNumber(rewardsAvailable).minus(totalRewards); - const nonZeroPoolRewards = poolRewards.filter(r => !r.isZero()); - const poolsWithNonZeroRewards = _.filter(activePools, (p, i) => !poolRewards[i].isZero()); const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(activePools, poolRewards); // Assert the `RewardsPaid` logs. const rewardsPaidEvents = getRewardsPaidEvents(finalizationLogs); - expect(rewardsPaidEvents.length).to.eq(poolsWithNonZeroRewards.length); + expect(rewardsPaidEvents.length).to.eq(poolsWithStake.length); for (const i of _.times(rewardsPaidEvents.length)) { const event = rewardsPaidEvents[i]; - const pool = poolsWithNonZeroRewards[i]; - const reward = nonZeroPoolRewards[i]; + const pool = poolsWithStake[i]; + const reward = poolRewards[i]; const [operatorReward, membersReward] = splitRewards(pool, reward); expect(event.epoch).to.bignumber.eq(currentEpoch); assertRoughlyEquals(event.operatorReward, operatorReward); @@ -179,10 +178,11 @@ blockchainTests.resets('finalizer unit tests', env => { // Assert the `RecordStakingPoolRewards` logs. const recordStakingPoolRewardsEvents = getRecordStakingPoolRewardsEvents(finalizationLogs); + expect(recordStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length); for (const i of _.times(recordStakingPoolRewardsEvents.length)) { const event = recordStakingPoolRewardsEvents[i]; - const pool = poolsWithNonZeroRewards[i]; - const reward = nonZeroPoolRewards[i]; + const pool = poolsWithStake[i]; + const reward = poolRewards[i]; expect(event.poolId).to.eq(pool.poolId); assertRoughlyEquals(event.totalReward, reward); assertRoughlyEquals(event.membersStake, pool.membersStake); @@ -191,7 +191,7 @@ blockchainTests.resets('finalizer unit tests', env => { // Assert the `DepositStakingPoolRewards` logs. // Make sure they all sum up to the totals. const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs); - { + if (depositStakingPoolRewardsEvents.length > 0) { const totalDepositedOperatorRewards = BigNumber.sum( ...depositStakingPoolRewardsEvents.map(e => e.operatorReward), ); @@ -406,6 +406,17 @@ blockchainTests.resets('finalizer unit tests', env => { return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs); }); + it('can finalize with no rewards', async () => { + await testContract.drainBalance.awaitTransactionSuccessAsync(); + const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + const receipts = await Promise.all( + pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), + ); + const allLogs = _.flatten(receipts.map(r => r.logs)); + return assertFinalizationLogsAndBalancesAsync(0, pools, allLogs); + }); + it('ignores a non-active pool', async () => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const nonActivePoolId = hexRandom(); From 3ad7728a0e92e41b917b86eab407716759877bca Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 21 Sep 2019 03:42:38 -0400 Subject: [PATCH 43/52] `@0x/contracts-staking`: Remove `IStructs.CumulativeRewardInfo`, etc. `@0x/contracts-staking`: Convert all rewards to WETH. `@0x/contracts-staking`: Style changes. `@0x/contracts-staking`: Address misc. review comments. `@0x/contracts-staking`: Make `LibFractions` scaling a separate step. --- .../staking/contracts/src/ReadOnlyProxy.sol | 2 - contracts/staking/contracts/src/Staking.sol | 6 +- .../staking/contracts/src/StakingProxy.sol | 10 +- .../contracts/src/fees/MixinExchangeFees.sol | 88 +++---- .../immutable/MixinDeploymentConstants.sol | 11 + .../contracts/src/immutable/MixinStorage.sol | 23 +- .../contracts/src/interfaces/IEthVault.sol | 22 +- .../interfaces/IStakingPoolRewardVault.sol | 30 +-- .../contracts/src/interfaces/IStructs.sol | 25 +- .../contracts/src/libs/LibCobbDouglas.sol | 8 +- .../contracts/src/stake/MixinStake.sol | 6 +- .../staking_pools/MixinCumulativeRewards.sol | 53 ++-- .../staking_pools/MixinStakingPoolRewards.sol | 151 ++++++----- .../contracts/src/sys/MixinAbstract.sol | 35 +-- .../contracts/src/sys/MixinFinalizer.sol | 237 +++++++----------- .../staking/contracts/src/sys/MixinParams.sol | 28 +++ .../staking/contracts/src/vaults/EthVault.sol | 54 ++-- .../src/vaults/StakingPoolRewardVault.sol | 32 +-- .../contracts/test/TestDelegatorRewards.sol | 16 +- .../staking/contracts/test/TestFinalizer.sol | 87 +------ .../staking/contracts/test/TestStaking.sol | 15 +- .../contracts/test/TestStorageLayout.sol | 14 +- .../staking/test/actors/finalizer_actor.ts | 10 +- .../test/cumulative_reward_tracking_test.ts | 2 +- contracts/staking/test/epoch_test.ts | 2 +- contracts/staking/test/rewards_test.ts | 14 +- contracts/staking/test/stake_test.ts | 2 +- .../test/unit_tests/delegator_reward_test.ts | 2 +- contracts/staking/test/utils/api_wrapper.ts | 39 ++- .../cumulative_reward_tracking_simulation.ts | 28 +-- .../utils/contracts/src/LibFractions.sol | 73 ++++-- 31 files changed, 503 insertions(+), 622 deletions(-) diff --git a/contracts/staking/contracts/src/ReadOnlyProxy.sol b/contracts/staking/contracts/src/ReadOnlyProxy.sol index 7aa9eba73d..4e3277c908 100644 --- a/contracts/staking/contracts/src/ReadOnlyProxy.sol +++ b/contracts/staking/contracts/src/ReadOnlyProxy.sol @@ -23,8 +23,6 @@ import "./libs/LibProxy.sol"; contract ReadOnlyProxy is - MixinConstants, - Ownable, MixinStorage { using LibProxy for address; diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index d0d4dd2945..f8810a1028 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -37,17 +37,17 @@ contract Staking is MixinStorage, MixinStakingPoolModifiers, MixinExchangeManager, - MixinParams, MixinScheduler, + MixinParams, MixinStakeStorage, MixinStakingPoolMakers, MixinStakeBalances, MixinCumulativeRewards, MixinStakingPoolRewards, + MixinFinalizer, MixinStakingPool, MixinStake, - MixinExchangeFees, - MixinFinalizer + MixinExchangeFees { // this contract can receive ETH // solhint-disable no-empty-blocks diff --git a/contracts/staking/contracts/src/StakingProxy.sol b/contracts/staking/contracts/src/StakingProxy.sol index 09ad728a3a..85045ed24d 100644 --- a/contracts/staking/contracts/src/StakingProxy.sol +++ b/contracts/staking/contracts/src/StakingProxy.sol @@ -75,7 +75,7 @@ contract StakingProxy is /// @dev Attach a staking contract; future calls will be delegated to the staking contract. /// Note that this is callable only by this contract's owner. - /// @param _stakingContract Address of staking contract. + /// @param _stakingContract Address of staking contract. /// @param _wethProxyAddress The address that can transfer WETH for fees. /// Use address in storage if NIL_ADDRESS is passed in. /// @param _ethVaultAddress Address of the EthVault contract. @@ -209,6 +209,14 @@ contract StakingProxy is )); } + // Minimum stake must be > 1 + if (minimumStake < 2) { + LibRichErrors.rrevert( + LibStakingRichErrors.InvalidParamValueError( + LibStakingRichErrors.InvalidParamValueErrorCode.InvalidMinimumPoolStake + )); + } + // ERC20Proxy and Vault contract addresses must always be initialized if (address(wethAssetProxy) == NIL_ADDRESS) { LibRichErrors.rrevert( diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index ffbf767363..9c64afc37f 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -20,6 +20,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; @@ -27,22 +28,11 @@ import "../libs/LibCobbDouglas.sol"; import "../immutable/MixinDeploymentConstants.sol"; import "../interfaces/IStructs.sol"; import "../stake/MixinStakeBalances.sol"; -import "../sys/MixinAbstract.sol"; +import "../sys/MixinFinalizer.sol"; import "../staking_pools/MixinStakingPool.sol"; import "./MixinExchangeManager.sol"; -/// @dev This mixin contains the logic for 0x protocol fees. -/// Protocol fees are sent by 0x exchanges every time there is a trade. -/// If the maker has associated their address with a pool (see -/// MixinStakingPool.sol), then the fee will be attributed to their pool. -/// At the end of an epoch the maker and their pool will receive a rebate -/// that is proportional to (i) the fee volume attributed to their pool -/// over the epoch, and (ii) the amount of stake provided by the maker and -/// their delegators. Note that delegated stake (see MixinStake) is -/// weighted less than stake provided by directly by the maker; this is a -/// disincentive for market makers to monopolize a single pool that they -/// all delegate to. contract MixinExchangeFees is IStakingEvents, MixinAbstract, @@ -58,6 +48,7 @@ contract MixinExchangeFees is MixinStakeBalances, MixinCumulativeRewards, MixinStakingPoolRewards, + MixinFinalizer, MixinStakingPool { using LibSafeMath for uint256; @@ -70,9 +61,7 @@ contract MixinExchangeFees is /// @param protocolFeePaid The protocol fee that should be paid. function payProtocolFee( address makerAddress, - // solhint-disable-next-line address payerAddress, - // solhint-disable-next-line uint256 protocolFeePaid ) external @@ -108,9 +97,9 @@ contract MixinExchangeFees is } // Look up the pool for this epoch. - uint256 currentEpoch = currentEpoch; + uint256 currentEpoch_ = currentEpoch; mapping (bytes32 => IStructs.ActivePool) storage activePoolsThisEpoch = - _getActivePoolsFromEpoch(currentEpoch); + _getActivePoolsFromEpoch(currentEpoch_); IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId]; // If the pool was previously inactive in this epoch, initialize it. @@ -128,32 +117,19 @@ contract MixinExchangeFees is // Emit an event so keepers know what pools to pass into // `finalize()`. - emit StakingPoolActivated(currentEpoch, poolId); + emit StakingPoolActivated(currentEpoch_, poolId); } // Credit the fees to the pool. pool.feesCollected = pool.feesCollected.safeAdd(protocolFeePaid); // Increase the total fees collected this epoch. - totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd( - protocolFeePaid - ); + totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd(protocolFeePaid); // Store the pool. activePoolsThisEpoch[poolId] = pool; } - /// @dev Returns the total amount of fees collected thus far, in the current - /// epoch. - /// @return _totalFeesCollectedThisEpoch Total fees collected this epoch. - function getTotalProtocolFeesThisEpoch() - external - view - returns (uint256 _totalFeesCollectedThisEpoch) - { - _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; - } - /// @dev Returns the total balance of this contract, including WETH. /// @return totalBalance Total balance. function getTotalBalance() @@ -162,8 +138,9 @@ contract MixinExchangeFees is returns (uint256 totalBalance) { totalBalance = address(this).balance.safeAdd( - IEtherToken(WETH_ADDRESS).balanceOf(address(this)) + IEtherToken(_getWETHAddress()).balanceOf(address(this)) ); + return totalBalance; } /// @dev Get information on an active staking pool in this epoch. @@ -175,6 +152,7 @@ contract MixinExchangeFees is returns (IStructs.ActivePool memory pool) { pool = _getActivePoolFromEpoch(currentEpoch, poolId); + return pool; } /// @dev Computes the members and weighted stake for a pool at the current @@ -184,8 +162,8 @@ contract MixinExchangeFees is /// @return membersStake Non-operator stake in the pool. /// @return weightedStake Weighted stake of the pool. function _computeMembersAndWeightedStake( - bytes32 poolId, - uint256 totalStake + bytes32 poolId, + uint256 totalStake ) private view @@ -197,10 +175,13 @@ contract MixinExchangeFees is ).currentEpochBalance; membersStake = totalStake.safeSub(operatorStake); weightedStake = operatorStake.safeAdd( - membersStake - .safeMul(rewardDelegatedStakeWeight) - .safeDiv(PPM_DENOMINATOR) + LibMath.getPartialAmountFloor( + rewardDelegatedStakeWeight, + PPM_DENOMINATOR, + membersStake + ) ); + return (membersStake, weightedStake); } /// @dev Checks that the protocol fee passed into `payProtocolFee()` is @@ -211,21 +192,24 @@ contract MixinExchangeFees is private view { - if (protocolFeePaid == 0 || - (msg.value != protocolFeePaid && msg.value != 0)) { - LibRichErrors.rrevert( - LibStakingRichErrors.InvalidProtocolFeePaymentError( - protocolFeePaid == 0 ? - LibStakingRichErrors - .ProtocolFeePaymentErrorCodes - .ZeroProtocolFeePaid : - LibStakingRichErrors - .ProtocolFeePaymentErrorCodes - .MismatchedFeeAndPayment, - protocolFeePaid, - msg.value - ) - ); + if (protocolFeePaid != 0) { + return; } + if (msg.value == protocolFeePaid || msg.value == 0) { + return; + } + LibRichErrors.rrevert( + LibStakingRichErrors.InvalidProtocolFeePaymentError( + protocolFeePaid == 0 ? + LibStakingRichErrors + .ProtocolFeePaymentErrorCodes + .ZeroProtocolFeePaid : + LibStakingRichErrors + .ProtocolFeePaymentErrorCodes + .MismatchedFeeAndPayment, + protocolFeePaid, + msg.value + ) + ); } } diff --git a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol index 53d716e318..3a61137588 100644 --- a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol +++ b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol @@ -58,4 +58,15 @@ contract MixinDeploymentConstants { LibRichErrors.rrevert(LibStakingRichErrors.InvalidWethAssetDataError()); } } + + /// @dev An overridable way to access the deployed WETH address. + /// Must be view to allow overrides to access state. + /// @return wethAddress The address of the configured WETH contract. + function _getWETHAddress() + internal + view + returns (address wethAddress) + { + return WETH_ADDRESS; + } } diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 2e247d1a1b..38445d7975 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -143,30 +143,13 @@ contract MixinStorage is /// @dev State information for each active pool in an epoch. /// In practice, we only store state for `currentEpoch % 2`. - mapping(uint256 => mapping(bytes32 => IStructs.ActivePool)) - internal - _activePoolsByEpoch; + mapping (uint256 => mapping (bytes32 => IStructs.ActivePool)) internal _activePoolsByEpoch; /// @dev Number of pools activated in the current epoch. uint256 public numActivePoolsThisEpoch; - /// @dev Rewards (ETH) available to the epoch being finalized (the previous - /// epoch). This is simply the balance of the contract at the end of - /// the epoch. - uint256 public unfinalizedRewardsAvailable; - - /// @dev The number of active pools in the last epoch that have yet to be - /// finalized through `finalizePools()`. - uint256 public unfinalizedPoolsRemaining; - - /// @dev The total fees collected for the epoch being finalized. - uint256 public unfinalizedTotalFeesCollected; - - /// @dev The total fees collected for the epoch being finalized. - uint256 public unfinalizedTotalWeightedStake; - - /// @dev How many rewards were paid at the end of finalization. - uint256 totalRewardsPaidLastEpoch; + /// @dev State for unfinalized rewards. + IStructs.UnfinalizedState public unfinalizedState; /// @dev Adds owner as an authorized address. constructor() diff --git a/contracts/staking/contracts/src/interfaces/IEthVault.sol b/contracts/staking/contracts/src/interfaces/IEthVault.sol index 225658768f..cf5e790402 100644 --- a/contracts/staking/contracts/src/interfaces/IEthVault.sol +++ b/contracts/staking/contracts/src/interfaces/IEthVault.sol @@ -42,29 +42,27 @@ interface IEthVault { uint256 amount ); - /// @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 - /// same transaction. + /// @dev Deposit an `amount` of WETH for `owner` into the vault. + /// The staking contract should have granted the vault an allowance + /// because it will pull the WETH via `transferFrom()`. /// Note that this is only callable by the staking contract. - /// @param owner Owner of the ETH. + /// @param owner Owner of the WETH. /// @param amount Amount of deposit. - function recordDepositFor(address owner, uint256 amount) + function depositFor(address owner, uint256 amount) external; - /// @dev Withdraw an `amount` of ETH to `msg.sender` from the vault. - /// Note that only the Staking contract can call this. - /// Note that this can only be called when *not* in Catostrophic Failure mode. - /// @param amount of ETH to withdraw. + /// @dev Withdraw an `amount` of WETH to `msg.sender` from the vault. + /// @param amount of WETH to withdraw. function withdraw(uint256 amount) external; - /// @dev Withdraw ALL ETH to `msg.sender` from the vault. + /// @dev Withdraw ALL WETH to `msg.sender` from the vault. function withdrawAll() external returns (uint256); - /// @dev Returns the balance in ETH of the `owner` - /// @return Balance in ETH. + /// @dev Returns the balance in WETH of the `owner` + /// @return Balance in WETH. function balanceOf(address owner) external view diff --git a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol index ed5745e5d6..15b71f2b49 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol @@ -23,40 +23,40 @@ pragma experimental ABIEncoderV2; /// @dev This vault manages staking pool rewards. interface IStakingPoolRewardVault { - /// @dev Emitted when Eth is deposited into the vault. + /// @dev Emitted when WETH is deposited into the vault. /// @param sender Address of sender (`msg.sender`). - /// @param poolId that owns of Eth. - /// @param amount of Eth deposited. + /// @param poolId that owns of WETH. + /// @param amount of WETH deposited. event EthDepositedIntoVault( address indexed sender, bytes32 indexed poolId, uint256 amount ); - /// @dev Emitted when rewards are transferred out fo the vault. + /// @dev Emitted when rewards are transferred out of the vault. /// @param poolId Unique Id of pool. /// @param to Address to send funds to. - /// @param amount Amount of ETH to transfer. + /// @param amount Amount of WETH to transfer. event PoolRewardTransferred( bytes32 indexed poolId, - address to, + address indexed to, 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. + /// @dev Deposit an amount of WETH for `poolId` into the vault. + /// The staking contract should have granted the vault an allowance + /// because it will pull the WETH via `transferFrom()`. /// Note that this is only callable by the staking contract. - /// @param poolId Pool that holds the ETH. + /// @param poolId Pool that holds the WETH. /// @param amount Amount of deposit. - function recordDepositFor(bytes32 poolId, uint256 amount) + function depositFor(bytes32 poolId, uint256 amount) external; - /// @dev Withdraw some amount in ETH from a pool. + /// @dev Withdraw some amount in WETH from a pool. /// Note that this is only callable by the staking contract. /// @param poolId Unique Id of pool. /// @param to Address to send funds to. - /// @param amount Amount of ETH to transfer. + /// @param amount Amount of WETH to transfer. function transfer( bytes32 poolId, address payable to, @@ -64,8 +64,8 @@ interface IStakingPoolRewardVault { ) external; - /// @dev Returns the balance in ETH of `poolId` - /// @return Balance in ETH. + /// @dev Returns the balance in WETH of `poolId` + /// @return Balance in WETH. function balanceOf(bytes32 poolId) external view diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index 4d738ea3dd..f53ac81efb 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -32,6 +32,23 @@ interface IStructs { uint256 membersStake; } + /// @dev Holds state for unfinalized epoch rewards. + /// @param rewardsAvailable Rewards (ETH) available to the epoch + /// being finalized (the previous epoch). This is simply the balance + /// of the contract at the end of the epoch. + /// @param poolsRemaining The number of active pools in the last + /// epoch that have yet to be finalized through `finalizePools()`. + /// @param totalFeesCollected The total fees collected for the epoch being finalized. + /// @param totalWeightedStake The total fees collected for the epoch being finalized. + /// @param totalRewardsFinalized Amount of rewards that have been paid during finalization. + struct UnfinalizedState { + uint256 rewardsAvailable; + uint256 poolsRemaining; + uint256 totalFeesCollected; + uint256 totalWeightedStake; + uint256 totalRewardsFinalized; + } + /// @dev Encapsulates a balance for the current and next epochs. /// Note that these balances may be stale if the current epoch /// is greater than `currentEpoch`. @@ -87,14 +104,6 @@ interface IStructs { 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; - } - /// @dev Holds the metadata for a staking pool. /// @param initialized True iff the balance struct is initialized. /// @param operator of the pool. diff --git a/contracts/staking/contracts/src/libs/LibCobbDouglas.sol b/contracts/staking/contracts/src/libs/LibCobbDouglas.sol index fa5c0e62d3..aec288fe1f 100644 --- a/contracts/staking/contracts/src/libs/LibCobbDouglas.sol +++ b/contracts/staking/contracts/src/libs/LibCobbDouglas.sol @@ -41,7 +41,7 @@ library LibCobbDouglas { /// @param alphaNumerator Numerator of `alpha` in the cobb-douglas function. /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas /// function. - /// @return ownerRewards Rewards owned to the staking pool. + /// @return rewards Rewards owed to the staking pool. function cobbDouglas( uint256 totalRewards, uint256 fees, @@ -53,12 +53,12 @@ library LibCobbDouglas { ) internal pure - returns (uint256 ownerRewards) + returns (uint256 rewards) { int256 feeRatio = LibFixedMath.toFixed(fees, totalFees); int256 stakeRatio = LibFixedMath.toFixed(stake, totalStake); if (feeRatio == 0 || stakeRatio == 0) { - return ownerRewards = 0; + return rewards = 0; } // The cobb-doublas function has the form: // `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)` @@ -93,6 +93,6 @@ library LibCobbDouglas { LibFixedMath.mul(stakeRatio, n) : LibFixedMath.div(stakeRatio, n); // Multiply the above with totalRewards. - ownerRewards = LibFixedMath.uintMul(n, totalRewards); + rewards = LibFixedMath.uintMul(n, totalRewards); } } diff --git a/contracts/staking/contracts/src/stake/MixinStake.sol b/contracts/staking/contracts/src/stake/MixinStake.sol index b68d080be5..412f224f7a 100644 --- a/contracts/staking/contracts/src/stake/MixinStake.sol +++ b/contracts/staking/contracts/src/stake/MixinStake.sol @@ -158,10 +158,8 @@ contract MixinStake is : 0; // execute move - IStructs.StoredBalance storage fromPtr = - _getBalancePtrFromStatus(owner, from.status); - IStructs.StoredBalance storage toPtr = - _getBalancePtrFromStatus(owner, to.status); + IStructs.StoredBalance storage fromPtr = _getBalancePtrFromStatus(owner, from.status); + IStructs.StoredBalance storage toPtr = _getBalancePtrFromStatus(owner, to.status); _moveStake(fromPtr, toPtr, amount); // update global total of stake in the statuses being moved between diff --git a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol index ef0b4be8b7..06fd6c5c8f 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol @@ -80,7 +80,7 @@ contract MixinCumulativeRewards is if (_cumulativeRewardsByPoolReferenceCounter[poolId][epoch] != 0) { return false; } - // Must not be the most recent CR. + // Must not be the most recently *stored* CR. if (_cumulativeRewardsByPoolLastStored[poolId] == epoch) { return false; } @@ -150,25 +150,6 @@ contract MixinCumulativeRewards is delete _cumulativeRewardsByPool[poolId][epoch]; } - /// @dev Returns info on most recent cumulative reward. - function _getMostRecentCumulativeRewardInfo(bytes32 poolId) - internal - view - 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. @@ -219,14 +200,13 @@ contract MixinCumulativeRewards is /// @dev Adds a dependency on a cumulative reward for a given epoch. /// @param poolId Unique Id of pool. /// @param epoch Epoch to remove dependency from. - /// @param mostRecentCumulativeRewardInfo Info for the most recent - /// cumulative reward (value and epoch) + /// @param mostRecentCumulativeReward The most recent cumulative reward. /// @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, + IStructs.Fraction memory mostRecentCumulativeReward, bool isDependent ) internal @@ -235,7 +215,7 @@ contract MixinCumulativeRewards is _addDependencyOnCumulativeReward( poolId, epoch, - mostRecentCumulativeRewardInfo + mostRecentCumulativeReward ); } else { _removeDependencyOnCumulativeReward( @@ -313,7 +293,7 @@ contract MixinCumulativeRewards is } // Compute reward - reward = LibFractions.scaleFractionalDifference( + reward = LibFractions.scaleDifference( endReward.numerator, endReward.denominator, beginReward.numerator, @@ -322,15 +302,26 @@ contract MixinCumulativeRewards is ); } + /// @dev Fetch the most recent cumulative reward entry for a pool. + /// @param poolId Unique ID of pool. + /// @return cumulativeReward The most recent cumulative reward `poolId`. + function _getMostRecentCumulativeReward(bytes32 poolId) + internal + view + returns (IStructs.Fraction memory cumulativeReward) + { + uint256 lastStoredEpoch = _cumulativeRewardsByPoolLastStored[poolId]; + return _cumulativeRewardsByPool[poolId][lastStoredEpoch]; + } + /// @dev Adds a dependency on a cumulative reward for a given epoch. /// @param poolId Unique Id of pool. /// @param epoch Epoch to remove dependency from. - /// @param mostRecentCumulativeRewardInfo Info on the most recent cumulative - /// reward. + /// @param mostRecentCumulativeReward The most recent cumulative reward. function _addDependencyOnCumulativeReward( bytes32 poolId, uint256 epoch, - IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo + IStructs.Fraction memory mostRecentCumulativeReward ) private { @@ -342,7 +333,7 @@ contract MixinCumulativeRewards is _trySetCumulativeReward( poolId, epoch, - mostRecentCumulativeRewardInfo.cumulativeReward + mostRecentCumulativeReward ); } @@ -356,10 +347,8 @@ contract MixinCumulativeRewards is private { // Remove dependency by decreasing reference counter - uint256 newReferenceCounter = - _cumulativeRewardsByPoolReferenceCounter[poolId][epoch].safeSub(1); _cumulativeRewardsByPoolReferenceCounter[poolId][epoch] = - newReferenceCounter; + _cumulativeRewardsByPoolReferenceCounter[poolId][epoch].safeSub(1); // Clear cumulative reward from state, if it is no longer needed _tryUnsetCumulativeReward(poolId, epoch); diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index b44a10a52b..d3cb07f36b 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -84,11 +84,12 @@ contract MixinStakingPoolRewards is (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); // Get the operators' portion. - (reward,) = _splitStakingPoolRewards( + (reward,) = _computeSplitStakingPoolRewards( pool.operatorShare, unfinalizedTotalRewards, unfinalizedMembersStake ); + return reward; } /// @dev Computes the reward balance in ETH of a specific member of a pool. @@ -106,12 +107,12 @@ contract MixinStakingPoolRewards is (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); // Get the members' portion. - (, uint256 unfinalizedMembersReward) = _splitStakingPoolRewards( + (, uint256 unfinalizedMembersReward) = _computeSplitStakingPoolRewards( pool.operatorShare, unfinalizedTotalRewards, unfinalizedMembersStake ); - reward = _computeRewardBalanceOfDelegator( + return _computeRewardBalanceOfDelegator( poolId, _loadUnsyncedBalance(_delegatedStakeToPoolByOwner[member][poolId]), currentEpoch, @@ -137,7 +138,7 @@ contract MixinStakingPoolRewards is ) internal { - // Rransfer any rewards from the transient pool vault to the eth vault; + // 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( @@ -165,17 +166,17 @@ contract MixinStakingPoolRewards is } /// @dev Handles a pool's reward at the current epoch. - /// This will compute the reward split and record the cumulative - /// reward, which is used to compute each delegator's portion of the - /// members' reward. It will NOT make any transfers to the eth or - /// reward vaults. That should be done with a separate call to - /// `_depositStakingPoolRewards()``. + /// This will split the reward between the operator and members, + /// depositing them into their respective vaults, and update the + /// accounting needed to allow members to withdraw their individual + /// rewards. /// @param poolId Unique Id of pool. /// @param reward received by the pool. /// @param membersStake the amount of non-operator delegated stake that /// will split the reward. - /// @return operatorReward - function _recordStakingPoolRewards( + /// @return operatorReward Portion of `reward` given to the pool operator. + /// @return membersReward Portion of `reward` given to the pool members. + function _depositStakingPoolRewards( bytes32 poolId, uint256 reward, uint256 membersStake @@ -186,71 +187,61 @@ contract MixinStakingPoolRewards is IStructs.Pool memory pool = _poolById[poolId]; // Split the reward between operator and members - (operatorReward, membersReward) = - _splitStakingPoolRewards(pool.operatorShare, reward, membersStake); - - // Record the operator's reward in the eth vault. - ethVault.recordDepositFor(pool.operator, operatorReward); + (operatorReward, membersReward) = _computeSplitStakingPoolRewards( + pool.operatorShare, + reward, + membersStake + ); + // Deposit the operator's reward in the eth vault. + ethVault.depositFor(pool.operator, operatorReward); if (membersReward == 0) { - return (operatorReward, membersReward); + return (0, 0); } - // Record the members reward in the reward vault. - rewardVault.recordDepositFor(poolId, membersReward); - - // Cache a storage pointer to the cumulative rewards for `poolId` - // indexed by epoch. - mapping (uint256 => IStructs.Fraction) - storage - _cumulativeRewardsByPoolPtr = _cumulativeRewardsByPool[poolId]; + // Deposit the members' reward in the reward vault. + rewardVault.depositFor(poolId, membersReward); // 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]; + IStructs.Fraction memory mostRecentCumulativeReward = + _getMostRecentCumulativeReward(poolId); // Compute new cumulative reward - (uint256 numerator, uint256 denominator) = LibFractions.addFractions( - mostRecentCumulativeRewards.numerator, - mostRecentCumulativeRewards.denominator, - membersReward, - membersStake - ); + IStructs.Fraction memory cumulativeReward; + (cumulativeReward.numerator, cumulativeReward.denominator) = + LibFractions.add( + mostRecentCumulativeReward.numerator, + mostRecentCumulativeReward.denominator, + membersReward, + membersStake + ); + // Normalize to prevent overflows. + (cumulativeReward.numerator, cumulativeReward.denominator) = + LibFractions.normalize( + cumulativeReward.numerator, + cumulativeReward.denominator + ); - // store cumulative rewards and set most recent + // Store cumulative rewards for this epoch. _forceSetCumulativeReward( poolId, currentEpoch, - IStructs.Fraction(numerator, denominator) + cumulativeReward ); - } - /// @dev Deposit rewards into the eth vault and reward vault for pool - /// operators and members rewards, respectively. This should be called - /// in tandem with `_recordStakingPoolRewards()`. We separate them - /// so we can bath deposits, because ETH transfers are expensive. - /// @param operatorReward Operator rewards. - /// @param membersReward Operator rewards. - function _depositStakingPoolRewards( - uint256 operatorReward, - uint256 membersReward - ) - internal - { - address(uint160(address(ethVault))).transfer(operatorReward); - address(uint160(address(rewardVault))).transfer(membersReward); + return (operatorReward, membersReward); } - /// @dev Split a pool reward between the operator and members based on - /// the `operatorShare` and `membersStake`. + /// @dev Compute the split of 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( + /// @return operatorReward Portion of `totalReward` given to the pool operator. + /// @return membersReward Portion of `totalReward` given to the pool members. + function _computeSplitStakingPoolRewards( uint32 operatorShare, uint256 totalReward, uint256 membersStake @@ -269,6 +260,7 @@ contract MixinStakingPoolRewards is ); membersReward = totalReward - operatorReward; } + return (operatorReward, membersReward); } /// @dev Transfers a delegators accumulated rewards from the transient pool @@ -286,7 +278,7 @@ contract MixinStakingPoolRewards is private { // Ensure the pool is finalized. - _finalizePool(poolId); + finalizePool(poolId); // Compute balance owed to delegator uint256 balance = _computeRewardBalanceOfDelegator( @@ -302,13 +294,10 @@ contract MixinStakingPoolRewards is return; } - // Transfer from transient Reward Pool vault to ETH Vault - ethVault.recordDepositFor(member, balance); - rewardVault.transfer( - poolId, - address(uint160(address(ethVault))), - balance - ); + // Transfer from RewardVault to this contract. + rewardVault.transfer(poolId, address(uint160(address(this))), balance); + // Transfer to EthVault. + ethVault.depositFor(member, balance); } /// @dev Computes the reward balance in ETH of a specific member of a pool. @@ -333,14 +322,14 @@ contract MixinStakingPoolRewards is // There can be no rewards in epoch 0 because there is no delegated // stake. if (currentEpoch == 0) { - return reward = 0; + return 0; } // There can be no rewards if the last epoch when stake was synced is // equal to the current epoch, because all prior rewards, including // rewards finalized this epoch have been claimed. if (unsyncedStake.currentEpoch == currentEpoch) { - return reward = 0; + return 0; } // If there are unfinalized rewards this epoch, compute the member's @@ -348,13 +337,15 @@ contract MixinStakingPoolRewards is if (unfinalizedMembersReward != 0 && unfinalizedMembersStake != 0) { // Unfinalized rewards are always earned from stake in // the prior epoch so we want the stake at `currentEpoch-1`. - uint256 _stake = unsyncedStake.currentEpoch >= currentEpoch - 1 ? + uint256 _stake = unsyncedStake.currentEpoch >= currentEpoch.safeSub(1) ? unsyncedStake.currentEpochBalance : unsyncedStake.nextEpochBalance; if (_stake != 0) { - reward = _stake - .safeMul(unfinalizedMembersReward) - .safeDiv(unfinalizedMembersStake); + reward = LibMath.getPartialAmountFloor( + unfinalizedMembersReward, + unfinalizedMembersStake, + _stake + ); } } @@ -365,32 +356,30 @@ contract MixinStakingPoolRewards is // If the stake has been touched since the last reward epoch, // it has already been claimed. if (unsyncedStake.currentEpoch >= lastRewardEpoch) { - return reward; + return 0; } // From here we know: `unsyncedStake.currentEpoch < currentEpoch > 0`. - if (unsyncedStake.currentEpoch >= lastRewardEpoch) { - return reward; - } - + uint256 nextStakeEpoch = uint256(unsyncedStake.currentEpoch).safeAdd(1); reward = reward.safeAdd( _computeMemberRewardOverInterval( poolId, unsyncedStake.currentEpochBalance, unsyncedStake.currentEpoch, - unsyncedStake.currentEpoch + 1 + nextStakeEpoch ) ); - if (unsyncedStake.currentEpoch + 1 < lastRewardEpoch) { + if (nextStakeEpoch < lastRewardEpoch) { reward = reward.safeAdd( _computeMemberRewardOverInterval( poolId, unsyncedStake.nextEpochBalance, - unsyncedStake.currentEpoch + 1, + nextStakeEpoch, lastRewardEpoch ) ); } + return reward; } /// @dev Adds or removes cumulative reward dependencies for a delegator. @@ -415,8 +404,8 @@ contract MixinStakingPoolRewards is // Get the most recent cumulative reward, which will serve as a // reference point when updating dependencies - IStructs.CumulativeRewardInfo memory mostRecentCumulativeRewardInfo = - _getMostRecentCumulativeRewardInfo(poolId); + IStructs.Fraction memory mostRecentCumulativeReward = + _getMostRecentCumulativeReward(poolId); // Record dependency on current epoch. if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 @@ -425,7 +414,7 @@ contract MixinStakingPoolRewards is _addOrRemoveDependencyOnCumulativeReward( poolId, _delegatedStakeToPoolByOwner.currentEpoch, - mostRecentCumulativeRewardInfo, + mostRecentCumulativeReward, isDependent ); } @@ -435,7 +424,7 @@ contract MixinStakingPoolRewards is _addOrRemoveDependencyOnCumulativeReward( poolId, uint256(_delegatedStakeToPoolByOwner.currentEpoch).safeAdd(1), - mostRecentCumulativeRewardInfo, + mostRecentCumulativeReward, isDependent ); } diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol index 987b695b74..5be73219cf 100644 --- a/contracts/staking/contracts/src/sys/MixinAbstract.sol +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -29,54 +29,29 @@ contract MixinAbstract { /// @dev Computes the reward owed to a pool during finalization. /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. - /// @return operatorReward The reward owed to the pool operator. + /// @return totalReward The total reward owed to a pool. /// @return membersStake The total stake for all non-operator members in /// this pool. function _getUnfinalizedPoolRewards(bytes32 poolId) internal view returns ( - uint256 reward, + uint256 totalReward, uint256 membersStake ); - /// @dev Get an active pool from an epoch by its ID. - /// @param epoch The epoch the pool was/will be active in. - /// @param poolId The ID of the pool. - /// @return pool The pool with ID `poolId` that was active in `epoch`. - function _getActivePoolFromEpoch( - uint256 epoch, - bytes32 poolId - ) - internal - view - returns (IStructs.ActivePool memory pool); - - /// @dev Get a mapping of active pools from an epoch. - /// This uses the formula `epoch % 2` as the epoch index in order - /// to reuse state, because we only need to remember, at most, two - /// epochs at once. - /// @return activePools The pools that were active in `epoch`. - function _getActivePoolsFromEpoch( - uint256 epoch - ) - internal - view - returns (mapping (bytes32 => IStructs.ActivePool) storage activePools); - /// @dev Instantly finalizes a single pool that was active in the previous /// epoch, crediting it rewards and sending those rewards to the reward /// and eth vault. This can be called by internal functions that need /// to finalize a pool immediately. Does nothing if the pool is already - /// finalized. Does nothing if the pool was not active or was already - /// finalized. + /// finalized or was not active in the previous epoch. /// @param poolId The pool ID to finalize. /// @return operatorReward The reward credited to the pool operator. /// @return membersReward The reward credited to the pool members. /// @return membersStake The total stake for all non-operator members in /// this pool. - function _finalizePool(bytes32 poolId) - internal + function finalizePool(bytes32 poolId) + public returns ( uint256 operatorReward, uint256 membersReward, diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index b58fccf14a..58557eb7bc 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -58,43 +58,41 @@ contract MixinFinalizer is /// Throws if not enough time has passed between epochs or if the /// previous epoch was not fully finalized. /// If there were no active pools in the closing epoch, the epoch - /// will be instantly finalized here. Otherwise, `finalizePools()` + /// will be instantly finalized here. Otherwise, `finalizePool()` /// should be called on each active pool afterwards. - /// @return _unfinalizedPoolsRemaining The number of unfinalized pools. + /// @return poolsRemaining The number of unfinalized pools. function endEpoch() external - returns (uint256 _unfinalizedPoolsRemaining) + returns (uint256 poolsRemaining) { uint256 closingEpoch = currentEpoch; // Make sure the previous epoch has been fully finalized. - if (unfinalizedPoolsRemaining != 0) { + if (poolsRemaining != 0) { LibRichErrors.rrevert( LibStakingRichErrors.PreviousEpochNotFinalizedError( closingEpoch.safeSub(1), - unfinalizedPoolsRemaining + poolsRemaining ) ); } - // Unrwap any WETH protocol fees. - _unwrapWETH(); - - // Populate finalization state. - unfinalizedPoolsRemaining = - _unfinalizedPoolsRemaining = numActivePoolsThisEpoch; - unfinalizedRewardsAvailable = address(this).balance; - unfinalizedTotalFeesCollected = totalFeesCollectedThisEpoch; - unfinalizedTotalWeightedStake = totalWeightedStakeThisEpoch; - totalRewardsPaidLastEpoch = 0; + // Set up unfinalized state. + IStructs.UnfinalizedState memory state; + state.rewardsAvailable = _wrapBalanceToWETHAndGetBalance(); + state.poolsRemaining = poolsRemaining = numActivePoolsThisEpoch; + state.totalFeesCollected = totalFeesCollectedThisEpoch; + state.totalWeightedStake = totalWeightedStakeThisEpoch; + state.totalRewardsFinalized = 0; + unfinalizedState = state; // Emit an event. emit EpochEnded( closingEpoch, - unfinalizedPoolsRemaining, - unfinalizedRewardsAvailable, - unfinalizedTotalFeesCollected, - unfinalizedTotalWeightedStake + state.poolsRemaining, + state.rewardsAvailable, + state.totalFeesCollected, + state.totalWeightedStake ); // Reset current epoch state. @@ -106,91 +104,8 @@ contract MixinFinalizer is _goToNextEpoch(); // If there were no active pools, the epoch is already finalized. - if (unfinalizedPoolsRemaining == 0) { - emit EpochFinalized(closingEpoch, 0, unfinalizedRewardsAvailable); - } - } - - /// @dev Finalizes pools that were active in the previous epoch, paying out - /// rewards to the reward and eth vault. Keepers should call this - /// function repeatedly until all active pools that were emitted in in - /// a `StakingPoolActivated` in the prior epoch have been finalized. - /// Pools that have already been finalized will be silently ignored. - /// We deliberately try not to revert here in case multiple parties - /// are finalizing pools. - /// @param poolIds List of active pool IDs to finalize. - /// @return _unfinalizedPoolsRemaining The number of unfinalized pools left. - function finalizePools(bytes32[] calldata poolIds) - external - returns (uint256 _unfinalizedPoolsRemaining) - { - uint256 epoch = currentEpoch; - // There are no pools to finalize at epoch 0. - if (epoch == 0) { - return _unfinalizedPoolsRemaining = 0; - } - - uint256 poolsRemaining = unfinalizedPoolsRemaining; - // If there are no more unfinalized pools remaining, there's nothing - // to do. if (poolsRemaining == 0) { - return _unfinalizedPoolsRemaining = 0; - } - - // Pointer to the active pools in the last epoch. - mapping(bytes32 => IStructs.ActivePool) storage activePools = - _getActivePoolsFromEpoch(epoch - 1); - uint256 numPoolIds = poolIds.length; - uint256 rewardsPaid = 0; - uint256 totalOperatorRewardsPaid = 0; - uint256 totalMembersRewardsPaid = 0; - - for (uint256 i = 0; i != numPoolIds && poolsRemaining != 0; ++i) - { - bytes32 poolId = poolIds[i]; - IStructs.ActivePool memory pool = activePools[poolId]; - - // Ignore pools that aren't active. - if (pool.feesCollected == 0) { - continue; - } - - (uint256 operatorReward, uint256 membersReward) = - _creditRewardsToPool(epoch, poolId, pool, rewardsPaid); - - totalOperatorRewardsPaid = - totalOperatorRewardsPaid.safeAdd(operatorReward); - totalMembersRewardsPaid = - totalMembersRewardsPaid.safeAdd(membersReward); - - rewardsPaid = rewardsPaid - .safeAdd(operatorReward) - .safeAdd(membersReward); - - // Decrease the number of unfinalized pools left. - poolsRemaining = poolsRemaining.safeSub(1); - } - - // Update finalization states. - if (rewardsPaid != 0) { - totalRewardsPaidLastEpoch = - totalRewardsPaidLastEpoch.safeAdd(rewardsPaid); - } - unfinalizedPoolsRemaining = _unfinalizedPoolsRemaining = poolsRemaining; - - // If there are no more unfinalized pools remaining, the epoch is - // finalized. - if (poolsRemaining == 0) { - emit EpochFinalized( - epoch - 1, - totalRewardsPaidLastEpoch, - unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) - ); - } - - // Deposit all the rewards at once. - if (rewardsPaid != 0) { - _depositStakingPoolRewards(totalOperatorRewardsPaid, totalMembersRewardsPaid); + emit EpochFinalized(closingEpoch, 0, state.rewardsAvailable); } } @@ -198,15 +113,14 @@ contract MixinFinalizer is /// epoch, crediting it rewards and sending those rewards to the reward /// and eth vault. This can be called by internal functions that need /// to finalize a pool immediately. Does nothing if the pool is already - /// finalized. Does nothing if the pool was not active or was already - /// finalized. + /// finalized or was not active in the previous epoch. /// @param poolId The pool ID to finalize. /// @return operatorReward The reward credited to the pool operator. /// @return membersReward The reward credited to the pool members. /// @return membersStake The total stake for all non-operator members in /// this pool. - function _finalizePool(bytes32 poolId) - internal + function finalizePool(bytes32 poolId) + public returns ( uint256 operatorReward, uint256 membersReward, @@ -216,46 +130,62 @@ contract MixinFinalizer is uint256 epoch = currentEpoch; // There are no pools to finalize at epoch 0. if (epoch == 0) { - return (operatorReward, membersReward, membersStake); + return (0, 0, 0); } + uint256 prevEpoch = epoch - 1; - IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(epoch - 1, poolId); - // Do nothing if the pool was not active (has no fees). + // Load the finalization state into memory. + IStructs.UnfinalizedState memory state = unfinalizedState; + + // If there are no more unfinalized pools remaining, there's nothing + // to do. + if (state.poolsRemaining == 0) { + return (0, 0, 0); + } + + IStructs.ActivePool memory pool = _getActivePoolFromEpoch(prevEpoch, poolId); + // Do nothing if the pool was not active or already finalized (has no fees). if (pool.feesCollected == 0) { return (operatorReward, membersReward, membersStake); } - (operatorReward, membersReward) = - _creditRewardsToPool(epoch, poolId, pool, 0); + (operatorReward, membersReward) = _creditRewardsToPool( + epoch, + poolId, + pool, + state + ); uint256 totalReward = operatorReward.safeAdd(membersReward); if (totalReward > 0) { - totalRewardsPaidLastEpoch = - totalRewardsPaidLastEpoch.safeAdd(totalReward); - _depositStakingPoolRewards(operatorReward, membersReward); + // Increase `totalRewardsFinalized`. + unfinalizedState.totalRewardsFinalized = + state.totalRewardsFinalized = + state.totalRewardsFinalized.safeAdd(totalReward); } // Decrease the number of unfinalized pools left. - uint256 poolsRemaining = unfinalizedPoolsRemaining; - unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1); + unfinalizedState.poolsRemaining = + state.poolsRemaining = + state.poolsRemaining.safeSub(1); // If there are no more unfinalized pools remaining, the epoch is // finalized. - if (poolsRemaining == 0) { + if (state.poolsRemaining == 0) { emit EpochFinalized( - epoch - 1, - totalRewardsPaidLastEpoch, - unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch) + prevEpoch, + state.totalRewardsFinalized, + state.rewardsAvailable.safeSub(state.totalRewardsFinalized) ); } membersStake = pool.membersStake; + return (operatorReward, membersReward, membersStake); } /// @dev Computes the reward owed to a pool during finalization. /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. - /// @return operatorReward The reward owed to the pool operator. + /// @return totalReward The total reward owed to a pool. /// @return membersStake The total stake for all non-operator members in /// this pool. function _getUnfinalizedPoolRewards(bytes32 poolId) @@ -269,11 +199,10 @@ contract MixinFinalizer is uint256 epoch = currentEpoch; // There are no pools to finalize at epoch 0. if (epoch == 0) { - return (reward, membersStake); + return (0, 0); } - IStructs.ActivePool memory pool = - _getActivePoolFromEpoch(epoch - 1, poolId); - reward = _getUnfinalizedPoolRewards(pool, 0); + IStructs.ActivePool memory pool = _getActivePoolFromEpoch(epoch - 1, poolId); + reward = _getUnfinalizedPoolRewards(pool, unfinalizedState); membersStake = pool.membersStake; } @@ -290,6 +219,7 @@ contract MixinFinalizer is returns (IStructs.ActivePool memory pool) { pool = _getActivePoolsFromEpoch(epoch)[poolId]; + return pool; } /// @dev Get a mapping of active pools from an epoch. @@ -305,26 +235,32 @@ contract MixinFinalizer is returns (mapping (bytes32 => IStructs.ActivePool) storage activePools) { activePools = _activePoolsByEpoch[epoch % 2]; + return activePools; } - /// @dev Converts the entire WETH balance of the contract into ETH. - function _unwrapWETH() + /// @dev Converts the entire ETH balance of the contract into WETH and + /// returns the total WETH balance of this contract. + /// @return The WETH balance of this contract. + function _wrapBalanceToWETHAndGetBalance() internal + returns (uint256 balance) { - uint256 wethBalance = IEtherToken(WETH_ADDRESS) - .balanceOf(address(this)); - if (wethBalance != 0) { - IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + IEtherToken weth = IEtherToken(_getWETHAddress()); + uint256 ethBalance = address(this).balance; + if (ethBalance != 0) { + weth.deposit.value((address(this).balance)); } + balance = weth.balanceOf(address(this)); + return balance; } /// @dev Computes the reward owed to a pool during finalization. /// @param pool The active pool. - /// @param unpaidRewards Rewards that have been credited but not finalized. + /// @param state The current state of finalization. /// @return rewards Unfinalized rewards for this pool. function _getUnfinalizedPoolRewards( IStructs.ActivePool memory pool, - uint256 unpaidRewards + IStructs.UnfinalizedState memory state ) private view @@ -333,27 +269,25 @@ contract MixinFinalizer is // There can't be any rewards if the pool was active or if it has // no stake. if (pool.feesCollected == 0) { - return rewards = 0; + return rewards; } - uint256 unfinalizedRewardsAvailable_ = unfinalizedRewardsAvailable; // Use the cobb-douglas function to compute the total reward. - rewards = LibCobbDouglas._cobbDouglas( - unfinalizedRewardsAvailable_, + rewards = LibCobbDouglas.cobbDouglas( + state.rewardsAvailable, pool.feesCollected, - unfinalizedTotalFeesCollected, + state.totalFeesCollected, pool.weightedStake, - unfinalizedTotalWeightedStake, + state.totalWeightedStake, cobbDouglasAlphaNumerator, cobbDouglasAlphaDenominator ); // Clip the reward to always be under - // `unfinalizedRewardsAvailable - totalRewardsPaid - unpaidRewards`, + // `rewardsAvailable - totalRewardsPaid`, // in case cobb-douglas overflows, which should be unlikely. - uint256 rewardsRemaining = unfinalizedRewardsAvailable_ - .safeSub(totalRewardsPaidLastEpoch) - .safeSub(unpaidRewards); + uint256 rewardsRemaining = + state.rewardsAvailable.safeSub(state.totalRewardsFinalized); if (rewardsRemaining < rewards) { rewards = rewardsRemaining; } @@ -364,30 +298,29 @@ contract MixinFinalizer is /// @param epoch The current epoch. /// @param poolId The pool ID to finalize. /// @param pool The active pool to finalize. - /// @param unpaidRewards Rewards that have been credited but not finalized. - /// @return rewards Rewards. + /// @param state The current state of finalization. /// @return operatorReward The reward credited to the pool operator. /// @return membersReward The reward credited to the pool members. function _creditRewardsToPool( uint256 epoch, bytes32 poolId, IStructs.ActivePool memory pool, - uint256 unpaidRewards + IStructs.UnfinalizedState memory state ) private returns (uint256 operatorReward, uint256 membersReward) { // Clear the pool state so we don't finalize it again, and to recoup // some gas. - delete _getActivePoolsFromEpoch(epoch - 1)[poolId]; + delete _getActivePoolsFromEpoch(epoch.safeSub(1))[poolId]; // Compute the rewards. - uint256 rewards = _getUnfinalizedPoolRewards(pool, unpaidRewards); + uint256 rewards = _getUnfinalizedPoolRewards(pool, state); - // Credit the pool. + // Pay the pool. // Note that we credit at the CURRENT epoch even though these rewards // were earned in the previous epoch. - (operatorReward, membersReward) = _recordStakingPoolRewards( + (operatorReward, membersReward) = _depositStakingPoolRewards( poolId, rewards, pool.membersStake diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index cf8c14ab51..2c0b7b29e6 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -18,9 +18,11 @@ pragma solidity ^0.5.9; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; import "../immutable/MixinStorage.sol"; +import "../immutable/MixinDeploymentConstants.sol"; import "../interfaces/IStakingEvents.sol"; import "../interfaces/IEthVault.sol"; import "../interfaces/IStakingPoolRewardVault.sol"; @@ -31,6 +33,7 @@ import "../libs/LibStakingRichErrors.sol"; contract MixinParams is IStakingEvents, MixinConstants, + MixinDeploymentConstants, Ownable, MixinStorage { @@ -194,6 +197,11 @@ contract MixinParams is ) private { + _transferWETHAllownces( + [address(ethVault), address(rewardVault)], + [_ethVaultAddress, _rewardVaultAddress] + ); + epochDurationInSeconds = _epochDurationInSeconds; rewardDelegatedStakeWeight = _rewardDelegatedStakeWeight; minimumPoolStake = _minimumPoolStake; @@ -218,4 +226,24 @@ contract MixinParams is _zrxVaultAddress ); } + + /// @dev Rescind the WETH allowance for `oldSpenders` and grant `newSpenders` + /// an unlimited allowance. + /// @param oldSpenders Addresses to remove allowance from. + /// @param newSpenders Addresses to grant allowance to. + function _transferWETHAllownces( + address[2] memory oldSpenders, + address[2] memory newSpenders + ) + private + { + IEtherToken weth = IEtherToken(_getWETHAddress()); + // Grant new allowances. + for (uint256 i = 0; i < oldSpenders.length; i++) { + // Rescind old allowance. + weth.approve(oldSpenders[i], 0); + // Grant new allowance. + weth.approve(newSpenders[i], uint256(-1)); + } + } } diff --git a/contracts/staking/contracts/src/vaults/EthVault.sol b/contracts/staking/contracts/src/vaults/EthVault.sol index fb31f44ed7..56c05ae11b 100644 --- a/contracts/staking/contracts/src/vaults/EthVault.sol +++ b/contracts/staking/contracts/src/vaults/EthVault.sol @@ -18,52 +18,52 @@ pragma solidity ^0.5.9; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../interfaces/IEthVault.sol"; +import "../immutable/MixinDeploymentConstants.sol"; import "./MixinVaultCore.sol"; -/// @dev This vault manages ETH. +/// @dev This vault manages WETH. contract EthVault is IEthVault, IVaultCore, + MixinDeploymentConstants, Ownable, MixinVaultCore { using LibSafeMath for uint256; - // mapping from Owner to ETH balance + // mapping from Owner to WETH balance mapping (address => uint256) internal _balances; - // solhint-disable no-empty-blocks - /// @dev Payable fallback for bulk-deposits. - 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 - /// same transaction. + /// @dev Deposit an `amount` of WETH for `owner` into the vault. + /// The staking contract should have granted the vault an allowance + /// because it will pull the WETH via `transferFrom()`. /// Note that this is only callable by the staking contract. - /// @param owner Owner of the ETH. + /// @param owner Owner of the WETH. /// @param amount Amount of deposit. - function recordDepositFor(address owner, uint256 amount) + function depositFor(address owner, uint256 amount) external onlyStakingProxy { + // Transfer WETH from the staking contract into this contract. + IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); + // Credit the owner. _balances[owner] = _balances[owner].safeAdd(amount); emit EthDepositedIntoVault(msg.sender, owner, amount); } - /// @dev Withdraw an `amount` of ETH to `msg.sender` from the vault. - /// Note that only the Staking contract can call this. - /// Note that this can only be called when *not* in Catostrophic Failure mode. - /// @param amount of ETH to withdraw. + /// @dev Withdraw an `amount` of WETH to `msg.sender` from the vault. + /// @param amount of WETH to withdraw. function withdraw(uint256 amount) external { _withdrawFrom(msg.sender, amount); } - /// @dev Withdraw ALL ETH to `msg.sender` from the vault. + /// @dev Withdraw ALL WETH to `msg.sender` from the vault. function withdrawAll() external returns (uint256 totalBalance) @@ -72,13 +72,13 @@ contract EthVault is address payable owner = msg.sender; totalBalance = _balances[owner]; - // withdraw ETH to owner + // withdraw WETH to owner _withdrawFrom(owner, totalBalance); return totalBalance; } - /// @dev Returns the balance in ETH of the `owner` - /// @return Balance in ETH. + /// @dev Returns the balance in WETH of the `owner` + /// @return Balance in WETH. function balanceOf(address owner) external view @@ -87,21 +87,19 @@ contract EthVault is return _balances[owner]; } - /// @dev Withdraw an `amount` of ETH to `owner` from the vault. - /// @param owner of ETH. - /// @param amount of ETH to withdraw. + /// @dev Withdraw an `amount` of WETH to `owner` from the vault. + /// @param owner of WETH. + /// @param amount of WETH to withdraw. function _withdrawFrom(address payable owner, uint256 amount) internal { - // update balance - // note that this call will revert if trying to withdraw more - // than the current balance + //Uupdate balance. _balances[owner] = _balances[owner].safeSub(amount); + // withdraw WETH to owner + IEtherToken(_getWETHAddress()).transfer(msg.sender, amount); + // notify emit EthWithdrawnFromVault(msg.sender, owner, amount); - - // withdraw ETH to owner - owner.transfer(amount); } } diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 3e25a5dc91..4a6ac1a770 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -19,20 +19,21 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; import "../libs/LibSafeDowncast.sol"; import "./MixinVaultCore.sol"; import "../interfaces/IStakingPoolRewardVault.sol"; -import "../immutable/MixinConstants.sol"; +import "../immutable/MixinDeploymentConstants.sol"; /// @dev This vault manages staking pool rewards. contract StakingPoolRewardVault is IStakingPoolRewardVault, IVaultCore, - MixinConstants, + MixinDeploymentConstants, Ownable, MixinVaultCore { @@ -41,29 +42,28 @@ contract StakingPoolRewardVault is // mapping from poolId to Pool metadata mapping (bytes32 => uint256) internal _balanceByPoolId; - // solhint-disable no-empty-blocks - /// @dev Payable fallback for bulk-deposits. - 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 - /// same transaction. + /// @dev Deposit an amount of WETH for `poolId` into the vault. + /// The staking contract should have granted the vault an allowance + /// because it will pull the WETH via `transferFrom()`. /// Note that this is only callable by the staking contract. - /// @param poolId Pool that holds the ETH. + /// @param poolId Pool that holds the WETH. /// @param amount Amount of deposit. - function recordDepositFor(bytes32 poolId, uint256 amount) + function depositFor(bytes32 poolId, uint256 amount) external onlyStakingProxy { + // Transfer WETH from the staking contract into this contract. + IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); + // Credit the pool. _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(amount); emit EthDepositedIntoVault(msg.sender, poolId, amount); } - /// @dev Withdraw some amount in ETH from a pool. + /// @dev Withdraw some amount in WETH from a pool. /// Note that this is only callable by the staking contract. /// @param poolId Unique Id of pool. /// @param to Address to send funds to. - /// @param amount Amount of ETH to transfer. + /// @param amount Amount of WETH to transfer. function transfer( bytes32 poolId, address payable to, @@ -73,7 +73,7 @@ contract StakingPoolRewardVault is onlyStakingProxy { _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeSub(amount); - to.transfer(amount); + IEtherToken(_getWETHAddress()).transfer(to, amount); emit PoolRewardTransferred( poolId, to, @@ -81,8 +81,8 @@ contract StakingPoolRewardVault is ); } - /// @dev Returns the balance in ETH of `poolId` - /// @return Balance in ETH. + /// @dev Returns the balance in WETH of `poolId` + /// @return Balance in WETH. function balanceOf(bytes32 poolId) external view diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 922ff0f5d7..6bf5dcaad0 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -94,8 +94,8 @@ contract TestDelegatorRewards is _initGenesisCumulativeRewards(poolId); } - /// @dev Expose/wrap `_recordStakingPoolRewards`. - function recordStakingPoolRewards( + /// @dev Expose/wrap `_depositStakingPoolRewards`. + function depositStakingPoolRewards( bytes32 poolId, address payable operatorAddress, uint256 operatorReward, @@ -109,7 +109,7 @@ contract TestDelegatorRewards is _setOperatorShare(poolId, operatorReward, membersReward); _initGenesisCumulativeRewards(poolId); - _recordStakingPoolRewards( + _depositStakingPoolRewards( poolId, operatorReward + membersReward, membersStake @@ -206,8 +206,8 @@ contract TestDelegatorRewards is ); } - /// @dev `IEthVault.recordDepositFor()`,` overridden to just emit events. - function recordDepositFor( + /// @dev `IEthVault.depositFor()`,` overridden to just emit events. + function depositFor( address owner, uint256 amount ) @@ -219,9 +219,9 @@ contract TestDelegatorRewards is ); } - /// @dev `IStakingPoolRewardVault.recordDepositFor()`,` + /// @dev `IStakingPoolRewardVault.depositFor()`,` /// overridden to just emit events. - function recordDepositFor( + function depositFor( bytes32 poolId, uint256 membersReward ) @@ -252,7 +252,7 @@ contract TestDelegatorRewards is uint256 totalRewards = reward.operatorReward + reward.membersReward; membersStake = reward.membersStake; (operatorReward, membersReward) = - _recordStakingPoolRewards(poolId, totalRewards, membersStake); + _depositStakingPoolRewards(poolId, totalRewards, membersStake); emit FinalizePool(poolId, operatorReward, membersReward, membersStake); } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 5d8d0eea26..7b0acac671 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -97,16 +97,6 @@ contract TestFinalizer is numActivePoolsThisEpoch += 1; } - /// @dev Expose `_finalizePool()` - function finalizePool(bytes32 poolId) - external - returns (FinalizedPoolRewards memory reward) - { - (reward.operatorReward, - reward.membersReward, - reward.membersStake) = _finalizePool(poolId); - } - /// @dev Drain the balance of this contract. function drainBalance() external @@ -114,35 +104,6 @@ contract TestFinalizer is address(0).transfer(address(this).balance); } - /// @dev Get finalization-related state variables. - function getFinalizationState() - external - view - returns ( - uint256 _balance, - uint256 _currentEpoch, - uint256 _closingEpoch, - uint256 _numActivePoolsThisEpoch, - uint256 _totalFeesCollectedThisEpoch, - uint256 _totalWeightedStakeThisEpoch, - uint256 _unfinalizedPoolsRemaining, - uint256 _unfinalizedRewardsAvailable, - uint256 _unfinalizedTotalFeesCollected, - uint256 _unfinalizedTotalWeightedStake - ) - { - _balance = address(this).balance; - _currentEpoch = currentEpoch; - _closingEpoch = currentEpoch - 1; - _numActivePoolsThisEpoch = numActivePoolsThisEpoch; - _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; - _totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch; - _unfinalizedPoolsRemaining = unfinalizedPoolsRemaining; - _unfinalizedRewardsAvailable = unfinalizedRewardsAvailable; - _unfinalizedTotalFeesCollected = unfinalizedTotalFeesCollected; - _unfinalizedTotalWeightedStake = unfinalizedTotalWeightedStake; - } - /// @dev Compute Cobb-Douglas. function cobbDouglas( uint256 totalRewards, @@ -155,7 +116,7 @@ contract TestFinalizer is view returns (uint256 ownerRewards) { - ownerRewards = LibCobbDouglas._cobbDouglas( + ownerRewards = LibCobbDouglas.cobbDouglas( totalRewards, ownerFees, totalFees, @@ -185,8 +146,8 @@ contract TestFinalizer is pool = _getActivePoolFromEpoch(epoch, poolId); } - /// @dev Overridden to log and do some basic math. - function _recordStakingPoolRewards( + /// @dev Overridden to log and transfer to receivers. + function _depositStakingPoolRewards( bytes32 poolId, uint256 reward, uint256 membersStake @@ -194,52 +155,16 @@ contract TestFinalizer is internal returns (uint256 operatorReward, uint256 membersReward) { + uint32 operatorShare = _operatorSharesByPool[poolId]; (operatorReward, membersReward) = - _splitReward(poolId, reward, membersStake); - emit RecordStakingPoolRewards( - poolId, - reward, - membersStake - ); - } - - /// @dev Overridden to log and transfer to receivers. - function _depositStakingPoolRewards( - uint256 operatorReward, - uint256 membersReward - ) - internal - { - emit DepositStakingPoolRewards(operatorReward, membersReward); + _computeSplitStakingPoolRewards(operatorShare, reward, membersStake); address(_operatorRewardsReceiver).transfer(operatorReward); address(_membersRewardsReceiver).transfer(membersReward); + emit DepositStakingPoolRewards(operatorReward, membersReward); } /// @dev Overriden to just increase the epoch counter. function _goToNextEpoch() internal { currentEpoch += 1; } - - // solhint-disable no-empty-blocks - /// @dev Overridden to do nothing. - function _unwrapWETH() internal {} - - /// @dev Split a pool's total reward between the operator and members. - function _splitReward( - bytes32 poolId, - uint256 amount, - uint256 membersStake - ) - private - view - returns (uint256 operatorReward, uint256 membersReward) - { - uint32 operatorShare = _operatorSharesByPool[poolId]; - (operatorReward, membersReward) = _splitStakingPoolRewards( - operatorShare, - amount, - membersStake - ); - } - } diff --git a/contracts/staking/contracts/test/TestStaking.sol b/contracts/staking/contracts/test/TestStaking.sol index 3241f43e8d..b38874e927 100644 --- a/contracts/staking/contracts/test/TestStaking.sol +++ b/contracts/staking/contracts/test/TestStaking.sol @@ -25,6 +25,12 @@ import "../src/Staking.sol"; contract TestStaking is Staking { + address internal _wethAddress; + + constructor(address wethAddress) public { + _wethAddress = wethAddress; + } + /// @dev Overridden to avoid hard-coded WETH. function getTotalBalance() external @@ -34,9 +40,8 @@ contract TestStaking is totalBalance = address(this).balance; } - // Stub out `_unwrapWETH` to prevent the calls to `finalizeFees` from failing in tests - // that do not relate to protocol fee payments in WETH. - function _unwrapWETH() - internal - {} // solhint-disable-line no-empty-blocks + /// @dev Overridden to use _wethAddress; + function _getWETHAddress() internal view returns (address) { + return _wethAddress; + } } diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index 1cb748fe18..fdb00b505a 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -135,19 +135,7 @@ contract TestStorageLayout is if sub(numActivePoolsThisEpoch_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) - if sub(unfinalizedRewardsAvailable_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - - if sub(unfinalizedPoolsRemaining_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - - if sub(unfinalizedTotalFeesCollected_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - - if sub(unfinalizedTotalWeightedStake_slot, slot) { revertIncorrectStorageSlot() } - slot := add(slot, 1) - - if sub(totalRewardsPaidLastEpoch_slot, slot) { revertIncorrectStorageSlot() } + if sub(unfinalizedState_slot, slot) { revertIncorrectStorageSlot() } slot := add(slot, 1) } } diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 74298fd708..17c1e57d44 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -118,8 +118,10 @@ 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, + computeRewardBalanceOfOperator, + } = this._stakingApiWrapper.stakingContract; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { @@ -142,7 +144,7 @@ export class FinalizerActor extends BaseActor { private async _getDelegatorStakesByPoolIdAsync( delegatorsByPoolId: DelegatorsByPoolId, ): Promise { - const getStakeDelegatedToPoolByOwner = this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner; + const { getStakeDelegatedToPoolByOwner } = this._stakingApiWrapper.stakingContract; const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {}; for (const poolId of Object.keys(delegatorsByPoolId)) { const delegators = delegatorsByPoolId[poolId]; @@ -247,7 +249,7 @@ export class FinalizerActor extends BaseActor { } const rewards = await Promise.all( activePools.map(async pool => - this._stakingApiWrapper.utils.cobbDouglas( + this._stakingApiWrapper.utils.cobbDouglasAsync( totalRewards, pool.feesCollected, totalFeesCollected, diff --git a/contracts/staking/test/cumulative_reward_tracking_test.ts b/contracts/staking/test/cumulative_reward_tracking_test.ts index 35e2258f97..2f84aeacb1 100644 --- a/contracts/staking/test/cumulative_reward_tracking_test.ts +++ b/contracts/staking/test/cumulative_reward_tracking_test.ts @@ -28,7 +28,7 @@ blockchainTests.resets('Cumulative Reward Tracking', env => { // set up ERC20Wrapper erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); // deploy staking contracts - stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); + stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); simulation = new CumulativeRewardTrackingSimulation(stakingApiWrapper, actors); await simulation.deployAndConfigureTestContractsAsync(env); }); diff --git a/contracts/staking/test/epoch_test.ts b/contracts/staking/test/epoch_test.ts index 3d7e9c5b0b..13ca7e6635 100644 --- a/contracts/staking/test/epoch_test.ts +++ b/contracts/staking/test/epoch_test.ts @@ -23,7 +23,7 @@ blockchainTests('Epochs', env => { // set up ERC20Wrapper erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); // deploy staking contracts - stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); + stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); }); describe('Epochs & TimeLocks', () => { it('basic epochs & timeLock periods', async () => { diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index bc93f968a3..92902aca29 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -1,5 +1,5 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; -import { blockchainTests, describe, expect } from '@0x/contracts-test-utils'; +import { blockchainTests, describe, expect, shortZip } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; @@ -42,7 +42,7 @@ blockchainTests.resets('Testing Rewards', env => { // set up ERC20Wrapper erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); // deploy staking contracts - stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); + stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); // set up staking parameters await stakingApiWrapper.utils.setParamsAsync({ minimumPoolStake: new BigNumber(1), @@ -600,16 +600,16 @@ blockchainTests.resets('Testing Rewards', env => { // skip epoch, so staker can start earning rewards await payProtocolFeeAndFinalize(); // undelegate some stake - const unstakeAmount = toBaseUnitAmount(2.5); + const undelegateAmount = toBaseUnitAmount(2.5); await staker.moveStakeAsync( new StakeInfo(StakeStatus.Delegated, poolId), new StakeInfo(StakeStatus.Active), - unstakeAmount, + undelegateAmount, ); // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); - // Unstake nothing to move the rewards into the EthVault. + // Undelegate 0 stake to move the rewards into the EthVault. await staker.moveStakeAsync( new StakeInfo(StakeStatus.Delegated, poolId), new StakeInfo(StakeStatus.Active), @@ -624,7 +624,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; + const stakersAndStake = shortZip(stakers, stakeAmounts); for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeWithPoolAsync(poolId, stakeAmount); } @@ -655,7 +655,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; + const stakersAndStake = shortZip(stakers, stakeAmounts); for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeWithPoolAsync(poolId, stakeAmount); } diff --git a/contracts/staking/test/stake_test.ts b/contracts/staking/test/stake_test.ts index 171ab949a7..bdeba00631 100644 --- a/contracts/staking/test/stake_test.ts +++ b/contracts/staking/test/stake_test.ts @@ -36,7 +36,7 @@ blockchainTests.resets('Stake Statuses', env => { // set up ERC20Wrapper erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); // deploy staking contracts - stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); + stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); // setup new staker staker = new StakerActor(actors[0], stakingApiWrapper); diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index 712121fe54..a36a5ab97a 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -57,7 +57,7 @@ blockchainTests.resets('delegator unit rewards', env => { }; // Generate a deterministic operator address based on the poolId. _opts.operator = poolIdToOperator(_opts.poolId); - await testContract.recordStakingPoolRewards.awaitTransactionSuccessAsync( + await testContract.depositStakingPoolRewards.awaitTransactionSuccessAsync( _opts.poolId, _opts.operator, new BigNumber(_opts.operatorReward), diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 0a45cc5616..8891fb7e38 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -1,9 +1,9 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; -import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; +import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; -import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { BlockParamLiteral, ContractArtifact, DecodedLogArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { @@ -17,6 +17,7 @@ import { StakingPoolRewardVaultContract, StakingProxyContract, TestCobbDouglasContract, + TestStakingContract, ZrxVaultContract, } from '../../src'; @@ -48,14 +49,20 @@ export class StakingApiWrapper { await this._web3Wrapper.mineBlockAsync(); }, - skipToNextEpochAndFinalizeAsync: async (): Promise => { + skipToNextEpochAndFinalizeAsync: async (): Promise => { await this.utils.fastForwardToNextEpochAsync(); const endOfEpochInfo = await this.utils.endEpochAsync(); - const receipt = await this.stakingContract.finalizePools.awaitTransactionSuccessAsync( - endOfEpochInfo.activePoolIds, - ); - logUtils.log(`Finalization cost ${receipt.gasUsed} gas`); - return receipt; + let totalGasUsed = 0; + const allLogs = [] as LogEntry[]; + for (const poolId of endOfEpochInfo.activePoolIds) { + const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync( + poolId, + ); + totalGasUsed += receipt.gasUsed; + allLogs.splice(allLogs.length, 0, receipt.logs); + } + logUtils.log(`Finalization cost ${totalGasUsed} gas`); + return allLogs; }, endEpochAsync: async (): Promise => { @@ -144,7 +151,7 @@ export class StakingApiWrapper { ) as any) as StakingParams; }, - cobbDouglas: async ( + cobbDouglasAsync: async ( totalRewards: BigNumber, ownerFees: BigNumber, totalFees: BigNumber, @@ -219,13 +226,23 @@ export async function deployAndConfigureContractsAsync( const [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, constants.DUMMY_TOKEN_DECIMALS); await erc20Wrapper.setBalancesAndAllowancesAsync(); + // deploy WETH + const wethContract = await WETH9Contract.deployFrom0xArtifactAsync( + erc20Artifacts.WETH9, + env.provider, + txDefaults, + artifacts, + ); + // deploy staking contract - const stakingContract = await StakingContract.deployFrom0xArtifactAsync( - customStakingArtifact !== undefined ? customStakingArtifact : artifacts.Staking, + const stakingContract = await TestStakingContract.deployFrom0xArtifactAsync( + customStakingArtifact !== undefined ? customStakingArtifact : artifacts.TestStaking, env.provider, env.txDefaults, artifacts, + wethContract.address, ); + // deploy read-only proxy const readOnlyProxyContract = await ReadOnlyProxyContract.deployFrom0xArtifactAsync( artifacts.ReadOnlyProxy, diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index 56f7ef9d6e..50686db577 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -1,10 +1,9 @@ import { BlockchainTestsEnvironment, constants, expect, txDefaults } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; -import { DecodedLogArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; -import { TestCumulativeRewardTrackingContract } from '../../generated-wrappers/test_cumulative_reward_tracking'; -import { artifacts } from '../../src'; +import { artifacts, TestCumulativeRewardTrackingContract, IStakingEvents } from '../../src'; import { StakingApiWrapper } from './api_wrapper'; import { toBaseUnitAmount } from './number_utils'; @@ -118,20 +117,21 @@ export class CumulativeRewardTrackingSimulation { CumulativeRewardTrackingSimulation._assertTestLogs(expectedTestLogs, testLogs); } - private async _executeActionsAsync(actions: TestAction[]): Promise { - let logs: DecodedLogArgs[] = []; + private async _executeActionsAsync(actions: TestAction[]): Promise>> { + const combinedLogs = [] as Array>; for (const action of actions) { - let txReceipt: TransactionReceiptWithDecodedLogs; + let receipt: TransactionReceiptWithDecodedLogs; + let logs = [] as DecodedLogEntry; switch (action) { case TestAction.Finalize: - txReceipt = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); + logs = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); break; case TestAction.Delegate: await this._stakingApiWrapper.stakingContract.stake.sendTransactionAsync(this._amountToStake, { from: this._staker, }); - txReceipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync( + receipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync( new StakeInfo(StakeStatus.Active), new StakeInfo(StakeStatus.Delegated, this._poolId), this._amountToStake, @@ -140,7 +140,7 @@ export class CumulativeRewardTrackingSimulation { break; case TestAction.Undelegate: - txReceipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync( + receipt = await this._stakingApiWrapper.stakingContract.moveStake.awaitTransactionSuccessAsync( new StakeInfo(StakeStatus.Delegated, this._poolId), new StakeInfo(StakeStatus.Active), this._amountToStake, @@ -149,7 +149,7 @@ export class CumulativeRewardTrackingSimulation { break; case TestAction.PayProtocolFee: - txReceipt = await this._stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync( + receipt = await this._stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync( this._poolOperator, this._takerAddress, this._protocolFeeAmount, @@ -158,12 +158,12 @@ export class CumulativeRewardTrackingSimulation { break; case TestAction.CreatePool: - txReceipt = await this._stakingApiWrapper.stakingContract.createStakingPool.awaitTransactionSuccessAsync( + receipt = await this._stakingApiWrapper.stakingContract.createStakingPool.awaitTransactionSuccessAsync( 0, true, { from: this._poolOperator }, ); - const createStakingPoolLog = txReceipt.logs[0]; + const createStakingPoolLog = logs[0]; // tslint:disable-next-line no-unnecessary-type-assertion this._poolId = (createStakingPoolLog as DecodedLogArgs).args.poolId; break; @@ -171,8 +171,8 @@ export class CumulativeRewardTrackingSimulation { default: throw new Error('Unrecognized test action'); } - logs = logs.concat(txReceipt.logs); + combinedLogs.splice(combinedLogs.length - 1, 0, logs); } - return logs; + return combinedLogs; } } diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index 39dadf1aca..bc6f441f02 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -7,17 +7,14 @@ library LibFractions { using LibSafeMath for uint256; - /// @dev Maximum value for addition result components. - uint256 constant internal RESCALE_THRESHOLD = 10 ** 27; - /// @dev Safely adds two fractions `n1/d1 + n2/d2` /// @param n1 numerator of `1` /// @param d1 denominator of `1` /// @param n2 numerator of `2` /// @param d2 denominator of `2` - /// @return numerator of sum - /// @return denominator of sum - function addFractions( + /// @return numerator Numerator of sum + /// @return denominator Denominator of sum + function add( uint256 n1, uint256 d1, uint256 n2, @@ -40,15 +37,61 @@ library LibFractions { .safeMul(d2) .safeAdd(n2.safeMul(d1)); denominator = d1.safeMul(d2); + return (numerator, denominator); + } - // If either the numerator or the denominator are > RESCALE_THRESHOLD, - // re-scale them to prevent overflows in future operations. - if (numerator > RESCALE_THRESHOLD || denominator > RESCALE_THRESHOLD) { + /// @dev Rescales a fraction to prevent overflows during addition if either + /// the numerator or the denominator are > `maxValue`. + /// @param numerator The numerator. + /// @param denominator The denominator. + /// @param maxValue The maximum value allowed for both the numerator and + /// denominator. + /// @return scaledNumerator The rescaled numerator. + /// @return scaledDenominator The rescaled denominator. + function normalize( + uint256 numerator, + uint256 denominator, + uint256 maxValue + ) + internal + pure + returns ( + uint256 scaledNumerator, + uint256 scaledDenominator + ) + { + // If either the numerator or the denominator are > `maxValue`, + // re-scale them by `maxValue` to prevent overflows in future operations. + if (numerator > maxValue || denominator > maxValue) { uint256 rescaleBase = numerator >= denominator ? numerator : denominator; - rescaleBase /= RESCALE_THRESHOLD; - numerator = numerator.safeDiv(rescaleBase); - denominator = denominator.safeDiv(rescaleBase); + rescaleBase /= maxValue; + scaledNumerator = numerator.safeDiv(rescaleBase); + scaledDenominator = denominator.safeDiv(rescaleBase); + } else { + scaledNumerator = numerator; + scaledDenominator = denominator; } + return (scaledNumerator, scaledDenominator); + } + + /// @dev Rescales a fraction to prevent overflows during addition if either + /// the numerator or the denominator are > 2 ** 127. + /// @param numerator The numerator. + /// @param denominator The denominator. + /// @return scaledNumerator The rescaled numerator. + /// @return scaledDenominator The rescaled denominator. + function normalize( + uint256 numerator, + uint256 denominator + ) + internal + pure + returns ( + uint256 scaledNumerator, + uint256 scaledDenominator + ) + { + return normalize(numerator, denominator, 2 ** 127); } /// @dev Safely scales the difference between two fractions. @@ -57,8 +100,8 @@ library LibFractions { /// @param n2 numerator of `2` /// @param d2 denominator of `2` /// @param s scalar to multiply by difference. - /// @return result = `s * (n1/d1 - n2/d2)`. - function scaleFractionalDifference( + /// @return result `s * (n1/d1 - n2/d2)`. + function scaleDifference( uint256 n1, uint256 d1, uint256 n2, @@ -81,7 +124,7 @@ library LibFractions { .safeMul(d2) .safeSub(n2.safeMul(d1)); uint256 tmp = numerator.safeDiv(d2); - result = s + return s .safeMul(tmp) .safeDiv(d1); } From 6a29654d7df9c7fba0590cf03685818ecc9d59d1 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 21 Sep 2019 23:50:34 -0400 Subject: [PATCH 44/52] `@0x/contracts-staking`: Updating tests and making the contracts testable. --- .../contracts/src/fees/MixinExchangeFees.sol | 33 +-- .../contracts/src/sys/MixinAbstract.sol | 28 +- .../contracts/src/sys/MixinFinalizer.sol | 2 +- .../staking/contracts/src/sys/MixinParams.sol | 2 +- .../staking/contracts/src/vaults/EthVault.sol | 13 +- .../src/vaults/StakingPoolRewardVault.sol | 13 +- .../test/TestCumulativeRewardTracking.sol | 4 +- .../contracts/test/TestMixinParams.sol | 54 ++++ .../contracts/test/TestProtocolFees.sol | 31 +- .../staking/contracts/test/TestStaking.sol | 20 +- ...esERC20Proxy.sol => TestStakingNoWETH.sol} | 34 +-- .../test/cumulative_reward_tracking_test.ts | 2 - contracts/staking/test/params.ts | 24 +- contracts/staking/test/protocol_fees.ts | 21 +- .../staking/test/unit_tests/finalizer_test.ts | 278 ++++-------------- contracts/staking/test/utils/api_wrapper.ts | 31 +- .../cumulative_reward_tracking_simulation.ts | 38 +-- contracts/staking/test/utils/types.ts | 3 + 18 files changed, 283 insertions(+), 348 deletions(-) create mode 100644 contracts/staking/contracts/test/TestMixinParams.sol rename contracts/staking/contracts/test/{TestProtocolFeesERC20Proxy.sol => TestStakingNoWETH.sol} (54%) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 9c64afc37f..f537a6a0d6 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -192,24 +192,23 @@ contract MixinExchangeFees is private view { - if (protocolFeePaid != 0) { - return; + if (protocolFeePaid == 0) { + LibRichErrors.rrevert( + LibStakingRichErrors.InvalidProtocolFeePaymentError( + LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid, + protocolFeePaid, + msg.value + ) + ); } - if (msg.value == protocolFeePaid || msg.value == 0) { - return; + if (msg.value != protocolFeePaid && msg.value != 0) { + LibRichErrors.rrevert( + LibStakingRichErrors.InvalidProtocolFeePaymentError( + LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, + protocolFeePaid, + msg.value + ) + ); } - LibRichErrors.rrevert( - LibStakingRichErrors.InvalidProtocolFeePaymentError( - protocolFeePaid == 0 ? - LibStakingRichErrors - .ProtocolFeePaymentErrorCodes - .ZeroProtocolFeePaid : - LibStakingRichErrors - .ProtocolFeePaymentErrorCodes - .MismatchedFeeAndPayment, - protocolFeePaid, - msg.value - ) - ); } } diff --git a/contracts/staking/contracts/src/sys/MixinAbstract.sol b/contracts/staking/contracts/src/sys/MixinAbstract.sol index 5be73219cf..b8801917d6 100644 --- a/contracts/staking/contracts/src/sys/MixinAbstract.sol +++ b/contracts/staking/contracts/src/sys/MixinAbstract.sol @@ -26,20 +26,6 @@ import "../interfaces/IStructs.sol"; /// cyclical dependencies. contract MixinAbstract { - /// @dev Computes the reward owed to a pool during finalization. - /// Does nothing if the pool is already finalized. - /// @param poolId The pool's ID. - /// @return totalReward The total reward owed to a pool. - /// @return membersStake The total stake for all non-operator members in - /// this pool. - function _getUnfinalizedPoolRewards(bytes32 poolId) - internal - view - returns ( - uint256 totalReward, - uint256 membersStake - ); - /// @dev Instantly finalizes a single pool that was active in the previous /// epoch, crediting it rewards and sending those rewards to the reward /// and eth vault. This can be called by internal functions that need @@ -57,4 +43,18 @@ contract MixinAbstract { uint256 membersReward, uint256 membersStake ); + + /// @dev Computes the reward owed to a pool during finalization. + /// Does nothing if the pool is already finalized. + /// @param poolId The pool's ID. + /// @return totalReward The total reward owed to a pool. + /// @return membersStake The total stake for all non-operator members in + /// this pool. + function _getUnfinalizedPoolRewards(bytes32 poolId) + internal + view + returns ( + uint256 totalReward, + uint256 membersStake + ); } diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 58557eb7bc..1b8980b771 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -248,7 +248,7 @@ contract MixinFinalizer is IEtherToken weth = IEtherToken(_getWETHAddress()); uint256 ethBalance = address(this).balance; if (ethBalance != 0) { - weth.deposit.value((address(this).balance)); + weth.deposit.value((address(this).balance))(); } balance = weth.balanceOf(address(this)); return balance; diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index 2c0b7b29e6..91381b6848 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -235,7 +235,7 @@ contract MixinParams is address[2] memory oldSpenders, address[2] memory newSpenders ) - private + internal { IEtherToken weth = IEtherToken(_getWETHAddress()); // Grant new allowances. diff --git a/contracts/staking/contracts/src/vaults/EthVault.sol b/contracts/staking/contracts/src/vaults/EthVault.sol index 56c05ae11b..374d548451 100644 --- a/contracts/staking/contracts/src/vaults/EthVault.sol +++ b/contracts/staking/contracts/src/vaults/EthVault.sol @@ -21,7 +21,6 @@ pragma solidity ^0.5.9; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../interfaces/IEthVault.sol"; -import "../immutable/MixinDeploymentConstants.sol"; import "./MixinVaultCore.sol"; @@ -29,15 +28,21 @@ import "./MixinVaultCore.sol"; contract EthVault is IEthVault, IVaultCore, - MixinDeploymentConstants, Ownable, MixinVaultCore { using LibSafeMath for uint256; + // Address of the WETH contract. + IEtherToken public weth; // mapping from Owner to WETH balance mapping (address => uint256) internal _balances; + /// @param wethAddress Address of the WETH contract. + constructor(address wethAddress) public { + weth = IEtherToken(wethAddress); + } + /// @dev Deposit an `amount` of WETH for `owner` into the vault. /// The staking contract should have granted the vault an allowance /// because it will pull the WETH via `transferFrom()`. @@ -49,7 +54,7 @@ contract EthVault is onlyStakingProxy { // Transfer WETH from the staking contract into this contract. - IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); + weth.transferFrom(msg.sender, address(this), amount); // Credit the owner. _balances[owner] = _balances[owner].safeAdd(amount); emit EthDepositedIntoVault(msg.sender, owner, amount); @@ -97,7 +102,7 @@ contract EthVault is _balances[owner] = _balances[owner].safeSub(amount); // withdraw WETH to owner - IEtherToken(_getWETHAddress()).transfer(msg.sender, amount); + weth.transfer(msg.sender, amount); // notify emit EthWithdrawnFromVault(msg.sender, owner, amount); diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 4a6ac1a770..682755412b 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -26,22 +26,27 @@ import "../libs/LibStakingRichErrors.sol"; import "../libs/LibSafeDowncast.sol"; import "./MixinVaultCore.sol"; import "../interfaces/IStakingPoolRewardVault.sol"; -import "../immutable/MixinDeploymentConstants.sol"; /// @dev This vault manages staking pool rewards. contract StakingPoolRewardVault is IStakingPoolRewardVault, IVaultCore, - MixinDeploymentConstants, Ownable, MixinVaultCore { using LibSafeMath for uint256; + // Address of the WETH contract. + IEtherToken public weth; // mapping from poolId to Pool metadata mapping (bytes32 => uint256) internal _balanceByPoolId; + /// @param wethAddress Address of the WETH contract. + constructor(address wethAddress) public { + weth = IEtherToken(wethAddress); + } + /// @dev Deposit an amount of WETH for `poolId` into the vault. /// The staking contract should have granted the vault an allowance /// because it will pull the WETH via `transferFrom()`. @@ -53,7 +58,7 @@ contract StakingPoolRewardVault is onlyStakingProxy { // Transfer WETH from the staking contract into this contract. - IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); + weth.transferFrom(msg.sender, address(this), amount); // Credit the pool. _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(amount); emit EthDepositedIntoVault(msg.sender, poolId, amount); @@ -73,7 +78,7 @@ contract StakingPoolRewardVault is onlyStakingProxy { _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeSub(amount); - IEtherToken(_getWETHAddress()).transfer(to, amount); + weth.transfer(to, amount); emit PoolRewardTransferred( poolId, to, diff --git a/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol b/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol index 54d5896d9a..a185138ca9 100644 --- a/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol +++ b/contracts/staking/contracts/test/TestCumulativeRewardTracking.sol @@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2; import "./TestStaking.sol"; +// solhint-disable no-empty-blocks contract TestCumulativeRewardTracking is TestStaking { @@ -39,7 +40,8 @@ contract TestCumulativeRewardTracking is uint256 epoch ); - // solhint-disable-next-line no-empty-blocks + constructor(address wethAddress) public TestStaking(wethAddress) {} + function init(address, address, address payable, address) public {} function _forceSetCumulativeReward( diff --git a/contracts/staking/contracts/test/TestMixinParams.sol b/contracts/staking/contracts/test/TestMixinParams.sol new file mode 100644 index 0000000000..cf21e8ce63 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinParams.sol @@ -0,0 +1,54 @@ +/* + + 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 "../src/interfaces/IEthVault.sol"; +import "../src/interfaces/IStakingPoolRewardVault.sol"; +import "../src/sys/MixinParams.sol"; + + +// solhint-disable no-empty-blocks +contract TestMixinParams is + MixinParams +{ + + event WETHApprove(address spender, uint256 amount); + + /// @dev Sets the eth and reward vault addresses. + function setVaultAddresses( + address ethVaultAddress, + address rewardVaultAddress + ) + external + { + ethVault = IEthVault(ethVaultAddress); + rewardVault = IStakingPoolRewardVault(rewardVaultAddress); + } + + /// @dev WETH `approve()` function that just logs events. + function approve(address spender, uint256 amount) external returns (bool) { + emit WETHApprove(spender, amount); + } + + /// @dev Overridden return this contract's address. + function _getWETHAddress() internal view returns (address) { + return address(this); + } +} diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index 88b9baf29e..a9d98d5a6e 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -21,11 +21,11 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; import "../src/interfaces/IStructs.sol"; -import "../src/Staking.sol"; +import "./TestStakingNoWETH.sol"; contract TestProtocolFees is - Staking + TestStakingNoWETH { struct TestPool { uint256 operatorStake; @@ -33,13 +33,22 @@ contract TestProtocolFees is mapping(address => bool) isMaker; } + event ERC20ProxyTransferFrom( + bytes assetData, + address from, + address to, + uint256 amount + ); + mapping(bytes32 => TestPool) private _testPools; mapping(address => bytes32) private _makersToTestPoolIds; - constructor(address exchangeAddress, address wethProxyAddress) public { + constructor(address exchangeAddress) public { init( - wethProxyAddress, - address(1), // vault addresses must be non-zero + // Use this contract as the ERC20Proxy. + address(this), + // vault addresses must be non-zero + address(1), address(1), address(1) ); @@ -81,6 +90,18 @@ contract TestProtocolFees is } } + /// @dev The ERC20Proxy `transferFrom()` function. + function transferFrom( + bytes calldata assetData, + address from, + address to, + uint256 amount + ) + external + { + emit ERC20ProxyTransferFrom(assetData, from, to, amount); + } + /// @dev Overridden to use test pools. function getStakingPoolIdOfMaker(address makerAddress) public diff --git a/contracts/staking/contracts/test/TestStaking.sol b/contracts/staking/contracts/test/TestStaking.sol index b38874e927..e4a48ce2a5 100644 --- a/contracts/staking/contracts/test/TestStaking.sol +++ b/contracts/staking/contracts/test/TestStaking.sol @@ -25,23 +25,17 @@ import "../src/Staking.sol"; contract TestStaking is Staking { - address internal _wethAddress; + address public testWethAddress; constructor(address wethAddress) public { - _wethAddress = wethAddress; + testWethAddress = wethAddress; } - /// @dev Overridden to avoid hard-coded WETH. - function getTotalBalance() - external - view - returns (uint256 totalBalance) - { - totalBalance = address(this).balance; - } - - /// @dev Overridden to use _wethAddress; + /// @dev Overridden to use testWethAddress; function _getWETHAddress() internal view returns (address) { - return _wethAddress; + // `testWethAddress` will not be set on the proxy this contract is + // attached to, so we need to access the storage of the deployed + // instance of this contract. + return TestStaking(address(uint160(stakingContract))).testWethAddress(); } } diff --git a/contracts/staking/contracts/test/TestProtocolFeesERC20Proxy.sol b/contracts/staking/contracts/test/TestStakingNoWETH.sol similarity index 54% rename from contracts/staking/contracts/test/TestProtocolFeesERC20Proxy.sol rename to contracts/staking/contracts/test/TestStakingNoWETH.sol index 5a83d2f37d..94b7f380ac 100644 --- a/contracts/staking/contracts/test/TestProtocolFeesERC20Proxy.sol +++ b/contracts/staking/contracts/test/TestStakingNoWETH.sol @@ -17,28 +17,28 @@ */ pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; -import "@0x/contracts-asset-proxy/contracts/src/ERC20Proxy.sol"; +import "../src/Staking.sol"; -contract TestProtocolFeesERC20Proxy is - ERC20Proxy +// solhint-disable no-empty-blocks +/// @dev A version of the staking contract with WETH-related functions +/// overridden to do nothing. +contract TestStakingNoWETH is + Staking { - event TransferFromCalled( - bytes assetData, - address from, - address to, - uint256 amount - ); - - function transferFrom( - bytes calldata assetData, - address from, - address to, - uint256 amount + function _transferWETHAllownces( + address[2] memory oldSpenders, + address[2] memory newSpenders ) - external + internal + {} + + function _wrapBalanceToWETHAndGetBalance() + internal + returns (uint256 balance) { - emit TransferFromCalled(assetData, from, to, amount); + return address(this).balance; } } diff --git a/contracts/staking/test/cumulative_reward_tracking_test.ts b/contracts/staking/test/cumulative_reward_tracking_test.ts index 2f84aeacb1..bdc8e50a59 100644 --- a/contracts/staking/test/cumulative_reward_tracking_test.ts +++ b/contracts/staking/test/cumulative_reward_tracking_test.ts @@ -2,8 +2,6 @@ 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'; diff --git a/contracts/staking/test/params.ts b/contracts/staking/test/params.ts index 9e76ab9d6a..90d27ca819 100644 --- a/contracts/staking/test/params.ts +++ b/contracts/staking/test/params.ts @@ -1,20 +1,28 @@ import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils'; import { AuthorizableRevertErrors, BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; -import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../src/'; +import { + artifacts, + IStakingEventsParamsSetEventArgs, + TestMixinParamsContract, + TestMixinParamsEvents, + TestMixinParamsWETHApproveEventArgs, +} from '../src/'; import { constants as stakingConstants } from './utils/constants'; import { StakingParams } from './utils/types'; blockchainTests('Configurable Parameters unit tests', env => { - let testContract: MixinParamsContract; + let testContract: TestMixinParamsContract; let authorizedAddress: string; let notAuthorizedAddress: string; before(async () => { [authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync(); - testContract = await MixinParamsContract.deployFrom0xArtifactAsync( - artifacts.MixinParams, + testContract = await TestMixinParamsContract.deployFrom0xArtifactAsync( + artifacts.TestMixinParams, env.provider, env.txDefaults, artifacts, @@ -22,7 +30,7 @@ blockchainTests('Configurable Parameters unit tests', env => { }); blockchainTests.resets('setParams()', () => { - async function setParamsAndAssertAsync(params: Partial, from?: string): Promise { + async function setParamsAndAssertAsync(params: Partial, from?: string): Promise { const _params = { ...stakingConstants.DEFAULT_PARAMS, ...params, @@ -41,8 +49,9 @@ blockchainTests('Configurable Parameters unit tests', env => { { from }, ); // Assert event. - expect(receipt.logs.length).to.eq(1); - const event = filterLogsToArguments(receipt.logs, 'ParamsSet')[0]; + const events = filterLogsToArguments(receipt.logs, 'ParamsSet'); + expect(events.length).to.eq(1); + const event = events[0]; expect(event.epochDurationInSeconds).to.bignumber.eq(_params.epochDurationInSeconds); expect(event.rewardDelegatedStakeWeight).to.bignumber.eq(_params.rewardDelegatedStakeWeight); expect(event.minimumPoolStake).to.bignumber.eq(_params.minimumPoolStake); @@ -65,6 +74,7 @@ blockchainTests('Configurable Parameters unit tests', env => { expect(actual[7]).to.eq(_params.ethVaultAddress); expect(actual[8]).to.eq(_params.rewardVaultAddress); expect(actual[9]).to.eq(_params.zrxVaultAddress); + return receipt; } it('throws if not called by an authorized address', async () => { diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index 6bf9dab613..c77c863f75 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -17,8 +17,8 @@ import { IStakingEventsEvents, IStakingEventsStakingPoolActivatedEventArgs, TestProtocolFeesContract, - TestProtocolFeesERC20ProxyContract, - TestProtocolFeesERC20ProxyTransferFromCalledEventArgs, + TestProtocolFeesERC20ProxyTransferFromEventArgs, + TestProtocolFeesEvents, } from '../src'; import { getRandomInteger } from './utils/number_utils'; @@ -34,14 +34,6 @@ blockchainTests('Protocol Fee Unit Tests', env => { before(async () => { [ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync(); - // Deploy the erc20Proxy for testing. - const proxy = await TestProtocolFeesERC20ProxyContract.deployFrom0xArtifactAsync( - artifacts.TestProtocolFeesERC20Proxy, - env.provider, - env.txDefaults, - {}, - ); - // Deploy the protocol fees contract. testContract = await TestProtocolFeesContract.deployFrom0xArtifactAsync( artifacts.TestProtocolFees, @@ -52,7 +44,6 @@ blockchainTests('Protocol Fee Unit Tests', env => { }, artifacts, exchangeAddress, - proxy.address, ); wethAssetData = await testContract.getWethAssetData.callAsync(); @@ -168,9 +159,9 @@ blockchainTests('Protocol Fee Unit Tests', env => { describe('ETH fees', () => { function assertNoWETHTransferLogs(logs: LogEntry[]): void { - const logsArgs = filterLogsToArguments( + const logsArgs = filterLogsToArguments( logs, - 'TransferFromCalled', + TestProtocolFeesEvents.ERC20ProxyTransferFrom, ); expect(logsArgs).to.deep.eq([]); } @@ -233,9 +224,9 @@ blockchainTests('Protocol Fee Unit Tests', env => { describe('WETH fees', () => { function assertWETHTransferLogs(logs: LogEntry[], fromAddress: string, amount: BigNumber): void { - const logsArgs = filterLogsToArguments( + const logsArgs = filterLogsToArguments( logs, - 'TransferFromCalled', + TestProtocolFeesEvents.ERC20ProxyTransferFrom, ); expect(logsArgs.length).to.eq(1); for (const args of logsArgs) { diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 5b551cdc3e..581299ed77 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -91,36 +91,35 @@ blockchainTests.resets('finalizer unit tests', env => { } interface FinalizationState { - balance: Numberish; - currentEpoch: number; - closingEpoch: number; - numActivePoolsThisEpoch: number; - totalFeesCollectedThisEpoch: Numberish; - totalWeightedStakeThisEpoch: Numberish; - unfinalizedPoolsRemaining: number; - unfinalizedRewardsAvailable: Numberish; - unfinalizedTotalFeesCollected: Numberish; - unfinalizedTotalWeightedStake: Numberish; + rewardsAvailable: Numberish; + poolsRemaining: number; + totalFeesCollected: Numberish; + totalWeightedStake: Numberish; + totalRewardsFinalized: Numberish; } - async function getFinalizationStateAsync(): Promise { - const r = await testContract.getFinalizationState.callAsync(); + async function getUnfinalizedStateAsync(): Promise { + const r = await testContract.unfinalizedState.callAsync(); return { - balance: r[0], - currentEpoch: r[1].toNumber(), - closingEpoch: r[2].toNumber(), - numActivePoolsThisEpoch: r[3].toNumber(), - totalFeesCollectedThisEpoch: r[4], - totalWeightedStakeThisEpoch: r[5], - unfinalizedPoolsRemaining: r[6].toNumber(), - unfinalizedRewardsAvailable: r[7], - unfinalizedTotalFeesCollected: r[8], - unfinalizedTotalWeightedStake: r[9], + rewardsAvailable: r[0], + poolsRemaining: r[1].toNumber(), + totalFeesCollected: r[2], + totalWeightedStake: r[3], + totalRewardsFinalized: r[4], }; } + async function finalizePoolsAsync(poolIds: string[]): Promise { + const logs = [] as LogEntry[]; + for (const poolId of poolIds) { + const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); + logs.splice(logs.length - 1, 0, ...receipt.logs); + } + return logs; + } + async function assertFinalizationStateAsync(expected: Partial): Promise { - const actual = await getFinalizationStateAsync(); + const actual = await getUnfinalizedStateAsync(); assertEqualNumberFields(actual, expected); } @@ -248,7 +247,7 @@ blockchainTests.resets('finalizer unit tests', env => { if (new BigNumber(pool.membersStake).isZero()) { return [new BigNumber(totalReward), ZERO_AMOUNT]; } - const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); + const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_UP); const membersShare = new BigNumber(totalReward).minus(operatorShare); return [operatorShare, membersShare]; } @@ -337,12 +336,12 @@ blockchainTests.resets('finalizer unit tests', env => { // Add a pool so there is state to clear. await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); + const epoch = await testContract.currentEpoch.callAsync(); + expect(epoch).to.bignumber.eq(INITIAL_EPOCH + 1); return assertFinalizationStateAsync({ - currentEpoch: INITIAL_EPOCH + 1, - closingEpoch: INITIAL_EPOCH, - numActivePoolsThisEpoch: 0, - totalFeesCollectedThisEpoch: 0, - totalWeightedStakeThisEpoch: 0, + poolsRemaining: 0, + totalFeesCollected: 0, + totalWeightedStake: 0, }); }); @@ -351,10 +350,10 @@ blockchainTests.resets('finalizer unit tests', env => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); return assertFinalizationStateAsync({ - unfinalizedPoolsRemaining: 1, - unfinalizedRewardsAvailable: INITIAL_BALANCE, - unfinalizedTotalFeesCollected: pool.feesCollected, - unfinalizedTotalWeightedStake: pool.weightedStake, + poolsRemaining: 1, + rewardsAvailable: INITIAL_BALANCE, + totalFeesCollected: pool.feesCollected, + totalWeightedStake: pool.weightedStake, }); }); @@ -367,181 +366,35 @@ blockchainTests.resets('finalizer unit tests', env => { }); }); - describe('finalizePools()', () => { - it('does nothing if there were no active pools', async () => { - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const poolId = hexRandom(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([poolId]); - expect(receipt.logs).to.deep.eq([]); - }); - - it('does nothing if no pools are passed in', async () => { - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([]); - expect(receipt.logs).to.deep.eq([]); - }); - - it('can finalize a single pool', async () => { - const pool = await addActivePoolAsync(); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs); - }); - - it('can finalize multiple pools', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const poolIds = pools.map(p => p.poolId); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, receipt.logs); - }); - - it('can finalize multiple pools over multiple transactions', async () => { - const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipts = await Promise.all( - pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs); - }); - - it('can finalize with no rewards', async () => { - await testContract.drainBalance.awaitTransactionSuccessAsync(); - const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipts = await Promise.all( - pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - return assertFinalizationLogsAndBalancesAsync(0, pools, allLogs); - }); - - it('ignores a non-active pool', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const nonActivePoolId = hexRandom(); - const poolIds = _.shuffle([...pools.map(p => p.poolId), nonActivePoolId]); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - expect(rewardsPaidEvents.length).to.eq(pools.length); - for (const event of rewardsPaidEvents) { - expect(event.poolId).to.not.eq(nonActivePoolId); - } - }); - - it('ignores a finalized pool', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const poolIds = pools.map(p => p.poolId); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const [finalizedPool] = _.sampleSize(pools, 1); - await testContract.finalizePools.awaitTransactionSuccessAsync([finalizedPool.poolId]); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - expect(rewardsPaidEvents.length).to.eq(pools.length - 1); - for (const event of rewardsPaidEvents) { - expect(event.poolId).to.not.eq(finalizedPool.poolId); - } - }); - - it('resets pool state after finalizing it', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const pool = _.sample(pools) as ActivePoolOpts; - await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); - const poolState = await testContract.getActivePoolFromEpoch.callAsync( - new BigNumber(INITIAL_EPOCH), - pool.poolId, - ); - expect(poolState.feesCollected).to.bignumber.eq(0); - expect(poolState.weightedStake).to.bignumber.eq(0); - expect(poolState.membersStake).to.bignumber.eq(0); - }); - - it('`rewardsPaid` is the sum of all pool rewards', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const poolIds = pools.map(p => p.poolId); - await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); - const expectedTotalRewardsPaid = BigNumber.sum( - ...rewardsPaidEvents.map(e => e.membersReward.plus(e.operatorReward)), - ); - const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - expect(totalRewardsPaid).to.bignumber.eq(expectedTotalRewardsPaid); - }); - - it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => { - const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); - const poolIds = pools.map(p => p.poolId); - let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; - expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); - receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); - }); - - it('`rewardsPaid` <= `rewardsAvailable` with two equal pools', async () => { - const pool1 = await addActivePoolAsync(); - const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId')); - const poolIds = [pool1, pool2].map(p => p.poolId); - let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; - receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); - }); - - blockchainTests.optional('`rewardsPaid` fuzzing', async () => { - const numTests = 32; - for (const i of _.times(numTests)) { - const numPools = _.random(1, 32); - it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => { - const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync())); - const poolIds = pools.map(p => p.poolId); - let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; - receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0]; - expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); - }); - } - }); - }); - describe('_finalizePool()', () => { it('does nothing if there were no active pools', async () => { await testContract.endEpoch.awaitTransactionSuccessAsync(); const poolId = hexRandom(); - const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); - expect(receipt.logs).to.deep.eq([]); + const logs = await finalizePoolsAsync([poolId]); + expect(logs).to.deep.eq([]); }); it('can finalize a pool', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); - return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs); + const logs = await finalizePoolsAsync([pool.poolId]); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], logs); }); it('can finalize multiple pools over multiple transactions', async () => { const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); - const receipts = await Promise.all( - pools.map(pool => testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId)), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs); + const logs = await finalizePoolsAsync(pools.map(p => p.poolId)); + return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, logs); }); it('ignores a finalized pool', async () => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); await testContract.endEpoch.awaitTransactionSuccessAsync(); const [finalizedPool] = _.sampleSize(pools, 1); - await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); - const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + await finalizePoolsAsync([finalizedPool.poolId]); + const logs = await finalizePoolsAsync([finalizedPool.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(logs); expect(rewardsPaidEvents).to.deep.eq([]); }); @@ -549,7 +402,7 @@ blockchainTests.resets('finalizer unit tests', env => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pool = _.sample(pools) as ActivePoolOpts; await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); + await finalizePoolsAsync([pool.poolId]); const poolState = await testContract.getActivePoolFromEpoch.callAsync( new BigNumber(INITIAL_EPOCH), pool.poolId, @@ -564,11 +417,8 @@ blockchainTests.resets('finalizer unit tests', env => { const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); - const receipts = await Promise.all( - pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + const logs = await finalizePoolsAsync(pools.map(r => r.poolId)); + const { rewardsPaid } = getEpochFinalizedEvents(logs)[0]; expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); }); @@ -577,11 +427,8 @@ blockchainTests.resets('finalizer unit tests', env => { const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId')); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; - const receipts = await Promise.all( - [pool1, pool2].map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + const logs = await finalizePoolsAsync([pool1, pool2].map(r => r.poolId)); + const { rewardsPaid } = getEpochFinalizedEvents(logs)[0]; expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); }); @@ -593,11 +440,8 @@ blockchainTests.resets('finalizer unit tests', env => { const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync())); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; - const receipts = await Promise.all( - pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), - ); - const allLogs = _.flatten(receipts.map(r => r.logs)); - const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0]; + const logs = await finalizePoolsAsync(pools.map(r => r.poolId)); + const { rewardsPaid } = getEpochFinalizedEvents(logs)[0]; expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); }); } @@ -608,7 +452,7 @@ blockchainTests.resets('finalizer unit tests', env => { it('can advance the epoch after the prior epoch is finalized', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + await finalizePoolsAsync([pool.poolId]); await testContract.endEpoch.awaitTransactionSuccessAsync(); return expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); }); @@ -616,25 +460,25 @@ blockchainTests.resets('finalizer unit tests', env => { it('does not reward a pool that was only active 2 epochs ago', async () => { const pool1 = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + await finalizePoolsAsync([pool1.poolId]); await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + const logs = await finalizePoolsAsync([pool1.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(logs); expect(rewardsPaidEvents).to.deep.eq([]); }); it('does not reward a pool that was only active 3 epochs ago', async () => { const pool1 = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); + await finalizePoolsAsync([pool1.poolId]); await testContract.endEpoch.awaitTransactionSuccessAsync(); await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 3); - const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); - const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); + const logs = await finalizePoolsAsync([pool1.poolId]); + const rewardsPaidEvents = getRewardsPaidEvents(logs); expect(rewardsPaidEvents).to.deep.eq([]); }); @@ -642,11 +486,11 @@ blockchainTests.resets('finalizer unit tests', env => { const poolIds = _.times(3, () => hexRandom()); await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); await testContract.endEpoch.awaitTransactionSuccessAsync(); - let receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); - const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(receipt.logs)[0]; + const finalizeLogs = await finalizePoolsAsync(poolIds); + const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(finalizeLogs)[0]; await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); - receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); - const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; + const {logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0]; expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); }); }); @@ -694,7 +538,7 @@ blockchainTests.resets('finalizer unit tests', env => { it('returns empty if pool was only active in the 2 epochs ago', async () => { const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + await finalizePoolsAsync([pool.poolId]); return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); }); @@ -702,7 +546,7 @@ blockchainTests.resets('finalizer unit tests', env => { const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const [pool] = _.sampleSize(pools, 1); await testContract.endEpoch.awaitTransactionSuccessAsync(); - await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); + await finalizePoolsAsync([pool.poolId]); return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 8891fb7e38..7b999c411f 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -3,7 +3,7 @@ import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } f import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; -import { BlockParamLiteral, ContractArtifact, DecodedLogArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { @@ -12,29 +12,29 @@ import { IStakingEventsEpochEndedEventArgs, IStakingEventsStakingPoolActivatedEventArgs, ReadOnlyProxyContract, - StakingContract, - StakingEvents, StakingPoolRewardVaultContract, StakingProxyContract, TestCobbDouglasContract, TestStakingContract, + TestStakingEvents, ZrxVaultContract, } from '../../src'; import { constants as stakingConstants } from './constants'; -import { EndOfEpochInfo, StakingParams } from './types'; +import { DecodedLogs, EndOfEpochInfo, StakingParams } from './types'; export class StakingApiWrapper { // The address of the real Staking.sol contract public stakingContractAddress: string; // The StakingProxy.sol contract wrapped as a StakingContract to borrow API - public stakingContract: StakingContract; + public stakingContract: TestStakingContract; // The StakingProxy.sol contract as a StakingProxyContract public stakingProxyContract: StakingProxyContract; public zrxVaultContract: ZrxVaultContract; public ethVaultContract: EthVaultContract; public rewardVaultContract: StakingPoolRewardVaultContract; public zrxTokenContract: DummyERC20TokenContract; + public wethContract: WETH9Contract; public cobbDouglasContract: TestCobbDouglasContract; public utils = { // Epoch Utils @@ -49,17 +49,17 @@ export class StakingApiWrapper { await this._web3Wrapper.mineBlockAsync(); }, - skipToNextEpochAndFinalizeAsync: async (): Promise => { + skipToNextEpochAndFinalizeAsync: async (): Promise => { await this.utils.fastForwardToNextEpochAsync(); const endOfEpochInfo = await this.utils.endEpochAsync(); let totalGasUsed = 0; - const allLogs = [] as LogEntry[]; + const allLogs = [] as DecodedLogs; for (const poolId of endOfEpochInfo.activePoolIds) { const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync( poolId, ); totalGasUsed += receipt.gasUsed; - allLogs.splice(allLogs.length, 0, receipt.logs); + allLogs.splice(allLogs.length, 0, ...(receipt.logs as DecodedLogs)); } logUtils.log(`Finalization cost ${totalGasUsed} gas`); return allLogs; @@ -70,7 +70,7 @@ export class StakingApiWrapper { const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync(); const [epochEndedEvent] = filterLogsToArguments( receipt.logs, - StakingEvents.EpochEnded, + TestStakingEvents.EpochEnded, ); return { closingEpoch: epochEndedEvent.epoch, @@ -85,11 +85,11 @@ export class StakingApiWrapper { const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync(); const events = filterLogsToArguments( await this.stakingContract.getLogsAsync( - StakingEvents.StakingPoolActivated, + TestStakingEvents.StakingPoolActivated, { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, { epoch: new BigNumber(_epoch) }, ), - StakingEvents.StakingPoolActivated, + TestStakingEvents.StakingPoolActivated, ); return events.map(e => e.poolId); }, @@ -177,11 +177,12 @@ export class StakingApiWrapper { env: BlockchainTestsEnvironment, ownerAddress: string, stakingProxyContract: StakingProxyContract, - stakingContract: StakingContract, + stakingContract: TestStakingContract, zrxVaultContract: ZrxVaultContract, ethVaultContract: EthVaultContract, rewardVaultContract: StakingPoolRewardVaultContract, zrxTokenContract: DummyERC20TokenContract, + wethContract: WETH9Contract, cobbDouglasContract: TestCobbDouglasContract, ) { this._web3Wrapper = env.web3Wrapper; @@ -189,13 +190,14 @@ export class StakingApiWrapper { this.ethVaultContract = ethVaultContract; this.rewardVaultContract = rewardVaultContract; this.zrxTokenContract = zrxTokenContract; + this.wethContract = wethContract; this.cobbDouglasContract = cobbDouglasContract; this.stakingContractAddress = stakingContract.address; this.stakingProxyContract = stakingProxyContract; // disguise the staking proxy as a StakingContract const logDecoderDependencies = _.mapValues({ ...artifacts, ...erc20Artifacts }, v => v.compilerOutput.abi); - this.stakingContract = new StakingContract( + this.stakingContract = new TestStakingContract( stakingProxyContract.address, env.provider, { @@ -256,6 +258,7 @@ export async function deployAndConfigureContractsAsync( env.provider, env.txDefaults, artifacts, + wethContract.address, ); // deploy reward vault const rewardVaultContract = await StakingPoolRewardVaultContract.deployFrom0xArtifactAsync( @@ -263,6 +266,7 @@ export async function deployAndConfigureContractsAsync( env.provider, env.txDefaults, artifacts, + wethContract.address, ); // deploy zrx vault const zrxVaultContract = await ZrxVaultContract.deployFrom0xArtifactAsync( @@ -311,6 +315,7 @@ export async function deployAndConfigureContractsAsync( ethVaultContract, rewardVaultContract, zrxTokenContract, + wethContract, cobbDouglasContract, ); } diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index 50686db577..40a388bb72 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -3,11 +3,11 @@ import { BigNumber } from '@0x/utils'; import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; -import { artifacts, TestCumulativeRewardTrackingContract, IStakingEvents } from '../../src'; +import { artifacts, TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../../src'; import { StakingApiWrapper } from './api_wrapper'; import { toBaseUnitAmount } from './number_utils'; -import { StakeInfo, StakeStatus } from './types'; +import { DecodedLogs, StakeInfo, StakeStatus } from './types'; export enum TestAction { Finalize, @@ -33,22 +33,22 @@ export class CumulativeRewardTrackingSimulation { private _testCumulativeRewardTrackingContract?: TestCumulativeRewardTrackingContract; private _poolId: string; - private static _extractTestLogs(txReceiptLogs: DecodedLogArgs[]): TestLog[] { + private static _extractTestLogs(txReceiptLogs: DecodedLogs): TestLog[] { const logs = []; for (const log of txReceiptLogs) { - if (log.event === 'SetMostRecentCumulativeReward') { + if (log.event === TestCumulativeRewardTrackingEvents.SetMostRecentCumulativeReward) { logs.push({ - event: 'SetMostRecentCumulativeReward', + event: log.event, epoch: log.args.epoch.toNumber(), }); - } else if (log.event === 'SetCumulativeReward') { + } else if (log.event === TestCumulativeRewardTrackingEvents.SetCumulativeReward) { logs.push({ - event: 'SetCumulativeReward', + event: log.event, epoch: log.args.epoch.toNumber(), }); - } else if (log.event === 'UnsetCumulativeReward') { + } else if (log.event === TestCumulativeRewardTrackingEvents.UnsetCumulativeReward) { logs.push({ - event: 'UnsetCumulativeReward', + event: log.event, epoch: log.args.epoch.toNumber(), }); } @@ -56,7 +56,7 @@ export class CumulativeRewardTrackingSimulation { return logs; } - private static _assertTestLogs(expectedSequence: TestLog[], txReceiptLogs: DecodedLogArgs[]): void { + private static _assertTestLogs(expectedSequence: TestLog[], txReceiptLogs: DecodedLogs): void { const logs = CumulativeRewardTrackingSimulation._extractTestLogs(txReceiptLogs); expect(logs.length).to.be.equal(expectedSequence.length); for (let i = 0; i < expectedSequence.length; i++) { @@ -90,6 +90,7 @@ export class CumulativeRewardTrackingSimulation { env.provider, txDefaults, artifacts, + this._stakingApiWrapper.wethContract.address, ); } @@ -117,11 +118,11 @@ export class CumulativeRewardTrackingSimulation { CumulativeRewardTrackingSimulation._assertTestLogs(expectedTestLogs, testLogs); } - private async _executeActionsAsync(actions: TestAction[]): Promise>> { - const combinedLogs = [] as Array>; + private async _executeActionsAsync(actions: TestAction[]): Promise { + const combinedLogs = [] as DecodedLogs; for (const action of actions) { - let receipt: TransactionReceiptWithDecodedLogs; - let logs = [] as DecodedLogEntry; + let receipt: TransactionReceiptWithDecodedLogs | undefined; + let logs = [] as DecodedLogs; switch (action) { case TestAction.Finalize: logs = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); @@ -163,15 +164,18 @@ export class CumulativeRewardTrackingSimulation { true, { from: this._poolOperator }, ); - const createStakingPoolLog = logs[0]; + const createStakingPoolLog = receipt.logs[0]; // tslint:disable-next-line no-unnecessary-type-assertion - this._poolId = (createStakingPoolLog as DecodedLogArgs).args.poolId; + this._poolId = (createStakingPoolLog as DecodedLogEntry).args.poolId; break; default: throw new Error('Unrecognized test action'); } - combinedLogs.splice(combinedLogs.length - 1, 0, logs); + if (receipt !== undefined) { + logs = receipt.logs as DecodedLogs; + } + combinedLogs.splice(combinedLogs.length - 1, 0, ...logs); } return combinedLogs; } diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index 3235eca5c0..4f375956c7 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -1,5 +1,6 @@ import { Numberish } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; +import { DecodedLogArgs, LogWithDecodedArgs } from 'ethereum-types'; import { constants } from './constants'; @@ -133,3 +134,5 @@ export interface OperatorByPoolId { export interface DelegatorsByPoolId { [key: string]: string[]; } + +export type DecodedLogs = Array>; From c72a15b488df2e2e702af828c28f933e2f5f7c13 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sun, 22 Sep 2019 12:03:53 -0400 Subject: [PATCH 45/52] `@0x/contracts-staking`: All tests back up and running. --- .../src/libs/LibStakingRichErrors.sol | 29 ------- .../staking_pools/MixinCumulativeRewards.sol | 39 +-------- .../staking_pools/MixinStakingPoolRewards.sol | 2 +- .../contracts/src/sys/MixinFinalizer.sol | 8 +- .../staking/contracts/src/sys/MixinParams.sol | 20 +++++ .../contracts/test/TestDelegatorRewards.sol | 15 ++-- .../staking/contracts/test/TestFinalizer.sol | 15 ++-- .../contracts/test/TestProtocolFees.sol | 12 +-- contracts/staking/test/actors/staker_actor.ts | 12 +++ contracts/staking/test/epoch_test.ts | 2 - contracts/staking/test/params.ts | 5 +- contracts/staking/test/rewards_test.ts | 39 +++------ contracts/staking/test/stake_test.ts | 2 - .../test/unit_tests/delegator_reward_test.ts | 8 +- .../staking/test/unit_tests/finalizer_test.ts | 82 +++++++------------ contracts/staking/test/utils/api_wrapper.ts | 4 +- .../cumulative_reward_tracking_simulation.ts | 2 +- .../order-utils/src/staking_revert_errors.ts | 22 ----- 18 files changed, 109 insertions(+), 209 deletions(-) diff --git a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol index 4ff7f2a0ef..7d3fe19e34 100644 --- a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol +++ b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol @@ -57,12 +57,6 @@ library LibStakingRichErrors { PoolIsFull } - enum CumulativeRewardIntervalErrorCode { - BeginEpochMustBeLessThanEndEpoch, - BeginEpochDoesNotHaveReward, - EndEpochDoesNotHaveReward - } - // bytes4(keccak256("MiscalculatedRewardsError(uint256,uint256)")) bytes4 internal constant MISCALCULATED_REWARDS_ERROR_SELECTOR = 0xf7806c4e; @@ -159,10 +153,6 @@ 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; - // bytes4(keccak256("PreviousEpochNotFinalizedError(uint256,uint256)")) bytes4 internal constant PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR = 0x614b800a; @@ -478,25 +468,6 @@ 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 - ); - } - function PreviousEpochNotFinalizedError( uint256 unfinalizedEpoch, uint256 unfinalizedPoolsRemaining diff --git a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol index 06fd6c5c8f..a7498677a2 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol @@ -247,50 +247,17 @@ contract MixinCumulativeRewards is } // Sanity check interval - if (beginEpoch > endEpoch) { - LibRichErrors.rrevert( - LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors - .CumulativeRewardIntervalErrorCode - .BeginEpochMustBeLessThanEndEpoch, - poolId, - beginEpoch, - endEpoch - ) - ); - } + require(beginEpoch <= endEpoch, "CR_INTERVAL_INVALID"); // Sanity check begin reward IStructs.Fraction memory beginReward = _cumulativeRewardsByPool[poolId][beginEpoch]; - if (!_isCumulativeRewardSet(beginReward)) { - LibRichErrors.rrevert( - LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors - .CumulativeRewardIntervalErrorCode - .BeginEpochDoesNotHaveReward, - poolId, - beginEpoch, - endEpoch - ) - ); - } + require(_isCumulativeRewardSet(beginReward), "CR_INTERVAL_INVALID_BEGIN"); // Sanity check end reward IStructs.Fraction memory endReward = _cumulativeRewardsByPool[poolId][endEpoch]; - if (!_isCumulativeRewardSet(endReward)) { - LibRichErrors.rrevert( - LibStakingRichErrors.CumulativeRewardIntervalError( - LibStakingRichErrors - .CumulativeRewardIntervalErrorCode - .EndEpochDoesNotHaveReward, - poolId, - beginEpoch, - endEpoch - ) - ); - } + require(_isCumulativeRewardSet(endReward), "CR_INTERVAL_INVALID_END"); // Compute reward reward = LibFractions.scaleDifference( diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index d3cb07f36b..21e2f4df33 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -356,7 +356,7 @@ contract MixinStakingPoolRewards is // If the stake has been touched since the last reward epoch, // it has already been claimed. if (unsyncedStake.currentEpoch >= lastRewardEpoch) { - return 0; + return reward; } // From here we know: `unsyncedStake.currentEpoch < currentEpoch > 0`. diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 1b8980b771..8208141ba2 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -66,19 +66,19 @@ contract MixinFinalizer is returns (uint256 poolsRemaining) { uint256 closingEpoch = currentEpoch; + IStructs.UnfinalizedState memory state = unfinalizedState; // Make sure the previous epoch has been fully finalized. - if (poolsRemaining != 0) { + if (state.poolsRemaining != 0) { LibRichErrors.rrevert( LibStakingRichErrors.PreviousEpochNotFinalizedError( - closingEpoch.safeSub(1), - poolsRemaining + closingEpoch - 1, + state.poolsRemaining ) ); } // Set up unfinalized state. - IStructs.UnfinalizedState memory state; state.rewardsAvailable = _wrapBalanceToWETHAndGetBalance(); state.poolsRemaining = poolsRemaining = numActivePoolsThisEpoch; state.totalFeesCollected = totalFeesCollectedThisEpoch; diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index 91381b6848..c585aae3c8 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -172,6 +172,26 @@ contract MixinParams is } } + /// @dev Rescind the WETH allowance for `oldSpenders` and grant `newSpenders` + /// an unlimited allowance. + /// @param oldSpenders Addresses to remove allowance from. + /// @param newSpenders Addresses to grant allowance to. + function _transferWETHAllownces( + address[2] memory oldSpenders, + address[2] memory newSpenders + ) + internal + { + IEtherToken weth = IEtherToken(_getWETHAddress()); + // Grant new allowances. + for (uint256 i = 0; i < oldSpenders.length; i++) { + // Rescind old allowance. + weth.approve(oldSpenders[i], 0); + // Grant new allowance. + weth.approve(newSpenders[i], uint256(-1)); + } + } + /// @dev Set all configurable parameters at once. /// @param _epochDurationInSeconds Minimum seconds between epochs. /// @param _rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm. diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 6bf5dcaad0..88eb9b5ead 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -22,11 +22,11 @@ pragma experimental ABIEncoderV2; import "../src/interfaces/IStructs.sol"; import "../src/interfaces/IStakingPoolRewardVault.sol"; import "../src/interfaces/IEthVault.sol"; -import "./TestStaking.sol"; +import "./TestStakingNoWETH.sol"; contract TestDelegatorRewards is - TestStaking + TestStakingNoWETH { event RecordDepositToEthVault( address owner, @@ -67,9 +67,9 @@ contract TestDelegatorRewards is mapping (uint256 => mapping (bytes32 => UnfinalizedPoolReward)) private unfinalizedPoolRewardsByEpoch; - /// @dev Expose _finalizePool - function internalFinalizePool(bytes32 poolId) external { - _finalizePool(poolId); + /// @dev Expose the original finalizePool + function originalFinalizePool(bytes32 poolId) external { + MixinFinalizer.finalizePool(poolId); } /// @dev Set unfinalized rewards for a pool in the current epoch. @@ -233,10 +233,11 @@ contract TestDelegatorRewards is ); } + // solhint-disable no-simple-event-func-name /// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in /// the current epoch and emit a event, - function _finalizePool(bytes32 poolId) - internal + function finalizePool(bytes32 poolId) + public returns ( uint256 operatorReward, uint256 membersReward, diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 7b0acac671..2e897e914b 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -21,23 +21,18 @@ pragma experimental ABIEncoderV2; import "../src/interfaces/IStructs.sol"; import "../src/libs/LibCobbDouglas.sol"; -import "./TestStaking.sol"; +import "./TestStakingNoWETH.sol"; contract TestFinalizer is - TestStaking + TestStakingNoWETH { - event RecordStakingPoolRewards( + event DepositStakingPoolRewards( bytes32 poolId, - uint256 totalReward, + uint256 reward, uint256 membersStake ); - event DepositStakingPoolRewards( - uint256 operatorReward, - uint256 membersReward - ); - struct UnfinalizedPoolReward { uint256 totalReward; uint256 membersStake; @@ -160,7 +155,7 @@ contract TestFinalizer is _computeSplitStakingPoolRewards(operatorShare, reward, membersStake); address(_operatorRewardsReceiver).transfer(operatorReward); address(_membersRewardsReceiver).transfer(membersReward); - emit DepositStakingPoolRewards(operatorReward, membersReward); + emit DepositStakingPoolRewards(poolId, reward, membersStake); } /// @dev Overriden to just increase the epoch counter. diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index a9d98d5a6e..4fe8756290 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -68,18 +68,14 @@ contract TestProtocolFees is currentEpoch += 1; } - function getWethAssetData() external pure returns (bytes memory) { - return WETH_ASSET_DATA; - } - /// @dev Create a test pool. function createTestPool( bytes32 poolId, uint256 operatorStake, uint256 membersStake, - address[] memory makerAddresses + address[] calldata makerAddresses ) - public + external { TestPool storage pool = _testPools[poolId]; pool.operatorStake = operatorStake; @@ -102,6 +98,10 @@ contract TestProtocolFees is emit ERC20ProxyTransferFrom(assetData, from, to, amount); } + function getWethAssetData() external pure returns (bytes memory) { + return WETH_ASSET_DATA; + } + /// @dev Overridden to use test pools. function getStakingPoolIdOfMaker(address makerAddress) public diff --git a/contracts/staking/test/actors/staker_actor.ts b/contracts/staking/test/actors/staker_actor.ts index 07423f5668..cb240e43eb 100644 --- a/contracts/staking/test/actors/staker_actor.ts +++ b/contracts/staking/test/actors/staker_actor.ts @@ -155,6 +155,18 @@ export class StakerActor extends BaseActor { ); } + public async syncDelegatorRewardsAsync(poolId: string, revertError?: RevertError): Promise { + const txReceiptPromise = this._stakingApiWrapper.stakingContract.syncDelegatorRewards.awaitTransactionSuccessAsync( + poolId, + { from: this._owner }, + ); + if (revertError !== undefined) { + await expect(txReceiptPromise, 'expected revert error').to.revertWith(revertError); + return; + } + await txReceiptPromise; + } + public async goToNextEpochAsync(): Promise { // cache balances const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); diff --git a/contracts/staking/test/epoch_test.ts b/contracts/staking/test/epoch_test.ts index 13ca7e6635..ab5d470f34 100644 --- a/contracts/staking/test/epoch_test.ts +++ b/contracts/staking/test/epoch_test.ts @@ -2,8 +2,6 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; import { blockchainTests, expect } from '@0x/contracts-test-utils'; import * as _ from 'lodash'; -import { artifacts } from '../src'; - import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { constants as stakingConstants } from './utils/constants'; diff --git a/contracts/staking/test/params.ts b/contracts/staking/test/params.ts index 90d27ca819..abab21f8be 100644 --- a/contracts/staking/test/params.ts +++ b/contracts/staking/test/params.ts @@ -30,7 +30,10 @@ blockchainTests('Configurable Parameters unit tests', env => { }); blockchainTests.resets('setParams()', () => { - async function setParamsAndAssertAsync(params: Partial, from?: string): Promise { + async function setParamsAndAssertAsync( + params: Partial, + from?: string, + ): Promise { const _params = { ...stakingConstants.DEFAULT_PARAMS, ...params, diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 92902aca29..836a7e1d55 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -3,8 +3,6 @@ import { blockchainTests, describe, expect, shortZip } from '@0x/contracts-test- import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { artifacts } from '../src'; - import { FinalizerActor } from './actors/finalizer_actor'; import { PoolOperatorActor } from './actors/pool_operator_actor'; import { StakerActor } from './actors/staker_actor'; @@ -45,7 +43,7 @@ blockchainTests.resets('Testing Rewards', env => { stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); // set up staking parameters await stakingApiWrapper.utils.setParamsAsync({ - minimumPoolStake: new BigNumber(1), + minimumPoolStake: new BigNumber(2), cobbDouglasAlphaNumerator: new BigNumber(1), cobbDouglasAlphaDenominator: new BigNumber(6), rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address, @@ -60,7 +58,7 @@ blockchainTests.resets('Testing Rewards', env => { poolId = await poolOperator.createStakingPoolAsync(0, true); // Stake something in the pool or else it won't get any rewards. poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper); - await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(1)); + await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(2)); // set exchange address await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress); // associate operators for tracking in Finalizer @@ -609,18 +607,14 @@ blockchainTests.resets('Testing Rewards', env => { // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); - // Undelegate 0 stake to move the rewards into the EthVault. - await staker.moveStakeAsync( - new StakeInfo(StakeStatus.Delegated, poolId), - new StakeInfo(StakeStatus.Active), - toBaseUnitAmount(0), - ); + // Sync rewards to move the rewards into the EthVault. + await staker.syncDelegatorRewardsAsync(poolId); await validateEndBalances({ stakerRewardVaultBalance_1: toBaseUnitAmount(0), stakerEthVaultBalance_1: reward, }); }); - it(`should split payout between two delegators when undelegating`, async () => { + it(`should split payout between two delegators when syncing rewards`, async () => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both @@ -633,13 +627,9 @@ blockchainTests.resets('Testing Rewards', env => { // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); - // Undelegate 0 stake to move rewards from RewardVault into the EthVault. + // Sync rewards to move rewards from RewardVault into the EthVault. for (const [staker] of _.reverse(stakersAndStake)) { - await staker.moveStakeAsync( - new StakeInfo(StakeStatus.Delegated, poolId), - new StakeInfo(StakeStatus.Active), - toBaseUnitAmount(0), - ); + await staker.syncDelegatorRewardsAsync(poolId); } const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); await validateEndBalances({ @@ -651,7 +641,7 @@ blockchainTests.resets('Testing Rewards', env => { membersRewardVaultBalance: new BigNumber(1), // Rounding error }); }); - it(`delegator should not be credited payout twice by undelegating twice`, async () => { + it(`delegator should not be credited payout twice by syncing rewards twice`, async () => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both @@ -673,17 +663,10 @@ blockchainTests.resets('Testing Rewards', env => { poolRewardVaultBalance: reward, membersRewardVaultBalance: reward, }); - const undelegateZeroAsync = async (staker: StakerActor) => { - return staker.moveStakeAsync( - new StakeInfo(StakeStatus.Delegated, poolId), - new StakeInfo(StakeStatus.Active), - toBaseUnitAmount(0), - ); - }; - // First staker will undelegate 0 to get rewards transferred to EthVault. + // First staker will sync rewards to get rewards transferred to EthVault. const sneakyStaker = stakers[0]; const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0]; - await undelegateZeroAsync(sneakyStaker); + await sneakyStaker.syncDelegatorRewardsAsync(poolId); // Should have been credited the correct amount of rewards. let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( sneakyStaker.getOwner(), @@ -692,7 +675,7 @@ blockchainTests.resets('Testing Rewards', env => { sneakyStakerExpectedEthVaultBalance, ); // Now he'll try to do it again to see if he gets credited twice. - await undelegateZeroAsync(sneakyStaker); + await sneakyStaker.syncDelegatorRewardsAsync(poolId); /// The total amount credited should remain the same. sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( sneakyStaker.getOwner(), diff --git a/contracts/staking/test/stake_test.ts b/contracts/staking/test/stake_test.ts index bdeba00631..a8b0ad975d 100644 --- a/contracts/staking/test/stake_test.ts +++ b/contracts/staking/test/stake_test.ts @@ -4,8 +4,6 @@ import { StakingRevertErrors } from '@0x/order-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { artifacts } from '../src'; - import { StakerActor } from './actors/staker_actor'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { toBaseUnitAmount } from './utils/number_utils'; diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index a36a5ab97a..37d62ea8e3 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -223,7 +223,7 @@ blockchainTests.resets('delegator unit rewards', env => { } async function finalizePoolAsync(poolId: string): Promise> { - const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); + const receipt = await testContract.originalFinalizePool.awaitTransactionSuccessAsync(poolId); const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId); return { ethVaultDeposit, @@ -379,7 +379,7 @@ blockchainTests.resets('delegator unit rewards', env => { assertRoughlyEquals(delegatorReward, expectedDelegatorRewards); }); - it('has correct reward immediately after unstaking', async () => { + it('has correct reward immediately after undelegating', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) @@ -392,7 +392,7 @@ blockchainTests.resets('delegator unit rewards', env => { expect(delegatorReward).to.bignumber.eq(0); }); - it('has correct reward immediately after unstaking and restaking', async () => { + it('has correct reward immediately after undelegating and redelegating', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) @@ -406,7 +406,7 @@ blockchainTests.resets('delegator unit rewards', env => { expect(delegatorReward).to.bignumber.eq(0); }); - it('has correct reward immediately after unstaking, restaking, and rewarding fees', async () => { + it('has correct reward immediately after undelegating, redelegating, and rewarding fees', async () => { const poolId = hexRandom(); const { delegator, stake } = await delegateStakeAsync(poolId); await advanceEpochAsync(); // epoch 1 (stake now active) diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 581299ed77..45a29c452c 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -21,13 +21,8 @@ import { TestFinalizerContract, TestFinalizerDepositStakingPoolRewardsEventArgs as DepositStakingPoolRewardsEventArgs, TestFinalizerEvents, - TestFinalizerRecordStakingPoolRewardsEventArgs as RecordStakingPoolRewardsEventArgs, } from '../../src'; -import { - assertRoughlyEquals as _assertIntegerRoughlyEquals, - getRandomInteger, - toBaseUnitAmount, -} from '../utils/number_utils'; +import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils'; blockchainTests.resets('finalizer unit tests', env => { const { ZERO_AMOUNT } = constants; @@ -90,7 +85,7 @@ blockchainTests.resets('finalizer unit tests', env => { return _opts; } - interface FinalizationState { + interface UnfinalizedState { rewardsAvailable: Numberish; poolsRemaining: number; totalFeesCollected: Numberish; @@ -98,7 +93,7 @@ blockchainTests.resets('finalizer unit tests', env => { totalRewardsFinalized: Numberish; } - async function getUnfinalizedStateAsync(): Promise { + async function getUnfinalizedStateAsync(): Promise { const r = await testContract.unfinalizedState.callAsync(); return { rewardsAvailable: r[0], @@ -113,12 +108,12 @@ blockchainTests.resets('finalizer unit tests', env => { const logs = [] as LogEntry[]; for (const poolId of poolIds) { const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); - logs.splice(logs.length - 1, 0, ...receipt.logs); + logs.splice(logs.length, 0, ...receipt.logs); } return logs; } - async function assertFinalizationStateAsync(expected: Partial): Promise { + async function assertUnfinalizedStateAsync(expected: Partial): Promise { const actual = await getUnfinalizedStateAsync(); assertEqualNumberFields(actual, expected); } @@ -135,10 +130,6 @@ blockchainTests.resets('finalizer unit tests', env => { assertEqualNumberFields(events[0], args); } - function assertRoughlyEquals(actual: Numberish, expected: Numberish): void { - _assertIntegerRoughlyEquals(actual, expected, 5); - } - function assertEqualNumberFields(actual: T, expected: Partial): void { for (const key of Object.keys(actual)) { const a = (actual as any)[key] as BigNumber; @@ -171,42 +162,33 @@ blockchainTests.resets('finalizer unit tests', env => { const reward = poolRewards[i]; const [operatorReward, membersReward] = splitRewards(pool, reward); expect(event.epoch).to.bignumber.eq(currentEpoch); - assertRoughlyEquals(event.operatorReward, operatorReward); - assertRoughlyEquals(event.membersReward, membersReward); + assertIntegerRoughlyEquals(event.operatorReward, operatorReward); + assertIntegerRoughlyEquals(event.membersReward, membersReward); } - // Assert the `RecordStakingPoolRewards` logs. - const recordStakingPoolRewardsEvents = getRecordStakingPoolRewardsEvents(finalizationLogs); - expect(recordStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length); - for (const i of _.times(recordStakingPoolRewardsEvents.length)) { - const event = recordStakingPoolRewardsEvents[i]; + // Assert the `DepositStakingPoolRewards` logs. + const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs); + expect(depositStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length); + for (const i of _.times(depositStakingPoolRewardsEvents.length)) { + const event = depositStakingPoolRewardsEvents[i]; const pool = poolsWithStake[i]; const reward = poolRewards[i]; expect(event.poolId).to.eq(pool.poolId); - assertRoughlyEquals(event.totalReward, reward); - assertRoughlyEquals(event.membersStake, pool.membersStake); + assertIntegerRoughlyEquals(event.reward, reward); + assertIntegerRoughlyEquals(event.membersStake, pool.membersStake); } - - // Assert the `DepositStakingPoolRewards` logs. // Make sure they all sum up to the totals. - const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs); if (depositStakingPoolRewardsEvents.length > 0) { - const totalDepositedOperatorRewards = BigNumber.sum( - ...depositStakingPoolRewardsEvents.map(e => e.operatorReward), - ); - const totalDepositedMembersRewards = BigNumber.sum( - ...depositStakingPoolRewardsEvents.map(e => e.membersReward), - ); - assertRoughlyEquals(totalDepositedOperatorRewards, totalOperatorRewards); - assertRoughlyEquals(totalDepositedMembersRewards, totalMembersRewards); + const totalDepositRewards = BigNumber.sum(...depositStakingPoolRewardsEvents.map(e => e.reward)); + assertIntegerRoughlyEquals(totalDepositRewards, totalRewards); } // Assert the `EpochFinalized` logs. const epochFinalizedEvents = getEpochFinalizedEvents(finalizationLogs); expect(epochFinalizedEvents.length).to.eq(1); expect(epochFinalizedEvents[0].epoch).to.bignumber.eq(currentEpoch - 1); - assertRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards); - assertRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining); + assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards); + assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining); // Assert the receiver balances. await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards); @@ -214,9 +196,9 @@ blockchainTests.resets('finalizer unit tests', env => { async function assertReceiverBalancesAsync(operatorRewards: Numberish, membersRewards: Numberish): Promise { const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver); - assertRoughlyEquals(operatorRewardsBalance, operatorRewards); + assertIntegerRoughlyEquals(operatorRewardsBalance, operatorRewards); const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver); - assertRoughlyEquals(membersRewardsBalance, membersRewards); + assertIntegerRoughlyEquals(membersRewardsBalance, membersRewards); } async function calculatePoolRewardsAsync( @@ -269,13 +251,6 @@ blockchainTests.resets('finalizer unit tests', env => { return filterLogsToArguments(logs, IStakingEventsEvents.EpochFinalized); } - function getRecordStakingPoolRewardsEvents(logs: LogEntry[]): RecordStakingPoolRewardsEventArgs[] { - return filterLogsToArguments( - logs, - TestFinalizerEvents.RecordStakingPoolRewards, - ); - } - function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] { return filterLogsToArguments( logs, @@ -338,18 +313,19 @@ blockchainTests.resets('finalizer unit tests', env => { await testContract.endEpoch.awaitTransactionSuccessAsync(); const epoch = await testContract.currentEpoch.callAsync(); expect(epoch).to.bignumber.eq(INITIAL_EPOCH + 1); - return assertFinalizationStateAsync({ - poolsRemaining: 0, - totalFeesCollected: 0, - totalWeightedStake: 0, - }); + const numActivePools = await testContract.numActivePoolsThisEpoch.callAsync(); + const totalFees = await testContract.totalFeesCollectedThisEpoch.callAsync(); + const totalStake = await testContract.totalWeightedStakeThisEpoch.callAsync(); + expect(numActivePools).to.bignumber.eq(0); + expect(totalFees).to.bignumber.eq(0); + expect(totalStake).to.bignumber.eq(0); }); - it('prepares finalization state', async () => { + it('prepares unfinalized state', async () => { // Add a pool so there is state to clear. const pool = await addActivePoolAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync(); - return assertFinalizationStateAsync({ + return assertUnfinalizedStateAsync({ poolsRemaining: 1, rewardsAvailable: INITIAL_BALANCE, totalFeesCollected: pool.feesCollected, @@ -489,7 +465,7 @@ blockchainTests.resets('finalizer unit tests', env => { const finalizeLogs = await finalizePoolsAsync(poolIds); const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(finalizeLogs)[0]; await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); - const {logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync(); + const { logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0]; expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 7b999c411f..0d50fd91aa 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -55,9 +55,7 @@ export class StakingApiWrapper { let totalGasUsed = 0; const allLogs = [] as DecodedLogs; for (const poolId of endOfEpochInfo.activePoolIds) { - const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync( - poolId, - ); + const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync(poolId); totalGasUsed += receipt.gasUsed; allLogs.splice(allLogs.length, 0, ...(receipt.logs as DecodedLogs)); } diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index 40a388bb72..7549e709b8 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -175,7 +175,7 @@ export class CumulativeRewardTrackingSimulation { if (receipt !== undefined) { logs = receipt.logs as DecodedLogs; } - combinedLogs.splice(combinedLogs.length - 1, 0, ...logs); + combinedLogs.splice(combinedLogs.length, 0, ...logs); } return combinedLogs; } diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index 0d0f53dff4..bce6f135d3 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -40,12 +40,6 @@ 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( @@ -244,21 +238,6 @@ 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 }, - ); - } -} - export class PreviousEpochNotFinalizedError extends RevertError { constructor(closingEpoch?: BigNumber | number | string, unfinalizedPoolsRemaining?: BigNumber | number | string) { super( @@ -272,7 +251,6 @@ export class PreviousEpochNotFinalizedError extends RevertError { const types = [ AmountExceedsBalanceOfPoolError, BlockTimestampTooLowError, - CumulativeRewardIntervalError, EthVaultNotSetError, ExchangeAddressAlreadyRegisteredError, ExchangeAddressNotRegisteredError, From aa4ee2c166103d97bc1353b847fd9a32384604ab Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sun, 22 Sep 2019 13:08:59 -0400 Subject: [PATCH 46/52] `@0x/dev-utils`: Increase gas limit to 9e6 --- packages/dev-utils/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-utils/src/constants.ts b/packages/dev-utils/src/constants.ts index 7d4273e53c..4c7dae589a 100644 --- a/packages/dev-utils/src/constants.ts +++ b/packages/dev-utils/src/constants.ts @@ -1,6 +1,6 @@ export const constants = { RPC_URL: 'http://localhost:8545', RPC_PORT: 8545, - GAS_LIMIT: 8000000, + GAS_LIMIT: 9e6, TESTRPC_FIRST_ADDRESS: '0x5409ed021d9299bf6814279a6a1411a7e866a631', }; From e4126189dfd0bc02f1fbc1a1a6a29964be0d2eb9 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sun, 22 Sep 2019 13:09:44 -0400 Subject: [PATCH 47/52] `@0x/order-utils`: Fix staking error codes after rebase. --- packages/order-utils/src/staking_revert_errors.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index bce6f135d3..fccd286a05 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -23,16 +23,12 @@ export enum InvalidParamValueErrorCode { InvalidCobbDouglasAlpha, InvalidRewardDelegatedStakeWeight, InvalidMaximumMakersInPool, - InvalidWethProxyAddress, - InvalidEthVaultAddress, - InvalidRewardVaultAddress, - InvalidZrxVaultAddress, - InvalidEpochDuration, InvalidMinimumPoolStake, InvalidWethProxyAddress, InvalidEthVaultAddress, InvalidRewardVaultAddress, InvalidZrxVaultAddress, + InvalidEpochDuration, } export enum InitializationErrorCode { From eac4520406f0bc9e9a955c1728da0b68c597fe25 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sun, 22 Sep 2019 13:11:42 -0400 Subject: [PATCH 48/52] `@0x/contracts-staking`: Rebase against 3.0 --- contracts/staking/contracts/src/Staking.sol | 2 +- .../staking/contracts/src/StakingProxy.sol | 2 +- .../staking/contracts/src/sys/MixinParams.sol | 20 ---------- .../staking/contracts/src/vaults/EthVault.sol | 1 - .../src/vaults/StakingPoolRewardVault.sol | 3 +- .../staking/contracts/src/vaults/ZrxVault.sol | 3 +- .../test/TestAssertStorageParams.sol | 1 + .../contracts/test/TestStorageLayout.sol | 1 - contracts/staking/package.json | 2 +- contracts/staking/src/artifacts.ts | 6 ++- contracts/staking/src/wrappers.ts | 3 +- contracts/staking/test/migration.ts | 2 +- contracts/staking/test/params.ts | 39 ++++++++++++++++++- contracts/staking/test/utils/api_wrapper.ts | 5 +-- contracts/staking/tsconfig.json | 3 +- 15 files changed, 54 insertions(+), 39 deletions(-) diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index f8810a1028..c0864d0961 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -69,7 +69,7 @@ contract Staking is address payable _rewardVaultAddress, address _zrxVaultAddress ) - external + public onlyAuthorized { // DANGER! When performing upgrades, take care to modify this logic diff --git a/contracts/staking/contracts/src/StakingProxy.sol b/contracts/staking/contracts/src/StakingProxy.sol index 85045ed24d..173fd67d78 100644 --- a/contracts/staking/contracts/src/StakingProxy.sol +++ b/contracts/staking/contracts/src/StakingProxy.sol @@ -210,7 +210,7 @@ contract StakingProxy is } // Minimum stake must be > 1 - if (minimumStake < 2) { + if (minimumPoolStake < 2) { LibRichErrors.rrevert( LibStakingRichErrors.InvalidParamValueError( LibStakingRichErrors.InvalidParamValueErrorCode.InvalidMinimumPoolStake diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index c585aae3c8..9c0856b4ab 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -246,24 +246,4 @@ contract MixinParams is _zrxVaultAddress ); } - - /// @dev Rescind the WETH allowance for `oldSpenders` and grant `newSpenders` - /// an unlimited allowance. - /// @param oldSpenders Addresses to remove allowance from. - /// @param newSpenders Addresses to grant allowance to. - function _transferWETHAllownces( - address[2] memory oldSpenders, - address[2] memory newSpenders - ) - internal - { - IEtherToken weth = IEtherToken(_getWETHAddress()); - // Grant new allowances. - for (uint256 i = 0; i < oldSpenders.length; i++) { - // Rescind old allowance. - weth.approve(oldSpenders[i], 0); - // Grant new allowance. - weth.approve(newSpenders[i], uint256(-1)); - } - } } diff --git a/contracts/staking/contracts/src/vaults/EthVault.sol b/contracts/staking/contracts/src/vaults/EthVault.sol index 374d548451..d53f8b9614 100644 --- a/contracts/staking/contracts/src/vaults/EthVault.sol +++ b/contracts/staking/contracts/src/vaults/EthVault.sol @@ -28,7 +28,6 @@ import "./MixinVaultCore.sol"; contract EthVault is IEthVault, IVaultCore, - Ownable, MixinVaultCore { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index 682755412b..bc31598322 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -24,15 +24,14 @@ import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../libs/LibStakingRichErrors.sol"; import "../libs/LibSafeDowncast.sol"; -import "./MixinVaultCore.sol"; import "../interfaces/IStakingPoolRewardVault.sol"; +import "./MixinVaultCore.sol"; /// @dev This vault manages staking pool rewards. contract StakingPoolRewardVault is IStakingPoolRewardVault, IVaultCore, - Ownable, MixinVaultCore { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/vaults/ZrxVault.sol b/contracts/staking/contracts/src/vaults/ZrxVault.sol index 91f2f4bfcd..0ff8ef81ab 100644 --- a/contracts/staking/contracts/src/vaults/ZrxVault.sol +++ b/contracts/staking/contracts/src/vaults/ZrxVault.sol @@ -18,11 +18,11 @@ pragma solidity ^0.5.9; -import "../interfaces/IZrxVault.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "../interfaces/IZrxVault.sol"; import "./MixinVaultCore.sol"; @@ -36,7 +36,6 @@ import "./MixinVaultCore.sol"; contract ZrxVault is IVaultCore, IZrxVault, - Ownable, MixinVaultCore { using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/test/TestAssertStorageParams.sol b/contracts/staking/contracts/test/TestAssertStorageParams.sol index 7f05c934eb..07a3213723 100644 --- a/contracts/staking/contracts/test/TestAssertStorageParams.sol +++ b/contracts/staking/contracts/test/TestAssertStorageParams.sol @@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2; import "../src/StakingProxy.sol"; +// solhint-disable no-empty-blocks contract TestAssertStorageParams is StakingProxy { diff --git a/contracts/staking/contracts/test/TestStorageLayout.sol b/contracts/staking/contracts/test/TestStorageLayout.sol index fdb00b505a..3743525d8e 100644 --- a/contracts/staking/contracts/test/TestStorageLayout.sol +++ b/contracts/staking/contracts/test/TestStorageLayout.sol @@ -25,7 +25,6 @@ import "../src/interfaces/IStructs.sol"; contract TestStorageLayout is - MixinConstants, Ownable, MixinStorage { diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 6524ed8f68..4964728dd0 100644 --- a/contracts/staking/package.json +++ b/contracts/staking/package.json @@ -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|IStorage|IStorageInit|IStructs|IVaultCore|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolMakers|MixinStakingPoolModifiers|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinVaultCore|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json" + "abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStorage|IStorageInit|IStructs|IVaultCore|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolMakers|MixinStakingPoolModifiers|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinVaultCore|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayout|ZrxVault).json" }, "repository": { "type": "git", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index ee324dbada..3845d4a837 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -54,10 +54,11 @@ import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json' import * as TestLibProxy from '../generated-artifacts/TestLibProxy.json'; import * as TestLibProxyReceiver from '../generated-artifacts/TestLibProxyReceiver.json'; import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json'; +import * as TestMixinParams from '../generated-artifacts/TestMixinParams.json'; import * as TestMixinVaultCore from '../generated-artifacts/TestMixinVaultCore.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; -import * as TestProtocolFeesERC20Proxy from '../generated-artifacts/TestProtocolFeesERC20Proxy.json'; import * as TestStaking from '../generated-artifacts/TestStaking.json'; +import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json'; import * as TestStakingProxy from '../generated-artifacts/TestStakingProxy.json'; import * as TestStorageLayout from '../generated-artifacts/TestStorageLayout.json'; import * as ZrxVault from '../generated-artifacts/ZrxVault.json'; @@ -112,10 +113,11 @@ export const artifacts = { TestLibProxy: TestLibProxy as ContractArtifact, TestLibProxyReceiver: TestLibProxyReceiver as ContractArtifact, TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact, + TestMixinParams: TestMixinParams as ContractArtifact, TestMixinVaultCore: TestMixinVaultCore as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact, - TestProtocolFeesERC20Proxy: TestProtocolFeesERC20Proxy as ContractArtifact, TestStaking: TestStaking as ContractArtifact, + TestStakingNoWETH: TestStakingNoWETH as ContractArtifact, TestStakingProxy: TestStakingProxy as ContractArtifact, TestStorageLayout: TestStorageLayout as ContractArtifact, }; diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 916c8f8e69..5af5125178 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -52,10 +52,11 @@ export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_lib_proxy'; export * from '../generated-wrappers/test_lib_proxy_receiver'; export * from '../generated-wrappers/test_lib_safe_downcast'; +export * from '../generated-wrappers/test_mixin_params'; export * from '../generated-wrappers/test_mixin_vault_core'; export * from '../generated-wrappers/test_protocol_fees'; -export * from '../generated-wrappers/test_protocol_fees_erc20_proxy'; export * from '../generated-wrappers/test_staking'; +export * from '../generated-wrappers/test_staking_no_w_e_t_h'; export * from '../generated-wrappers/test_staking_proxy'; export * from '../generated-wrappers/test_storage_layout'; export * from '../generated-wrappers/zrx_vault'; diff --git a/contracts/staking/test/migration.ts b/contracts/staking/test/migration.ts index 473e51ba37..7ceec54cfa 100644 --- a/contracts/staking/test/migration.ts +++ b/contracts/staking/test/migration.ts @@ -24,7 +24,7 @@ blockchainTests('Migration tests', env => { before(async () => { [authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync(); stakingContract = await StakingContract.deployFrom0xArtifactAsync( - artifacts.Staking, + artifacts.TestStakingNoWETH, env.provider, env.txDefaults, artifacts, diff --git a/contracts/staking/test/params.ts b/contracts/staking/test/params.ts index abab21f8be..3b969a25eb 100644 --- a/contracts/staking/test/params.ts +++ b/contracts/staking/test/params.ts @@ -1,4 +1,4 @@ -import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils'; +import { blockchainTests, constants, expect, filterLogsToArguments, randomAddress } from '@0x/contracts-test-utils'; import { AuthorizableRevertErrors, BigNumber } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -89,6 +89,43 @@ blockchainTests('Configurable Parameters unit tests', env => { it('works if called by owner', async () => { return setParamsAndAssertAsync({}); }); + + describe('WETH allowance', () => { + it('rescinds allowance for old vaults and grants unlimited allowance to new ones', async () => { + const [oldEthVaultAddress, oldRewardVaultAddress, newEthVaultAddress, newRewardVaultAddress] = _.times( + 4, + () => randomAddress(), + ); + await testContract.setVaultAddresses.awaitTransactionSuccessAsync( + oldEthVaultAddress, + oldRewardVaultAddress, + ); + const { logs } = await setParamsAndAssertAsync({ + ethVaultAddress: newEthVaultAddress, + rewardVaultAddress: newRewardVaultAddress, + }); + const approveEvents = filterLogsToArguments( + logs, + TestMixinParamsEvents.WETHApprove, + ); + expect(approveEvents[0]).to.deep.eq({ + spender: oldEthVaultAddress, + amount: constants.ZERO_AMOUNT, + }); + expect(approveEvents[1]).to.deep.eq({ + spender: newEthVaultAddress, + amount: constants.MAX_UINT256, + }); + expect(approveEvents[2]).to.deep.eq({ + spender: oldRewardVaultAddress, + amount: constants.ZERO_AMOUNT, + }); + expect(approveEvents[3]).to.deep.eq({ + spender: newRewardVaultAddress, + amount: constants.MAX_UINT256, + }); + }); + }); }); }); // tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 0d50fd91aa..d5a8de30b8 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -1,7 +1,7 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; -import { BigNumber, logUtils } from '@0x/utils'; +import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -52,14 +52,11 @@ export class StakingApiWrapper { skipToNextEpochAndFinalizeAsync: async (): Promise => { await this.utils.fastForwardToNextEpochAsync(); const endOfEpochInfo = await this.utils.endEpochAsync(); - let totalGasUsed = 0; const allLogs = [] as DecodedLogs; for (const poolId of endOfEpochInfo.activePoolIds) { const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync(poolId); - totalGasUsed += receipt.gasUsed; allLogs.splice(allLogs.length, 0, ...(receipt.logs as DecodedLogs)); } - logUtils.log(`Finalization cost ${totalGasUsed} gas`); return allLogs; }, diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 9407f7f7b2..ec1b3032eb 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -52,10 +52,11 @@ "generated-artifacts/TestLibProxy.json", "generated-artifacts/TestLibProxyReceiver.json", "generated-artifacts/TestLibSafeDowncast.json", + "generated-artifacts/TestMixinParams.json", "generated-artifacts/TestMixinVaultCore.json", "generated-artifacts/TestProtocolFees.json", - "generated-artifacts/TestProtocolFeesERC20Proxy.json", "generated-artifacts/TestStaking.json", + "generated-artifacts/TestStakingNoWETH.json", "generated-artifacts/TestStakingProxy.json", "generated-artifacts/TestStorageLayout.json", "generated-artifacts/ZrxVault.json" From 6d20f0e9879e56159adbd5ff201b53537219752c Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sun, 22 Sep 2019 13:20:47 -0400 Subject: [PATCH 49/52] `@0x/contracts-staking`: Update compiler version in truffle config. --- contracts/staking/truffle-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking/truffle-config.js b/contracts/staking/truffle-config.js index 2bcbbed1e2..a5eb0c0dd7 100644 --- a/contracts/staking/truffle-config.js +++ b/contracts/staking/truffle-config.js @@ -82,7 +82,7 @@ module.exports = { // Configure your compilers compilers: { solc: { - version: '0.5.9', + version: '0.5.11', settings: { evmVersion: 'constantinople', optimizer: { From 9dd8c61a2f890491449a6622819ea0c0f41472b6 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 23 Sep 2019 11:55:02 -0400 Subject: [PATCH 50/52] `@0x/contract-staking`: Address review nits. `@0x/contracts-utils`: Use `safeDiv()` in `LibFractions.normalize()`. --- .../staking/contracts/src/fees/MixinExchangeFees.sol | 2 +- .../staking/test/unit_tests/delegator_reward_test.ts | 10 ++++------ contracts/staking/test/unit_tests/finalizer_test.ts | 9 +++++++++ contracts/utils/contracts/src/LibFractions.sol | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index f537a6a0d6..c838a8f50d 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -112,7 +112,7 @@ contract MixinExchangeFees is totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake); - // Increase the numberof active pools. + // Increase the number of active pools. numActivePoolsThisEpoch += 1; // Emit an event so keepers know what pools to pass into diff --git a/contracts/staking/test/unit_tests/delegator_reward_test.ts b/contracts/staking/test/unit_tests/delegator_reward_test.ts index 37d62ea8e3..f028db6b19 100644 --- a/contracts/staking/test/unit_tests/delegator_reward_test.ts +++ b/contracts/staking/test/unit_tests/delegator_reward_test.ts @@ -185,11 +185,9 @@ blockchainTests.resets('delegator unit rewards', env => { logs, TestDelegatorRewardsEvents.RecordDepositToEthVault, ); - if (ethVaultDepositArgs.length > 0 && delegator !== undefined) { - for (const event of ethVaultDepositArgs) { - if (event.owner === delegator) { - ethVaultDeposit = ethVaultDeposit.plus(event.amount); - } + for (const event of ethVaultDepositArgs) { + if (event.owner === delegator) { + ethVaultDeposit = ethVaultDeposit.plus(event.amount); } } const rewardVaultDepositArgs = filterLogsToArguments( @@ -420,7 +418,7 @@ blockchainTests.resets('delegator unit rewards', env => { // rewards paid for stake in epoch 3. const { membersReward: reward } = await rewardPoolAsync({ poolId, membersStake: stake }); const delegatorReward = await getDelegatorRewardBalanceAsync(poolId, delegator); - expect(delegatorReward).to.bignumber.eq(reward); + assertRoughlyEquals(delegatorReward, reward); }); it('ignores rewards paid in the same epoch the stake was first active in', async () => { diff --git a/contracts/staking/test/unit_tests/finalizer_test.ts b/contracts/staking/test/unit_tests/finalizer_test.ts index 45a29c452c..ea86acb7f3 100644 --- a/contracts/staking/test/unit_tests/finalizer_test.ts +++ b/contracts/staking/test/unit_tests/finalizer_test.ts @@ -556,6 +556,15 @@ blockchainTests.resets('finalizer unit tests', env => { }); }); + it('computes a reward with 0% < operatorShare < 100%', async () => { + const pool = await addActivePoolAsync({ operatorShare: Math.random() }); + await testContract.endEpoch.awaitTransactionSuccessAsync(); + return assertUnfinalizedPoolRewardsAsync(pool.poolId, { + totalReward: INITIAL_BALANCE, + membersStake: pool.membersStake, + }); + }); + it('computes a reward with 100% operatorShare', async () => { const pool = await addActivePoolAsync({ operatorShare: 1 }); await testContract.endEpoch.awaitTransactionSuccessAsync(); diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index bc6f441f02..f5a05706c6 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -64,7 +64,7 @@ library LibFractions { // re-scale them by `maxValue` to prevent overflows in future operations. if (numerator > maxValue || denominator > maxValue) { uint256 rescaleBase = numerator >= denominator ? numerator : denominator; - rescaleBase /= maxValue; + rescaleBase = rescaleBase.safeDiv(maxValue); scaledNumerator = numerator.safeDiv(rescaleBase); scaledDenominator = denominator.safeDiv(rescaleBase); } else { From 196cc4313f24eb2e6d775b69b995a70750c1dbbf Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 23 Sep 2019 13:14:14 -0400 Subject: [PATCH 51/52] `@0x/contract-staking`: Make solidity uniformly hideous ;-). --- .../contracts/src/fees/MixinExchangeFees.sol | 9 +++---- .../contracts/src/libs/LibCobbDouglas.sol | 4 +-- .../staking_pools/MixinCumulativeRewards.sol | 3 +-- .../staking_pools/MixinStakingPoolRewards.sol | 15 ++++------- .../contracts/src/sys/MixinFinalizer.sol | 3 +-- .../contracts/test/TestDelegatorRewards.sol | 27 +++++++------------ .../staking/contracts/test/TestFinalizer.sol | 17 +++++++----- 7 files changed, 32 insertions(+), 46 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index c838a8f50d..91e9f92403 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -89,8 +89,7 @@ contract MixinExchangeFees is return; } - uint256 poolStake = - getTotalStakeDelegatedToPool(poolId).currentEpochBalance; + uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; // Ignore pools with dust stake. if (poolStake < minimumPoolStake) { return; @@ -105,12 +104,10 @@ contract MixinExchangeFees is // If the pool was previously inactive in this epoch, initialize it. if (pool.feesCollected == 0) { // Compute member and total weighted stake. - (pool.membersStake, pool.weightedStake) = - _computeMembersAndWeightedStake(poolId, poolStake); + (pool.membersStake, pool.weightedStake) = _computeMembersAndWeightedStake(poolId, poolStake); // Increase the total weighted stake. - totalWeightedStakeThisEpoch = - totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake); + totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake); // Increase the number of active pools. numActivePoolsThisEpoch += 1; diff --git a/contracts/staking/contracts/src/libs/LibCobbDouglas.sol b/contracts/staking/contracts/src/libs/LibCobbDouglas.sol index aec288fe1f..347758bde2 100644 --- a/contracts/staking/contracts/src/libs/LibCobbDouglas.sol +++ b/contracts/staking/contracts/src/libs/LibCobbDouglas.sol @@ -71,9 +71,9 @@ library LibCobbDouglas { // `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))` // Compute - // `e^(alpha * (ln(feeRatio/stakeRatio)))` if feeRatio <= stakeRatio + // `e^(alpha * ln(feeRatio/stakeRatio))` if feeRatio <= stakeRatio // or - // `e^(ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio + // `e^(alpa * ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio int256 n = feeRatio <= stakeRatio ? LibFixedMath.div(feeRatio, stakeRatio) : LibFixedMath.div(stakeRatio, feeRatio); diff --git a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol index a7498677a2..decfa1fc05 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinCumulativeRewards.sol @@ -162,8 +162,7 @@ contract MixinCumulativeRewards is internal { // Check if we should do any work - uint256 currentMostRecentEpoch = - _cumulativeRewardsByPoolLastStored[poolId]; + uint256 currentMostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId]; if (epoch == currentMostRecentEpoch) { return; } diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol index 21e2f4df33..f52f89d325 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol @@ -81,8 +81,7 @@ contract MixinStakingPoolRewards is // rewards. IStructs.Pool memory pool = _poolById[poolId]; // Get any unfinalized rewards. - (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = - _getUnfinalizedPoolRewards(poolId); + (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); // Get the operators' portion. (reward,) = _computeSplitStakingPoolRewards( pool.operatorShare, @@ -203,21 +202,18 @@ contract MixinStakingPoolRewards is // 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. - IStructs.Fraction memory mostRecentCumulativeReward = - _getMostRecentCumulativeReward(poolId); + IStructs.Fraction memory mostRecentCumulativeReward = _getMostRecentCumulativeReward(poolId); // Compute new cumulative reward IStructs.Fraction memory cumulativeReward; - (cumulativeReward.numerator, cumulativeReward.denominator) = - LibFractions.add( + (cumulativeReward.numerator, cumulativeReward.denominator) = LibFractions.add( mostRecentCumulativeReward.numerator, mostRecentCumulativeReward.denominator, membersReward, membersStake ); // Normalize to prevent overflows. - (cumulativeReward.numerator, cumulativeReward.denominator) = - LibFractions.normalize( + (cumulativeReward.numerator, cumulativeReward.denominator) = LibFractions.normalize( cumulativeReward.numerator, cumulativeReward.denominator ); @@ -404,8 +400,7 @@ contract MixinStakingPoolRewards is // Get the most recent cumulative reward, which will serve as a // reference point when updating dependencies - IStructs.Fraction memory mostRecentCumulativeReward = - _getMostRecentCumulativeReward(poolId); + IStructs.Fraction memory mostRecentCumulativeReward = _getMostRecentCumulativeReward(poolId); // Record dependency on current epoch. if (_delegatedStakeToPoolByOwner.currentEpochBalance != 0 diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 8208141ba2..624e724d4f 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -286,8 +286,7 @@ contract MixinFinalizer is // Clip the reward to always be under // `rewardsAvailable - totalRewardsPaid`, // in case cobb-douglas overflows, which should be unlikely. - uint256 rewardsRemaining = - state.rewardsAvailable.safeSub(state.totalRewardsFinalized); + uint256 rewardsRemaining = state.rewardsAvailable.safeSub(state.totalRewardsFinalized); if (rewardsRemaining < rewards) { rewards = rewardsRemaining; } diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 88eb9b5ead..1686f49e4b 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -82,8 +82,7 @@ contract TestDelegatorRewards is ) external { - unfinalizedPoolRewardsByEpoch[currentEpoch][poolId] = - UnfinalizedPoolReward({ + unfinalizedPoolRewardsByEpoch[currentEpoch][poolId] = UnfinalizedPoolReward({ operatorReward: operatorReward, membersReward: membersReward, membersStake: membersStake @@ -132,10 +131,8 @@ contract TestDelegatorRewards is external { _initGenesisCumulativeRewards(poolId); - IStructs.StoredBalance memory initialStake = - _delegatedStakeToPoolByOwner[delegator][poolId]; - IStructs.StoredBalance storage _stake = - _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance storage _stake = _delegatedStakeToPoolByOwner[delegator][poolId]; _stake.isInitialized = true; _stake.currentEpochBalance += uint96(stake); _stake.nextEpochBalance += uint96(stake); @@ -159,10 +156,8 @@ contract TestDelegatorRewards is external { _initGenesisCumulativeRewards(poolId); - IStructs.StoredBalance memory initialStake = - _delegatedStakeToPoolByOwner[delegator][poolId]; - IStructs.StoredBalance storage _stake = - _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance storage _stake = _delegatedStakeToPoolByOwner[delegator][poolId]; if (_stake.currentEpoch < currentEpoch) { _stake.currentEpochBalance = _stake.nextEpochBalance; } @@ -188,10 +183,8 @@ contract TestDelegatorRewards is external { _initGenesisCumulativeRewards(poolId); - IStructs.StoredBalance memory initialStake = - _delegatedStakeToPoolByOwner[delegator][poolId]; - IStructs.StoredBalance storage _stake = - _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance memory initialStake = _delegatedStakeToPoolByOwner[delegator][poolId]; + IStructs.StoredBalance storage _stake = _delegatedStakeToPoolByOwner[delegator][poolId]; if (_stake.currentEpoch < currentEpoch) { _stake.currentEpochBalance = _stake.nextEpochBalance; } @@ -244,8 +237,7 @@ contract TestDelegatorRewards is uint256 membersStake ) { - UnfinalizedPoolReward memory reward = - unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; + UnfinalizedPoolReward memory reward = unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; delete unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; _setOperatorShare(poolId, reward.operatorReward, reward.membersReward); @@ -266,8 +258,7 @@ contract TestDelegatorRewards is uint256 membersStake ) { - UnfinalizedPoolReward storage reward = - unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; + UnfinalizedPoolReward storage reward = unfinalizedPoolRewardsByEpoch[currentEpoch][poolId]; totalReward = reward.operatorReward + reward.membersReward; membersStake = reward.membersStake; } diff --git a/contracts/staking/contracts/test/TestFinalizer.sol b/contracts/staking/contracts/test/TestFinalizer.sol index 2e897e914b..4ff64b5d33 100644 --- a/contracts/staking/contracts/test/TestFinalizer.sol +++ b/contracts/staking/contracts/test/TestFinalizer.sol @@ -77,8 +77,9 @@ contract TestFinalizer is external { require(feesCollected > 0, "FEES_MUST_BE_NONZERO"); - mapping (bytes32 => IStructs.ActivePool) storage activePools = - _getActivePoolsFromEpoch(currentEpoch); + mapping (bytes32 => IStructs.ActivePool) storage activePools = _getActivePoolsFromEpoch( + currentEpoch + ); IStructs.ActivePool memory pool = activePools[poolId]; require(pool.feesCollected == 0, "POOL_ALREADY_ADDED"); _operatorSharesByPool[poolId] = operatorShare; @@ -128,8 +129,9 @@ contract TestFinalizer is view returns (UnfinalizedPoolReward memory reward) { - (reward.totalReward, reward.membersStake) = - _getUnfinalizedPoolRewards(poolId); + (reward.totalReward, reward.membersStake) = _getUnfinalizedPoolRewards( + poolId + ); } /// @dev Expose `_getActivePoolFromEpoch`. @@ -151,8 +153,11 @@ contract TestFinalizer is returns (uint256 operatorReward, uint256 membersReward) { uint32 operatorShare = _operatorSharesByPool[poolId]; - (operatorReward, membersReward) = - _computeSplitStakingPoolRewards(operatorShare, reward, membersStake); + (operatorReward, membersReward) = _computeSplitStakingPoolRewards( + operatorShare, + reward, + membersStake + ); address(_operatorRewardsReceiver).transfer(operatorReward); address(_membersRewardsReceiver).transfer(membersReward); emit DepositStakingPoolRewards(poolId, reward, membersStake); From 388329799176bbb15d7640889589d1a9ab9103b3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 23 Sep 2019 14:21:18 -0400 Subject: [PATCH 52/52] `@0x/contracts-staking`: Remove `getTotalBalance()` function. `@0x/contracts-staking`: Fix linter errors. --- .../contracts/src/fees/MixinExchangeFees.sol | 13 ------------- .../staking/contracts/test/TestDelegatorRewards.sol | 8 ++++---- contracts/staking/test/actors/finalizer_actor.ts | 4 +++- contracts/staking/test/utils/api_wrapper.ts | 6 ++++++ 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 91e9f92403..6fb78108da 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -127,19 +127,6 @@ contract MixinExchangeFees is activePoolsThisEpoch[poolId] = pool; } - /// @dev Returns the total balance of this contract, including WETH. - /// @return totalBalance Total balance. - function getTotalBalance() - external - view - returns (uint256 totalBalance) - { - totalBalance = address(this).balance.safeAdd( - IEtherToken(_getWETHAddress()).balanceOf(address(this)) - ); - return totalBalance; - } - /// @dev Get information on an active staking pool in this epoch. /// @param poolId Pool Id to query. /// @return pool ActivePool struct. diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol index 1686f49e4b..628cdc555e 100644 --- a/contracts/staking/contracts/test/TestDelegatorRewards.sol +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -83,10 +83,10 @@ contract TestDelegatorRewards is external { unfinalizedPoolRewardsByEpoch[currentEpoch][poolId] = UnfinalizedPoolReward({ - operatorReward: operatorReward, - membersReward: membersReward, - membersStake: membersStake - }); + operatorReward: operatorReward, + membersReward: membersReward, + membersStake: membersStake + }); // Lazily initialize this pool. _poolById[poolId].operator = operatorAddress; _setOperatorShare(poolId, operatorReward, membersReward); diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 17c1e57d44..ad1ce3ca75 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -241,7 +241,9 @@ export class FinalizerActor extends BaseActor { this._stakingApiWrapper.stakingContract.getActiveStakingPoolThisEpoch.callAsync(poolId), ), ); - const totalRewards = await this._stakingApiWrapper.stakingContract.getTotalBalance.callAsync(); + const totalRewards = await this._stakingApiWrapper.utils.getEthAndWethBalanceOfAsync( + this._stakingApiWrapper.stakingContract.address, + ); const totalFeesCollected = BigNumber.sum(...activePools.map(p => p.feesCollected)); const totalWeightedStake = BigNumber.sum(...activePools.map(p => p.weightedStake)); if (totalRewards.eq(0) || totalFeesCollected.eq(0) || totalWeightedStake.eq(0)) { diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index d5a8de30b8..e3121fba35 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -128,6 +128,12 @@ export class StakingApiWrapper { ); }, + getEthAndWethBalanceOfAsync: async (address: string): Promise => { + const ethBalance = await this._web3Wrapper.getBalanceInWeiAsync(address); + const wethBalance = await this.wethContract.balanceOf.callAsync(address); + return BigNumber.sum(ethBalance, wethBalance); + }, + getParamsAsync: async (): Promise => { return (_.zipObject( [