From 73f1aca4a13d505ba7baed4e2080f58a943bdecc Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 10 Sep 2019 21:42:19 -0400 Subject: [PATCH] `@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.