From 93740259240d3a09b401a12bb784c2408f318dd3 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:21:51 +0200 Subject: [PATCH] feat(StakeManager): add capabilities to register vaults This commit introduces changes related to vault registrations in the stake manager. The stake manager needs to keep track of the vaults a users creates so it can aggregate accumulated MP across vaults for any given user. The `StakeVault` now comes with a `register()` function which needs to be called to register itself with the stake manager. `StakeManager` has a new `onlyRegisteredVault` modifier that ensures only registered vaults can actually `stake` and `unstake`. Closes #70 --- .gas-report | 126 +++++++++++---------- .gas-snapshot | 114 ++++++++++--------- certora/specs/EmergencyMode.spec | 9 +- src/RewardsStreamerMP.sol | 138 +++++++++++++++++++++-- src/StakeVault.sol | 19 +++- src/interfaces/IStakeManager.sol | 1 + src/interfaces/IStakeVault.sol | 10 ++ test/RewardsStreamerMP.t.sol | 88 +++++++++++++++ test/StakeVault.test.sol | 14 ++- test/mocks/StackOverflowStakeManager.sol | 4 + 10 files changed, 391 insertions(+), 132 deletions(-) create mode 100644 src/interfaces/IStakeVault.sol diff --git a/.gas-report b/.gas-report index 7553415..962975e 100644 --- a/.gas-report +++ b/.gas-report @@ -15,87 +15,99 @@ | src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | | |------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2116504 | 9735 | | | | | +| 2491772 | 11482 | | | | | | Function Name | min | avg | median | max | # calls | | MAX_LOCKUP_PERIOD | 294 | 294 | 294 | 294 | 23 | | MAX_MULTIPLIER | 251 | 251 | 251 | 251 | 30 | -| MIN_LOCKUP_PERIOD | 252 | 252 | 252 | 252 | 11 | +| MIN_LOCKUP_PERIOD | 297 | 297 | 297 | 297 | 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 | 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 | +| STAKING_TOKEN | 2428 | 2428 | 2428 | 2428 | 292 | +| emergencyModeEnabled | 2398 | 2398 | 2398 | 2398 | 7 | +| enableEmergencyMode | 2463 | 19370 | 24655 | 24655 | 8 | +| getAccount | 1639 | 1639 | 1639 | 1639 | 72 | +| getStakedBalance | 2629 | 2629 | 2629 | 2629 | 1 | +| getUserTotalMP | 9230 | 9230 | 9230 | 9230 | 1 | +| getUserTotalMaxMP | 3123 | 3123 | 3123 | 3123 | 1 | +| getUserTotalStakedBalance | 15162 | 15162 | 15162 | 15162 | 1 | +| getUserVaults | 5245 | 5245 | 5245 | 5245 | 4 | +| initialize | 115611 | 115611 | 115611 | 115611 | 59 | +| isTrustedCodehash | 519 | 519 | 519 | 519 | 231 | +| lastRewardTime | 373 | 1373 | 1373 | 2373 | 2 | +| leave | 56613 | 56613 | 56613 | 56613 | 1 | +| lock | 12041 | 34212 | 16370 | 74225 | 3 | +| proxiableUUID | 331 | 331 | 331 | 331 | 3 | +| registerVault | 55866 | 72745 | 72966 | 72966 | 233 | | 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 | +| rewardStartTime | 352 | 1352 | 1352 | 2352 | 2 | +| rewardsBalanceOf | 1294 | 1294 | 1294 | 1294 | 4 | +| setReward | 2561 | 50875 | 60256 | 102573 | 7 | +| setTrustedCodehash | 26243 | 26243 | 26243 | 26243 | 59 | +| stake | 131211 | 170364 | 178028 | 198507 | 66 | +| totalMP | 373 | 373 | 373 | 373 | 81 | +| totalMaxMP | 350 | 350 | 350 | 350 | 81 | +| totalRewardsAccrued | 351 | 351 | 351 | 351 | 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 | +| totalStaked | 396 | 396 | 396 | 396 | 82 | +| unstake | 60706 | 61254 | 60706 | 64269 | 13 | +| updateAccountMP | 15464 | 18542 | 17966 | 35066 | 21 | | updateGlobalState | 11066 | 28094 | 25315 | 110295 | 21 | -| upgradeToAndCall | 3125 | 9237 | 10766 | 10766 | 5 | +| upgradeToAndCall | 3225 | 9387 | 10926 | 10936 | 5 | | src/StakeManagerProxy.sol:StakeManagerProxy contract | | | | | | |------------------------------------------------------|-----------------|-------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 256004 | 1231 | | | | | +| 256079 | 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 | +| MIN_LOCKUP_PERIOD | 724 | 3996 | 5224 | 5224 | 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 | +| STAKING_TOKEN | 7355 | 7355 | 7355 | 7355 | 292 | +| emergencyModeEnabled | 7325 | 7325 | 7325 | 7325 | 7 | +| enableEmergencyMode | 28458 | 45359 | 50643 | 50643 | 8 | +| getAccount | 2093 | 2093 | 2093 | 2093 | 72 | +| getStakedBalance | 7559 | 7559 | 7559 | 7559 | 1 | +| getUserTotalMP | 9660 | 9660 | 9660 | 9660 | 1 | +| getUserTotalMaxMP | 3553 | 3553 | 3553 | 3553 | 1 | +| getUserTotalStakedBalance | 15592 | 15592 | 15592 | 15592 | 1 | +| getUserVaults | 5681 | 6806 | 5681 | 10181 | 4 | +| implementation | 343 | 808 | 343 | 2343 | 382 | +| isTrustedCodehash | 949 | 949 | 949 | 949 | 231 | +| lastRewardTime | 800 | 1800 | 1800 | 2800 | 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 | +| rewardStartTime | 779 | 4029 | 4029 | 7279 | 2 | +| rewardsBalanceOf | 1724 | 1724 | 1724 | 1724 | 4 | +| setReward | 28841 | 77189 | 86614 | 128859 | 7 | +| setTrustedCodehash | 52889 | 52889 | 52889 | 52889 | 59 | +| totalMP | 800 | 800 | 800 | 800 | 81 | +| totalMaxMP | 777 | 777 | 777 | 777 | 81 | +| totalRewardsAccrued | 778 | 778 | 778 | 778 | 3 | | totalRewardsSupply | 1430 | 2541 | 2194 | 11670 | 30 | -| totalStaked | 778 | 778 | 778 | 778 | 81 | -| updateAccountMP | 41756 | 44834 | 44258 | 61358 | 21 | +| totalStaked | 823 | 823 | 823 | 823 | 82 | +| updateAccountMP | 41823 | 44901 | 44325 | 61425 | 21 | | updateGlobalState | 37054 | 54082 | 51303 | 136283 | 21 | -| upgradeToAndCall | 29768 | 35875 | 37402 | 37402 | 5 | +| upgradeToAndCall | 29868 | 36025 | 37562 | 37572 | 5 | | src/StakeVault.sol:StakeVault contract | | | | | | |----------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 1374566 | 6483 | | | | | +| 1420425 | 6695 | | | | | | Function Name | min | avg | median | max | # calls | | STAKING_TOKEN | 216 | 216 | 216 | 216 | 1 | -| 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 | 94528 | 99898 | 107496 | 14 | -| withdraw | 42271 | 42271 | 42271 | 42271 | 1 | +| emergencyExit | 36353 | 48857 | 48091 | 65191 | 7 | +| leave | 33507 | 131513 | 60783 | 370978 | 4 | +| lock | 33245 | 60706 | 50779 | 108022 | 4 | +| owner | 2339 | 2339 | 2339 | 2339 | 233 | +| register | 87015 | 103894 | 104115 | 104115 | 233 | +| stake | 33411 | 241651 | 252532 | 273059 | 67 | +| stakeManager | 368 | 368 | 368 | 368 | 233 | +| trustStakeManager | 28953 | 28953 | 28953 | 28953 | 1 | +| unstake | 33282 | 96931 | 102420 | 110233 | 14 | +| withdraw | 42289 | 42289 | 42289 | 42289 | 1 | | src/XPNFTToken.sol:XPNFTToken contract | | | | | | @@ -171,19 +183,19 @@ | Deployment Cost | Deployment Size | | | | | | 625454 | 3260 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46330 | 46339 | 46342 | 46342 | 227 | +| approve | 46330 | 46339 | 46342 | 46342 | 238 | | balanceOf | 558 | 989 | 558 | 2558 | 139 | -| mint | 51279 | 56555 | 51279 | 68379 | 243 | +| mint | 51279 | 56523 | 51279 | 68379 | 251 | | transfer | 34384 | 42934 | 42934 | 51484 | 2 | | test/mocks/StackOverflowStakeManager.sol:StackOverflowStakeManager contract | | | | | | |-----------------------------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 1033443 | 4615 | | | | | +| 1026739 | 4584 | | | | | | Function Name | min | avg | median | max | # calls | | leave | 391 | 161316 | 161316 | 322322 | 334 | -| proxiableUUID | 319 | 319 | 319 | 319 | 1 | +| proxiableUUID | 341 | 341 | 341 | 341 | 1 | | test/mocks/XPProviderMock.sol:XPProviderMock contract | | | | | | diff --git a/.gas-snapshot b/.gas-snapshot index 115d2e1..9ec2f7c 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,69 @@ -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) +EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92646) +EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 297824) +EmergencyExitTest:test_EmergencyExitBasic() (gas: 384518) +EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 659358) +EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 392437) +EmergencyExitTest:test_EmergencyExitWithLock() (gas: 392015) +EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 377401) +EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39408) +IntegrationTest:testStakeFoo() (gas: 1179318) +LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 2927052) +LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 294955) +LeaveTest:test_TrustNewStakeManager() (gas: 3004493) +LockTest:test_LockFailsWithInvalidPeriod() (gas: 310018) +LockTest:test_LockFailsWithNoStake() (gas: 63598) +LockTest:test_LockWithoutPriorLock() (gas: 391137) +MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1745462) +MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 717207) 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) +RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 670984) +RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160214) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39323) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39346) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39359) +RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 611915) RewardsStreamerTest:testStake() (gas: 869181) -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) +StakeTest:test_StakeMultipleAccounts() (gas: 494656) +StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 500594) +StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 831165) +StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517651) +StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539579) +StakeTest:test_StakeOneAccount() (gas: 277040) +StakeTest:test_StakeOneAccountAndRewards() (gas: 283009) +StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 499939) +StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 496276) +StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 301895) +StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 301884) +StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 301951) StakingTokenTest:testStakeToken() (gas: 10422) -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) +UnstakeTest:test_StakeMultipleAccounts() (gas: 494678) +UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 500594) +UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 831142) +UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517673) +UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539601) +UnstakeTest:test_StakeOneAccount() (gas: 277063) +UnstakeTest:test_StakeOneAccountAndRewards() (gas: 283031) +UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 499961) +UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 496256) +UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 301895) +UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 301884) +UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 301995) +UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 542969) +UnstakeTest:test_UnstakeMultipleAccounts() (gas: 693417) +UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 787224) +UnstakeTest:test_UnstakeOneAccount() (gas: 473460) +UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 495130) +UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 404531) +UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 531635) +UpgradeTest:test_RevertWhenNotOwner() (gas: 2568028) +UpgradeTest:test_UpgradeStakeManager() (gas: 2841596) +VaultRegistrationTest:test_VaultRegistration() (gas: 62211) +WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 310679) XPNFTTokenTest:testApproveNotAllowed() (gas: 10500) XPNFTTokenTest:testGetApproved() (gas: 10523) XPNFTTokenTest:testIsApprovedForAll() (gas: 10698) diff --git a/certora/specs/EmergencyMode.spec b/certora/specs/EmergencyMode.spec index 4c399d0..db1ece5 100644 --- a/certora/specs/EmergencyMode.spec +++ b/certora/specs/EmergencyMode.spec @@ -29,7 +29,14 @@ definition isViewFunction(method f) returns bool = ( f.selector == sig:streamer.rewardAmount().selector || f.selector == sig:streamer.totalRewardsAccrued().selector || f.selector == sig:streamer.rewardStartTime().selector || - f.selector == sig:streamer.rewardEndTime().selector + f.selector == sig:streamer.rewardEndTime().selector || + f.selector == sig:streamer.getUserTotalMP(address).selector || + f.selector == sig:streamer.getUserTotalMaxMP(address).selector || + f.selector == sig:streamer.getUserTotalStakedBalance(address).selector || + f.selector == sig:streamer.vaults(address,uint256).selector || + f.selector == sig:streamer.vaultOwners(address).selector || + f.selector == sig:streamer.registerVault().selector || + f.selector == sig:streamer.getUserVaults(address).selector ); definition isOwnableFunction(method f) returns bool = ( diff --git a/src/RewardsStreamerMP.sol b/src/RewardsStreamerMP.sol index f0af4ee..fb5a8f4 100644 --- a/src/RewardsStreamerMP.sol +++ b/src/RewardsStreamerMP.sol @@ -6,6 +6,7 @@ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IStakeManager } from "./interfaces/IStakeManager.sol"; +import { IStakeVault } from "./interfaces/IStakeVault.sol"; import { TrustedCodehashAccess } from "./TrustedCodehashAccess.sol"; // Rewards Streamer with Multiplier Points @@ -16,6 +17,9 @@ contract RewardsStreamerMP is TrustedCodehashAccess, ReentrancyGuardUpgradeable { + error StakingManager__InvalidVault(); + error StakingManager__VaultNotRegistered(); + error StakingManager__VaultAlreadyRegistered(); error StakingManager__AmountCannotBeZero(); error StakingManager__TransferFailed(); error StakingManager__InsufficientBalance(); @@ -57,7 +61,16 @@ contract RewardsStreamerMP is uint256 lockUntil; } - mapping(address account => Account data) public accounts; + mapping(address vault => Account data) public accounts; + mapping(address owner => address[] vault) public vaults; + mapping(address vault => address owner) public vaultOwners; + + modifier onlyRegisteredVault() { + if (vaultOwners[msg.sender] == address(0)) { + revert StakingManager__VaultNotRegistered(); + } + _; + } modifier onlyNotEmergencyMode() { if (emergencyModeEnabled) { @@ -83,7 +96,95 @@ contract RewardsStreamerMP is _checkOwner(); } - function stake(uint256 amount, uint256 lockPeriod) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant { + /** + * @notice Registers a vault with its owner. Called by the vault itself during initialization. + * @dev Only callable by contracts with trusted codehash + */ + function registerVault() external onlyTrustedCodehash { + address vault = msg.sender; + address owner = IStakeVault(vault).owner(); + + if (vaultOwners[vault] != address(0)) { + revert StakingManager__VaultAlreadyRegistered(); + } + + // Verify this is a legitimate vault by checking it points to stakeManager + if (address(IStakeVault(vault).stakeManager()) != address(this)) { + revert StakingManager__InvalidVault(); + } + + vaultOwners[vault] = owner; + vaults[owner].push(vault); + } + + /** + * @notice Get the vaults owned by a user + * @param user The address of the user + * @return The vaults owned by the user + */ + function getUserVaults(address user) external view returns (address[] memory) { + return vaults[user]; + } + + /** + * @notice Get the total multiplier points for a user + * @dev Iterates over all vaults owned by the user and sums the multiplier points + * @param user The address of the user + * @return The total multiplier points for the user + */ + function getUserTotalMP(address user) external view returns (uint256) { + address[] memory userVaults = vaults[user]; + uint256 userTotalMP = 0; + + for (uint256 i = 0; i < userVaults.length; i++) { + Account storage account = accounts[userVaults[i]]; + userTotalMP += account.accountMP + _getAccountAccruedMP(account); + } + return userTotalMP; + } + + /** + * @notice Get the total maximum multiplier points for a user + * @dev Iterates over all vaults owned by the user and sums the maximum multiplier points + * @param user The address of the user + * @return The total maximum multiplier points for the user + */ + function getUserTotalMaxMP(address user) external view returns (uint256) { + address[] memory userVaults = vaults[user]; + uint256 userTotalMaxMP = 0; + + for (uint256 i = 0; i < userVaults.length; i++) { + userTotalMaxMP += accounts[userVaults[i]].maxMP; + } + return userTotalMaxMP; + } + + /** + * @notice Get the total staked balance for a user + * @dev Iterates over all vaults owned by the user and sums the staked balances + * @param user The address of the user + * @return The total staked balance for the user + */ + function getUserTotalStakedBalance(address user) external view returns (uint256) { + address[] memory userVaults = vaults[user]; + uint256 userTotalStake = 0; + + for (uint256 i = 0; i < userVaults.length; i++) { + userTotalStake += accounts[userVaults[i]].stakedBalance; + } + return userTotalStake; + } + + function stake( + uint256 amount, + uint256 lockPeriod + ) + external + onlyTrustedCodehash + onlyNotEmergencyMode + onlyRegisteredVault + nonReentrant + { if (amount == 0) { revert StakingManager__AmountCannotBeZero(); } @@ -127,7 +228,13 @@ contract RewardsStreamerMP is account.lastMPUpdateTime = block.timestamp; } - function lock(uint256 lockPeriod) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant { + function lock(uint256 lockPeriod) + external + onlyTrustedCodehash + onlyNotEmergencyMode + onlyRegisteredVault + nonReentrant + { if (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD) { revert StakingManager__InvalidLockingPeriod(); } @@ -160,7 +267,13 @@ contract RewardsStreamerMP is account.lastMPUpdateTime = block.timestamp; } - function unstake(uint256 amount) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant { + function unstake(uint256 amount) + external + onlyTrustedCodehash + onlyNotEmergencyMode + onlyRegisteredVault + nonReentrant + { Account storage account = accounts[msg.sender]; if (amount > account.stakedBalance) { revert StakingManager__InsufficientBalance(); @@ -316,22 +429,19 @@ contract RewardsStreamerMP is lastRewardTime = block.timestamp < rewardEndTime ? block.timestamp : rewardEndTime; } - function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal view returns (uint256) { + function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal pure returns (uint256) { uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKUP_PERIOD; return amount * lockMultiplier / SCALE_FACTOR; } - function _updateAccountMP(address accountAddress) internal { - Account storage account = accounts[accountAddress]; - + function _getAccountAccruedMP(Account storage account) internal view returns (uint256) { if (account.maxMP == 0 || account.stakedBalance == 0) { - account.lastMPUpdateTime = block.timestamp; - return; + return 0; } uint256 timeDiff = block.timestamp - account.lastMPUpdateTime; if (timeDiff == 0) { - return; + return 0; } uint256 accruedMP = (timeDiff * account.stakedBalance * MP_RATE_PER_YEAR) / (365 days * SCALE_FACTOR); @@ -339,6 +449,12 @@ contract RewardsStreamerMP is if (account.accountMP + accruedMP > account.maxMP) { accruedMP = account.maxMP - account.accountMP; } + return accruedMP; + } + + function _updateAccountMP(address accountAddress) internal { + Account storage account = accounts[accountAddress]; + uint256 accruedMP = _getAccountAccruedMP(account); account.accountMP += accruedMP; account.lastMPUpdateTime = block.timestamp; diff --git a/src/StakeVault.sol b/src/StakeVault.sol index 7ab2581..3d09caa 100644 --- a/src/StakeVault.sol +++ b/src/StakeVault.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.26; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IStakeManagerProxy } from "./interfaces/IStakeManagerProxy.sol"; +import { IStakeVault } from "./interfaces/IStakeVault.sol"; /** * @title StakeVault @@ -12,7 +13,7 @@ import { IStakeManagerProxy } from "./interfaces/IStakeManagerProxy.sol"; * @notice A contract to secure user stakes and manage staking with IStakeManager. * @dev This contract is owned by the user and allows staking, unstaking, and withdrawing tokens. */ -contract StakeVault is Ownable { +contract StakeVault is IStakeVault, Ownable { error StakeVault__NotEnoughAvailableBalance(); error StakeVault__InvalidDestinationAddress(); error StakeVault__UpdateNotAvailable(); @@ -26,7 +27,7 @@ contract StakeVault is Ownable { //if is needed that STAKING_TOKEN to be a variable, StakeManager should be changed to check codehash and //StakeVault(msg.sender).STAKING_TOKEN() IERC20 public immutable STAKING_TOKEN; - IStakeManagerProxy private stakeManager; + IStakeManagerProxy public stakeManager; address public stakeManagerImplementationAddress; /** @@ -71,6 +72,20 @@ contract StakeVault is Ownable { stakeManagerImplementationAddress = stakeManagerAddress; } + /** + * @notice Registers the vault with the stake manager. + */ + function register() public { + stakeManager.registerVault(); + } + + /** + * @notice Returns the address of the current owner. + */ + function owner() public view override(Ownable, IStakeVault) returns (address) { + return super.owner(); + } + /** * @notice Stake tokens for a specified time. * @param _amount The amount of tokens to stake. diff --git a/src/interfaces/IStakeManager.sol b/src/interfaces/IStakeManager.sol index 6b44e22..0cc62ea 100644 --- a/src/interfaces/IStakeManager.sol +++ b/src/interfaces/IStakeManager.sol @@ -11,6 +11,7 @@ interface IStakeManager is ITrustedCodehashAccess { error StakingManager__StakeIsTooLow(); error StakingManager__NotAllowedToLeave(); + function registerVault() external; function stake(uint256 _amount, uint256 _seconds) external; function lock(uint256 _seconds) external; function unstake(uint256 _amount) external; diff --git a/src/interfaces/IStakeVault.sol b/src/interfaces/IStakeVault.sol new file mode 100644 index 0000000..dcc96c4 --- /dev/null +++ b/src/interfaces/IStakeVault.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { IStakeManagerProxy } from "./IStakeManagerProxy.sol"; + +interface IStakeVault { + function owner() external view returns (address); + function stakeManager() external view returns (IStakeManagerProxy); + function register() external; +} diff --git a/test/RewardsStreamerMP.t.sol b/test/RewardsStreamerMP.t.sol index c4297a1..9d65177 100644 --- a/test/RewardsStreamerMP.t.sol +++ b/test/RewardsStreamerMP.t.sol @@ -32,6 +32,14 @@ contract RewardsStreamerMPTest is Test { address proxy = address(new StakeManagerProxy(impl, initializeData)); streamer = RewardsStreamerMP(proxy); + // Create a temporary vault just to get the codehash + StakeVault tempVault = new StakeVault(address(this), IStakeManagerProxy(address(streamer))); + bytes32 vaultCodeHash = address(tempVault).codehash; + + // Register the codehash before creating any user vaults + vm.prank(admin); + streamer.setTrustedCodehash(vaultCodeHash, true); + address[4] memory accounts = [alice, bob, charlie, dave]; for (uint256 i = 0; i < accounts.length; i++) { // ensure user has tokens @@ -85,9 +93,23 @@ contract RewardsStreamerMPTest is Test { assertEq(accountInfo.maxMP, p.maxMP, "wrong account max MP"); } + struct CheckUserTotalsParams { + address user; + uint256 totalStakedBalance; + uint256 totalMP; + uint256 totalMaxMP; + } + + function checkUserTotals(CheckUserTotalsParams memory p) public view { + assertEq(streamer.getUserTotalStakedBalance(p.user), p.totalStakedBalance, "wrong user total stake balance"); + assertEq(streamer.getUserTotalMP(p.user), p.totalMP, "wrong user total MP"); + assertEq(streamer.getUserTotalMaxMP(p.user), p.totalMaxMP, "wrong user total MP"); + } + function _createTestVault(address owner) internal returns (StakeVault vault) { vm.prank(owner); vault = new StakeVault(owner, IStakeManagerProxy(address(streamer))); + vault.register(); if (!streamer.isTrustedCodehash(address(vault).codehash)) { vm.prank(admin); @@ -137,6 +159,21 @@ contract RewardsStreamerMPTest is Test { } } +contract VaultRegistrationTest is RewardsStreamerMPTest { + function setUp() public virtual override { + super.setUp(); + } + + function test_VaultRegistration() public view { + address[4] memory accounts = [alice, bob, charlie, dave]; + for (uint256 i = 0; i < accounts.length; i++) { + address[] memory userVaults = streamer.getUserVaults(accounts[i]); + assertEq(userVaults.length, 1, "wrong number of vaults"); + assertEq(userVaults[0], vaults[accounts[i]], "wrong vault address"); + } + } +} + contract IntegrationTest is RewardsStreamerMPTest { function setUp() public virtual override { super.setUp(); @@ -2041,3 +2078,54 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { assertApproxEqAbs(streamer.rewardsBalanceOf(vaults[alice]), 1000e18, tolerance); } } + +contract MultipleVaultsStakeTest is RewardsStreamerMPTest { + StakeVault public vault1; + StakeVault public vault2; + StakeVault public vault3; + + function setUp() public override { + super.setUp(); + + vault1 = _createTestVault(alice); + vault2 = _createTestVault(alice); + vault3 = _createTestVault(alice); + + vm.startPrank(alice); + stakingToken.approve(address(vault1), 10_000e18); + stakingToken.approve(address(vault2), 10_000e18); + stakingToken.approve(address(vault3), 10_000e18); + vm.stopPrank(); + } + + function _stakeWithVault(address account, StakeVault vault, uint256 amount, uint256 lockupTime) public { + vm.prank(account); + vault.stake(amount, lockupTime); + } + + function test_StakeMultipleVaults() public { + // Alice vault1 stakes 10 tokens + _stakeWithVault(alice, vault1, 10e18, 0); + + // Alice vault2 stakes 20 tokens + _stakeWithVault(alice, vault2, 20e18, 0); + + // Alice vault3 stakes 30 tokens + _stakeWithVault(alice, vault3, 60e18, 0); + + checkStreamer( + CheckStreamerParams({ + totalStaked: 90e18, + totalMP: 90e18, + totalMaxMP: 450e18, + stakingBalance: 90e18, + rewardBalance: 0, + rewardIndex: 0 + }) + ); + + checkUserTotals( + CheckUserTotalsParams({ user: alice, totalStakedBalance: 90e18, totalMP: 90e18, totalMaxMP: 450e18 }) + ); + } +} diff --git a/test/StakeVault.test.sol b/test/StakeVault.test.sol index 8316e22..b363148 100644 --- a/test/StakeVault.test.sol +++ b/test/StakeVault.test.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { IStakeManagerProxy } from "../src/interfaces/IStakeManagerProxy.sol"; import { StakeManagerProxy } from "../src/StakeManagerProxy.sol"; @@ -24,10 +23,7 @@ contract StakeVaultTest is Test { function _createTestVault(address owner) internal returns (StakeVault vault) { vm.prank(owner); vault = new StakeVault(owner, IStakeManagerProxy(address(streamer))); - - if (!streamer.isTrustedCodehash(address(vault).codehash)) { - streamer.setTrustedCodehash(address(vault).codehash, true); - } + vault.register(); } function setUp() public virtual { @@ -41,6 +37,14 @@ contract StakeVaultTest is Test { streamer = RewardsStreamerMP(proxy); stakingToken.mint(alice, 10_000e18); + + // Create a temporary vault just to get the codehash + StakeVault tempVault = new StakeVault(address(this), IStakeManagerProxy(address(streamer))); + bytes32 vaultCodeHash = address(tempVault).codehash; + + // Register the codehash before creating any user vaults + streamer.setTrustedCodehash(vaultCodeHash, true); + stakeVault = _createTestVault(alice); vm.prank(alice); diff --git a/test/mocks/StackOverflowStakeManager.sol b/test/mocks/StackOverflowStakeManager.sol index a57e0c9..e76a427 100644 --- a/test/mocks/StackOverflowStakeManager.sol +++ b/test/mocks/StackOverflowStakeManager.sol @@ -65,4 +65,8 @@ contract StackOverflowStakeManager is function getAccount(address _account) external view returns (Account memory) { return accounts[_account]; } + + function registerVault() external override { + // implementation + } }