From 98f4c9022139f8c5df3166c5fe6ca0be2a413cb8 Mon Sep 17 00:00:00 2001 From: Ana Julia Date: Sun, 7 Jul 2024 19:59:58 -0300 Subject: [PATCH] checkpoint --- src/RewardsDistributor.sol | 10 ++- src/Staking.sol | 18 ++--- test/Staking.integration.t.sol | 116 ++++++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 35 deletions(-) diff --git a/src/RewardsDistributor.sol b/src/RewardsDistributor.sol index 78ba1d8..486c489 100644 --- a/src/RewardsDistributor.sol +++ b/src/RewardsDistributor.sol @@ -76,7 +76,15 @@ contract RewardsDistributor is Ownable2Step, IRewardsDistributor { // difference in time since last update uint256 timeDelta = block.timestamp - rewardConfiguration.lastUpdate; - if (rewardConfiguration.emissionRate != 0 && timeDelta != 0) { + // the contract must have funds to distribute + // we don't want to revert in case its zero to not block the staking contract + uint256 funds = rewardToken.balanceOf(address(this)); + + if ( + rewardConfiguration.emissionRate != 0 && + timeDelta != 0 && + funds != 0 + ) { rewards = rewardConfiguration.emissionRate * timeDelta; // update the last update timestamp diff --git a/src/Staking.sol b/src/Staking.sol index ce8a10c..8f8d15d 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -374,7 +374,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { /// @param _rewardsDistributor The address of the rewards distributor contract function setRewardsDistributor( address _rewardsDistributor - ) external onlyOwner updateRewards { + ) external onlyOwner { rewardsDistributor = IRewardsDistributor(_rewardsDistributor); emit NewRewardsDistributor(_rewardsDistributor); @@ -382,9 +382,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { /// @notice Set the lock period /// @param _lockPeriod The lock period in seconds - function setLockPeriod( - uint256 _lockPeriod - ) external onlyOwner updateRewards { + function setLockPeriod(uint256 _lockPeriod) external onlyOwner { lockPeriod = _lockPeriod; emit NewLockPeriod(_lockPeriod); @@ -401,10 +399,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { /// @notice Set a keyper /// @param keyper The keyper address /// @param isKeyper Whether the keyper is a keyper or not - function setKeyper( - address keyper, - bool isKeyper - ) external onlyOwner updateRewards { + function setKeyper(address keyper, bool isKeyper) external onlyOwner { _setKeyper(keyper, isKeyper); } @@ -414,7 +409,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { function setKeypers( address[] memory _keypers, bool isKeyper - ) external onlyOwner updateRewards { + ) external onlyOwner { for (uint256 i = 0; i < _keypers.length; i++) { _setKeyper(_keypers[i], isKeyper); } @@ -452,6 +447,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { /// @return The maximum amount of assets that a keyper can withdraw function maxWithdraw(address keyper) public view virtual returns (uint256) { uint256 shares = balanceOf(keyper); + require(shares > 0, KeyperHasNoShares()); uint256 assets = convertToAssets(shares); @@ -461,7 +457,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { : minStake; if (assets < compare) { - // TODO check this + // need this branch as convertToAssets rounds down return 0; } else { return assets - compare; @@ -481,7 +477,7 @@ contract Staking is ERC20VotesUpgradeable, Ownable2StepUpgradeable { uint256 compare = locked >= minStake ? locked : minStake; if (assets < compare) { - // TODO check this + // need this branch as convertToAssets rounds down return 0; } else { return assets - compare; diff --git a/test/Staking.integration.t.sol b/test/Staking.integration.t.sol index bc0e077..8f43c9a 100644 --- a/test/Staking.integration.t.sol +++ b/test/Staking.integration.t.sol @@ -30,15 +30,6 @@ contract StakingIntegrationTest is Test { (staking, rewardsDistributor) = deployScript.run(); } - function _boundUnlockedTime(uint256 _time) internal view returns (uint256) { - return - bound( - _time, - vm.getBlockTimestamp() + LOCK_PERIOD, - vm.getBlockTimestamp() + 105 weeks - ); - } - function _boundRealisticTimeAhead( uint256 _time ) internal pure returns (uint256) { @@ -63,7 +54,7 @@ contract StakingIntegrationTest is Test { IERC20(STAKING_TOKEN).transfer(address(rewardsDistributor), poolSize); } - function _calculateAPR( + function _calculateReturnOverPrincipal( uint256 _rewardsReceived, uint256 _staked, uint256 _days @@ -124,24 +115,28 @@ contract StakingIntegrationTest is Test { uint256 rewardsReceived = staking.claimRewards(0); - uint256 APR = _calculateAPR(rewardsReceived, staked, jump); + uint256 APR = _calculateReturnOverPrincipal( + rewardsReceived, + staked, + jump + ); // 1% error margin assertApproxEqAbs(APR, 20e18, 1e18); } - function testFork_FirstDepositorsAlwaysReceiveMoreRewards() public { + function testForkFuzz_MultipleDepositorsStakeMinAmountDifferentTimestamp( + uint256 _jump + ) public { uint256 depositorsCount = 400; _setRewardAndFund(); - uint256 jumpBetweenStakes = 1 hours; + _jump = bound(_jump, 1 minutes, 12 hours); uint256[] memory timeStaked = new uint256[](depositorsCount); uint256 previousDepositorShares; - uint256 timestampFirstStake = vm.getBlockTimestamp(); - for (uint256 i = 1; i <= depositorsCount; i++) { address participant = address(uint160(i)); @@ -163,12 +158,11 @@ contract StakingIntegrationTest is Test { timeStaked[i - 1] = vm.getBlockTimestamp(); - _jumpAhead(jumpBetweenStakes); + _jumpAhead(_jump); } uint256 previousRewardsReceived; - // collect rewards and calculate rewards for (uint256 i = 1; i <= depositorsCount; i++) { address participant = address(uint160(i)); @@ -185,16 +179,87 @@ contract StakingIntegrationTest is Test { assertGt(rewardsReceived, previousRewardsReceived); } - _jumpAhead(jumpBetweenStakes); - uint256 assetsAfter = staking.convertToAssets( staking.balanceOf(participant) ); - assertApproxEqAbs(assetsAfter, MIN_STAKE, 2); + assertApproxEqAbs(assetsAfter, MIN_STAKE, 1e18); + } + } + + function testFork_ClaimRewardsAtTheEndOfSemester() public { + _setRewardAndFund(); + + uint256 staked = (CIRCULATION_SUPPLY * 25) / 100; + + deal(STAKING_TOKEN, address(this), staked); + + vm.prank(CONTRACT_OWNER); + staking.setKeyper(address(this), true); + + IERC20(STAKING_TOKEN).approve(address(staking), staked); + staking.stake(staked); + + uint256 jump = 86 days; + + _jumpAhead(jump); + + vm.prank(CONTRACT_OWNER); + staking.setKeyper(address(1), true); + + uint256 rewardsReceived = staking.claimRewards(0); + + uint256 APR = _calculateReturnOverPrincipal( + rewardsReceived, + staked, + jump + ); + + // 1% error margin + assertApproxEqAbs(APR, 20e18, 1e18); + } + + function testFork_ClaimRewardsEveryDayAndReestakeUntilEndSemester() public { + _setRewardAndFund(); + + uint256 staked = (CIRCULATION_SUPPLY * 25) / 100; + + deal(STAKING_TOKEN, address(this), staked); + + vm.prank(CONTRACT_OWNER); + staking.setKeyper(address(this), true); + + IERC20(STAKING_TOKEN).approve(address(staking), staked); + staking.stake(staked); + + uint256 previousTimestamp = vm.getBlockTimestamp(); + + for (uint256 i = 1; i < 2064; i++) { + _jumpAhead(1 hours); + + previousTimestamp = vm.getBlockTimestamp(); + uint256 rewardsReceived = staking.claimRewards(0); + + IERC20(STAKING_TOKEN).approve(address(staking), rewardsReceived); + staking.stake(rewardsReceived); } + + _jumpAhead(1 hours); + + uint256 assets = staking.convertToAssets( + staking.balanceOf(address(this)) + ); + + uint256 APR = _calculateReturnOverPrincipal( + assets - staked, + staked, + 86 days + ); + + // 1% error margin + assertApproxEqAbs(APR, 20e18, 1e18); } - function testForkFuzz_MultipleDepositorsStakeMinStakeSameBlock( + function testForkFuzz_MultipleDepositorsStakeMinStakeSameTimestamp( uint256 _depositorsCount, uint256 _jump ) public { @@ -226,8 +291,6 @@ contract StakingIntegrationTest is Test { uint256 expectedRewardPerKeyper = expectedRewardsDistributed / depositors.length; - uint256 APR = _calculateAPR(expectedRewardPerKeyper, MIN_STAKE, _jump); - _jumpAhead(_jump); // collect rewards @@ -240,6 +303,13 @@ contract StakingIntegrationTest is Test { vm.stopPrank(); assertApproxEqAbs(rewards, expectedRewardPerKeyper, 0.1e18); + + uint256 APR = _calculateReturnOverPrincipal( + rewards, + MIN_STAKE, + _jump + ); + assertApproxEqAbs(APR, 20e18, 1e18); } } }