From 61d104075ca528dd92624a3e65bbb3df000ea176 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Mon, 23 Sep 2024 11:06:40 -0400 Subject: [PATCH] Convert reward accounting to be tracked on a per-deposit basis UniStaker tracks rewards earned by individual addresses which are specified as the beneficiary of deposits. The same address may earn rewards from tokens which have been staked in multiple deposits. This commit converts the system to track rewards on a per-deposit basis instead. Each deposit earns rewards according to the tokens staked in it. The test suite has been updated to reflect this change. The meaning of beneficiary changed as well. It is no longer the address that earns the rewards, but instead is the address that has permissions to withdrawal the rewards earned by a given deposit. Co-Author: Keating --- src/GovernanceStaker.sol | 162 ++-- test/GovernanceStaker.invariants.t.sol | 27 - test/GovernanceStaker.t.sol | 947 +++++----------------- test/helpers/GovernanceStaker.handler.sol | 18 +- 4 files changed, 311 insertions(+), 843 deletions(-) diff --git a/src/GovernanceStaker.sol b/src/GovernanceStaker.sol index 43ab2a8..f7f01b8 100644 --- a/src/GovernanceStaker.sol +++ b/src/GovernanceStaker.sol @@ -53,7 +53,9 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce ); /// @notice Emitted when a beneficiary claims their earned reward. - event RewardClaimed(address indexed beneficiary, uint256 amount); + event RewardClaimed( + DepositIdentifier indexed depositId, address indexed beneficiary, uint256 amount + ); /// @notice Emitted when this contract is notified of a new reward. event RewardNotified(uint256 amount, address notifier); @@ -93,12 +95,28 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce /// @param balance The deposit's staked balance. /// @param owner The owner of this deposit. /// @param delegatee The governance delegate who receives the voting weight for this deposit. - /// @param beneficiary The address that accrues staking rewards earned by this deposit. + /// @param beneficiary The address which has the right to withdraw rewards earned by this + /// deposit. + /// @param earningPower The "power" this deposit has as it pertains to earning rewards, which + /// accrue to this deposit at a rate proportional to its share of the total earning power of the + /// system. + /// @param rewardPerTokenCheckpoint Checkpoint of the reward per token accumulator for this + /// deposit. It represents the value of the global accumulator at the last time a given deposit's + /// rewards were calculated and stored. The difference between the global value and this value + /// can be used to calculate the interim rewards earned by given deposit. + /// @param scaledUnclaimedRewardCheckpoint Checkpoint of the unclaimed rewards earned by a given + /// deposit with the scale factor included. This value is stored any time an action is taken that + /// specifically impacts the rate at which rewards are earned by a given deposit. Total unclaimed + /// rewards for a deposit are thus this value plus all rewards earned after this checkpoint was + /// taken. This value is reset to zero when the deposit's rewards are claimed. struct Deposit { uint256 balance; address owner; address delegatee; address beneficiary; + uint256 earningPower; + uint256 rewardPerTokenCheckpoint; + uint256 scaledUnclaimedRewardCheckpoint; } /// @notice Type hash used when encoding data for `stakeOnBehalf` calls. @@ -123,7 +141,7 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce ); /// @notice Type hash used when encoding data for `claimRewardOnBehalf` calls. bytes32 public constant CLAIM_REWARD_TYPEHASH = - keccak256("ClaimReward(address beneficiary,uint256 nonce,uint256 deadline)"); + keccak256("ClaimReward(uint256 depositId,uint256 nonce,uint256 deadline)"); /// @notice ERC20 token in which rewards are denominated and distributed. IERC20 public immutable REWARD_TOKEN; @@ -150,9 +168,6 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce /// @notice Tracks the total staked by a depositor across all unique deposits. mapping(address depositor => uint256 amount) public depositorTotalStaked; - /// @notice Tracks the total stake actively earning rewards for a given beneficiary account. - mapping(address beneficiary => uint256 amount) public earningPower; - /// @notice Stores the metadata associated with a given deposit. mapping(DepositIdentifier depositId => Deposit deposit) public deposits; @@ -173,19 +188,6 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce /// @notice Checkpoint value of the global reward per token accumulator. uint256 public rewardPerTokenAccumulatedCheckpoint; - /// @notice Checkpoint of the reward per token accumulator on a per account basis. It represents - /// the value of the global accumulator at the last time a given beneficiary's rewards were - /// calculated and stored. The difference between the global value and this value can be - /// used to calculate the interim rewards earned by given account. - mapping(address account => uint256) public beneficiaryRewardPerTokenCheckpoint; - - /// @notice Checkpoint of the unclaimed rewards earned by a given beneficiary with the scale - /// factor included. This value is stored any time an action is taken that specifically impacts - /// the rate at which rewards are earned by a given beneficiary account. Total unclaimed rewards - /// for an account are thus this value plus all rewards earned after this checkpoint was taken. - /// This value is reset to zero when a beneficiary account claims their earned rewards. - mapping(address account => uint256 amount) public scaledUnclaimedRewardCheckpoint; - /// @notice Maps addresses to whether they are authorized to call `notifyRewardAmount`. mapping(address rewardNotifier => bool) public isRewardNotifier; @@ -238,18 +240,19 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked; } - /// @notice Live value of the unclaimed rewards earned by a given beneficiary account. It is the - /// sum of the last checkpoint value of their unclaimed rewards with the live calculation of the + /// @notice Live value of the unclaimed rewards earned by a given deposit. It is the + /// sum of the last checkpoint value of the unclaimed rewards with the live calculation of the /// rewards that have accumulated for this account in the interim. This value can only increase, - /// until it is reset to zero once the beneficiary account claims their unearned rewards. + /// until it is reset to zero once the unearned rewards are claimed. /// /// Note that the contract tracks the unclaimed rewards internally with the scale factor /// included, in order to avoid the accrual of precision losses as users takes actions that /// cause rewards to be checkpointed. This external helper method is useful for integrations, and /// returns the value after it has been scaled down to the reward token's raw decimal amount. - /// @return Live value of the unclaimed rewards earned by a given beneficiary account. - function unclaimedReward(address _beneficiary) external view returns (uint256) { - return _scaledUnclaimedReward(_beneficiary) / SCALE_FACTOR; + /// @param _depositId Identifier of the deposit in question. + /// @return Live value of the unclaimed rewards earned by a given deposit. + function unclaimedReward(DepositIdentifier _depositId) external view returns (uint256) { + return _scaledUnclaimedReward(deposits[_depositId]) / SCALE_FACTOR; } /// @notice Stake tokens to a new deposit. The caller must pre-approve the staking contract to @@ -468,10 +471,10 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce _alterDelegatee(deposit, _depositId, _newDelegatee); } - /// @notice For an existing deposit, change the beneficiary to which staking rewards are - /// accruing. + /// @notice For an existing deposit, change the beneficiary account which has the right to + /// withdraw staking rewards. /// @param _depositId Unique identifier of the deposit which will have its beneficiary altered. - /// @param _newBeneficiary Address of the new rewards beneficiary. + /// @param _newBeneficiary Address of the new beneficiary. /// @dev The new beneficiary may not be the zero address. The message sender must be the owner of /// the deposit. function alterBeneficiary(DepositIdentifier _depositId, address _newBeneficiary) external { @@ -480,10 +483,11 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce _alterBeneficiary(deposit, _depositId, _newBeneficiary); } - /// @notice For an existing deposit, change the beneficiary to which staking rewards are - /// accruing on behalf of a user, using a signature to validate the user's intent. + /// @notice For an existing deposit, change the beneficiary account which has the right to + /// withdraw staking rewards accruing on behalf of a user, using a signature to validate the + /// user's intent. /// @param _depositId Unique identifier of the deposit which will have its beneficiary altered. - /// @param _newBeneficiary Address of the new rewards beneficiary. + /// @param _newBeneficiary Address of the new beneficiary. /// @param _depositor Address of the user on whose behalf this stake is being made. /// @param _deadline The timestamp after which the signature should expire. /// @param _signature Signature of the user authorizing this stake. @@ -562,34 +566,42 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce _withdraw(deposit, _depositId, _amount); } - /// @notice Claim reward tokens the message sender has earned as a stake beneficiary. Tokens are - /// sent to the message sender. + /// @notice Claim reward tokens earned by a given deposit. Message sender must be the beneficiary + /// address of the deposit. Tokens are sent to the beneficiary address. + /// @param _depositId Identifier of the deposit from which accrued rewards will be claimed. /// @return Amount of reward tokens claimed. - function claimReward() external returns (uint256) { - return _claimReward(msg.sender); + function claimReward(DepositIdentifier _depositId) external returns (uint256) { + Deposit storage deposit = deposits[_depositId]; + if (deposit.beneficiary != msg.sender) { + revert GovernanceStaker__Unauthorized("not beneficiary", msg.sender); + } + return _claimReward(_depositId, deposit); } - /// @notice Claim earned reward tokens for a beneficiary, using a signature to validate the - /// beneficiary's intent. Tokens are sent to the beneficiary. - /// @param _beneficiary Address of the beneficiary who will receive the reward. + /// @notice Claim reward tokens earned by a given deposit, using a signature to validate the + /// caller's intent. The signer must be the beneficiary address of the deposit Tokens are sent to + /// the beneficiary. + /// @param _depositId The identifier for the deposit for which to claim rewards. /// @param _deadline The timestamp after which the signature should expire. /// @param _signature Signature of the beneficiary authorizing this reward claim. /// @return Amount of reward tokens claimed. - function claimRewardOnBehalf(address _beneficiary, uint256 _deadline, bytes memory _signature) - external - returns (uint256) - { + function claimRewardOnBehalf( + DepositIdentifier _depositId, + uint256 _deadline, + bytes memory _signature + ) external returns (uint256) { _revertIfPastDeadline(_deadline); + Deposit storage deposit = deposits[_depositId]; _revertIfSignatureIsNotValidNow( - _beneficiary, + deposit.beneficiary, _hashTypedDataV4( keccak256( - abi.encode(CLAIM_REWARD_TYPEHASH, _beneficiary, _useNonce(_beneficiary), _deadline) + abi.encode(CLAIM_REWARD_TYPEHASH, _depositId, _useNonce(deposit.beneficiary), _deadline) ) ), _signature ); - return _claimReward(_beneficiary); + return _claimReward(_depositId, deposit); } /// @notice Called by an authorized rewards notifier to alert the staking contract that a new @@ -640,18 +652,15 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce emit RewardNotified(_amount, msg.sender); } - /// @notice Live value of the unclaimed rewards earned by a given beneficiary account with the + /// @notice Live value of the unclaimed rewards earned by a given deposit with the /// scale factor included. Used internally for calculating reward checkpoints while minimizing /// precision loss. - /// @return Live value of the unclaimed rewards earned by a given beneficiary account with the + /// @return Live value of the unclaimed rewards earned by a given deposit with the /// scale factor included. /// @dev See documentation for the public, non-scaled `unclaimedReward` method for more details. - function _scaledUnclaimedReward(address _beneficiary) internal view returns (uint256) { - return scaledUnclaimedRewardCheckpoint[_beneficiary] - + ( - earningPower[_beneficiary] - * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary]) - ); + function _scaledUnclaimedReward(Deposit storage deposit) internal view returns (uint256) { + return deposit.scaledUnclaimedRewardCheckpoint + + (deposit.earningPower * (rewardPerTokenAccumulated() - deposit.rewardPerTokenCheckpoint)); } /// @notice Allows an address to increment their nonce and therefore invalidate any pending signed @@ -705,19 +714,20 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce _revertIfAddressZero(_beneficiary); _checkpointGlobalReward(); - _checkpointReward(_beneficiary); DelegationSurrogate _surrogate = _fetchOrDeploySurrogate(_delegatee); _depositId = _useDepositId(); totalStaked += _amount; depositorTotalStaked[_depositor] += _amount; - earningPower[_beneficiary] += _amount; deposits[_depositId] = Deposit({ balance: _amount, owner: _depositor, delegatee: _delegatee, - beneficiary: _beneficiary + beneficiary: _beneficiary, + earningPower: _amount, + rewardPerTokenCheckpoint: rewardPerTokenAccumulatedCheckpoint, + scaledUnclaimedRewardCheckpoint: 0 }); _stakeTokenSafeTransferFrom(_depositor, address(_surrogate), _amount); emit StakeDeposited(_depositor, _depositId, _amount, _amount); @@ -732,13 +742,13 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce internal { _checkpointGlobalReward(); - _checkpointReward(deposit.beneficiary); + _checkpointReward(deposit); DelegationSurrogate _surrogate = surrogates[deposit.delegatee]; totalStaked += _amount; depositorTotalStaked[deposit.owner] += _amount; - earningPower[deposit.beneficiary] += _amount; + deposit.earningPower += _amount; deposit.balance += _amount; _stakeTokenSafeTransferFrom(deposit.owner, address(_surrogate), _amount); emit StakeDeposited(deposit.owner, _depositId, _amount, deposit.balance); @@ -769,14 +779,9 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce address _newBeneficiary ) internal { _revertIfAddressZero(_newBeneficiary); - _checkpointGlobalReward(); - _checkpointReward(deposit.beneficiary); - earningPower[deposit.beneficiary] -= deposit.balance; - _checkpointReward(_newBeneficiary); emit BeneficiaryAltered(_depositId, deposit.beneficiary, _newBeneficiary); deposit.beneficiary = _newBeneficiary; - earningPower[_newBeneficiary] += deposit.balance; } /// @notice Internal convenience method which withdraws the stake from an existing deposit. @@ -786,12 +791,12 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce internal { _checkpointGlobalReward(); - _checkpointReward(deposit.beneficiary); + _checkpointReward(deposit); deposit.balance -= _amount; // overflow prevents withdrawing more than balance totalStaked -= _amount; depositorTotalStaked[deposit.owner] -= _amount; - earningPower[deposit.beneficiary] -= _amount; + deposit.earningPower -= _amount; _stakeTokenSafeTransferFrom(address(surrogates[deposit.delegatee]), deposit.owner, _amount); emit StakeWithdrawn(_depositId, _amount, deposit.balance); } @@ -800,19 +805,22 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce /// @return Amount of reward tokens claimed. /// @dev This method must only be called after proper authorization has been completed. /// @dev See public claimReward methods for additional documentation. - function _claimReward(address _beneficiary) internal returns (uint256) { + function _claimReward(DepositIdentifier _depositId, Deposit storage deposit) + internal + returns (uint256) + { _checkpointGlobalReward(); - _checkpointReward(_beneficiary); + _checkpointReward(deposit); - uint256 _reward = scaledUnclaimedRewardCheckpoint[_beneficiary] / SCALE_FACTOR; + uint256 _reward = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR; if (_reward == 0) return 0; // retain sub-wei dust that would be left due to the precision loss - scaledUnclaimedRewardCheckpoint[_beneficiary] = - scaledUnclaimedRewardCheckpoint[_beneficiary] - (_reward * SCALE_FACTOR); - emit RewardClaimed(_beneficiary, _reward); + deposit.scaledUnclaimedRewardCheckpoint = + deposit.scaledUnclaimedRewardCheckpoint - (_reward * SCALE_FACTOR); + emit RewardClaimed(_depositId, deposit.beneficiary, _reward); - SafeERC20.safeTransfer(REWARD_TOKEN, _beneficiary, _reward); + SafeERC20.safeTransfer(REWARD_TOKEN, deposit.beneficiary, _reward); return _reward; } @@ -823,14 +831,14 @@ contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonce } /// @notice Checkpoints the unclaimed rewards and reward per token accumulator of a given - /// beneficiary account. - /// @param _beneficiary The account for which reward parameters will be checkpointed. + /// deposit. + /// @param deposit The deposit for which the reward parameters will be checkpointed. /// @dev This is a sensitive internal helper method that must only be called after global rewards /// accumulator has been checkpointed. It assumes the global `rewardPerTokenCheckpoint` is up to /// date. - function _checkpointReward(address _beneficiary) internal { - scaledUnclaimedRewardCheckpoint[_beneficiary] = _scaledUnclaimedReward(_beneficiary); - beneficiaryRewardPerTokenCheckpoint[_beneficiary] = rewardPerTokenAccumulatedCheckpoint; + function _checkpointReward(Deposit storage deposit) internal { + deposit.scaledUnclaimedRewardCheckpoint = _scaledUnclaimedReward(deposit); + deposit.rewardPerTokenCheckpoint = rewardPerTokenAccumulatedCheckpoint; } /// @notice Internal helper method which sets the admin address. diff --git a/test/GovernanceStaker.invariants.t.sol b/test/GovernanceStaker.invariants.t.sol index 19f3603..8020174 100644 --- a/test/GovernanceStaker.invariants.t.sol +++ b/test/GovernanceStaker.invariants.t.sol @@ -48,10 +48,6 @@ contract GovernanceStakerInvariants is Test { assertEq(govStaker.totalStaked(), handler.reduceDepositors(0, this.accumulateDeposits)); } - function invariant_Sum_of_beneficiary_earning_power_equals_total_stake() public { - assertEq(govStaker.totalStaked(), handler.reduceBeneficiaries(0, this.accumulateEarningPower)); - } - function invariant_Sum_of_surrogate_balance_equals_total_stake() public { assertEq(govStaker.totalStaked(), handler.reduceDelegates(0, this.accumulateSurrogateBalance)); } @@ -70,13 +66,6 @@ contract GovernanceStakerInvariants is Test { ); } - function invariant_Sum_of_unclaimed_reward_should_be_less_than_or_equal_to_total_rewards() public { - assertLe( - handler.reduceBeneficiaries(0, this.accumulateUnclaimedReward), - rewardToken.balanceOf(address(govStaker)) - ); - } - function invariant_RewardPerTokenAccumulatedCheckpoint_should_be_greater_or_equal_to_the_last_rewardPerTokenAccumulatedCheckpoint( ) public view { assertGe( @@ -96,22 +85,6 @@ contract GovernanceStakerInvariants is Test { return balance + govStaker.depositorTotalStaked(depositor); } - function accumulateEarningPower(uint256 earningPower, address caller) - external - view - returns (uint256) - { - return earningPower + govStaker.earningPower(caller); - } - - function accumulateUnclaimedReward(uint256 unclaimedReward, address beneficiary) - external - view - returns (uint256) - { - return unclaimedReward + govStaker.unclaimedReward(beneficiary); - } - function accumulateSurrogateBalance(uint256 balance, address delegate) external view diff --git a/test/GovernanceStaker.t.sol b/test/GovernanceStaker.t.sol index 638e434..d7893f6 100644 --- a/test/GovernanceStaker.t.sol +++ b/test/GovernanceStaker.t.sol @@ -130,13 +130,23 @@ contract GovernanceStakerTest is Test, PercentAssertions { view returns (GovernanceStaker.Deposit memory) { - (uint256 _balance, address _owner, address _delegatee, address _beneficiary) = - govStaker.deposits(_depositId); + ( + uint256 _balance, + address _owner, + address _delegatee, + address _beneficiary, + uint256 _earningPower, + uint256 _rewardPerTokenCheckpoint, + uint256 _scaledUnclaimedRewardCheckpoint + ) = govStaker.deposits(_depositId); return GovernanceStaker.Deposit({ balance: _balance, owner: _owner, delegatee: _delegatee, - beneficiary: _beneficiary + beneficiary: _beneficiary, + earningPower: _earningPower, + rewardPerTokenCheckpoint: _rewardPerTokenCheckpoint, + scaledUnclaimedRewardCheckpoint: _scaledUnclaimedRewardCheckpoint }); } @@ -596,92 +606,6 @@ contract Stake is GovernanceStakerTest { assertEq(_deposit2.delegatee, _delegatee2); } - function testFuzz_AssignsEarningPowerToDepositorIfNoBeneficiaryIsSpecified( - address _depositor, - uint256 _amount, - address _delegatee - ) public { - _amount = _boundMintAmount(_amount); - _mintGovToken(_depositor, _amount); - - GovernanceStaker.DepositIdentifier _depositId = _stake(_depositor, _amount, _delegatee); - GovernanceStaker.Deposit memory _deposit = _fetchDeposit(_depositId); - - assertEq(govStaker.earningPower(_depositor), _amount); - assertEq(_deposit.beneficiary, _depositor); - } - - function testFuzz_AssignsEarningPowerToTheBeneficiaryProvided( - address _depositor, - uint256 _amount, - address _delegatee, - address _beneficiary - ) public { - _amount = _boundMintAmount(_amount); - _mintGovToken(_depositor, _amount); - - GovernanceStaker.DepositIdentifier _depositId = - _stake(_depositor, _amount, _delegatee, _beneficiary); - GovernanceStaker.Deposit memory _deposit = _fetchDeposit(_depositId); - - assertEq(govStaker.earningPower(_beneficiary), _amount); - assertEq(_deposit.beneficiary, _beneficiary); - } - - function testFuzz_AssignsEarningPowerToDifferentBeneficiariesForDifferentDepositsFromTheSameDepositor( - address _depositor, - uint256 _amount1, - uint256 _amount2, - address _delegatee, - address _beneficiary1, - address _beneficiary2 - ) public { - vm.assume(_beneficiary1 != _beneficiary2); - _amount1 = _boundMintAmount(_amount1); - _amount2 = _boundMintAmount(_amount2); - _mintGovToken(_depositor, _amount1 + _amount2); - - // Perform both deposits and track their identifiers separately - GovernanceStaker.DepositIdentifier _depositId1 = - _stake(_depositor, _amount1, _delegatee, _beneficiary1); - GovernanceStaker.DepositIdentifier _depositId2 = - _stake(_depositor, _amount2, _delegatee, _beneficiary2); - GovernanceStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1); - GovernanceStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2); - - // Check that the earning power has been recorded independently - assertEq(_deposit1.beneficiary, _beneficiary1); - assertEq(govStaker.earningPower(_beneficiary1), _amount1); - assertEq(_deposit2.beneficiary, _beneficiary2); - assertEq(govStaker.earningPower(_beneficiary2), _amount2); - } - - function testFuzz_AssignsEarningPowerToTheSameBeneficiarySpecifiedByTwoDifferentDepositors( - address _depositor1, - address _depositor2, - uint256 _amount1, - uint256 _amount2, - address _delegatee, - address _beneficiary - ) public { - _amount1 = _boundMintAmount(_amount1); - _amount2 = _boundMintAmount(_amount2); - _mintGovToken(_depositor1, _amount1); - _mintGovToken(_depositor2, _amount2); - - // Perform both deposits and track their identifiers separately - GovernanceStaker.DepositIdentifier _depositId1 = - _stake(_depositor1, _amount1, _delegatee, _beneficiary); - GovernanceStaker.DepositIdentifier _depositId2 = - _stake(_depositor2, _amount2, _delegatee, _beneficiary); - GovernanceStaker.Deposit memory _deposit1 = _fetchDeposit(_depositId1); - GovernanceStaker.Deposit memory _deposit2 = _fetchDeposit(_depositId2); - - assertEq(_deposit1.beneficiary, _beneficiary); - assertEq(_deposit2.beneficiary, _beneficiary); - assertEq(govStaker.earningPower(_beneficiary), _amount1 + _amount2); - } - mapping(GovernanceStaker.DepositIdentifier depositId => bool isUsed) isIdUsed; function test_NeverReusesADepositIdentifier() public { @@ -1122,28 +1046,6 @@ contract StakeMore is GovernanceStakerTest { assertEq(govToken.balanceOf(address(_surrogate)), _depositAmount + _addAmount); } - function testFuzz_AddsToExistingBeneficiaryEarningPower( - address _depositor, - uint256 _depositAmount, - uint256 _addAmount, - address _delegatee, - address _beneficiary - ) public { - GovernanceStaker.DepositIdentifier _depositId; - (_depositAmount, _depositId) = - _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); - - _addAmount = _boundToRealisticStake(_addAmount); - _mintGovToken(_depositor, _addAmount); - - vm.startPrank(_depositor); - govToken.approve(address(govStaker), _addAmount); - govStaker.stakeMore(_depositId, _addAmount); - vm.stopPrank(); - - assertEq(govStaker.earningPower(_beneficiary), _depositAmount + _addAmount); - } - function testFuzz_AddsToTheTotalStaked( address _depositor, uint256 _depositAmount, @@ -2142,8 +2044,6 @@ contract AlterBeneficiary is GovernanceStakerTest { GovernanceStaker.Deposit memory _deposit = _fetchDeposit(_depositId); assertEq(_deposit.beneficiary, _newBeneficiary); - assertEq(govStaker.earningPower(_newBeneficiary), _depositAmount); - assertEq(govStaker.earningPower(_firstBeneficiary), 0); } function testFuzz_AllowsStakerToReiterateTheirBeneficiary( @@ -2164,7 +2064,6 @@ contract AlterBeneficiary is GovernanceStakerTest { GovernanceStaker.Deposit memory _deposit = _fetchDeposit(_depositId); assertEq(_deposit.beneficiary, _beneficiary); - assertEq(govStaker.earningPower(_beneficiary), _depositAmount); } function testFuzz_EmitsAnEventWhenBeneficiaryAltered( @@ -2589,147 +2488,6 @@ contract Withdraw is GovernanceStakerTest { assertEq(govStaker.totalStaked(), _depositAmount1 + _depositAmount2 - _withdrawalAmount); } - function testFuzz_RemovesEarningPowerFromADepositorWhoHadSelfAssignedIt( - address _depositor, - uint256 _depositAmount, - address _delegatee, - uint256 _withdrawalAmount - ) public { - GovernanceStaker.DepositIdentifier _depositId; - (_depositAmount, _depositId) = _boundMintAndStake(_depositor, _depositAmount, _delegatee); - _withdrawalAmount = uint256(bound(_withdrawalAmount, 0, _depositAmount)); - - vm.prank(_depositor); - govStaker.withdraw(_depositId, _withdrawalAmount); - - assertEq(govStaker.earningPower(_depositor), _depositAmount - _withdrawalAmount); - } - - function testFuzz_RemovesEarningPowerFromABeneficiary( - address _depositor, - uint256 _depositAmount, - address _delegatee, - address _beneficiary, - uint256 _withdrawalAmount - ) public { - GovernanceStaker.DepositIdentifier _depositId; - (_depositAmount, _depositId) = - _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); - _withdrawalAmount = uint256(bound(_withdrawalAmount, 0, _depositAmount)); - - vm.prank(_depositor); - govStaker.withdraw(_depositId, _withdrawalAmount); - - assertEq(govStaker.earningPower(_beneficiary), _depositAmount - _withdrawalAmount); - } - - function testFuzz_RemovesEarningPowerFromABeneficiaryAssignedByTwoDepositors( - address _depositor1, - address _depositor2, - uint256 _depositAmount1, - uint256 _depositAmount2, - address _delegatee, - address _beneficiary, - uint256 _withdrawalAmount1, - uint256 _withdrawalAmount2 - ) public { - GovernanceStaker.DepositIdentifier _depositId1; - (_depositAmount1, _depositId1) = - _boundMintAndStake(_depositor1, _depositAmount1, _delegatee, _beneficiary); - _withdrawalAmount1 = uint256(bound(_withdrawalAmount1, 0, _depositAmount1)); - - GovernanceStaker.DepositIdentifier _depositId2; - (_depositAmount2, _depositId2) = - _boundMintAndStake(_depositor2, _depositAmount2, _delegatee, _beneficiary); - _withdrawalAmount2 = uint256(bound(_withdrawalAmount2, 0, _depositAmount2)); - - vm.prank(_depositor1); - govStaker.withdraw(_depositId1, _withdrawalAmount1); - - assertEq( - govStaker.earningPower(_beneficiary), _depositAmount1 - _withdrawalAmount1 + _depositAmount2 - ); - - vm.prank(_depositor2); - govStaker.withdraw(_depositId2, _withdrawalAmount2); - - assertEq( - govStaker.earningPower(_beneficiary), - _depositAmount1 - _withdrawalAmount1 + _depositAmount2 - _withdrawalAmount2 - ); - } - - function testFuzz_RemovesEarningPowerFromDifferentBeneficiariesOfTheSameDepositor( - address _depositor, - uint256 _depositAmount1, - uint256 _depositAmount2, - address _delegatee, - address _beneficiary1, - address _beneficiary2, - uint256 _withdrawalAmount1, - uint256 _withdrawalAmount2 - ) public { - vm.assume(_beneficiary1 != _beneficiary2); - - GovernanceStaker.DepositIdentifier _depositId1; - (_depositAmount1, _depositId1) = - _boundMintAndStake(_depositor, _depositAmount1, _delegatee, _beneficiary1); - _withdrawalAmount1 = uint256(bound(_withdrawalAmount1, 0, _depositAmount1)); - - GovernanceStaker.DepositIdentifier _depositId2; - (_depositAmount2, _depositId2) = - _boundMintAndStake(_depositor, _depositAmount2, _delegatee, _beneficiary2); - _withdrawalAmount2 = uint256(bound(_withdrawalAmount2, 0, _depositAmount2)); - - vm.prank(_depositor); - govStaker.withdraw(_depositId1, _withdrawalAmount1); - - assertEq(govStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1); - assertEq(govStaker.earningPower(_beneficiary2), _depositAmount2); - - vm.prank(_depositor); - govStaker.withdraw(_depositId2, _withdrawalAmount2); - - assertEq(govStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1); - assertEq(govStaker.earningPower(_beneficiary2), _depositAmount2 - _withdrawalAmount2); - } - - function testFuzz_RemovesEarningPowerFromDifferentBeneficiariesAndDifferentDepositors( - address _depositor1, - address _depositor2, - uint256 _depositAmount1, - uint256 _depositAmount2, - address _delegatee, - address _beneficiary1, - address _beneficiary2, - uint256 _withdrawalAmount1, - uint256 _withdrawalAmount2 - ) public { - vm.assume(_beneficiary1 != _beneficiary2); - - GovernanceStaker.DepositIdentifier _depositId1; - (_depositAmount1, _depositId1) = - _boundMintAndStake(_depositor1, _depositAmount1, _delegatee, _beneficiary1); - _withdrawalAmount1 = uint256(bound(_withdrawalAmount1, 0, _depositAmount1)); - - GovernanceStaker.DepositIdentifier _depositId2; - (_depositAmount2, _depositId2) = - _boundMintAndStake(_depositor2, _depositAmount2, _delegatee, _beneficiary2); - _withdrawalAmount2 = uint256(bound(_withdrawalAmount2, 0, _depositAmount2)); - - vm.prank(_depositor1); - govStaker.withdraw(_depositId1, _withdrawalAmount1); - - assertEq(govStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1); - assertEq(govStaker.earningPower(_beneficiary2), _depositAmount2); - - vm.prank(_depositor2); - govStaker.withdraw(_depositId2, _withdrawalAmount2); - - assertEq(govStaker.earningPower(_beneficiary1), _depositAmount1 - _withdrawalAmount1); - assertEq(govStaker.earningPower(_beneficiary2), _depositAmount2 - _withdrawalAmount2); - } - function testFuzz_EmitsAnEventWhenThereIsAWithdrawal( address _depositor, uint256 _depositAmount, @@ -3167,17 +2925,7 @@ contract GovernanceStakerRewardsTest is GovernanceStakerTest { console2.log("-----------------------------------------------"); } - function __dumpDebugDepositorRewards(address _depositor) public view { - console2.log("earningPower[_depositor]"); - console2.log(govStaker.earningPower(_depositor)); - console2.log("beneficiaryRewardPerTokenCheckpoint[_depositor]"); - console2.log(govStaker.beneficiaryRewardPerTokenCheckpoint(_depositor)); - console2.log("scaledUnclaimedRewardCheckpoint[_depositor]"); - console2.log(govStaker.scaledUnclaimedRewardCheckpoint(_depositor)); - console2.log("unclaimedReward(_depositor)"); - console2.log(govStaker.unclaimedReward(_depositor)); - console2.log("-----------------------------------------------"); - } + function __dumpDebugDepositorRewards(address _depositor) public view {} function _jumpAheadByPercentOfRewardDuration(uint256 _percent) public { uint256 _seconds = (_percent * govStaker.REWARD_DURATION()) / 100; @@ -3888,14 +3636,15 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // The full duration passes _jumpAheadByPercentOfRewardDuration(101); // The user should have earned all the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _rewardAmount); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _rewardAmount); } function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorAssignsABeneficiaryAndStakesForFullDuration( @@ -3908,52 +3657,15 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); // A user deposits staking tokens w/ a beneficiary - _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // The full duration passes _jumpAheadByPercentOfRewardDuration(101); // The beneficiary should have earned all the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_beneficiary), _rewardAmount); - } - - function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorUpdatesTheirBeneficiary( - address _depositor, - address _delegatee, - address _beneficiary1, - address _beneficiary2, - uint256 _stakeAmount, - uint256 _rewardAmount, - uint256 _percentDuration - ) public { - vm.assume( - _beneficiary1 != _beneficiary2 && _beneficiary1 != address(0) && _beneficiary2 != address(0) - ); - - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - _percentDuration = bound(_percentDuration, 0, 100); - - // A user deposits staking tokens w/ a beneficiary - (, GovernanceStaker.DepositIdentifier _depositId) = - _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary1); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // Part of the rewards duration passes - _jumpAheadByPercentOfRewardDuration(_percentDuration); - // The depositor alters their beneficiary - vm.prank(_depositor); - govStaker.alterBeneficiary(_depositId, _beneficiary2); - // The rest of the duration elapses - _jumpAheadByPercentOfRewardDuration(100 - _percentDuration); - - // The beneficiary should have earned all the rewards - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary1), _percentOf(_rewardAmount, _percentDuration) - ); - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary2), _percentOf(_rewardAmount, 100 - _percentDuration) - ); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _rewardAmount); } function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForPartialDuration( @@ -3967,7 +3679,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // One third of the duration passes @@ -3975,7 +3688,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // The user should have earned one third of the rewards assertLteWithinOnePercent( - govStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, _durationPercent) + govStaker.unclaimedReward(_depositId), _percentOf(_rewardAmount, _durationPercent) ); } @@ -3992,180 +3705,56 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Two thirds of the duration time passes _jumpAheadByPercentOfRewardDuration(66); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The rest of the duration elapses _jumpAheadByPercentOfRewardDuration(34); // The user should have earned 1/3rd of the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, 34)); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _percentOf(_rewardAmount, 34)); } - function testFuzz_CalculatesCorrectEarningsForASingleUserStakeForPartialDurationWithABeneficiary( + function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationWithNoNewRewards( address _depositor, address _delegatee, - address _beneficiary, uint256 _stakeAmount, uint256 _rewardAmount, - uint256 _durationPercent + uint16 _noRewardsSkip ) public { - vm.assume(_beneficiary != address(0)); - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - _durationPercent = bound(_durationPercent, 0, 100); - // A user deposits staking tokens and assigns a beneficiary - _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); + // A user deposits staking tokens + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); - // Some portion of the duration passes - _jumpAheadByPercentOfRewardDuration(_durationPercent); - // The beneficiary should have earned a portion of the rewards equal to the amount of the - // duration that has passed - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary), _percentOf(_rewardAmount, _durationPercent) - ); + // The full duration passes + _jumpAheadByPercentOfRewardDuration(100); + // Time moves forward with no rewards + _jumpAheadByPercentOfRewardDuration(_noRewardsSkip); + + // Send new rewards, which should have no impact on the amount earned until time elapses + _mintTransferAndNotifyReward(_rewardAmount); + + // The user should have earned all the rewards + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _rewardAmount); } - function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsPartiallyThroughTheDurationWithABeneficiary( + function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationAndClaims( address _depositor, address _delegatee, - address _beneficiary, uint256 _stakeAmount, - uint256 _rewardAmount + uint256 _rewardAmount, + uint256 _durationPercent ) public { - vm.assume(_beneficiary != address(0)); + vm.assume(_depositor != address(govStaker)); (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + _durationPercent = bound(_durationPercent, 0, 100); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // Two thirds of the duration time passes - _jumpAheadByPercentOfRewardDuration(66); - // A user deposits staking tokens and assigns a beneficiary - _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); - // The rest of the duration elapses - _jumpAheadByPercentOfRewardDuration(34); - - // The beneficiary should have earned 1/3rd of the reward - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary), _percentOf(_rewardAmount, 34) - ); - } - - function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationWithNoNewRewards( - address _depositor, - address _delegatee, - uint256 _stakeAmount, - uint256 _rewardAmount, - uint16 _noRewardsSkip - ) public { - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - - // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - - // The full duration passes - _jumpAheadByPercentOfRewardDuration(100); - // Time moves forward with no rewards - _jumpAheadByPercentOfRewardDuration(_noRewardsSkip); - - // Send new rewards, which should have no impact on the amount earned until time elapses - _mintTransferAndNotifyReward(_rewardAmount); - - // The user should have earned all the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _rewardAmount); - } - - function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationWithDelayedReward( - address _depositor, - address _delegatee, - uint256 _stakeAmount, - uint256 _rewardAmount, - uint16 _noRewardsSkip - ) public { - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - - // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - - // The full duration passes - _jumpAheadByPercentOfRewardDuration(100); - // Time moves forward with no rewards - _jumpAheadByPercentOfRewardDuration(_noRewardsSkip); - - // Send new rewards - _mintTransferAndNotifyReward(_rewardAmount); - // We end another full duration - _jumpAheadByPercentOfRewardDuration(100); - - // The user should have earned all the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _rewardAmount * 2); - } - - function testFuzz_CalculatesCorrectEarningsWhenASingleDepositorUpdatesTheirBeneficiaryWithNoNewRewards( - address _depositor, - address _delegatee, - address _beneficiary1, - address _beneficiary2, - uint256 _stakeAmount, - uint256 _rewardAmount, - uint256 _percentDuration, - uint16 _noRewardsSkip - ) public { - vm.assume( - _beneficiary1 != _beneficiary2 && _beneficiary1 != address(0) && _beneficiary2 != address(0) - ); - - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - _percentDuration = bound(_percentDuration, 0, 100); - - // A user deposits staking tokens w/ a beneficiary - (, GovernanceStaker.DepositIdentifier _depositId) = - _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary1); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // Part of the rewards duration passes - _jumpAheadByPercentOfRewardDuration(_percentDuration); - - // The depositor alters their beneficiary - vm.prank(_depositor); - govStaker.alterBeneficiary(_depositId, _beneficiary2); - - // The rest of the duration elapses - _jumpAheadByPercentOfRewardDuration(100 - _percentDuration); - - // Skip ahead with no rewards - _jumpAheadByPercentOfRewardDuration(_noRewardsSkip); - - // Send new rewards, which should have no impact on the amount earned until time elapses - _mintTransferAndNotifyReward(_rewardAmount); - - // The beneficiaries should have earned all the rewards for the first duration - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary1), _percentOf(_rewardAmount, _percentDuration) - ); - assertLteWithinOnePercent( - govStaker.unclaimedReward(_beneficiary2), _percentOf(_rewardAmount, 100 - _percentDuration) - ); - } - - function testFuzz_CalculatesCorrectEarningsForASingleUserThatDepositsStakeForTheFullDurationAndClaims( - address _depositor, - address _delegatee, - uint256 _stakeAmount, - uint256 _rewardAmount, - uint256 _durationPercent - ) public { - vm.assume(_depositor != address(govStaker)); - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - _durationPercent = bound(_durationPercent, 0, 100); - - // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + // A user deposits staking tokens + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); @@ -4174,7 +3763,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // The depositor claims the rewards vm.prank(_depositor); - govStaker.claimReward(); + govStaker.claimReward(_depositId); // Send new rewards _mintTransferAndNotifyReward(_rewardAmount); @@ -4187,7 +3776,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // The depositor should have earned a portion of the rewards equal to the amount of the next // duration that has passed. assertLteWithinOnePercent( - govStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, _durationPercent) + govStaker.unclaimedReward(_depositId), _percentOf(_rewardAmount, _durationPercent) ); } @@ -4203,7 +3792,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); @@ -4212,7 +3802,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // The depositor claims the reward vm.prank(_depositor); - govStaker.claimReward(); + govStaker.claimReward(_depositId); // We skip ahead to the end of the duration _jumpAheadByPercentOfRewardDuration(100 - _durationPercent); @@ -4224,7 +3814,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { assertLteWithinOnePercent(balance, _percentOf(_rewardAmount, _durationPercent)); // The depositor earned the portion of the reward after the rewards were claimed assertLteWithinOnePercent( - govStaker.unclaimedReward(_depositor), _percentOf(_rewardAmount, 100 - _durationPercent) + govStaker.unclaimedReward(_depositId), _percentOf(_rewardAmount, 100 - _durationPercent) ); } @@ -4239,19 +3829,21 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); // A user deposits staking tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); // Some time passes _jumpAhead(3000); // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // The full duration passes _jumpAheadByPercentOfRewardDuration(101); // Each user should have earned half of the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _percentOf(_rewardAmount, 50)); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _percentOf(_rewardAmount, 50)); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _percentOf(_rewardAmount, 50)); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _percentOf(_rewardAmount, 50)); } function testFuzz_CalculatesCorrectEarningsForTwoUsersWhenOneStakesMorePartiallyThroughTheDuration( @@ -4270,7 +3862,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Some time passes _jumpAhead(3000); // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // One third of the duration passes @@ -4294,8 +3887,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 25), 66); // Each user should have earned half of the rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsForTwoUsersThatDepositEqualStakeForFullDurationAndBothClaim( @@ -4310,11 +3903,13 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); // A user deposits staking tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); // Some time passes _jumpAhead(3000); // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // The full duration passes @@ -4322,11 +3917,11 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Depositor 1 claims vm.prank(_depositor1); - govStaker.claimReward(); + govStaker.claimReward(_depositId1); // Depositor 2 claims vm.prank(_depositor2); - govStaker.claimReward(); + govStaker.claimReward(_depositId2); uint256 depositor1Balance = govStaker.REWARD_TOKEN().balanceOf(address(_depositor1)); uint256 depositor2Balance = govStaker.REWARD_TOKEN().balanceOf(address(_depositor2)); @@ -4336,8 +3931,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { assertLteWithinOnePercent(depositor2Balance, _percentOf(_rewardAmount, 50)); // Each user should have earned nothing since they both claimed their rewards - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), 0); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), 0); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), 0); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), 0); } function testFuzz_CalculatesCorrectEarningsForTwoUsersWhenOneStakesMorePartiallyThroughTheDurationAndOneClaims( @@ -4357,14 +3952,15 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Some time passes _jumpAhead(3000); // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // One third of the duration passes _jumpAheadByPercentOfRewardDuration(34); // The first depositor claims their reward vm.prank(_depositor1); - govStaker.claimReward(); + govStaker.claimReward(_depositId1); // The first depositor triples their deposit by staking 2x more _mintGovToken(_depositor1, 2 * _stakeAmount); vm.startPrank(_depositor1); @@ -4384,8 +3980,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { uint256 depositor1Balance = govStaker.REWARD_TOKEN().balanceOf(address(_depositor1)); uint256 depositor2Balance = govStaker.REWARD_TOKEN().balanceOf(address(_depositor2)); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); // Depositor 1 should have received the reward they earned from before they claimed assertLteWithinOnePercent(depositor1Balance, _percentOf(_percentOf(_rewardAmount, 50), 34)); @@ -4393,204 +3989,6 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { assertLteWithinOnePercent(depositor2Balance, 0); } - function testFuzz_CalculatesCorrectEarningsForFourUsersThatDepositEqualStakeForFullDurationWhereOneIsABeneficiaryOfTwoOthers( - address _depositor1, - address _depositor2, - address _depositor3, - address _depositor4, - address _delegatee, - uint256 _stakeAmount, - uint256 _rewardAmount - ) public { - vm.assume( - _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3 - && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4 - && _depositor3 != _depositor4 - ); - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - - // A user deposits staking tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits and adds the first depositor as the beneficiary - _boundMintAndStake(_depositor3, _stakeAmount, _delegatee, _depositor1); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits and adds the first depositor as the beneficiary - _boundMintAndStake(_depositor4, _stakeAmount, _delegatee, _depositor1); - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // The full duration passes - _jumpAheadByPercentOfRewardDuration(101); - - // The first depositor has earn 3/4 of the and depositor 2 should earn a quarter of the reward - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _percentOf(_rewardAmount, 75)); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _percentOf(_rewardAmount, 25)); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor3), 0); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor4), 0); - } - - function testFuzz_CalculatesCorrectEarningsForFourUsersWhenOneStakesMorePartiallyThroughTheDurationAndTwoBeneficiaries( - address _depositor1, - address _depositor2, - address _depositor3, - address _depositor4, - address _delegatee, - uint256 _stakeAmount, - uint256 _rewardAmount - ) public { - vm.assume( - _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3 - && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4 - && _depositor3 != _depositor4 - ); - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - - // A user deposits staking tokens - (, GovernanceStaker.DepositIdentifier _depositId1) = - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens and adds the second depositor as - // a beneficiary - _boundMintAndStake(_depositor3, _stakeAmount, _delegatee, _depositor2); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens and adds the first depositor as - // a beneficiary - _boundMintAndStake(_depositor4, _stakeAmount, _delegatee, _depositor1); - - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // One third of the duration passes - _jumpAheadByPercentOfRewardDuration(34); - // The first user doubles their stake - _mintGovToken(_depositor1, _stakeAmount); - vm.startPrank(_depositor1); - govToken.approve(address(govStaker), _stakeAmount); - govStaker.stakeMore(_depositId1, _stakeAmount); - vm.stopPrank(); - // The rest of the duration passes - _jumpAheadByPercentOfRewardDuration(66); - - // Depositor 1 earns half the reward for one third the time and three fifths for two thirds of - // the time - uint256 _depositor1ExpectedEarnings = - _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 60), 66); - // Depositor 2 earns half the reward for one third the time and two fifths for two thirds of - // the time - uint256 _depositor2ExpectedEarnings = - _percentOf(_percentOf(_rewardAmount, 50), 34) + _percentOf(_percentOf(_rewardAmount, 40), 66); - - // The third and fourth depositor earn nothing because they are sending their rewards to a - // beneficiary - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor3), 0); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor4), 0); - } - - function testFuzz_CalculatesCorrectEarningsForFourUsersWhenTwoStakeMorePartiallyThroughTheDurationAndOneBeneficiary( - address _depositor1, - address _depositor2, - address _depositor3, - address _depositor4, - uint256 _stakeAmount, - uint256 _rewardAmount - ) public { - vm.assume( - _depositor1 != _depositor2 && _depositor1 != _depositor2 && _depositor2 != _depositor3 - && _depositor1 != _depositor3 && _depositor1 != _depositor4 && _depositor2 != _depositor4 - && _depositor3 != _depositor4 - ); - (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); - - // A user deposits staking tokens - (, GovernanceStaker.DepositIdentifier _depositId1) = - _boundMintAndStake(_depositor1, _stakeAmount, _depositor1); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens - (, GovernanceStaker.DepositIdentifier _depositId2) = - _boundMintAndStake(_depositor2, _stakeAmount, _depositor1); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens - (, GovernanceStaker.DepositIdentifier _depositId3) = - _boundMintAndStake(_depositor3, _stakeAmount, _depositor1); - // Some time passes - _jumpAhead(3000); - // Another depositor deposits the same number of staking tokens - _boundMintAndStake(_depositor4, _stakeAmount, _depositor1); - - // The contract is notified of a reward - _mintTransferAndNotifyReward(_rewardAmount); - // One quarter of the duration passes - _jumpAheadByPercentOfRewardDuration(25); - // The first user doubles their deposit - _mintGovToken(_depositor1, _stakeAmount); - vm.startPrank(_depositor1); - govToken.approve(address(govStaker), _stakeAmount); - govStaker.stakeMore(_depositId1, _stakeAmount); - vm.stopPrank(); - - // Another quarter of the duration passes - _jumpAheadByPercentOfRewardDuration(25); - // The second users doubles their deposit - vm.startPrank(_depositor2); - _mintGovToken(_depositor2, _stakeAmount); - vm.startPrank(_depositor2); - govToken.approve(address(govStaker), _stakeAmount); - govStaker.stakeMore(_depositId2, _stakeAmount); - vm.stopPrank(); - - // The third user changes their beneficiary - vm.startPrank(_depositor3); - govStaker.alterBeneficiary(_depositId3, _depositor1); - vm.stopPrank(); - - // The first depositor withdraws half of their deposit - vm.startPrank(_depositor1); - govStaker.withdraw(_depositId1, _stakeAmount); - vm.stopPrank(); - - // The rest of the duration passes - _jumpAheadByPercentOfRewardDuration(50); - - // Depositor 1 earns 25% of the reward for one quarter of the time and 40% of the reward three - // quarter of the time - uint256 _depositor1ExpectedEarnings = - _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 40), 75); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - - // Depositor 2 earns a quarter of the reward for one quarter of the time, a fifth of the - // reward one quarter of the time, and 40 percent of the reward half the time - uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount, 25), 25) - + _percentOf(_percentOf(_rewardAmount, 20), 25) + _percentOf(_percentOf(_rewardAmount, 40), 50); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); - - // Depositor 3 earns 25% of the reward for a quarter of the time, 20% of the reward a quarter of - // the time and no reward half the time. - uint256 _depositor3ExpectedEarnings = - _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 20), 25); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor3), _depositor3ExpectedEarnings); - - // Depositor 4 earns 25% of the reward for a quarter of the time, 20% of the reward 3 quarters - // of the time. - uint256 _depositor4ExpectedEarnings = - _percentOf(_percentOf(_rewardAmount, 25), 25) + _percentOf(_percentOf(_rewardAmount, 20), 75); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor4), _depositor4ExpectedEarnings); - } - function testFuzz_CalculatesCorrectEarningsWhenAUserStakesThroughTheDurationAndAnotherStakesPartially( address _depositor1, address _depositor2, @@ -4602,7 +4000,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); // The first user stakes some tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); // A small amount of time passes _jumpAhead(3000); // The contract is notified of a reward @@ -4610,7 +4009,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Two thirds of the duration time elapses _jumpAheadByPercentOfRewardDuration(66); // A second user stakes the same amount of tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // The rest of the duration elapses _jumpAheadByPercentOfRewardDuration(34); @@ -4621,8 +4021,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Depositor 2 earns 1/2 the rewards for 1/3rd of the duration time uint256 _depositor2ExpectedEarnings = _percentOf(_percentOf(_rewardAmount, 50), 34); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsWhenAUserDepositsAndThereAreTwoRewards( @@ -4636,7 +4036,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount2) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount2); // A user stakes tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount1); // Two thirds of duration elapses @@ -4653,7 +4054,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // reward plus the second reward. uint256 _depositorExpectedEarnings = _percentOf(_rewardAmount1, 66) + _percentOf(_percentOf(_rewardAmount1, 34) + _rewardAmount2, 34); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _depositorExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _depositorExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositForPartialDurationsAndThereAreTwoRewards( @@ -4673,11 +4074,13 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // One quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // A user stakes some tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); // Another 40 percent of the duration time elapses _jumpAheadByPercentOfRewardDuration(40); // Another user stakes some tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // Another quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // The contract receives another reward, resetting the duration @@ -4701,8 +4104,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 40) + _depositor2ExpectedEarnings; - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositDifferentAmountsForPartialDurationsAndThereAreTwoRewards( @@ -4723,11 +4126,13 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // One quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // A user stakes some tokens - _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); // Another 40 percent of the duration time elapses _jumpAheadByPercentOfRewardDuration(40); // Another user stakes some tokens - _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); // Another quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // The contract receives another reward, resetting the duration @@ -4754,8 +4159,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 40) + (_stakeAmount1 * _combinedPhaseExpectedTotalRewards) / _combinedStake; - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } // Could potentially add duration @@ -4772,7 +4177,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { (_stakeAmount, _rewardAmount3) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount3); // A user stakes tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount1); // Two thirds of duration elapses @@ -4797,7 +4203,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { + _percentOf( _percentOf(_percentOf(_rewardAmount1, 60) + _rewardAmount2, 70) + _rewardAmount3, 30 ); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor), _depositorExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId), _depositorExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositForPartialDurationsAndThereAreThreeRewards( @@ -4819,11 +4225,13 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // One quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // A user stakes some tokens - _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount, _delegatee); // Another 20 percent of the duration time elapses _jumpAheadByPercentOfRewardDuration(20); // Another user stakes some tokens - _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount, _delegatee); // Another 20 percent of the duration time elapses _jumpAheadByPercentOfRewardDuration(20); // The contract receives another reward, resetting the duration @@ -4860,8 +4268,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 20) + _depositor2ExpectedEarnings; - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } function testFuzz_CalculatesCorrectEarningsWhenTwoUsersDepositDifferentAmountsForPartialDurationsAndThereAreThreeRewards( @@ -4884,11 +4292,13 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // One quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(25); // A user stakes some tokens - _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); // Another 40 percent of the duration time elapses _jumpAheadByPercentOfRewardDuration(20); // Another user stakes some tokens - _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); // Another quarter of the duration elapses _jumpAheadByPercentOfRewardDuration(20); // The contract receives another reward, resetting the duration @@ -4923,8 +4333,8 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { uint256 _depositor1ExpectedEarnings = _percentOf(_rewardAmount1, 20) + (_stakeAmount1 * _combinedPhaseExpectedTotalRewards) / _combinedStake; - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor1), _depositor1ExpectedEarnings); - assertLteWithinOnePercent(govStaker.unclaimedReward(_depositor2), _depositor2ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId1), _depositor1ExpectedEarnings); + assertLteWithinOnePercent(govStaker.unclaimedReward(_depositId2), _depositor2ExpectedEarnings); } function testFuzz_CalculatesEarningsThatAreLessThanOrEqualToRewardsReceived( @@ -4943,18 +4353,20 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId1) = + _boundMintAndStake(_depositor1, _stakeAmount1, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); // Another user deposits stake - _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId2) = + _boundMintAndStake(_depositor2, _stakeAmount2, _delegatee); // The rest of the duration elapses _jumpAheadByPercentOfRewardDuration(100 - _durationPercent); - uint256 _earned1 = govStaker.unclaimedReward(_depositor1); - uint256 _earned2 = govStaker.unclaimedReward(_depositor2); + uint256 _earned1 = govStaker.unclaimedReward(_depositId1); + uint256 _earned2 = govStaker.unclaimedReward(_depositId2); // Rewards earned by depositors should always at most equal to the actual reward amount assertLteWithinOnePercent(_earned1 + _earned2, _rewardAmount); @@ -4982,8 +4394,10 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { vm.stopPrank(); // User deposit staking tokens - _stake(_depositor1, _smallDepositAmount, _delegatee); - _stake(_depositor2, _smallDepositAmount, _delegatee); + GovernanceStaker.DepositIdentifier _depositId1 = + _stake(_depositor1, _smallDepositAmount, _delegatee); + GovernanceStaker.DepositIdentifier _depositId2 = + _stake(_depositor2, _smallDepositAmount, _delegatee); _stake(_depositor3, _largeDepositAmount, _delegatee); // Every block _attacker deposits 0 stake and assigns _depositor1 as beneficiary, thus leading @@ -4999,7 +4413,7 @@ contract UnclaimedReward is GovernanceStakerRewardsTest { // Despite the attempted griefing attack, the unclaimed rewards for the two depositors should // be ~the same. assertLteWithinOnePercent( - govStaker.unclaimedReward(_depositor1), govStaker.unclaimedReward(_depositor2) + govStaker.unclaimedReward(_depositId1), govStaker.unclaimedReward(_depositId2) ); } } @@ -5018,16 +4432,17 @@ contract ClaimReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); - uint256 _earned = govStaker.unclaimedReward(_depositor); + uint256 _earned = govStaker.unclaimedReward(_depositId); vm.prank(_depositor); - govStaker.claimReward(); + govStaker.claimReward(_depositId); assertEq(rewardToken.balanceOf(_depositor), _earned); } @@ -5045,16 +4460,17 @@ contract ClaimReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); - uint256 _earned = govStaker.unclaimedReward(_depositor); + uint256 _earned = govStaker.unclaimedReward(_depositId); vm.prank(_depositor); - uint256 _claimedAmount = govStaker.claimReward(); + uint256 _claimedAmount = govStaker.claimReward(_depositId); assertEq(_earned, _claimedAmount); } @@ -5070,16 +4486,46 @@ contract ClaimReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 0, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); vm.prank(_depositor); - govStaker.claimReward(); + govStaker.claimReward(_depositId); - assertEq(govStaker.unclaimedReward(_depositor), 0); + assertEq(govStaker.unclaimedReward(_depositId), 0); + } + + function testFuzz_AllowsABeneficiaryWhoIsNotTheDepositOwnerToClaimRewards( + address _depositor, + address _delegatee, + address _beneficiary, + uint256 _stakeAmount, + uint256 _rewardAmount, + uint256 _durationPercent + ) public { + vm.assume(_beneficiary != address(govStaker)); + (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + _durationPercent = bound(_durationPercent, 1, 100); + + // A user deposits staking tokens + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // A portion of the duration passes + _jumpAheadByPercentOfRewardDuration(_durationPercent); + + uint256 _earned = govStaker.unclaimedReward(_depositId); + + vm.prank(_beneficiary); + govStaker.claimReward(_depositId); + + assertEq(govStaker.unclaimedReward(_depositId), 0); + assertEq(rewardToken.balanceOf(_beneficiary), _earned); } function testFuzz_EmitsAnEventWhenRewardsAreClaimed( @@ -5093,19 +4539,52 @@ contract ClaimReward is GovernanceStakerRewardsTest { _durationPercent = bound(_durationPercent, 1, 100); // A user deposits staking tokens - _boundMintAndStake(_depositor, _stakeAmount, _delegatee); + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee); // The contract is notified of a reward _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); - uint256 _earned = govStaker.unclaimedReward(_depositor); + uint256 _earned = govStaker.unclaimedReward(_depositId); vm.expectEmit(); - emit GovernanceStaker.RewardClaimed(_depositor, _earned); + emit GovernanceStaker.RewardClaimed(_depositId, _depositor, _earned); vm.prank(_depositor); - govStaker.claimReward(); + govStaker.claimReward(_depositId); + } + + function testFuzz_RevertIf_TheCallerIsNotTheDepositBeneficiary( + address _depositor, + address _delegatee, + address _beneficiary, + address _notBeneficiary, + uint256 _stakeAmount, + uint256 _rewardAmount, + uint256 _durationPercent + ) public { + vm.assume(_notBeneficiary != _beneficiary && _notBeneficiary != _depositor); + (_stakeAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_stakeAmount, _rewardAmount); + _durationPercent = bound(_durationPercent, 1, 100); + + // A user deposits staking tokens + (, GovernanceStaker.DepositIdentifier _depositId) = + _boundMintAndStake(_depositor, _stakeAmount, _delegatee, _beneficiary); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // A portion of the duration passes + _jumpAheadByPercentOfRewardDuration(_durationPercent); + + vm.prank(_notBeneficiary); + vm.expectRevert( + abi.encodeWithSelector( + GovernanceStaker.GovernanceStaker__Unauthorized.selector, + bytes32("not beneficiary"), + _notBeneficiary + ) + ); + govStaker.claimReward(_depositId); } } @@ -5140,7 +4619,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { _jumpAheadByPercentOfRewardDuration(_durationPercent); _deadline = bound(_deadline, block.timestamp, type(uint256).max); - uint256 _earned = govStaker.unclaimedReward(_beneficiary); + uint256 _earned = govStaker.unclaimedReward(_depositId); stdstore.target(address(govStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write( _currentNonce @@ -5148,7 +4627,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { bytes32 _message = keccak256( abi.encode( - govStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, govStaker.nonces(_beneficiary), _deadline + govStaker.CLAIM_REWARD_TYPEHASH(), _depositId, govStaker.nonces(_beneficiary), _deadline ) ); @@ -5157,7 +4636,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash); vm.prank(_sender); - govStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + govStaker.claimRewardOnBehalf(_depositId, _deadline, _signature); assertEq(rewardToken.balanceOf(_beneficiary), _earned); } @@ -5190,7 +4669,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { _jumpAheadByPercentOfRewardDuration(_durationPercent); _deadline = bound(_deadline, block.timestamp, type(uint256).max); - uint256 _earned = govStaker.unclaimedReward(_beneficiary); + uint256 _earned = govStaker.unclaimedReward(_depositId); stdstore.target(address(govStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write( _currentNonce @@ -5198,7 +4677,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { bytes32 _message = keccak256( abi.encode( - govStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, govStaker.nonces(_beneficiary), _deadline + govStaker.CLAIM_REWARD_TYPEHASH(), _depositId, govStaker.nonces(_beneficiary), _deadline ) ); @@ -5207,7 +4686,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash); vm.prank(_sender); - uint256 _claimedAmount = govStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + uint256 _claimedAmount = govStaker.claimRewardOnBehalf(_depositId, _deadline, _signature); assertEq(_earned, _claimedAmount); } @@ -5256,7 +4735,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { vm.expectRevert(GovernanceStaker.GovernanceStaker__InvalidSignature.selector); vm.prank(_sender); - govStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + govStaker.claimRewardOnBehalf(_depositId, _deadline, _signature); } function testFuzz_RevertIf_DeadlineExpired( @@ -5301,7 +4780,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { vm.expectRevert(GovernanceStaker.GovernanceStaker__ExpiredDeadline.selector); vm.prank(_sender); - govStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + govStaker.claimRewardOnBehalf(_depositId, _deadline, _signature); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -5351,7 +4830,7 @@ contract ClaimRewardOnBehalf is GovernanceStakerRewardsTest { vm.expectRevert(GovernanceStaker.GovernanceStaker__InvalidSignature.selector); vm.prank(_sender); - govStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + govStaker.claimRewardOnBehalf(_depositId, _deadline, _signature); } } @@ -5488,7 +4967,7 @@ contract Multicall is GovernanceStakerRewardsTest { govStaker.multicall(_calls); vm.stopPrank(); - (uint256 _amountResult,, address _delegateeResult, address _beneficiaryResult) = + (uint256 _amountResult,, address _delegateeResult, address _beneficiaryResult,,,) = govStaker.deposits(_depositId); assertEq(govStaker.depositorTotalStaked(_depositor), _stakeAmount0 + _stakeAmount1); assertEq(_amountResult, _stakeAmount0 + _stakeAmount1); diff --git a/test/helpers/GovernanceStaker.handler.sol b/test/helpers/GovernanceStaker.handler.sol index 45f408d..8983cc7 100644 --- a/test/helpers/GovernanceStaker.handler.sol +++ b/test/helpers/GovernanceStaker.handler.sol @@ -127,7 +127,7 @@ contract GovernanceStakerHandler is CommonBase, StdCheats, StdUtils { vm.assume(_depositIds[_currentActor].length > 0); GovernanceStaker.DepositIdentifier _depositId = GovernanceStaker.DepositIdentifier.wrap(_getActorRandDepositId(_actorDepositSeed)); - (uint256 _balance,,,) = govStaker.deposits(_depositId); + (uint256 _balance,,,,,,) = govStaker.deposits(_depositId); _amount = uint256(_bound(_amount, 0, _balance)); vm.startPrank(_currentActor); stakeToken.approve(address(govStaker), _amount); @@ -146,7 +146,7 @@ contract GovernanceStakerHandler is CommonBase, StdCheats, StdUtils { vm.assume(_depositIds[_currentActor].length > 0); GovernanceStaker.DepositIdentifier _depositId = GovernanceStaker.DepositIdentifier.wrap(_getActorRandDepositId(_actorDepositSeed)); - (uint256 _balance,,,) = govStaker.deposits(_depositId); + (uint256 _balance,,,,,,) = govStaker.deposits(_depositId); _amount = uint256(_bound(_amount, 0, _balance)); vm.startPrank(_currentActor); govStaker.withdraw(_depositId, _amount); @@ -154,10 +154,18 @@ contract GovernanceStakerHandler is CommonBase, StdCheats, StdUtils { ghost_stakeWithdrawn += _amount; } - function claimReward(uint256 _actorSeed) public countCall("claimReward") doCheckpoints { - _useActor(_beneficiaries, _actorSeed); + function claimReward(uint256 _actorSeed, uint256 _actorDepositSeed) + public + countCall("claimReward") + doCheckpoints + { + _useActor(_depositors, _actorSeed); + vm.assume(_currentActor != address(0)); + vm.assume(_depositIds[_currentActor].length > 0); + GovernanceStaker.DepositIdentifier _depositId = + GovernanceStaker.DepositIdentifier.wrap(_getActorRandDepositId(_actorDepositSeed)); vm.startPrank(_currentActor); - uint256 rewardsClaimed = govStaker.claimReward(); + uint256 rewardsClaimed = govStaker.claimReward(_depositId); vm.stopPrank(); ghost_rewardsClaimed += rewardsClaimed; }