Skip to content

Commit

Permalink
feat(RewardsStreamerMP): introduce emergency mode and ability to leave
Browse files Browse the repository at this point in the history
This adds a new emergency mode that can be enabled by the owner of the
system. When in emergency mode, stakers or `StakeVault`s can leave the
system immediately.

Closes #66
  • Loading branch information
0x-r4bbit committed Nov 26, 2024
1 parent 5bc7ebf commit d883a7a
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 77 deletions.
42 changes: 22 additions & 20 deletions .gas-report
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,36 @@
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1327847 | 6066 | | | | |
| 1462118 | 6698 | | | | |
| Function Name | min | avg | median | max | # calls |
| MAX_LOCKUP_PERIOD | 228 | 228 | 228 | 228 | 23 |
| MAX_MULTIPLIER | 274 | 274 | 274 | 274 | 30 |
| MIN_LOCKUP_PERIOD | 275 | 275 | 275 | 275 | 11 |
| MP_RATE_PER_YEAR | 231 | 231 | 231 | 231 | 3 |
| SCALE_FACTOR | 295 | 295 | 295 | 295 | 41 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 140 |
| accountedRewards | 373 | 931 | 373 | 2373 | 68 |
| getAccount | 1596 | 1596 | 1596 | 1596 | 67 |
| isTrustedCodehash | 496 | 996 | 496 | 2496 | 140 |
| rewardIndex | 351 | 380 | 351 | 2351 | 68 |
| setTrustedCodehash | 47926 | 47926 | 47926 | 47926 | 35 |
| totalMP | 330 | 330 | 330 | 330 | 71 |
| totalMaxMP | 373 | 373 | 373 | 373 | 71 |
| totalStaked | 352 | 352 | 352 | 352 | 71 |
| updateAccountMP | 34632 | 36870 | 37134 | 37134 | 19 |
| updateGlobalState | 30008 | 55588 | 47387 | 80334 | 25 |
| SCALE_FACTOR | 273 | 273 | 273 | 273 | 41 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 172 |
| accountedRewards | 329 | 856 | 329 | 2329 | 72 |
| enableEmergencyMode | 23482 | 40389 | 45674 | 45674 | 8 |
| getAccount | 1596 | 1596 | 1596 | 1596 | 71 |
| isTrustedCodehash | 496 | 996 | 496 | 2496 | 172 |
| rewardIndex | 373 | 400 | 373 | 2373 | 72 |
| setTrustedCodehash | 47926 | 47926 | 47926 | 47926 | 43 |
| totalMP | 330 | 330 | 330 | 330 | 75 |
| totalMaxMP | 351 | 351 | 351 | 351 | 75 |
| totalStaked | 330 | 330 | 330 | 330 | 75 |
| updateAccountMP | 36758 | 38996 | 39260 | 39260 | 19 |
| updateGlobalState | 32134 | 60366 | 49513 | 82460 | 28 |


| src/StakeVault.sol:StakeVault contract | | | | | |
|----------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 894174 | 4243 | | | | |
| 1019470 | 4836 | | | | |
| Function Name | min | avg | median | max | # calls |
| leave | 38706 | 105489 | 127912 | 141592 | 7 |
| lock | 36236 | 58390 | 40615 | 98319 | 3 |
| stake | 196956 | 234305 | 240649 | 261178 | 48 |
| unstake | 83287 | 111971 | 101308 | 144002 | 13 |
| stake | 199207 | 236942 | 242900 | 263429 | 55 |
| unstake | 85027 | 114046 | 103483 | 146178 | 13 |


| src/XPNFTToken.sol:XPNFTToken contract | | | | | |
Expand Down Expand Up @@ -118,10 +120,10 @@
| Deployment Cost | Deployment Size | | | | |
| 639406 | 3369 | | | | |
| Function Name | min | avg | median | max | # calls |
| approve | 46334 | 46343 | 46346 | 46346 | 180 |
| balanceOf | 561 | 1351 | 561 | 2561 | 291 |
| mint | 51284 | 58959 | 51284 | 68384 | 196 |
| transfer | 34390 | 48070 | 51490 | 51490 | 10 |
| approve | 46334 | 46343 | 46346 | 46346 | 220 |
| balanceOf | 561 | 1356 | 561 | 2561 | 342 |
| mint | 51284 | 58817 | 51284 | 68384 | 236 |
| transfer | 34390 | 48859 | 51490 | 51490 | 13 |


| test/mocks/XPProviderMock.sol:XPProviderMock contract | | | | | |
Expand Down
78 changes: 43 additions & 35 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,45 +1,53 @@
IntegrationTest:testStakeFoo() (gas: 1471121)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 285795)
LockTest:test_LockFailsWithNoStake() (gas: 51253)
LockTest:test_LockWithoutPriorLock() (gas: 378889)
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 79785)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 290524)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 395074)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 890698)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 566151)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 393722)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 560526)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 34651)
IntegrationTest:testStakeFoo() (gas: 1490136)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 288046)
LockTest:test_LockFailsWithNoStake() (gas: 51231)
LockTest:test_LockWithoutPriorLock() (gas: 381096)
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 92874)
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 60081)
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35818)
NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 109345)
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 50653)
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35993)
RewardsStreamerTest:testStake() (gas: 869874)
StakeTest:test_StakeMultipleAccounts() (gas: 488809)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634232)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801063)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494911)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516313)
StakeTest:test_StakeOneAccount() (gas: 282063)
StakeTest:test_StakeOneAccountAndRewards() (gas: 427482)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488293)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483447)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295896)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295974)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 296085)
UnstakeTest:test_StakeMultipleAccounts() (gas: 488831)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634209)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801085)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494888)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516335)
UnstakeTest:test_StakeOneAccount() (gas: 282086)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 427504)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488315)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483427)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295941)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295974)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 296063)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 500049)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 680902)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1002330)
UnstakeTest:test_UnstakeOneAccount() (gas: 474602)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 488113)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 579585)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 510049)
StakeTest:test_StakeMultipleAccounts() (gas: 493311)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 640706)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 818146)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 499237)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 520639)
StakeTest:test_StakeOneAccount() (gas: 284248)
StakeTest:test_StakeOneAccountAndRewards() (gas: 431705)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 498828)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 493960)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298103)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 298115)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 298204)
UnstakeTest:test_StakeMultipleAccounts() (gas: 493267)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 640750)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 818145)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 499236)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 520683)
UnstakeTest:test_StakeOneAccount() (gas: 284271)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 431749)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 498872)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 493962)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298060)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 298115)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 298204)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 508529)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 688752)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1014285)
UnstakeTest:test_UnstakeOneAccount() (gas: 480135)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 496593)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 585940)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 518397)
XPNFTTokenTest:testApproveNotAllowed() (gas: 10507)
XPNFTTokenTest:testGetApproved() (gas: 10531)
XPNFTTokenTest:testIsApprovedForAll() (gas: 10705)
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,8 @@ jobs:
strategy:
fail-fast: false
max-parallel: 16
matrix:
rule:
- verify:rewards_streamer_mp
- verify:emergency_mode
- verify:xp_token
21 changes: 21 additions & 0 deletions certora/confs/EmergencyMode.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"src/RewardsStreamerMP.sol",
"certora/helpers/ERC20A.sol"
],
"link" : [
"RewardsStreamerMP:STAKING_TOKEN=ERC20A",
"RewardsStreamerMP:REWARD_TOKEN=ERC20A"
],
"msg": "Verifying RewardsStreamerMP.sol",
"rule_sanity": "basic",
"verify": "RewardsStreamerMP:certora/specs/EmergencyMode.spec",
"parametric_contracts": ["RewardsStreamerMP"],
"optimistic_loop": true,
"loop_iter": "3",
"packages": [
"forge-std=lib/forge-std/src",
"@openzeppelin=lib/openzeppelin-contracts"
]
}

55 changes: 55 additions & 0 deletions certora/specs/EmergencyMode.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using RewardsStreamerMP as streamer;
using ERC20A as staked;

methods {
function emergencyModeEnabled() external returns (bool) envfree;
}

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 ||
f.selector == sig:streamer.totalStaked().selector ||
f.selector == sig:streamer.totalMaxMP().selector ||
f.selector == sig:streamer.totalMP().selector ||
f.selector == sig:streamer.accounts(address).selector ||
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
);

definition isOwnableFunction(method f) returns bool = (
f.selector == sig:streamer.renounceOwnership().selector ||
f.selector == sig:streamer.transferOwnership(address).selector
);

definition isTrustedCodehashAccessFunction(method f) returns bool = (
f.selector == sig:streamer.setTrustedCodehash(bytes32, bool).selector ||
f.selector == sig:streamer.isTrustedCodehash(bytes32).selector
);

rule accountCanOnlyLeaveInEmergencyMode(method f) {
env e;
calldataarg args;

require emergencyModeEnabled() == true;

f@withrevert(e, args);
bool isReverted = lastReverted;

assert !isReverted => f.selector == sig:streamer.leave().selector ||
isViewFunction(f)||
isOwnableFunction(f)||
isTrustedCodehashAccessFunction(f);
}

1 change: 1 addition & 0 deletions certora/specs/RewardsStreamerMP.spec
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ methods {
function lastMPUpdatedTime() external returns (uint256) envfree;
function updateGlobalState() external;
function updateAccountMP(address accountAddress) external;
function emergencyModeEnabled() external returns (bool) envfree;
}

ghost mathint sumOfBalances {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
"scripts": {
"clean": "rm -rf cache out",
"lint": "pnpm lint:sol && pnpm prettier:check",
"verify": "pnpm verify:rewards_streamer_mp && pnpm verify:xp_token",
"verify": "pnpm verify:rewards_streamer_mp && pnpm verify:xp_token && pnpm verify:emergency_mode",
"verify:rewards_streamer_mp": "certoraRun certora/confs/RewardsStreamerMP.conf",
"verify:emergency_mode": "certoraRun certora/confs/EmergencyMode.conf",
"verify:xp_token": "certoraRun certora/confs/XPToken.conf",
"lint:sol": "forge fmt --check && pnpm solhint {script,src,test,certora}/**/*.sol",
"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
Expand Down
Loading

0 comments on commit d883a7a

Please sign in to comment.