mirror of
https://github.com/vacp2p/staking-reward-streamer.git
synced 2026-01-08 20:48:00 -05:00
feat(RewardsStreamerMP): introduce leave() function
This function allows users to `leave()` the system if they can't or don't want to trust the stake manager. This is the case when the owner of the stake manager performs an upgrade. In case of such an upgrade, the stake manager will be marked as not trusted which prevents the user from staking, unstaking, locking etc. The user can then either explicitly trust stake manager (will happen in future changes) to enable the vault's functionality again, or, `leave()` the system at which point it will try to perform a benign `leave()` operation and then move the funds out of the vault. Closes #66
This commit is contained in:
116
.gas-report
116
.gas-report
@@ -1,30 +1,3 @@
|
||||
| lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy contract | | | | | |
|
||||
|-------------------------------------------------------------------------------------------|-----------------|-------|--------|-------|---------|
|
||||
| Deployment Cost | Deployment Size | | | | |
|
||||
| 259350 | 1159 | | | | |
|
||||
| Function Name | min | avg | median | max | # calls |
|
||||
| MAX_LOCKUP_PERIOD | 644 | 1426 | 644 | 5144 | 23 |
|
||||
| MAX_MULTIPLIER | 667 | 1567 | 667 | 5167 | 30 |
|
||||
| MIN_LOCKUP_PERIOD | 646 | 3918 | 5146 | 5146 | 11 |
|
||||
| MP_RATE_PER_YEAR | 602 | 602 | 602 | 602 | 3 |
|
||||
| SCALE_FACTOR | 600 | 600 | 600 | 600 | 41 |
|
||||
| STAKING_TOKEN | 7253 | 7253 | 7253 | 7253 | 182 |
|
||||
| accountedRewards | 722 | 1289 | 722 | 2722 | 74 |
|
||||
| emergencyModeEnabled | 7270 | 7270 | 7270 | 7270 | 7 |
|
||||
| enableEmergencyMode | 28422 | 45323 | 50607 | 50607 | 8 |
|
||||
| getAccount | 2020 | 2020 | 2020 | 2020 | 71 |
|
||||
| getStakedBalance | 7464 | 7464 | 7464 | 7464 | 1 |
|
||||
| isTrustedCodehash | 938 | 1454 | 938 | 2938 | 182 |
|
||||
| rewardIndex | 766 | 793 | 766 | 2766 | 74 |
|
||||
| setTrustedCodehash | 52794 | 52794 | 52794 | 52794 | 47 |
|
||||
| totalMP | 723 | 723 | 723 | 723 | 77 |
|
||||
| totalMaxMP | 722 | 722 | 722 | 722 | 77 |
|
||||
| totalStaked | 723 | 723 | 723 | 723 | 78 |
|
||||
| updateAccountMP | 41679 | 43917 | 44181 | 44181 | 19 |
|
||||
| updateGlobalState | 36999 | 67243 | 56463 | 89414 | 28 |
|
||||
| upgradeToAndCall | 29734 | 33540 | 33540 | 37346 | 2 |
|
||||
|
||||
|
||||
| src/RewardsStreamer.sol:RewardsStreamer contract | | | | | |
|
||||
|--------------------------------------------------|-----------------|--------|--------|--------|---------|
|
||||
| Deployment Cost | Deployment Size | | | | |
|
||||
@@ -42,46 +15,77 @@
|
||||
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|
||||
|------------------------------------------------------|-----------------|--------|--------|--------|---------|
|
||||
| Deployment Cost | Deployment Size | | | | |
|
||||
| 2084418 | 9588 | | | | |
|
||||
| 2134697 | 9822 | | | | |
|
||||
| Function Name | min | avg | median | max | # calls |
|
||||
| MAX_LOCKUP_PERIOD | 272 | 272 | 272 | 272 | 23 |
|
||||
| MAX_MULTIPLIER | 295 | 295 | 295 | 295 | 30 |
|
||||
| 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 | 228 | 228 | 228 | 228 | 41 |
|
||||
| STAKING_TOKEN | 2381 | 2381 | 2381 | 2381 | 182 |
|
||||
| accountedRewards | 350 | 917 | 350 | 2350 | 74 |
|
||||
| SCALE_FACTOR | 294 | 294 | 294 | 294 | 41 |
|
||||
| STAKING_TOKEN | 2381 | 2381 | 2381 | 2381 | 194 |
|
||||
| accountedRewards | 373 | 951 | 373 | 2373 | 76 |
|
||||
| emergencyModeEnabled | 2398 | 2398 | 2398 | 2398 | 7 |
|
||||
| enableEmergencyMode | 2482 | 19389 | 24674 | 24674 | 8 |
|
||||
| getAccount | 1621 | 1621 | 1621 | 1621 | 71 |
|
||||
| getStakedBalance | 2589 | 2589 | 2589 | 2589 | 1 |
|
||||
| initialize | 137795 | 137795 | 137795 | 137795 | 47 |
|
||||
| isTrustedCodehash | 563 | 1079 | 563 | 2563 | 182 |
|
||||
| enableEmergencyMode | 2460 | 19367 | 24652 | 24652 | 8 |
|
||||
| getAccount | 1621 | 1621 | 1621 | 1621 | 72 |
|
||||
| getStakedBalance | 2567 | 2567 | 2567 | 2567 | 1 |
|
||||
| initialize | 137773 | 137773 | 137773 | 137773 | 50 |
|
||||
| isTrustedCodehash | 563 | 1078 | 563 | 2563 | 194 |
|
||||
| leave | 62061 | 62061 | 62061 | 62061 | 1 |
|
||||
| lock | 9839 | 33584 | 14168 | 76747 | 3 |
|
||||
| proxiableUUID | 297 | 297 | 297 | 297 | 1 |
|
||||
| rewardIndex | 394 | 421 | 394 | 2394 | 74 |
|
||||
| setTrustedCodehash | 26203 | 26203 | 26203 | 26203 | 47 |
|
||||
| stake | 134612 | 171004 | 176214 | 196693 | 57 |
|
||||
| totalMP | 351 | 351 | 351 | 351 | 77 |
|
||||
| totalMaxMP | 350 | 350 | 350 | 350 | 77 |
|
||||
| totalStaked | 351 | 351 | 351 | 351 | 78 |
|
||||
| unstake | 63925 | 84721 | 63925 | 120391 | 13 |
|
||||
| updateAccountMP | 15375 | 17613 | 17877 | 17877 | 19 |
|
||||
| proxiableUUID | 363 | 363 | 363 | 363 | 3 |
|
||||
| rewardIndex | 394 | 420 | 394 | 2394 | 76 |
|
||||
| setTrustedCodehash | 26226 | 26226 | 26226 | 26226 | 50 |
|
||||
| stake | 134677 | 171329 | 176279 | 196758 | 60 |
|
||||
| totalMP | 329 | 329 | 329 | 329 | 79 |
|
||||
| totalMaxMP | 373 | 373 | 373 | 373 | 79 |
|
||||
| totalStaked | 329 | 329 | 329 | 329 | 80 |
|
||||
| unstake | 63997 | 84793 | 63997 | 120464 | 13 |
|
||||
| updateAccountMP | 15353 | 17591 | 17855 | 17855 | 19 |
|
||||
| updateGlobalState | 11066 | 41310 | 30530 | 63481 | 28 |
|
||||
| upgradeToAndCall | 3146 | 6955 | 6955 | 10765 | 2 |
|
||||
| upgradeToAndCall | 3124 | 8887 | 10809 | 10809 | 4 |
|
||||
|
||||
|
||||
| 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 | 194 |
|
||||
| accountedRewards | 800 | 1378 | 800 | 2800 | 76 |
|
||||
| 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 | 935 | 343 | 2343 | 277 |
|
||||
| isTrustedCodehash | 993 | 1508 | 993 | 2993 | 194 |
|
||||
| rewardIndex | 821 | 847 | 821 | 2821 | 76 |
|
||||
| setTrustedCodehash | 52872 | 52872 | 52872 | 52872 | 50 |
|
||||
| totalMP | 756 | 756 | 756 | 756 | 79 |
|
||||
| totalMaxMP | 800 | 800 | 800 | 800 | 79 |
|
||||
| totalStaked | 756 | 756 | 756 | 756 | 80 |
|
||||
| updateAccountMP | 41712 | 43950 | 44214 | 44214 | 19 |
|
||||
| updateGlobalState | 37054 | 67298 | 56518 | 89469 | 28 |
|
||||
| upgradeToAndCall | 29767 | 35525 | 37445 | 37445 | 4 |
|
||||
|
||||
|
||||
| src/StakeVault.sol:StakeVault contract | | | | | |
|
||||
|----------------------------------------|-----------------|--------|--------|--------|---------|
|
||||
| Deployment Cost | Deployment Size | | | | |
|
||||
| 1086958 | 5110 | | | | |
|
||||
| 1374543 | 6483 | | | | |
|
||||
| Function Name | min | avg | median | max | # calls |
|
||||
| STAKING_TOKEN | 216 | 216 | 216 | 216 | 1 |
|
||||
| emergencyExit | 36298 | 48802 | 48036 | 65136 | 7 |
|
||||
| lock | 43256 | 66100 | 47633 | 107411 | 3 |
|
||||
| stake | 206077 | 242480 | 247679 | 268206 | 57 |
|
||||
| unstake | 90514 | 120704 | 110342 | 153215 | 13 |
|
||||
| withdraw | 42194 | 42194 | 42194 | 42194 | 1 |
|
||||
| emergencyExit | 36353 | 48857 | 48091 | 65191 | 7 |
|
||||
| leave | 33507 | 53144 | 33507 | 92418 | 3 |
|
||||
| lock | 33245 | 60236 | 48577 | 110544 | 4 |
|
||||
| stake | 33454 | 242405 | 250826 | 271353 | 61 |
|
||||
| trustStakeManager | 28997 | 28997 | 28997 | 28997 | 1 |
|
||||
| unstake | 33260 | 117303 | 113502 | 156376 | 14 |
|
||||
| withdraw | 42227 | 42227 | 42227 | 42227 | 1 |
|
||||
|
||||
|
||||
| src/XPNFTToken.sol:XPNFTToken contract | | | | | |
|
||||
@@ -157,9 +161,9 @@
|
||||
| Deployment Cost | Deployment Size | | | | |
|
||||
| 625454 | 3260 | | | | |
|
||||
| Function Name | min | avg | median | max | # calls |
|
||||
| approve | 46330 | 46339 | 46342 | 46342 | 232 |
|
||||
| balanceOf | 558 | 1380 | 558 | 2558 | 338 |
|
||||
| mint | 51279 | 58862 | 51279 | 68379 | 248 |
|
||||
| approve | 46330 | 46339 | 46342 | 46342 | 247 |
|
||||
| balanceOf | 558 | 1387 | 558 | 2558 | 347 |
|
||||
| mint | 51279 | 58819 | 51279 | 68379 | 263 |
|
||||
| transfer | 34384 | 48853 | 51484 | 51484 | 13 |
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 89646)
|
||||
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 292881)
|
||||
EmergencyExitTest:test_EmergencyExitBasic() (gas: 395173)
|
||||
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 823179)
|
||||
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 538257)
|
||||
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 384549)
|
||||
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 529680)
|
||||
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39344)
|
||||
IntegrationTest:testStakeFoo() (gas: 1559037)
|
||||
LockTest:test_LockFailsWithInvalidPeriod() (gas: 299830)
|
||||
LockTest:test_LockFailsWithNoStake() (gas: 58241)
|
||||
LockTest:test_LockWithoutPriorLock() (gas: 396542)
|
||||
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)
|
||||
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 85934)
|
||||
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 58332)
|
||||
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35804)
|
||||
@@ -17,41 +20,41 @@ NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 102512)
|
||||
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 49555)
|
||||
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35979)
|
||||
RewardsStreamerTest:testStake() (gas: 869181)
|
||||
StakeTest:test_StakeMultipleAccounts() (gas: 506030)
|
||||
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 661839)
|
||||
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 872090)
|
||||
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 520069)
|
||||
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 541881)
|
||||
StakeTest:test_StakeOneAccount() (gas: 290297)
|
||||
StakeTest:test_StakeOneAccountAndRewards() (gas: 446101)
|
||||
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 536165)
|
||||
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 532401)
|
||||
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 310643)
|
||||
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 310610)
|
||||
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 310721)
|
||||
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)
|
||||
StakingTokenTest:testStakeToken() (gas: 10422)
|
||||
UnstakeTest:test_StakeMultipleAccounts() (gas: 506074)
|
||||
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 661883)
|
||||
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 872089)
|
||||
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 520068)
|
||||
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 541925)
|
||||
UnstakeTest:test_StakeOneAccount() (gas: 290320)
|
||||
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 446145)
|
||||
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 536209)
|
||||
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 532403)
|
||||
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 310600)
|
||||
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 310610)
|
||||
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 310721)
|
||||
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 541859)
|
||||
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 714931)
|
||||
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1057383)
|
||||
UnstakeTest:test_UnstakeOneAccount() (gas: 499845)
|
||||
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 524183)
|
||||
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 608646)
|
||||
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 555708)
|
||||
UpgradeTest:test_RevertWhenNotOwner() (gas: 2159991)
|
||||
UpgradeTest:test_UpgradeStakeManager() (gas: 2445373)
|
||||
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 305631)
|
||||
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)
|
||||
XPNFTTokenTest:testApproveNotAllowed() (gas: 10500)
|
||||
XPNFTTokenTest:testGetApproved() (gas: 10523)
|
||||
XPNFTTokenTest:testIsApprovedForAll() (gas: 10698)
|
||||
|
||||
@@ -58,7 +58,8 @@ rule accountCanOnlyLeaveInEmergencyMode(method f) {
|
||||
f@withrevert(e, args);
|
||||
bool isReverted = lastReverted;
|
||||
|
||||
assert !isReverted => isViewFunction(f) ||
|
||||
assert !isReverted => f.selector == sig:streamer.leave().selector ||
|
||||
isViewFunction(f) ||
|
||||
isOwnableFunction(f) ||
|
||||
isTrustedCodehashAccessFunction(f) ||
|
||||
isInitializerFunction(f) ||
|
||||
|
||||
@@ -12,6 +12,7 @@ methods {
|
||||
function updateGlobalState() external;
|
||||
function updateAccountMP(address accountAddress) external;
|
||||
function emergencyModeEnabled() external returns (bool) envfree;
|
||||
function leave() external;
|
||||
}
|
||||
|
||||
ghost mathint sumOfBalances {
|
||||
|
||||
@@ -164,13 +164,16 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc
|
||||
if (block.timestamp < account.lockUntil) {
|
||||
revert StakingManager__TokensAreLocked();
|
||||
}
|
||||
_unstake(amount, account, msg.sender);
|
||||
}
|
||||
|
||||
function _unstake(uint256 amount, Account storage account, address accountAddress) internal {
|
||||
_updateGlobalState();
|
||||
_updateAccountMP(msg.sender);
|
||||
_updateAccountMP(accountAddress);
|
||||
|
||||
uint256 accountRewards = calculateAccountRewards(msg.sender);
|
||||
uint256 accountRewards = calculateAccountRewards(accountAddress);
|
||||
if (accountRewards > 0) {
|
||||
distributeRewards(msg.sender, accountRewards);
|
||||
distributeRewards(accountAddress, accountRewards);
|
||||
}
|
||||
|
||||
uint256 previousStakedBalance = account.stakedBalance;
|
||||
@@ -181,11 +184,28 @@ contract RewardsStreamerMP is UUPSUpgradeable, IStakeManager, TrustedCodehashAcc
|
||||
account.stakedBalance -= amount;
|
||||
account.accountMP -= mpToReduce;
|
||||
account.maxMP -= maxMPToReduce;
|
||||
account.accountRewardIndex = rewardIndex;
|
||||
totalMP -= mpToReduce;
|
||||
totalMaxMP -= maxMPToReduce;
|
||||
totalStaked -= amount;
|
||||
}
|
||||
|
||||
account.accountRewardIndex = rewardIndex;
|
||||
// @notice Allows an account to leave the system. This can happen when a
|
||||
// user doesn't agree with an upgrade of the stake manager.
|
||||
// @dev This function is protected by whitelisting the codehash of the caller.
|
||||
// This ensures `StakeVault`s will call this function only if they don't
|
||||
// trust the `StakeManager` (e.g. in case of an upgrade).
|
||||
function leave() external onlyTrustedCodehash nonReentrant {
|
||||
Account storage account = accounts[msg.sender];
|
||||
|
||||
if (account.stakedBalance > 0) {
|
||||
// calling `_unstake` to update accounting accordingly
|
||||
_unstake(account.stakedBalance, account, msg.sender);
|
||||
|
||||
// further cleanup that isn't done in `_unstake`
|
||||
account.accountRewardIndex = 0;
|
||||
account.lockUntil = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function _updateGlobalState() internal {
|
||||
|
||||
12
src/StakeManagerProxy.sol
Normal file
12
src/StakeManagerProxy.sol
Normal file
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
|
||||
contract StakeManagerProxy is ERC1967Proxy {
|
||||
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) { }
|
||||
|
||||
function implementation() external view returns (address) {
|
||||
return _implementation();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ pragma solidity ^0.8.26;
|
||||
|
||||
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import { IStakeManager } from "./interfaces/IStakeManager.sol";
|
||||
import { IStakeManagerProxy } from "./interfaces/IStakeManagerProxy.sol";
|
||||
|
||||
/**
|
||||
* @title StakeVault
|
||||
@@ -19,12 +19,15 @@ contract StakeVault is Ownable {
|
||||
error StakeVault__StakingFailed();
|
||||
error StakeVault__UnstakingFailed();
|
||||
error StakeVault__NotAllowedToExit();
|
||||
error StakeVault__NotAllowedToLeave();
|
||||
error StakeVault__StakeManagerImplementationNotTrusted();
|
||||
|
||||
//STAKING_TOKEN must be kept as an immutable, otherwise, StakeManager would accept StakeVaults with any token
|
||||
//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;
|
||||
IStakeManager private stakeManager;
|
||||
IStakeManagerProxy private stakeManager;
|
||||
address public stakeManagerImplementationAddress;
|
||||
|
||||
/**
|
||||
* @dev Emitted when tokens are staked.
|
||||
@@ -42,14 +45,30 @@ contract StakeVault is Ownable {
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyTrustedStakeManager() {
|
||||
if (!_stakeManagerImplementationTrusted()) {
|
||||
revert StakeVault__StakeManagerImplementationNotTrusted();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the contract with the owner, staked token, and stake manager.
|
||||
* @param _owner The address of the owner.
|
||||
* @param _stakeManager The address of the StakeManager contract.
|
||||
*/
|
||||
constructor(address _owner, IStakeManager _stakeManager) Ownable(_owner) {
|
||||
constructor(address _owner, IStakeManagerProxy _stakeManager) Ownable(_owner) {
|
||||
STAKING_TOKEN = _stakeManager.STAKING_TOKEN();
|
||||
stakeManager = _stakeManager;
|
||||
stakeManagerImplementationAddress = _stakeManager.implementation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Allows the owner to trust a new stake manager implementation.
|
||||
* @param stakeManagerAddress The address of the new stake manager implementation.
|
||||
*/
|
||||
function trustStakeManager(address stakeManagerAddress) external onlyOwner {
|
||||
stakeManagerImplementationAddress = stakeManagerAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +76,7 @@ contract StakeVault is Ownable {
|
||||
* @param _amount The amount of tokens to stake.
|
||||
* @param _seconds The time period to stake for.
|
||||
*/
|
||||
function stake(uint256 _amount, uint256 _seconds) external onlyOwner {
|
||||
function stake(uint256 _amount, uint256 _seconds) external onlyOwner onlyTrustedStakeManager {
|
||||
_stake(_amount, _seconds, msg.sender);
|
||||
}
|
||||
|
||||
@@ -67,7 +86,7 @@ contract StakeVault is Ownable {
|
||||
* @param _seconds The time period to stake for.
|
||||
* @param _from The address from which tokens will be transferred.
|
||||
*/
|
||||
function stake(uint256 _amount, uint256 _seconds, address _from) external onlyOwner {
|
||||
function stake(uint256 _amount, uint256 _seconds, address _from) external onlyOwner onlyTrustedStakeManager {
|
||||
_stake(_amount, _seconds, _from);
|
||||
}
|
||||
|
||||
@@ -75,7 +94,7 @@ contract StakeVault is Ownable {
|
||||
* @notice Lock the staked amount for a specified time.
|
||||
* @param _seconds The time period to lock the staked amount for.
|
||||
*/
|
||||
function lock(uint256 _seconds) external onlyOwner {
|
||||
function lock(uint256 _seconds) external onlyOwner onlyTrustedStakeManager {
|
||||
stakeManager.lock(_seconds);
|
||||
}
|
||||
|
||||
@@ -83,7 +102,7 @@ contract StakeVault is Ownable {
|
||||
* @notice Unstake a specified amount of tokens and send to the owner.
|
||||
* @param _amount The amount of tokens to unstake.
|
||||
*/
|
||||
function unstake(uint256 _amount) external onlyOwner {
|
||||
function unstake(uint256 _amount) external onlyOwner onlyTrustedStakeManager {
|
||||
_unstake(_amount, msg.sender);
|
||||
}
|
||||
|
||||
@@ -92,10 +111,44 @@ contract StakeVault is Ownable {
|
||||
* @param _amount The amount of tokens to unstake.
|
||||
* @param _destination The address to receive the unstaked tokens.
|
||||
*/
|
||||
function unstake(uint256 _amount, address _destination) external onlyOwner validDestination(_destination) {
|
||||
function unstake(
|
||||
uint256 _amount,
|
||||
address _destination
|
||||
)
|
||||
external
|
||||
onlyOwner
|
||||
validDestination(_destination)
|
||||
onlyTrustedStakeManager
|
||||
{
|
||||
_unstake(_amount, _destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw all tokens from the contract to the owner.
|
||||
*/
|
||||
function leave(address _destination) external onlyOwner validDestination(_destination) {
|
||||
if (_stakeManagerImplementationTrusted()) {
|
||||
// If the stakeManager is trusted, the vault cannot leave the system
|
||||
// and has to properly unstake instead (which might not be possible if
|
||||
// funds are locked).
|
||||
revert StakeVault__NotAllowedToLeave();
|
||||
}
|
||||
|
||||
// If the stakeManager is not trusted, we know there was an upgrade.
|
||||
// In this case, vaults are free to leave the system and move their funds back
|
||||
// to the owner.
|
||||
//
|
||||
// We have to `try/catch` here in case the upgrade was malicious and `leave()`
|
||||
// either doesn't exist on the new stake manager or reverts for some reason.
|
||||
// If it was a benign upgrade, it will cause the stake manager to properly update
|
||||
// its internal accounting before we move the funds out.
|
||||
try stakeManager.leave() {
|
||||
STAKING_TOKEN.transfer(_destination, STAKING_TOKEN.balanceOf(address(this)));
|
||||
} catch {
|
||||
STAKING_TOKEN.transfer(_destination, STAKING_TOKEN.balanceOf(address(this)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw tokens from the contract.
|
||||
* @param _token The IERC20 token to withdraw.
|
||||
@@ -136,13 +189,11 @@ contract StakeVault is Ownable {
|
||||
}
|
||||
|
||||
function _stake(uint256 _amount, uint256 _seconds, address _source) internal {
|
||||
stakeManager.stake(_amount, _seconds);
|
||||
bool success = STAKING_TOKEN.transferFrom(_source, address(this), _amount);
|
||||
if (!success) {
|
||||
revert StakeVault__StakingFailed();
|
||||
}
|
||||
|
||||
stakeManager.stake(_amount, _seconds);
|
||||
|
||||
emit Staked(_source, address(this), _amount, _seconds);
|
||||
}
|
||||
|
||||
@@ -184,4 +235,8 @@ contract StakeVault is Ownable {
|
||||
STAKING_TOKEN.transfer(_destination, STAKING_TOKEN.balanceOf(address(this)));
|
||||
}
|
||||
}
|
||||
|
||||
function _stakeManagerImplementationTrusted() internal view virtual returns (bool) {
|
||||
return stakeManagerImplementationAddress == stakeManager.implementation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol";
|
||||
|
||||
interface IStakeManager is ITrustedCodehashAccess {
|
||||
error StakeManager__FundsLocked();
|
||||
error StakeManager__InvalidLockTime();
|
||||
error StakeManager__InsufficientFunds();
|
||||
error StakeManager__StakeIsTooLow();
|
||||
error StakingManager__FundsLocked();
|
||||
error StakingManager__InvalidLockTime();
|
||||
error StakingManager__InsufficientFunds();
|
||||
error StakingManager__StakeIsTooLow();
|
||||
error StakingManager__NotAllowedToLeave();
|
||||
|
||||
function stake(uint256 _amount, uint256 _seconds) external;
|
||||
function lock(uint256 _seconds) external;
|
||||
function unstake(uint256 _amount) external;
|
||||
function leave() external;
|
||||
|
||||
function emergencyModeEnabled() external view returns (bool);
|
||||
function totalStaked() external view returns (uint256);
|
||||
|
||||
8
src/interfaces/IStakeManagerProxy.sol
Normal file
8
src/interfaces/IStakeManagerProxy.sol
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import { IStakeManager } from "./IStakeManager.sol";
|
||||
|
||||
interface IStakeManagerProxy is IStakeManager {
|
||||
function implementation() external view returns (address);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils
|
||||
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
|
||||
import { StakeVault } from "../src/StakeVault.sol";
|
||||
import { IStakeManagerProxy } from "../src/interfaces/IStakeManagerProxy.sol";
|
||||
import { StakeManagerProxy } from "../src/StakeManagerProxy.sol";
|
||||
import { MockToken } from "./mocks/MockToken.sol";
|
||||
|
||||
contract RewardsStreamerMPTest is Test {
|
||||
@@ -29,7 +31,7 @@ contract RewardsStreamerMPTest is Test {
|
||||
bytes memory initializeData =
|
||||
abi.encodeCall(RewardsStreamerMP.initialize, (address(this), address(stakingToken), address(rewardToken)));
|
||||
address impl = address(new RewardsStreamerMP());
|
||||
address proxy = address(new ERC1967Proxy(impl, initializeData));
|
||||
address proxy = address(new StakeManagerProxy(impl, initializeData));
|
||||
streamer = RewardsStreamerMP(proxy);
|
||||
|
||||
address[4] memory accounts = [alice, bob, charlie, dave];
|
||||
@@ -93,7 +95,7 @@ contract RewardsStreamerMPTest is Test {
|
||||
|
||||
function _createTestVault(address owner) internal returns (StakeVault vault) {
|
||||
vm.prank(owner);
|
||||
vault = new StakeVault(owner, streamer);
|
||||
vault = new StakeVault(owner, IStakeManagerProxy(address(streamer)));
|
||||
|
||||
if (!streamer.isTrustedCodehash(address(vault).codehash)) {
|
||||
streamer.setTrustedCodehash(address(vault).codehash, true);
|
||||
@@ -118,6 +120,12 @@ contract RewardsStreamerMPTest is Test {
|
||||
vault.emergencyExit(account);
|
||||
}
|
||||
|
||||
function _leave(address account) public {
|
||||
StakeVault vault = StakeVault(vaults[account]);
|
||||
vm.prank(account);
|
||||
vault.leave(account);
|
||||
}
|
||||
|
||||
function _addReward(uint256 amount) public {
|
||||
vm.prank(admin);
|
||||
rewardToken.transfer(address(streamer), amount);
|
||||
@@ -1840,3 +1848,103 @@ contract UpgradeTest is RewardsStreamerMPTest {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
contract LeaveTest is RewardsStreamerMPTest {
|
||||
function _upgradeStakeManager() internal {
|
||||
address newImpl = address(new RewardsStreamerMP());
|
||||
bytes memory initializeData;
|
||||
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
|
||||
}
|
||||
|
||||
function setUp() public override {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
function test_RevertWhenStakeManagerIsTrusted() public {
|
||||
_stake(alice, 10e18, 0);
|
||||
vm.expectRevert(StakeVault.StakeVault__NotAllowedToLeave.selector);
|
||||
_leave(alice);
|
||||
}
|
||||
|
||||
function test_LeaveShouldProperlyUpdateAccounting() public {
|
||||
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
|
||||
|
||||
_stake(alice, 100e18, 0);
|
||||
|
||||
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance - 100e18, "Alice should have staked tokens");
|
||||
|
||||
checkStreamer(
|
||||
CheckStreamerParams({
|
||||
totalStaked: 100e18,
|
||||
totalMP: 100e18,
|
||||
totalMaxMP: 500e18,
|
||||
stakingBalance: 100e18,
|
||||
rewardBalance: 0,
|
||||
rewardIndex: 0,
|
||||
accountedRewards: 0
|
||||
})
|
||||
);
|
||||
|
||||
_upgradeStakeManager();
|
||||
_leave(alice);
|
||||
|
||||
// stake manager properly updates accounting
|
||||
checkStreamer(
|
||||
CheckStreamerParams({
|
||||
totalStaked: 0,
|
||||
totalMP: 0,
|
||||
totalMaxMP: 0,
|
||||
stakingBalance: 0,
|
||||
rewardBalance: 0,
|
||||
rewardIndex: 0,
|
||||
accountedRewards: 0
|
||||
})
|
||||
);
|
||||
|
||||
// vault should be empty as funds have been moved out
|
||||
checkAccount(
|
||||
CheckAccountParams({
|
||||
account: vaults[alice],
|
||||
rewardBalance: 0,
|
||||
stakedBalance: 0,
|
||||
vaultBalance: 0,
|
||||
rewardIndex: 0,
|
||||
accountMP: 0,
|
||||
maxMP: 0
|
||||
})
|
||||
);
|
||||
|
||||
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice has all her funds back");
|
||||
}
|
||||
|
||||
function test_TrustNewStakeManager() public {
|
||||
// first, upgrade to new stake manager, marking it as not trusted
|
||||
_upgradeStakeManager();
|
||||
|
||||
// ensure vault functions revert if stake manager is not trusted
|
||||
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
|
||||
_stake(alice, 100e18, 0);
|
||||
|
||||
// ensure vault functions revert if stake manager is not trusted
|
||||
StakeVault vault = StakeVault(vaults[alice]);
|
||||
vm.prank(alice);
|
||||
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
|
||||
vault.lock(365 days);
|
||||
|
||||
// ensure vault functions revert if stake manager is not trusted
|
||||
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
|
||||
_unstake(alice, 100e18);
|
||||
|
||||
// now, trust the new stake manager
|
||||
address newStakeManagerImpl = IStakeManagerProxy(address(streamer)).implementation();
|
||||
vm.prank(alice);
|
||||
vault.trustStakeManager(newStakeManagerImpl);
|
||||
|
||||
// stake manager is now trusted, so functions are enabeled again
|
||||
_stake(alice, 100e18, 0);
|
||||
|
||||
// however, a trusted manager cannot be left
|
||||
vm.expectRevert(StakeVault.StakeVault__NotAllowedToLeave.selector);
|
||||
_leave(alice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
|
||||
import { StakeVault } from "../src/StakeVault.sol";
|
||||
import { MockToken } from "./mocks/MockToken.sol";
|
||||
@@ -21,7 +23,7 @@ contract StakeVaultTest is Test {
|
||||
|
||||
function _createTestVault(address owner) internal returns (StakeVault vault) {
|
||||
vm.prank(owner);
|
||||
vault = new StakeVault(owner, streamer);
|
||||
vault = new StakeVault(owner, IStakeManagerProxy(address(streamer)));
|
||||
|
||||
if (!streamer.isTrustedCodehash(address(vault).codehash)) {
|
||||
streamer.setTrustedCodehash(address(vault).codehash, true);
|
||||
@@ -35,7 +37,7 @@ contract StakeVaultTest is Test {
|
||||
bytes memory initializeData = abi.encodeWithSelector(
|
||||
RewardsStreamerMP.initialize.selector, address(this), address(stakingToken), address(rewardToken)
|
||||
);
|
||||
address proxy = address(new ERC1967Proxy(impl, initializeData));
|
||||
address proxy = address(new StakeManagerProxy(impl, initializeData));
|
||||
streamer = RewardsStreamerMP(proxy);
|
||||
|
||||
stakingToken.mint(alice, 10_000e18);
|
||||
@@ -48,7 +50,7 @@ contract StakeVaultTest is Test {
|
||||
|
||||
contract StakingTokenTest is StakeVaultTest {
|
||||
function setUp() public override {
|
||||
StakeVaultTest.setUp();
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
function testStakeToken() public view {
|
||||
@@ -58,7 +60,7 @@ contract StakingTokenTest is StakeVaultTest {
|
||||
|
||||
contract WithdrawTest is StakeVaultTest {
|
||||
function setUp() public override {
|
||||
StakeVaultTest.setUp();
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
function test_CannotWithdrawStakedFunds() public {
|
||||
|
||||
Reference in New Issue
Block a user