Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RewardsStreamerMP): introduce emergency mode and ability to leave #72

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions .gas-report
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,37 @@
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1327847 | 6066 | | | | |
| 1425736 | 6528 | | | | |
| 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 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 172 |
| accountedRewards | 351 | 906 | 351 | 2351 | 72 |
| emergencyModeEnabled | 2377 | 2377 | 2377 | 2377 | 7 |
| enableEmergencyMode | 23504 | 40411 | 45696 | 45696 | 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 | | | | |
| 1095864 | 5202 | | | | |
| Function Name | min | avg | median | max | # calls |
| lock | 36236 | 58390 | 40615 | 98319 | 3 |
| stake | 196956 | 234305 | 240649 | 261178 | 48 |
| unstake | 83287 | 111971 | 101308 | 144002 | 13 |
| emergencyExit | 31410 | 43924 | 43160 | 60260 | 7 |
| lock | 38362 | 60516 | 42741 | 100445 | 3 |
| stake | 199213 | 236948 | 242906 | 263435 | 55 |
| unstake | 84988 | 113999 | 103434 | 146128 | 13 |


| src/XPNFTToken.sol:XPNFTToken contract | | | | | |
Expand Down Expand Up @@ -118,10 +121,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 | 1381 | 561 | 2561 | 334 |
| 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: 79829)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 283234)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 379874)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 788714)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 517455)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 370304)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 507361)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 34607)
IntegrationTest:testStakeFoo() (gas: 1490300)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 290178)
LockTest:test_LockFailsWithNoStake() (gas: 53357)
LockTest:test_LockWithoutPriorLock() (gas: 383272)
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: 493279)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 640763)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 818252)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 499381)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 520783)
StakeTest:test_StakeOneAccount() (gas: 284277)
StakeTest:test_StakeOneAccountAndRewards() (gas: 431756)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 498901)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 494078)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298175)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 298187)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 298298)
UnstakeTest:test_StakeMultipleAccounts() (gas: 493323)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 640807)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 818251)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 499380)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 520827)
UnstakeTest:test_StakeOneAccount() (gas: 284300)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 431800)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 498945)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 494080)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298132)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 298187)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 298298)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 508511)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 688755)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1014239)
UnstakeTest:test_UnstakeOneAccount() (gas: 480152)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 496638)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 585964)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 518574)
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"
]
}

54 changes: 54 additions & 0 deletions certora/specs/EmergencyMode.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 => 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
28 changes: 22 additions & 6 deletions src/RewardsStreamerMP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
error StakingManager__CannotRestakeWithLockedFunds();
error StakingManager__TokensAreLocked();
error StakingManager__AlreadyLocked();
error StakingManager__EmergencyModeEnabled();

IERC20 public immutable STAKING_TOKEN;
IERC20 public immutable REWARD_TOKEN;
Expand All @@ -32,6 +33,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
uint256 public rewardIndex;
uint256 public accountedRewards;
uint256 public lastMPUpdatedTime;
bool public emergencyModeEnabled;

struct Account {
uint256 stakedBalance;
Expand All @@ -44,13 +46,20 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu

mapping(address account => Account data) public accounts;

modifier onlyNotEmergencyMode() {
if (emergencyModeEnabled) {
revert StakingManager__EmergencyModeEnabled();
}
_;
}

constructor(address _owner, address _stakingToken, address _rewardToken) TrustedCodehashAccess(_owner) {
STAKING_TOKEN = IERC20(_stakingToken);
REWARD_TOKEN = IERC20(_rewardToken);
lastMPUpdatedTime = block.timestamp;
}

function stake(uint256 amount, uint256 lockPeriod) external onlyTrustedCodehash nonReentrant {
function stake(uint256 amount, uint256 lockPeriod) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant {
if (amount == 0) {
revert StakingManager__AmountCannotBeZero();
}
Expand Down Expand Up @@ -99,7 +108,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
account.lastMPUpdateTime = block.timestamp;
}

function lock(uint256 lockPeriod) external onlyTrustedCodehash nonReentrant {
function lock(uint256 lockPeriod) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant {
if (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD) {
revert StakingManager__InvalidLockingPeriod();
}
Expand Down Expand Up @@ -132,7 +141,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
account.lastMPUpdateTime = block.timestamp;
}

function unstake(uint256 amount) external onlyTrustedCodehash nonReentrant {
function unstake(uint256 amount) external onlyTrustedCodehash onlyNotEmergencyMode nonReentrant {
Account storage account = accounts[msg.sender];
if (amount > account.stakedBalance) {
revert StakingManager__InsufficientBalance();
Expand Down Expand Up @@ -170,7 +179,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
updateRewardIndex();
}

function updateGlobalState() external {
function updateGlobalState() external onlyNotEmergencyMode {
_updateGlobalState();
}

Expand Down Expand Up @@ -246,7 +255,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
account.lastMPUpdateTime = block.timestamp;
}

function updateAccountMP(address accountAddress) external {
function updateAccountMP(address accountAddress) external onlyNotEmergencyMode {
_updateAccountMP(accountAddress);
}

Expand All @@ -272,7 +281,14 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
}
}

function getStakedBalance(address accountAddress) external view returns (uint256) {
function enableEmergencyMode() external onlyOwner {
if (emergencyModeEnabled) {
revert StakingManager__EmergencyModeEnabled();
}
emergencyModeEnabled = true;
}

function getStakedBalance(address accountAddress) public view returns (uint256) {
return accounts[accountAddress].stakedBalance;
}

Expand Down
Loading
Loading