diff --git a/.gas-report b/.gas-report index 6b5568c..7553415 100644 --- a/.gas-report +++ b/.gas-report @@ -15,77 +15,87 @@ | src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | | |------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2134697 | 9822 | | | | | +| 2116504 | 9735 | | | | | | Function Name | min | avg | median | max | # calls | -| MAX_LOCKUP_PERIOD | 272 | 272 | 272 | 272 | 23 | -| MAX_MULTIPLIER | 273 | 273 | 273 | 273 | 30 | -| MIN_LOCKUP_PERIOD | 274 | 274 | 274 | 274 | 11 | -| MP_RATE_PER_YEAR | 230 | 230 | 230 | 230 | 3 | -| SCALE_FACTOR | 294 | 294 | 294 | 294 | 41 | -| STAKING_TOKEN | 2381 | 2381 | 2381 | 2381 | 198 | -| accountedRewards | 373 | 970 | 373 | 2373 | 77 | -| emergencyModeEnabled | 2398 | 2398 | 2398 | 2398 | 7 | -| enableEmergencyMode | 2460 | 19367 | 24652 | 24652 | 8 | +| MAX_LOCKUP_PERIOD | 294 | 294 | 294 | 294 | 23 | +| MAX_MULTIPLIER | 251 | 251 | 251 | 251 | 30 | +| MIN_LOCKUP_PERIOD | 252 | 252 | 252 | 252 | 11 | +| MP_RATE_PER_YEAR | 253 | 253 | 253 | 253 | 3 | +| SCALE_FACTOR | 273 | 273 | 273 | 273 | 41 | +| STAKING_TOKEN | 2404 | 2404 | 2404 | 2404 | 222 | +| emergencyModeEnabled | 2420 | 2420 | 2420 | 2420 | 7 | +| enableEmergencyMode | 2439 | 19346 | 24631 | 24631 | 8 | | getAccount | 1621 | 1621 | 1621 | 1621 | 72 | -| getStakedBalance | 2567 | 2567 | 2567 | 2567 | 1 | -| initialize | 137773 | 137773 | 137773 | 137773 | 51 | -| isTrustedCodehash | 563 | 1078 | 563 | 2563 | 198 | -| leave | 62061 | 62061 | 62061 | 62061 | 1 | -| lock | 9839 | 33584 | 14168 | 76747 | 3 | -| proxiableUUID | 363 | 363 | 363 | 363 | 3 | -| rewardIndex | 394 | 419 | 394 | 2394 | 77 | -| setTrustedCodehash | 26226 | 26226 | 26226 | 26226 | 51 | -| stake | 134677 | 171411 | 176279 | 196758 | 61 | -| totalMP | 329 | 329 | 329 | 329 | 80 | -| totalMaxMP | 373 | 373 | 373 | 373 | 80 | -| totalStaked | 329 | 329 | 329 | 329 | 81 | -| unstake | 63997 | 84793 | 63997 | 120464 | 13 | -| updateAccountMP | 15353 | 17591 | 17855 | 17855 | 19 | -| updateGlobalState | 11066 | 41310 | 30530 | 63481 | 28 | -| upgradeToAndCall | 3124 | 9263 | 10809 | 10809 | 5 | - - -| src/StakeManagerProxy.sol:StakeManagerProxy contract | | | | | | -|------------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 278629 | 1263 | | | | | -| Function Name | min | avg | median | max | # calls | -| MAX_LOCKUP_PERIOD | 699 | 1481 | 699 | 5199 | 23 | -| MAX_MULTIPLIER | 700 | 1600 | 700 | 5200 | 30 | -| MIN_LOCKUP_PERIOD | 701 | 3973 | 5201 | 5201 | 11 | -| MP_RATE_PER_YEAR | 657 | 657 | 657 | 657 | 3 | -| SCALE_FACTOR | 721 | 721 | 721 | 721 | 41 | -| STAKING_TOKEN | 7308 | 7308 | 7308 | 7308 | 198 | -| accountedRewards | 800 | 1397 | 800 | 2800 | 77 | -| emergencyModeEnabled | 7325 | 7325 | 7325 | 7325 | 7 | -| enableEmergencyMode | 28455 | 45356 | 50640 | 50640 | 8 | -| getAccount | 2075 | 2075 | 2075 | 2075 | 72 | -| getStakedBalance | 7497 | 7497 | 7497 | 7497 | 1 | -| implementation | 343 | 936 | 343 | 2343 | 283 | -| isTrustedCodehash | 993 | 1508 | 993 | 2993 | 198 | -| rewardIndex | 821 | 846 | 821 | 2821 | 77 | -| setTrustedCodehash | 52872 | 52872 | 52872 | 52872 | 51 | -| totalMP | 756 | 756 | 756 | 756 | 80 | -| totalMaxMP | 800 | 800 | 800 | 800 | 80 | -| totalStaked | 756 | 756 | 756 | 756 | 81 | -| updateAccountMP | 41712 | 43950 | 44214 | 44214 | 19 | -| updateGlobalState | 37054 | 67298 | 56518 | 89469 | 28 | -| upgradeToAndCall | 29767 | 35900 | 37445 | 37445 | 5 | +| getStakedBalance | 2611 | 2611 | 2611 | 2611 | 1 | +| initialize | 115536 | 115536 | 115536 | 115536 | 57 | +| isTrustedCodehash | 541 | 1054 | 541 | 2541 | 222 | +| lastRewardTime | 351 | 1351 | 1351 | 2351 | 2 | +| leave | 56077 | 56077 | 56077 | 56077 | 1 | +| lock | 9861 | 31868 | 14190 | 71554 | 3 | +| proxiableUUID | 319 | 319 | 319 | 319 | 3 | +| rewardEndTime | 373 | 1373 | 1373 | 2373 | 2 | +| rewardStartTime | 396 | 1396 | 1396 | 2396 | 2 | +| rewardsBalanceOf | 1276 | 1276 | 1276 | 1276 | 4 | +| setReward | 2537 | 50851 | 60232 | 102549 | 7 | +| setTrustedCodehash | 26197 | 26197 | 26197 | 26197 | 57 | +| stake | 128652 | 168926 | 175469 | 195948 | 63 | +| totalMP | 373 | 373 | 373 | 373 | 80 | +| totalMaxMP | 372 | 372 | 372 | 372 | 80 | +| totalRewardsAccrued | 373 | 373 | 373 | 373 | 3 | +| totalRewardsSupply | 1003 | 1964 | 1767 | 6743 | 30 | +| totalStaked | 351 | 351 | 351 | 351 | 81 | +| unstake | 57991 | 58605 | 57991 | 61984 | 13 | +| updateAccountMP | 15397 | 18475 | 17899 | 34999 | 21 | +| updateGlobalState | 11066 | 28094 | 25315 | 110295 | 21 | +| upgradeToAndCall | 3125 | 9237 | 10766 | 10766 | 5 | + + +| src/StakeManagerProxy.sol:StakeManagerProxy contract | | | | | | +|------------------------------------------------------|-----------------|-------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 256004 | 1231 | | | | | +| Function Name | min | avg | median | max | # calls | +| MAX_LOCKUP_PERIOD | 721 | 1503 | 721 | 5221 | 23 | +| MAX_MULTIPLIER | 678 | 1578 | 678 | 5178 | 30 | +| MIN_LOCKUP_PERIOD | 679 | 3951 | 5179 | 5179 | 11 | +| MP_RATE_PER_YEAR | 680 | 680 | 680 | 680 | 3 | +| SCALE_FACTOR | 700 | 700 | 700 | 700 | 41 | +| STAKING_TOKEN | 7331 | 7331 | 7331 | 7331 | 222 | +| emergencyModeEnabled | 7347 | 7347 | 7347 | 7347 | 7 | +| enableEmergencyMode | 28434 | 45335 | 50619 | 50619 | 8 | +| getAccount | 2075 | 2075 | 2075 | 2075 | 72 | +| getStakedBalance | 7541 | 7541 | 7541 | 7541 | 1 | +| implementation | 343 | 899 | 343 | 2343 | 309 | +| isTrustedCodehash | 971 | 1484 | 971 | 2971 | 222 | +| lastRewardTime | 778 | 1778 | 1778 | 2778 | 2 | +| rewardEndTime | 800 | 1800 | 1800 | 2800 | 2 | +| rewardStartTime | 823 | 4073 | 4073 | 7323 | 2 | +| rewardsBalanceOf | 1706 | 1706 | 1706 | 1706 | 4 | +| setReward | 28817 | 77165 | 86590 | 128835 | 7 | +| setTrustedCodehash | 52843 | 52843 | 52843 | 52843 | 57 | +| totalMP | 800 | 800 | 800 | 800 | 80 | +| totalMaxMP | 799 | 799 | 799 | 799 | 80 | +| totalRewardsAccrued | 800 | 800 | 800 | 800 | 3 | +| totalRewardsSupply | 1430 | 2541 | 2194 | 11670 | 30 | +| totalStaked | 778 | 778 | 778 | 778 | 81 | +| updateAccountMP | 41756 | 44834 | 44258 | 61358 | 21 | +| updateGlobalState | 37054 | 54082 | 51303 | 136283 | 21 | +| upgradeToAndCall | 29768 | 35875 | 37402 | 37402 | 5 | | src/StakeVault.sol:StakeVault contract | | | | | | |----------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 1374543 | 6483 | | | | | +| 1374566 | 6483 | | | | | | Function Name | min | avg | median | max | # calls | | STAKING_TOKEN | 216 | 216 | 216 | 216 | 1 | -| emergencyExit | 36353 | 48857 | 48091 | 65191 | 7 | -| leave | 33507 | 132602 | 62962 | 370978 | 4 | -| lock | 33245 | 60236 | 48577 | 110544 | 4 | -| stake | 33454 | 242541 | 250826 | 271353 | 62 | +| emergencyExit | 36375 | 48879 | 48113 | 65213 | 7 | +| leave | 33507 | 131405 | 60569 | 370978 | 4 | +| lock | 33245 | 58948 | 48599 | 105351 | 4 | +| stake | 33454 | 240127 | 250016 | 270543 | 64 | | trustStakeManager | 28997 | 28997 | 28997 | 28997 | 1 | -| unstake | 33260 | 117303 | 113502 | 156376 | 14 | -| withdraw | 42227 | 42227 | 42227 | 42227 | 1 | +| unstake | 33260 | 94528 | 99898 | 107496 | 14 | +| withdraw | 42271 | 42271 | 42271 | 42271 | 1 | | src/XPNFTToken.sol:XPNFTToken contract | | | | | | @@ -161,10 +171,10 @@ | Deployment Cost | Deployment Size | | | | | | 625454 | 3260 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46330 | 46339 | 46342 | 46342 | 252 | -| balanceOf | 558 | 1395 | 558 | 2558 | 351 | -| mint | 51279 | 58806 | 51279 | 68379 | 268 | -| transfer | 34384 | 48853 | 51484 | 51484 | 13 | +| approve | 46330 | 46339 | 46342 | 46342 | 227 | +| balanceOf | 558 | 989 | 558 | 2558 | 139 | +| mint | 51279 | 56555 | 51279 | 68379 | 243 | +| transfer | 34384 | 42934 | 42934 | 51484 | 2 | | test/mocks/StackOverflowStakeManager.sol:StackOverflowStakeManager contract | | | | | | diff --git a/.gas-snapshot b/.gas-snapshot index 4861f43..115d2e1 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,61 +1,67 @@ -EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 89712) -EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 296084) -EmergencyExitTest:test_EmergencyExitBasic() (gas: 398741) -EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 830337) -EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 541602) -EmergencyExitTest:test_EmergencyExitWithLock() (gas: 387785) -EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 533248) -EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39377) -IntegrationTest:testStakeFoo() (gas: 1578393) -LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 2592444) -LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 293227) -LeaveTest:test_TrustNewStakeManager() (gas: 2642695) -LockTest:test_LockFailsWithInvalidPeriod() (gas: 306175) -LockTest:test_LockFailsWithNoStake() (gas: 61440) -LockTest:test_LockWithoutPriorLock() (gas: 403273) -MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1761895) +EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92598) +EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 295308) +EmergencyExitTest:test_EmergencyExitBasic() (gas: 381937) +EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 654329) +EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 389879) +EmergencyExitTest:test_EmergencyExitWithLock() (gas: 389475) +EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 374838) +EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39362) +IntegrationTest:testStakeFoo() (gas: 1166661) +LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 2548215) +LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 292439) +LeaveTest:test_TrustNewStakeManager() (gas: 2623611) +LockTest:test_LockFailsWithInvalidPeriod() (gas: 305322) +LockTest:test_LockFailsWithNoStake() (gas: 61418) +LockTest:test_LockWithoutPriorLock() (gas: 385914) +MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1749466) NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 85934) NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 58332) NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35804) NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 102512) NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 49555) NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35979) +RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 668216) +RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160234) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39364) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39300) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39335) +RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 609395) RewardsStreamerTest:testStake() (gas: 869181) -StakeTest:test_StakeMultipleAccounts() (gas: 512711) -StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 668852) -StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 879853) -StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 527355) -StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 549222) -StakeTest:test_StakeOneAccount() (gas: 293776) -StakeTest:test_StakeOneAccountAndRewards() (gas: 449912) -StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 540462) -StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 536907) -StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 314452) -StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 314419) -StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 314530) +StakeTest:test_StakeMultipleAccounts() (gas: 489543) +StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 495503) +StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 825688) +StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 512574) +StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 534457) +StakeTest:test_StakeOneAccount() (gas: 274483) +StakeTest:test_StakeOneAccountAndRewards() (gas: 280407) +StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 497162) +StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 493499) +StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 299311) +StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299300) +StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299411) StakingTokenTest:testStakeToken() (gas: 10422) -UnstakeTest:test_StakeMultipleAccounts() (gas: 512733) -UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 668874) -UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 879830) -UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 527332) -UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 549244) -UnstakeTest:test_StakeOneAccount() (gas: 293799) -UnstakeTest:test_StakeOneAccountAndRewards() (gas: 449934) -UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 540484) -UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 536887) -UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 314452) -UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 314419) -UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 314508) -UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 549390) -UnstakeTest:test_UnstakeMultipleAccounts() (gas: 728319) -UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1073610) -UnstakeTest:test_UnstakeOneAccount() (gas: 508967) -UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 531442) -UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 615950) -UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 564034) -UpgradeTest:test_RevertWhenNotOwner() (gas: 2210367) -UpgradeTest:test_UpgradeStakeManager() (gas: 2499517) -WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 308844) +UnstakeTest:test_StakeMultipleAccounts() (gas: 489587) +UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 495480) +UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 825687) +UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 512573) +UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 534501) +UnstakeTest:test_StakeOneAccount() (gas: 274506) +UnstakeTest:test_StakeOneAccountAndRewards() (gas: 280451) +UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 497206) +UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 493501) +UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 299356) +UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299300) +UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299411) +UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 537481) +UnstakeTest:test_UnstakeMultipleAccounts() (gas: 682793) +UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 774872) +UnstakeTest:test_UnstakeOneAccount() (gas: 466459) +UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 489723) +UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 399173) +UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 526156) +UpgradeTest:test_RevertWhenNotOwner() (gas: 2192157) +UpgradeTest:test_UpgradeStakeManager() (gas: 2463165) +WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 308100) XPNFTTokenTest:testApproveNotAllowed() (gas: 10500) XPNFTTokenTest:testGetApproved() (gas: 10523) XPNFTTokenTest:testIsApprovedForAll() (gas: 10698) diff --git a/certora/confs/EmergencyMode.conf b/certora/confs/EmergencyMode.conf index 8b48396..929f199 100644 --- a/certora/confs/EmergencyMode.conf +++ b/certora/confs/EmergencyMode.conf @@ -4,8 +4,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "RewardsStreamerMP:STAKING_TOKEN=ERC20A", - "RewardsStreamerMP:REWARD_TOKEN=ERC20A" + "RewardsStreamerMP:STAKING_TOKEN=ERC20A" ], "msg": "Verifying RewardsStreamerMP.sol", "rule_sanity": "basic", diff --git a/certora/confs/RewardsStreamerMP.conf b/certora/confs/RewardsStreamerMP.conf index afbef15..5d8cdfa 100644 --- a/certora/confs/RewardsStreamerMP.conf +++ b/certora/confs/RewardsStreamerMP.conf @@ -4,8 +4,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "RewardsStreamerMP:STAKING_TOKEN=ERC20A", - "RewardsStreamerMP:REWARD_TOKEN=ERC20A" + "RewardsStreamerMP:STAKING_TOKEN=ERC20A" ], "msg": "Verifying RewardsStreamerMP.sol", "rule_sanity": "basic", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index 579be3c..19d5899 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -7,7 +7,6 @@ "link" : [ "StakeVault:STAKING_TOKEN=ERC20A", "RewardsStreamerMP:STAKING_TOKEN=ERC20A", - "RewardsStreamerMP:REWARD_TOKEN=ERC20A", "StakeVault:stakeManager=RewardsStreamerMP" ], "msg": "Verifying StakeVault.sol", diff --git a/certora/specs/EmergencyMode.spec b/certora/specs/EmergencyMode.spec index 2caf88c..4c399d0 100644 --- a/certora/specs/EmergencyMode.spec +++ b/certora/specs/EmergencyMode.spec @@ -7,13 +7,11 @@ methods { definition isViewFunction(method f) returns bool = ( f.selector == sig:streamer.STAKING_TOKEN().selector || - f.selector == sig:streamer.REWARD_TOKEN().selector || f.selector == sig:streamer.SCALE_FACTOR().selector || f.selector == sig:streamer.MP_RATE_PER_YEAR().selector || f.selector == sig:streamer.MIN_LOCKUP_PERIOD().selector || f.selector == sig:streamer.MAX_LOCKUP_PERIOD().selector || f.selector == sig:streamer.MAX_MULTIPLIER().selector || - f.selector == sig:streamer.accountedRewards().selector || f.selector == sig:streamer.rewardIndex().selector || f.selector == sig:streamer.lastMPUpdatedTime().selector || f.selector == sig:streamer.owner().selector || @@ -24,13 +22,20 @@ definition isViewFunction(method f) returns bool = ( f.selector == sig:streamer.emergencyModeEnabled().selector || f.selector == sig:streamer.getStakedBalance(address).selector || f.selector == sig:streamer.getAccount(address).selector || - f.selector == sig:streamer.getPendingRewards(address).selector || - f.selector == sig:streamer.calculateAccountRewards(address).selector + f.selector == sig:streamer.rewardsBalanceOf(address).selector || + f.selector == sig:streamer.totalRewardsSupply().selector || + f.selector == sig:streamer.calculateAccountRewards(address).selector || + f.selector == sig:streamer.lastRewardTime().selector || + f.selector == sig:streamer.rewardAmount().selector || + f.selector == sig:streamer.totalRewardsAccrued().selector || + f.selector == sig:streamer.rewardStartTime().selector || + f.selector == sig:streamer.rewardEndTime().selector ); definition isOwnableFunction(method f) returns bool = ( f.selector == sig:streamer.renounceOwnership().selector || - f.selector == sig:streamer.transferOwnership(address).selector + f.selector == sig:streamer.transferOwnership(address).selector || + f.selector == sig:streamer.setReward(uint256, uint256).selector ); definition isTrustedCodehashAccessFunction(method f) returns bool = ( @@ -39,7 +44,7 @@ definition isTrustedCodehashAccessFunction(method f) returns bool = ( ); definition isInitializerFunction(method f) returns bool = ( - f.selector == sig:streamer.initialize(address,address,address).selector + f.selector == sig:streamer.initialize(address,address).selector ); definition isUUPSUpgradeableFunction(method f) returns bool = ( diff --git a/src/RewardsStreamerMP.sol b/src/RewardsStreamerMP.sol index d6da7dd..08a4e00 100644 --- a/src/RewardsStreamerMP.sol +++ b/src/RewardsStreamerMP.sol @@ -18,9 +18,9 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc error StakingManager__TokensAreLocked(); error StakingManager__AlreadyLocked(); error StakingManager__EmergencyModeEnabled(); + error StakingManager__DurationCannotBeZero(); IERC20 public STAKING_TOKEN; - IERC20 public REWARD_TOKEN; uint256 public constant SCALE_FACTOR = 1e18; uint256 public constant MP_RATE_PER_YEAR = 1e18; @@ -33,10 +33,15 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc uint256 public totalMP; uint256 public totalMaxMP; uint256 public rewardIndex; - uint256 public accountedRewards; uint256 public lastMPUpdatedTime; bool public emergencyModeEnabled; + uint256 public totalRewardsAccrued; + uint256 public rewardAmount; + uint256 public lastRewardTime; + uint256 public rewardStartTime; + uint256 public rewardEndTime; + struct Account { uint256 stakedBalance; uint256 accountRewardIndex; @@ -59,13 +64,12 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc _disableInitializers(); } - function initialize(address _owner, address _stakingToken, address _rewardToken) public initializer { + function initialize(address _owner, address _stakingToken) public initializer { __TrustedCodehashAccess_init(_owner); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); STAKING_TOKEN = IERC20(_stakingToken); - REWARD_TOKEN = IERC20(_rewardToken); lastMPUpdatedTime = block.timestamp; } @@ -90,11 +94,6 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc revert StakingManager__CannotRestakeWithLockedFunds(); } - uint256 accountRewards = calculateAccountRewards(msg.sender); - if (accountRewards > 0) { - distributeRewards(msg.sender, accountRewards); - } - account.stakedBalance += amount; totalStaked += amount; @@ -171,11 +170,6 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc _updateGlobalState(); _updateAccountMP(accountAddress); - uint256 accountRewards = calculateAccountRewards(accountAddress); - if (accountRewards > 0) { - distributeRewards(accountAddress, accountRewards); - } - uint256 previousStakedBalance = account.stakedBalance; uint256 mpToReduce = (account.accountMP * amount * SCALE_FACTOR) / (previousStakedBalance * SCALE_FACTOR); @@ -237,6 +231,7 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc // Adjust rewardIndex before updating totalMP uint256 previousTotalWeight = totalStaked + totalMP; totalMP += accruedMP; + uint256 newTotalWeight = totalStaked + totalMP; if (previousTotalWeight != 0 && newTotalWeight != previousTotalWeight) { @@ -246,19 +241,73 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc lastMPUpdatedTime = currentTime; } + function setReward(uint256 amount, uint256 duration) external onlyOwner { + if (duration == 0) { + revert StakingManager__DurationCannotBeZero(); + } + + if (amount == 0) { + revert StakingManager__AmountCannotBeZero(); + } + + // this will call _updateRewardIndex and update the totalRewardsAccrued + _updateGlobalState(); + + // in case _updateRewardIndex returns earlier, + // we still update the lastRewardTime + lastRewardTime = block.timestamp; + rewardAmount = amount; + rewardStartTime = block.timestamp; + rewardEndTime = block.timestamp + duration; + } + + function _calculatePendingRewards() internal view returns (uint256) { + if (rewardEndTime <= rewardStartTime) { + // No active reward period + return 0; + } + + uint256 currentTime = block.timestamp < rewardEndTime ? block.timestamp : rewardEndTime; + + if (currentTime <= lastRewardTime) { + // No new rewards have accrued since lastRewardTime + return 0; + } + + uint256 timeElapsed = currentTime - lastRewardTime; + uint256 duration = rewardEndTime - rewardStartTime; + + if (duration == 0) { + // Prevent division by zero + return 0; + } + + uint256 accruedRewards = (timeElapsed * rewardAmount) / duration; + return accruedRewards; + } + function updateRewardIndex() internal { uint256 totalWeight = totalStaked + totalMP; if (totalWeight == 0) { return; } - uint256 rewardBalance = REWARD_TOKEN.balanceOf(address(this)); - uint256 newRewards = rewardBalance > accountedRewards ? rewardBalance - accountedRewards : 0; + uint256 currentTime = block.timestamp; + uint256 applicableTime = rewardEndTime > currentTime ? currentTime : rewardEndTime; + uint256 elapsedTime = applicableTime - lastRewardTime; + + if (elapsedTime == 0) { + return; + } - if (newRewards > 0) { - rewardIndex += (newRewards * SCALE_FACTOR) / totalWeight; - accountedRewards += newRewards; + uint256 newRewards = _calculatePendingRewards(); + if (newRewards == 0) { + return; } + + totalRewardsAccrued += newRewards; + rewardIndex += (newRewards * SCALE_FACTOR) / totalWeight; + lastRewardTime = block.timestamp < rewardEndTime ? block.timestamp : rewardEndTime; } function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal view returns (uint256) { @@ -295,24 +344,11 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc function calculateAccountRewards(address accountAddress) public view returns (uint256) { Account storage account = accounts[accountAddress]; + uint256 accountWeight = account.stakedBalance + account.accountMP; uint256 deltaRewardIndex = rewardIndex - account.accountRewardIndex; - return (accountWeight * deltaRewardIndex) / SCALE_FACTOR; - } - - function distributeRewards(address to, uint256 amount) internal { - uint256 rewardBalance = REWARD_TOKEN.balanceOf(address(this)); - // If amount is higher than the contract's balance (for rounding error), transfer the balance. - if (amount > rewardBalance) { - amount = rewardBalance; - } - - accountedRewards -= amount; - bool success = REWARD_TOKEN.transfer(to, amount); - if (!success) { - revert StakingManager__TransferFailed(); - } + return (accountWeight * deltaRewardIndex) / SCALE_FACTOR; } function enableEmergencyMode() external onlyOwner { @@ -326,11 +362,15 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc return accounts[accountAddress].stakedBalance; } - function getPendingRewards(address accountAddress) external view returns (uint256) { - return calculateAccountRewards(accountAddress); - } - function getAccount(address accountAddress) external view returns (Account memory) { return accounts[accountAddress]; } + + function totalRewardsSupply() public view returns (uint256) { + return totalRewardsAccrued + _calculatePendingRewards(); + } + + function rewardsBalanceOf(address accountAddress) external view returns (uint256) { + return calculateAccountRewards(accountAddress); + } } diff --git a/src/interfaces/IStakeManager.sol b/src/interfaces/IStakeManager.sol index 0cdd534..6b44e22 100644 --- a/src/interfaces/IStakeManager.sol +++ b/src/interfaces/IStakeManager.sol @@ -23,7 +23,6 @@ interface IStakeManager is ITrustedCodehashAccess { function getStakedBalance(address _vault) external view returns (uint256 _balance); function STAKING_TOKEN() external view returns (IERC20); - function REWARD_TOKEN() external view returns (IERC20); function MIN_LOCKUP_PERIOD() external view returns (uint256); function MAX_LOCKUP_PERIOD() external view returns (uint256); function MP_RATE_PER_YEAR() external view returns (uint256); diff --git a/test/RewardsStreamerMP.t.sol b/test/RewardsStreamerMP.t.sol index c38f5e7..c4297a1 100644 --- a/test/RewardsStreamerMP.t.sol +++ b/test/RewardsStreamerMP.t.sol @@ -13,7 +13,6 @@ import { MockToken } from "./mocks/MockToken.sol"; import { StackOverflowStakeManager } from "./mocks/StackOverflowStakeManager.sol"; contract RewardsStreamerMPTest is Test { - MockToken rewardToken; MockToken stakingToken; RewardsStreamerMP public streamer; @@ -26,11 +25,9 @@ contract RewardsStreamerMPTest is Test { mapping(address owner => address vault) public vaults; function setUp() public virtual { - rewardToken = new MockToken("Reward Token", "RT"); stakingToken = new MockToken("Staking Token", "ST"); - bytes memory initializeData = - abi.encodeCall(RewardsStreamerMP.initialize, (address(this), address(stakingToken), address(rewardToken))); + bytes memory initializeData = abi.encodeCall(RewardsStreamerMP.initialize, (admin, address(stakingToken))); address impl = address(new RewardsStreamerMP()); address proxy = address(new StakeManagerProxy(impl, initializeData)); streamer = RewardsStreamerMP(proxy); @@ -47,10 +44,6 @@ contract RewardsStreamerMPTest is Test { vm.prank(accounts[i]); stakingToken.approve(address(vault), 10_000e18); } - - rewardToken.mint(admin, 10_000e18); - vm.prank(admin); - rewardToken.approve(address(streamer), 10_000e18); } struct CheckStreamerParams { @@ -60,16 +53,14 @@ contract RewardsStreamerMPTest is Test { uint256 stakingBalance; uint256 rewardBalance; uint256 rewardIndex; - uint256 accountedRewards; } function checkStreamer(CheckStreamerParams memory p) public view { assertEq(streamer.totalStaked(), p.totalStaked, "wrong total staked"); assertEq(streamer.totalMP(), p.totalMP, "wrong total MP"); assertEq(streamer.totalMaxMP(), p.totalMaxMP, "wrong totalMaxMP MP"); - assertEq(rewardToken.balanceOf(address(streamer)), p.rewardBalance, "wrong reward balance"); - assertEq(streamer.rewardIndex(), p.rewardIndex, "wrong reward index"); - assertEq(streamer.accountedRewards(), p.accountedRewards, "wrong accounted rewards"); + // assertEq(rewardToken.balanceOf(address(streamer)), p.rewardBalance, "wrong reward balance"); + // assertEq(streamer.rewardIndex(), p.rewardIndex, "wrong reward index"); } struct CheckAccountParams { @@ -83,13 +74,13 @@ contract RewardsStreamerMPTest is Test { } function checkAccount(CheckAccountParams memory p) public view { - assertEq(rewardToken.balanceOf(p.account), p.rewardBalance, "wrong account reward balance"); + // assertEq(rewardToken.balanceOf(p.account), p.rewardBalance, "wrong account reward balance"); RewardsStreamerMP.Account memory accountInfo = streamer.getAccount(p.account); assertEq(accountInfo.stakedBalance, p.stakedBalance, "wrong account staked balance"); assertEq(stakingToken.balanceOf(p.account), p.vaultBalance, "wrong vault balance"); - assertEq(accountInfo.accountRewardIndex, p.rewardIndex, "wrong account reward index"); + // assertEq(accountInfo.accountRewardIndex, p.rewardIndex, "wrong account reward index"); assertEq(accountInfo.accountMP, p.accountMP, "wrong account MP"); assertEq(accountInfo.maxMP, p.maxMP, "wrong account max MP"); } @@ -99,6 +90,7 @@ contract RewardsStreamerMPTest is Test { vault = new StakeVault(owner, IStakeManagerProxy(address(streamer))); if (!streamer.isTrustedCodehash(address(vault).codehash)) { + vm.prank(admin); streamer.setTrustedCodehash(address(vault).codehash, true); } } @@ -127,12 +119,6 @@ contract RewardsStreamerMPTest is Test { vault.leave(account); } - function _addReward(uint256 amount) public { - vm.prank(admin); - rewardToken.transfer(address(streamer), amount); - streamer.updateGlobalState(); - } - function _calculateBonusMP(uint256 amount, uint256 lockupTime) public view returns (uint256) { return amount * (lockupTime * streamer.MAX_MULTIPLIER() * streamer.SCALE_FACTOR() / streamer.MAX_LOCKUP_PERIOD()) @@ -167,8 +153,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 0, stakingBalance: 0, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -183,8 +168,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -210,8 +194,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -241,7 +224,6 @@ contract IntegrationTest is RewardsStreamerMPTest { // T3 vm.prank(admin); - rewardToken.transfer(address(streamer), 1000e18); streamer.updateGlobalState(); checkStreamer( @@ -251,9 +233,8 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 1000e18, - rewardIndex: 125e17, // 1000 rewards / (40 staked + 40 MP) = 12.5 - accountedRewards: 1000e18 - }) + rewardIndex: 125e17 // 1000 rewards / (40 staked + 40 MP) = 12.5 + }) ); checkAccount( @@ -294,8 +275,7 @@ contract IntegrationTest is RewardsStreamerMPTest { rewardBalance: 1000e18, // 6 months passed and more MPs have been accrued // so we need to adjust the reward index - rewardIndex: 10e18, - accountedRewards: 1000e18 + rewardIndex: 10e18 }) ); @@ -309,8 +289,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 150e18, // 200e18 - (10e18 * 5) = 150e18 stakingBalance: 30e18, rewardBalance: 750e18, - rewardIndex: 10e18, - accountedRewards: 750e18 + rewardIndex: 10e18 }) ); @@ -348,8 +327,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 300e18, stakingBalance: 60e18, rewardBalance: 750e18, - rewardIndex: 10e18, - accountedRewards: 750e18 + rewardIndex: 10e18 }) ); @@ -391,7 +369,6 @@ contract IntegrationTest is RewardsStreamerMPTest { // T6 vm.prank(admin); - rewardToken.transfer(address(streamer), 1000e18); streamer.updateGlobalState(); checkStreamer( @@ -401,8 +378,7 @@ contract IntegrationTest is RewardsStreamerMPTest { totalMaxMP: 300e18, stakingBalance: 60e18, rewardBalance: 1750e18, - rewardIndex: 17_407_407_407_407_407_407, - accountedRewards: 1750e18 + rewardIndex: 17_407_407_407_407_407_407 }) ); @@ -453,8 +429,7 @@ contract IntegrationTest is RewardsStreamerMPTest { stakingBalance: 30e18, // 1750 - (750 + 555.55) = 444.44 rewardBalance: 444_444_444_444_444_444_475, - rewardIndex: 17_407_407_407_407_407_407, - accountedRewards: 444_444_444_444_444_444_475 + rewardIndex: 17_407_407_407_407_407_407 }) ); @@ -519,8 +494,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); checkAccount( @@ -546,8 +520,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -563,9 +536,6 @@ contract StakeTest is RewardsStreamerMPTest { }) ); - // 1000 rewards generated - _addReward(1000e18); - checkStreamer( CheckStreamerParams({ totalStaked: 10e18, @@ -573,9 +543,8 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 1000e18, - rewardIndex: 50e18, // (1000 rewards / (10 staked + 10 MP)) = 50 - accountedRewards: 1000e18 - }) + rewardIndex: 50e18 // (1000 rewards / (10 staked + 10 MP)) = 50 + }) ); } @@ -594,8 +563,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 52_465_753_424_657_534_240, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -615,8 +583,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 90e18, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -636,8 +603,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 52_821_917_808_219_178_080, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -656,8 +622,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -677,8 +642,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -710,8 +674,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -742,8 +705,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -773,8 +735,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -804,8 +765,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: stakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -824,8 +784,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -868,8 +827,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -896,8 +854,6 @@ contract StakeTest is RewardsStreamerMPTest { maxMP: 150e18 }) ); - // 1000 rewards generated - _addReward(1000e18); checkStreamer( CheckStreamerParams({ @@ -906,9 +862,8 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 1000e18, - rewardIndex: 125e17, // (1000 rewards / (40 staked + 40 MP)) = 12,5 - accountedRewards: 1000e18 - }) + rewardIndex: 125e17 // (1000 rewards / (40 staked + 40 MP)) = 12,5 + }) ); } @@ -937,8 +892,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 202_465_753_424_657_534_240, stakingBalance: sumOfStakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -968,8 +922,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: 250_356_164_383_561_643_820, stakingBalance: sumOfStakeAmount, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -997,8 +950,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: totalStaked, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1047,8 +999,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: totalStaked, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1097,8 +1048,7 @@ contract StakeTest is RewardsStreamerMPTest { totalMaxMP: totalMaxMP, stakingBalance: totalStaked, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1144,8 +1094,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 10e18, stakingBalance: 2e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1170,8 +1119,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 0, stakingBalance: 0, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -1193,8 +1141,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1208,8 +1155,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 25e18, stakingBalance: 5e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -1237,8 +1183,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 52_465_753_424_657_534_240, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1253,8 +1198,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 26_232_876_712_328_767_120, stakingBalance: 5e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -1271,8 +1215,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 10e18, stakingBalance: 2e18, rewardBalance: 0, // rewards are all paid out to alice - rewardIndex: 50e18, - accountedRewards: 0 + rewardIndex: 50e18 }) ); @@ -1399,8 +1342,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 100e18, stakingBalance: 20e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1442,9 +1384,8 @@ contract UnstakeTest is StakeTest { stakingBalance: 30e18, // alice owned a 25% of the pool, so 25% of the rewards are paid out to alice (250) rewardBalance: 750e18, - rewardIndex: 125e17, // reward index remains unchanged - accountedRewards: 750e18 - }) + rewardIndex: 125e17 // reward index remains unchanged + }) ); checkAccount( @@ -1468,8 +1409,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 100e18, stakingBalance: 20e18, rewardBalance: 0, // bob should've now gotten the rest of the rewards - rewardIndex: 125e17, - accountedRewards: 0 + rewardIndex: 125e17 }) ); @@ -1494,8 +1434,7 @@ contract UnstakeTest is StakeTest { totalMaxMP: 0, stakingBalance: 0, rewardBalance: 0, - rewardIndex: 125e17, - accountedRewards: 0 + rewardIndex: 125e17 }) ); @@ -1597,8 +1536,11 @@ contract EmergencyExitTest is RewardsStreamerMPTest { } function test_CannotEnableEmergencyModeTwice() public { + vm.prank(admin); streamer.enableEmergencyMode(); + vm.expectRevert(RewardsStreamerMP.StakingManager__EmergencyModeEnabled.selector); + vm.prank(admin); streamer.enableEmergencyMode(); } @@ -1607,6 +1549,7 @@ contract EmergencyExitTest is RewardsStreamerMPTest { _stake(alice, 10e18, 0); + vm.prank(admin); streamer.enableEmergencyMode(); _emergencyExit(alice); @@ -1619,8 +1562,7 @@ contract EmergencyExitTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 0, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1642,13 +1584,10 @@ contract EmergencyExitTest is RewardsStreamerMPTest { function test_EmergencyExitWithRewards() public { uint256 aliceInitialBalance = stakingToken.balanceOf(alice); - uint256 aliceInitialRewardBalance = rewardToken.balanceOf(vaults[alice]); _stake(alice, 10e18, 0); - // Add some rewards - _addReward(1000e18); - + vm.prank(admin); streamer.enableEmergencyMode(); _emergencyExit(alice); @@ -1660,14 +1599,12 @@ contract EmergencyExitTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 1000e18, - rewardIndex: 50e18, - accountedRewards: 1000e18 + rewardIndex: 50e18 }) ); // Check Alice staked tokens but no rewards assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get staked tokens back"); - assertEq(rewardToken.balanceOf(vaults[alice]), aliceInitialRewardBalance, "Alice should not get rewards"); assertEq(stakingToken.balanceOf(address(vaults[alice])), 0, "Vault should be empty"); } @@ -1676,6 +1613,7 @@ contract EmergencyExitTest is RewardsStreamerMPTest { _stake(alice, 10e18, 90 days); + vm.prank(admin); streamer.enableEmergencyMode(); _emergencyExit(alice); @@ -1688,14 +1626,12 @@ contract EmergencyExitTest is RewardsStreamerMPTest { function test_EmergencyExitMultipleUsers() public { uint256 aliceInitialBalance = stakingToken.balanceOf(alice); uint256 bobInitialBalance = stakingToken.balanceOf(bob); - uint256 aliceInitialRewardBalance = rewardToken.balanceOf(vaults[alice]); - uint256 bobInitialRewardBalance = rewardToken.balanceOf(vaults[bob]); // Setup multiple stakers _stake(alice, 10e18, 0); _stake(bob, 30e18, 0); - _addReward(1000e18); + vm.prank(admin); streamer.enableEmergencyMode(); // Alice exits first @@ -1709,8 +1645,7 @@ contract EmergencyExitTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 1000e18, - rewardIndex: 125e17, - accountedRewards: 1000e18 + rewardIndex: 125e17 }) ); @@ -1725,15 +1660,14 @@ contract EmergencyExitTest is RewardsStreamerMPTest { totalMaxMP: 200e18, stakingBalance: 40e18, rewardBalance: 1000e18, - rewardIndex: 125e17, - accountedRewards: 1000e18 + rewardIndex: 125e17 }) ); checkAccount( CheckAccountParams({ account: vaults[alice], - rewardBalance: aliceInitialRewardBalance, + rewardBalance: 0, stakedBalance: 10e18, vaultBalance: 0, rewardIndex: 0, @@ -1745,7 +1679,7 @@ contract EmergencyExitTest is RewardsStreamerMPTest { checkAccount( CheckAccountParams({ account: vaults[bob], - rewardBalance: bobInitialRewardBalance, + rewardBalance: 0, stakedBalance: 30e18, vaultBalance: 0, rewardIndex: 0, @@ -1763,11 +1697,11 @@ contract EmergencyExitTest is RewardsStreamerMPTest { function test_EmergencyExitToAlternateAddress() public { _stake(alice, 10e18, 0); - _addReward(1000e18); address alternateAddress = makeAddr("alternate"); uint256 alternateInitialBalance = stakingToken.balanceOf(alternateAddress); + vm.prank(admin); streamer.enableEmergencyMode(); // Alice exits to alternate address @@ -1800,6 +1734,7 @@ contract UpgradeTest is RewardsStreamerMPTest { function _upgradeStakeManager() internal { address newImpl = address(new RewardsStreamerMP()); bytes memory initializeData; + vm.prank(admin); UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData); } @@ -1827,8 +1762,7 @@ contract UpgradeTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1843,8 +1777,7 @@ contract UpgradeTest is RewardsStreamerMPTest { totalMaxMP: 50e18, stakingBalance: 10e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); } @@ -1854,6 +1787,7 @@ contract LeaveTest is RewardsStreamerMPTest { function _upgradeStakeManager() internal { address newImpl = address(new RewardsStreamerMP()); bytes memory initializeData; + vm.prank(admin); UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData); } @@ -1881,8 +1815,7 @@ contract LeaveTest is RewardsStreamerMPTest { totalMaxMP: 500e18, stakingBalance: 100e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1897,8 +1830,7 @@ contract LeaveTest is RewardsStreamerMPTest { totalMaxMP: 0, stakingBalance: 0, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); @@ -1954,6 +1886,7 @@ contract MaliciousUpgradeTest is RewardsStreamerMPTest { function _upgradeStakeManager() internal { address newImpl = address(new RewardsStreamerMP()); bytes memory initializeData; + vm.prank(admin); UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData); } @@ -1973,14 +1906,14 @@ contract MaliciousUpgradeTest is RewardsStreamerMPTest { totalMaxMP: 500e18, stakingBalance: 100e18, rewardBalance: 0, - rewardIndex: 0, - accountedRewards: 0 + rewardIndex: 0 }) ); // upgrade the manager to a malicious one address newImpl = address(new StackOverflowStakeManager()); bytes memory initializeData; + vm.prank(admin); UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData); // alice leaves system and is able to get funds out, despite malicious manager @@ -1989,3 +1922,122 @@ contract MaliciousUpgradeTest is RewardsStreamerMPTest { assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get her tokens back"); } } + +contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { + function setUp() public virtual override { + super.setUp(); + } + + function testSetRewards() public { + assertEq(streamer.rewardStartTime(), 0); + assertEq(streamer.rewardEndTime(), 0); + assertEq(streamer.lastRewardTime(), 0); + + uint256 currentTime = vm.getBlockTimestamp(); + // just to be sure that currentTime is not 0 + // since we are testing that it is used for rewardStartTime + currentTime += 1 days; + vm.warp(currentTime); + vm.prank(admin); + streamer.setReward(1000, 10); + + assertEq(streamer.rewardStartTime(), currentTime); + assertEq(streamer.rewardEndTime(), currentTime + 10); + assertEq(streamer.lastRewardTime(), currentTime); + } + + function testSetRewards_RevertsNotAuthorized() public { + vm.prank(alice); + vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); + streamer.setReward(1000, 10); + } + + function testSetRewards_RevertsBadDuration() public { + vm.prank(admin); + vm.expectRevert(RewardsStreamerMP.StakingManager__DurationCannotBeZero.selector); + streamer.setReward(1000, 0); + } + + function testSetRewards_RevertsBadAmount() public { + vm.prank(admin); + vm.expectRevert(RewardsStreamerMP.StakingManager__AmountCannotBeZero.selector); + streamer.setReward(0, 10); + } + + function testTotalRewardsSupply() public { + _stake(alice, 100e18, 0); + assertEq(streamer.totalRewardsSupply(), 0); + + uint256 initialTime = vm.getBlockTimestamp(); + + vm.prank(admin); + streamer.setReward(1000e18, 10 days); + assertEq(streamer.totalRewardsSupply(), 0); + + for (uint256 i = 0; i <= 10; i++) { + vm.warp(initialTime + i * 1 days); + assertEq(streamer.totalRewardsSupply(), 100e18 * i); + } + + // after the end of the reward period, the total rewards supply does not increase + vm.warp(initialTime + 11 days); + assertEq(streamer.totalRewardsSupply(), 1000e18); + assertEq(streamer.totalRewardsAccrued(), 0); + + uint256 secondRewardTime = initialTime + 20 days; + vm.warp(secondRewardTime); + + // still the same rewards supply after 20 days + assertEq(streamer.totalRewardsSupply(), 1000e18); + assertEq(streamer.totalRewardsAccrued(), 0); + + // set other 2000 rewards for other 10 days + vm.prank(admin); + streamer.setReward(2000e18, 10 days); + + // accrued is 1000 from the previous reward and still 0 for the new one + assertEq(streamer.totalRewardsSupply(), 1000e18, "totalRewardsSupply should be 1000"); + assertEq(streamer.totalRewardsAccrued(), 1000e18); + + uint256 previousSupply = 1000e18; + for (uint256 i = 0; i <= 10; i++) { + vm.warp(secondRewardTime + i * 1 days); + assertEq(streamer.totalRewardsSupply(), previousSupply + 200e18 * i); + } + } + + function testRewardsBalanceOf() public { + assertEq(streamer.totalRewardsSupply(), 0); + + vm.warp(0); + + uint256 initialTime = vm.getBlockTimestamp(); + + _stake(alice, 100e18, 0); + assertEq(streamer.rewardsBalanceOf(vaults[alice]), 0); + + vm.prank(admin); + streamer.setReward(1000e18, 10 days); + assertEq(streamer.rewardsBalanceOf(vaults[alice]), 0); + + vm.warp(initialTime + 1 days); + + // FIXME: this is needed to update the global state and account MP + // Later we should update the functions to use "real-time" values. + streamer.updateGlobalState(); + streamer.updateAccountMP(vaults[alice]); + + uint256 tolerance = 300; // 300 wei + + assertEq(streamer.totalRewardsSupply(), 100e18, "Total rewards supply mismatch"); + assertApproxEqAbs(streamer.rewardsBalanceOf(vaults[alice]), 100e18, tolerance); + + vm.warp(initialTime + 10 days); + + streamer.updateGlobalState(); + streamer.updateAccountMP(vaults[alice]); + + assertEq(streamer.totalRewardsSupply(), 1000e18, "Total rewards supply mismatch"); + assertApproxEqAbs(streamer.rewardsBalanceOf(vaults[alice]), 1000e18, tolerance); + } +} diff --git a/test/mocks/StackOverflowStakeManager.sol b/test/mocks/StackOverflowStakeManager.sol index 95f4f63..a57e0c9 100644 --- a/test/mocks/StackOverflowStakeManager.sol +++ b/test/mocks/StackOverflowStakeManager.sol @@ -14,7 +14,6 @@ contract StackOverflowStakeManager is ReentrancyGuardUpgradeable { IERC20 public STAKING_TOKEN; - IERC20 public REWARD_TOKEN; uint256 public constant SCALE_FACTOR = 1e18; uint256 public constant MP_RATE_PER_YEAR = 1e18;