From a22da253c3b55c2b0d21bcace78b34a58d48c456 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Thu, 30 Jan 2025 12:28:31 -0300 Subject: [PATCH] refactor(RewardStreamerMP): extract MP and Stake mathematical formulas to abstract contracts --- .gas-report | 143 +++++++---------- .gas-snapshot | 129 ++++++++-------- certora/specs/EmergencyMode.spec | 7 +- src/RewardsStreamerMP.sol | 109 +++++-------- src/interfaces/IStakeConstants.sol | 17 ++ src/interfaces/IStakeManager.sol | 7 +- src/math/MultiplierPointMath.sol | 166 ++++++++++++++++++++ src/math/StakeMath.sol | 182 ++++++++++++++++++++++ test/RewardsStreamerMP.t.sol | 189 ++++++++++------------- test/mocks/StackOverflowStakeManager.sol | 3 +- 10 files changed, 615 insertions(+), 337 deletions(-) create mode 100644 src/interfaces/IStakeConstants.sol create mode 100644 src/math/MultiplierPointMath.sol create mode 100644 src/math/StakeMath.sol diff --git a/.gas-report b/.gas-report index e85c429..5315a9e 100644 --- a/.gas-report +++ b/.gas-report @@ -1,119 +1,92 @@ | script/DeployRewardsStreamerMP.s.sol:DeployRewardsStreamerMPScript contract | | | | | | |-----------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6351315 | 30400 | | | | | +| 6539451 | 31274 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5443041 | 5443041 | 5443041 | 5443041 | 63 | +| run | 5618268 | 5618268 | 5618268 | 5618268 | 64 | | script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | |---------------------------------------------------------|-----------------|-----|--------|-----|---------| | Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | +| 0 | 7333 | | | | | | Function Name | min | avg | median | max | # calls | -| activeNetworkConfig | 454 | 454 | 454 | 454 | 126 | +| activeNetworkConfig | 454 | 454 | 454 | 454 | 128 | | script/UpgradeRewardsStreamerMP.s.sol:UpgradeRewardsStreamerMPScript contract | | | | | | |-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 2952925 | 14574 | | | | | +| 3140997 | 15448 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 2482684 | 2482684 | 2482684 | 2482684 | 3 | +| run | 2657933 | 2657933 | 2657933 | 2657933 | 3 | | src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | | |------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2632986 | 12135 | | | | | +| 2821054 | 13009 | | | | | | Function Name | min | avg | median | max | # calls | -| MAX_LOCKUP_PERIOD | 371 | 371 | 371 | 371 | 6 | -| MAX_MULTIPLIER | 295 | 295 | 295 | 295 | 33 | -| MIN_LOCKUP_PERIOD | 252 | 252 | 252 | 252 | 15 | -| MP_RATE_PER_YEAR | 253 | 253 | 253 | 253 | 9 | -| STAKING_TOKEN | 428 | 2036 | 2428 | 2428 | 322 | +| MAX_LOCKUP_PERIOD | 382 | 382 | 382 | 382 | 4 | +| MAX_MULTIPLIER | 262 | 262 | 262 | 262 | 9 | +| MIN_LOCKUP_PERIOD | 308 | 308 | 308 | 308 | 15 | +| STAKING_TOKEN | 395 | 2003 | 2395 | 2395 | 327 | | emergencyModeEnabled | 2398 | 2398 | 2398 | 2398 | 7 | | enableEmergencyMode | 2507 | 19414 | 24699 | 24699 | 8 | -| getAccountTotalMaxMP | 3122 | 3122 | 3122 | 3122 | 1 | -| getAccountTotalStakedBalance | 15119 | 15119 | 15119 | 15119 | 1 | -| getAccountVaults | 5202 | 5202 | 5202 | 5202 | 4 | -| getStakedBalance | 2629 | 2629 | 2629 | 2629 | 1 | -| getVault | 1643 | 1643 | 1643 | 1643 | 72 | -| initialize | 115611 | 115611 | 115611 | 115611 | 65 | -| lastRewardTime | 373 | 1373 | 1373 | 2373 | 2 | -| leave | 59961 | 59961 | 59961 | 59961 | 1 | -| lock | 12041 | 35382 | 16458 | 77648 | 3 | -| mpBalanceOfAccount | 9228 | 9228 | 9228 | 9228 | 1 | -| proxiableUUID | 353 | 353 | 353 | 353 | 3 | -| registerVault | 55888 | 72788 | 72988 | 72988 | 257 | -| rewardEndTime | 373 | 1373 | 1373 | 2373 | 2 | -| rewardStartTime | 352 | 1352 | 1352 | 2352 | 2 | -| rewardsBalanceOf | 3231 | 6627 | 7074 | 7341 | 8 | -| setReward | 2561 | 58349 | 86319 | 105709 | 7 | -| setTrustedCodehash | 24243 | 24304 | 24243 | 26243 | 65 | -| stake | 134819 | 172398 | 179212 | 199545 | 66 | -| totalMPAccrued | 351 | 351 | 351 | 351 | 81 | -| totalMaxMP | 373 | 373 | 373 | 373 | 81 | -| totalRewardsAccrued | 373 | 373 | 373 | 373 | 3 | -| totalRewardsSupply | 1025 | 1984 | 1806 | 6765 | 30 | -| totalStaked | 374 | 374 | 374 | 374 | 82 | -| unstake | 64077 | 64613 | 64077 | 67567 | 13 | -| updateGlobalState | 14339 | 26645 | 28616 | 28616 | 19 | -| updateVaultMP | 12238 | 17353 | 17955 | 17955 | 19 | -| upgradeToAndCall | 3225 | 7901 | 8448 | 10936 | 5 | +| getAccountTotalMaxMP | 3133 | 3133 | 3133 | 3133 | 1 | +| getAccountTotalStakedBalance | 15173 | 15173 | 15173 | 15173 | 1 | +| getAccountVaults | 5225 | 5225 | 5225 | 5225 | 4 | +| getStakedBalance | 2618 | 2618 | 2618 | 2618 | 1 | +| getVault | 1621 | 1621 | 1621 | 1621 | 72 | +| initialize | 115654 | 115654 | 115654 | 115654 | 66 | +| lastRewardTime | 428 | 1428 | 1428 | 2428 | 2 | +| leave | 79955 | 79955 | 79955 | 79955 | 1 | +| lock | 14282 | 42729 | 42692 | 78446 | 259 | +| mpBalanceOfAccount | 10308 | 10308 | 10308 | 10308 | 1 | +| proxiableUUID | 387 | 387 | 387 | 387 | 3 | +| registerVault | 55866 | 72769 | 72966 | 72966 | 261 | +| rewardEndTime | 362 | 1362 | 1362 | 2362 | 2 | +| rewardStartTime | 407 | 1407 | 1407 | 2407 | 2 | +| rewardsBalanceOf | 2942 | 6975 | 7525 | 7746 | 8 | +| setReward | 2606 | 58415 | 86507 | 105754 | 7 | +| setTrustedCodehash | 24199 | 24259 | 24199 | 26199 | 66 | +| stake | 136333 | 179336 | 180726 | 201200 | 322 | +| totalMPAccrued | 384 | 384 | 384 | 384 | 81 | +| totalMaxMP | 406 | 406 | 406 | 406 | 81 | +| totalRewardsAccrued | 407 | 407 | 407 | 407 | 3 | +| totalRewardsSupply | 1036 | 1995 | 1817 | 6776 | 30 | +| totalStaked | 427 | 427 | 427 | 427 | 82 | +| unstake | 63857 | 64502 | 63857 | 68054 | 13 | +| updateGlobalState | 14339 | 26780 | 28759 | 28759 | 19 | +| updateVaultMP | 11707 | 17581 | 18273 | 18273 | 19 | +| upgradeToAndCall | 3181 | 7875 | 8438 | 10881 | 5 | -| src/StakeManagerProxy.sol:StakeManagerProxy contract | | | | | | -|------------------------------------------------------|-----------------|-------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 256467 | 1263 | | | | | -| Function Name | min | avg | median | max | # calls | -| MAX_LOCKUP_PERIOD | 798 | 3798 | 5298 | 5298 | 6 | -| MAX_MULTIPLIER | 722 | 1949 | 722 | 5222 | 33 | -| MIN_LOCKUP_PERIOD | 679 | 3379 | 5179 | 5179 | 15 | -| MP_RATE_PER_YEAR | 680 | 1180 | 680 | 5180 | 9 | -| STAKING_TOKEN | 855 | 6083 | 7355 | 7355 | 322 | -| emergencyModeEnabled | 7325 | 7325 | 7325 | 7325 | 7 | -| enableEmergencyMode | 28502 | 45403 | 50687 | 50687 | 8 | -| getAccountTotalMaxMP | 3552 | 3552 | 3552 | 3552 | 1 | -| getAccountTotalStakedBalance | 15549 | 15549 | 15549 | 15549 | 1 | -| getAccountVaults | 5638 | 6763 | 5638 | 10138 | 4 | -| getStakedBalance | 7559 | 7559 | 7559 | 7559 | 1 | -| getVault | 2097 | 2097 | 2097 | 2097 | 72 | -| implementation | 343 | 775 | 343 | 2343 | 412 | -| lastRewardTime | 800 | 1800 | 1800 | 2800 | 2 | -| mpBalanceOfAccount | 9658 | 9658 | 9658 | 9658 | 1 | -| rewardEndTime | 800 | 1800 | 1800 | 2800 | 2 | -| rewardStartTime | 779 | 4029 | 4029 | 7279 | 2 | -| rewardsBalanceOf | 3661 | 7057 | 7504 | 7771 | 8 | -| setReward | 28841 | 84663 | 112677 | 132067 | 7 | -| setTrustedCodehash | 52889 | 52889 | 52889 | 52889 | 2 | -| totalMPAccrued | 778 | 778 | 778 | 778 | 81 | -| totalMaxMP | 800 | 800 | 800 | 800 | 81 | -| totalRewardsAccrued | 800 | 800 | 800 | 800 | 3 | -| totalRewardsSupply | 1452 | 2561 | 2233 | 11692 | 30 | -| totalStaked | 801 | 801 | 801 | 801 | 82 | -| updateGlobalState | 40327 | 52633 | 54604 | 54604 | 19 | -| updateVaultMP | 38597 | 43712 | 44314 | 44314 | 19 | -| upgradeToAndCall | 29868 | 33720 | 33720 | 37572 | 2 | +| src/StakeManagerProxy.sol:StakeManagerProxy contract | | | | | | +|------------------------------------------------------|-----------------|------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 256510 | 1231 | | | | | +| Function Name | min | avg | median | max | # calls | +| fallback | 689 | 7187 | 2075 | 132112 | 790 | +| implementation | 343 | 1636 | 2343 | 2343 | 929 | | src/StakeVault.sol:StakeVault contract | | | | | | |----------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 1420425 | 6695 | | | | | +| 1420392 | 6695 | | | | | | Function Name | min | avg | median | max | # calls | | STAKING_TOKEN | 216 | 216 | 216 | 216 | 1 | | emergencyExit | 36353 | 48857 | 48091 | 65191 | 7 | -| leave | 33507 | 132182 | 62122 | 370978 | 4 | -| lock | 33245 | 61584 | 50823 | 111445 | 4 | -| owner | 2339 | 2339 | 2339 | 2339 | 257 | -| register | 87037 | 103937 | 104137 | 104137 | 257 | -| stake | 33411 | 243726 | 253716 | 274097 | 67 | -| stakeManager | 368 | 368 | 368 | 368 | 257 | +| leave | 33507 | 136181 | 70120 | 370978 | 4 | +| lock | 33245 | 79219 | 79302 | 112243 | 260 | +| owner | 2339 | 2339 | 2339 | 2339 | 261 | +| register | 87015 | 103918 | 104115 | 104115 | 261 | +| stake | 33411 | 253159 | 255230 | 275752 | 323 | +| stakeManager | 368 | 368 | 368 | 368 | 261 | | trustStakeManager | 28953 | 28953 | 28953 | 28953 | 1 | -| unstake | 33282 | 99955 | 105755 | 113604 | 14 | -| withdraw | 42289 | 42289 | 42289 | 42289 | 1 | +| unstake | 33282 | 99858 | 105888 | 113384 | 14 | +| withdraw | 42278 | 42278 | 42278 | 42278 | 1 | | src/XPNFTToken.sol:XPNFTToken contract | | | | | | @@ -189,18 +162,18 @@ | Deployment Cost | Deployment Size | | | | | | 625454 | 3260 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46330 | 46339 | 46342 | 46342 | 257 | +| approve | 46330 | 46339 | 46342 | 46342 | 261 | | balanceOf | 558 | 926 | 558 | 2558 | 103 | -| mint | 51279 | 56407 | 51279 | 68379 | 270 | +| mint | 51279 | 56395 | 51279 | 68379 | 274 | | test/mocks/StackOverflowStakeManager.sol:StackOverflowStakeManager contract | | | | | | |-----------------------------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 1026739 | 4584 | | | | | +| 1031089 | 4604 | | | | | | Function Name | min | avg | median | max | # calls | | leave | 391 | 161316 | 161316 | 322322 | 334 | -| proxiableUUID | 341 | 341 | 341 | 341 | 1 | +| proxiableUUID | 330 | 330 | 330 | 330 | 1 | | test/mocks/XPProviderMock.sol:XPProviderMock contract | | | | | | diff --git a/.gas-snapshot b/.gas-snapshot index 78a16b1..7cd36ff 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,74 +1,75 @@ -EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92734) -EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 299008) -EmergencyExitTest:test_EmergencyExitBasic() (gas: 385662) -EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 664160) -EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 393691) -EmergencyExitTest:test_EmergencyExitWithLock() (gas: 393030) -EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 378630) -EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39470) -IntegrationTest:testStakeFoo() (gas: 1212157) -LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 5836880) -LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 296161) -LeaveTest:test_TrustNewStakeManager() (gas: 5907277) -LockTest:test_LockFailsWithInvalidPeriod() (gas: 311224) -LockTest:test_LockFailsWithNoStake() (gas: 63663) -LockTest:test_LockWithoutPriorLock() (gas: 390931) -MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1746581) -MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 18995) -MathTest:test_CalcAccrueMP() (gas: 22229) -MathTest:test_CalcBonusMP() (gas: 17645) -MathTest:test_CalcInitialMP() (gas: 5330) -MathTest:test_CalcMaxAccruedMP() (gas: 15696) -MathTest:test_CalcMaxTotalMP() (gas: 23339) -MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 725540) +EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92757) +EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 300544) +EmergencyExitTest:test_EmergencyExitBasic() (gas: 387340) +EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 667427) +EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 395139) +EmergencyExitTest:test_EmergencyExitWithLock() (gas: 394708) +EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 380241) +EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39471) +IntegrationTest:testStakeFoo() (gas: 1218594) +LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 6214173) +LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 297675) +LeaveTest:test_TrustNewStakeManager() (gas: 6269901) +LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1002, μ: 344783, ~: 344801) +LockTest:test_LockFailsWithNoStake() (gas: 102637) +LockTest:test_LockFailsWithZero() (gas: 315022) +LockTest:test_LockWithoutPriorLock() (gas: 393335) +MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1752531) +MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 4996) +MathTest:test_CalcAccrueMP() (gas: 7990) +MathTest:test_CalcBonusMP() (gas: 18676) +MathTest:test_CalcInitialMP() (gas: 5352) +MathTest:test_CalcMaxAccruedMP() (gas: 4642) +MathTest:test_CalcMaxTotalMP() (gas: 19449) +MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 731369) 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: 486274) -RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160637) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39317) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39340) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39375) -RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 618553) -StakeTest:test_StakeMultipleAccounts() (gas: 499457) -StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505374) -StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842563) -StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515891) -StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 538001) -StakeTest:test_StakeOneAccount() (gas: 278207) -StakeTest:test_StakeOneAccountAndRewards() (gas: 284155) -StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507692) -StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499083) -StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298124) -StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299768) -StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299857) +RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 490632) +RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160880) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39384) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39407) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39442) +RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 620722) +StakeTest:test_StakeMultipleAccounts() (gas: 502561) +StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508596) +StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847390) +StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517705) +StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539649) +StakeTest:test_StakeOneAccount() (gas: 279841) +StakeTest:test_StakeOneAccountAndRewards() (gas: 285896) +StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510467) +StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500009) +StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111) +StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300696) +StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300763) StakingTokenTest:testStakeToken() (gas: 10422) -UnstakeTest:test_StakeMultipleAccounts() (gas: 499479) -UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505396) -UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842585) -UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515935) -UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 537957) -UnstakeTest:test_StakeOneAccount() (gas: 278230) -UnstakeTest:test_StakeOneAccountAndRewards() (gas: 284133) -UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507736) -UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499040) -UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298124) -UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299768) -UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299856) -UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546251) -UnstakeTest:test_UnstakeMultipleAccounts() (gas: 704925) -UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 800718) -UnstakeTest:test_UnstakeOneAccount() (gas: 479941) -UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 502893) -UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 409031) -UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 531430) -UpgradeTest:test_RevertWhenNotOwner() (gas: 2709437) -UpgradeTest:test_UpgradeStakeManager() (gas: 5749376) -VaultRegistrationTest:test_VaultRegistration() (gas: 62017) -WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 311841) +UnstakeTest:test_StakeMultipleAccounts() (gas: 502560) +UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508640) +UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847367) +UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517704) +UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539693) +UnstakeTest:test_StakeOneAccount() (gas: 279841) +UnstakeTest:test_StakeOneAccountAndRewards() (gas: 285874) +UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510511) +UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500008) +UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111) +UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300718) +UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300762) +UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546541) +UnstakeTest:test_UnstakeMultipleAccounts() (gas: 707663) +UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 803659) +UnstakeTest:test_UnstakeOneAccount() (gas: 481480) +UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 505028) +UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 410671) +UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 530083) +UpgradeTest:test_RevertWhenNotOwner() (gas: 2897740) +UpgradeTest:test_UpgradeStakeManager() (gas: 6114750) +VaultRegistrationTest:test_VaultRegistration() (gas: 62154) +WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 313397) 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 7cca7e4..7b35f88 100644 --- a/certora/specs/EmergencyMode.spec +++ b/certora/specs/EmergencyMode.spec @@ -9,7 +9,12 @@ definition isViewFunction(method f) returns bool = ( f.selector == sig:streamer.YEAR().selector || f.selector == sig:streamer.STAKING_TOKEN().selector || f.selector == sig:streamer.SCALE_FACTOR().selector || - f.selector == sig:streamer.MP_RATE_PER_YEAR().selector || + f.selector == sig:streamer.MP_APY().selector || + f.selector == sig:streamer.MP_MPY().selector || + f.selector == sig:streamer.MP_MPY_ABSOLUTE().selector || + f.selector == sig:streamer.ACCRUE_RATE().selector || + f.selector == sig:streamer.MIN_BALANCE().selector || + f.selector == sig:streamer.MAX_BALANCE().selector || f.selector == sig:streamer.MIN_LOCKUP_PERIOD().selector || f.selector == sig:streamer.MAX_LOCKUP_PERIOD().selector || f.selector == sig:streamer.MAX_MULTIPLIER().selector || diff --git a/src/RewardsStreamerMP.sol b/src/RewardsStreamerMP.sol index 31da2e5..299e0ea 100644 --- a/src/RewardsStreamerMP.sol +++ b/src/RewardsStreamerMP.sol @@ -10,6 +10,7 @@ import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { IStakeVault } from "./interfaces/IStakeVault.sol"; import { IRewardProvider } from "./interfaces/IRewardProvider.sol"; import { TrustedCodehashAccess } from "./TrustedCodehashAccess.sol"; +import { StakeMath } from "./math/StakeMath.sol"; // Rewards Streamer with Multiplier Points contract RewardsStreamerMP is @@ -18,7 +19,8 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGuardUpgradeable, - IRewardProvider + IRewardProvider, + StakeMath { error StakingManager__InvalidVault(); error StakingManager__VaultNotRegistered(); @@ -26,7 +28,7 @@ contract RewardsStreamerMP is error StakingManager__AmountCannotBeZero(); error StakingManager__TransferFailed(); error StakingManager__InsufficientBalance(); - error StakingManager__InvalidLockingPeriod(); + error StakingManager__LockingPeriodCannotBeZero(); error StakingManager__CannotRestakeWithLockedFunds(); error StakingManager__TokensAreLocked(); error StakingManager__AlreadyLocked(); @@ -36,12 +38,6 @@ contract RewardsStreamerMP is IERC20 public STAKING_TOKEN; uint256 public constant SCALE_FACTOR = 1e18; - uint256 public constant MP_RATE_PER_YEAR = 1; - - uint256 public constant YEAR = 365 days; - uint256 public constant MIN_LOCKUP_PERIOD = 90 days; - uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR; - uint256 public constant MAX_MULTIPLIER = 4; uint256 public totalStaked; uint256 public totalMPAccrued; @@ -193,10 +189,6 @@ contract RewardsStreamerMP is revert StakingManager__AmountCannotBeZero(); } - if (lockPeriod != 0 && (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD)) { - revert StakingManager__InvalidLockingPeriod(); - } - _updateGlobalState(); _updateVaultMP(msg.sender, true); @@ -204,29 +196,23 @@ contract RewardsStreamerMP is if (vault.lockUntil != 0 && vault.lockUntil > block.timestamp) { revert StakingManager__CannotRestakeWithLockedFunds(); } + (uint256 _deltaMpTotal, uint256 _deltaMPMax, uint256 _newLockEnd) = + _calculateStake(vault.stakedBalance, vault.maxMP, vault.lockUntil, block.timestamp, amount, lockPeriod); vault.stakedBalance += amount; totalStaked += amount; - uint256 initialMP = amount; - uint256 potentialMP = amount * MAX_MULTIPLIER; - uint256 bonusMP = 0; - if (lockPeriod != 0) { - bonusMP = _calculateBonusMP(amount, lockPeriod); - vault.lockUntil = block.timestamp + lockPeriod; + vault.lockUntil = _newLockEnd; } else { vault.lockUntil = 0; } - uint256 vaultMaxMP = initialMP + bonusMP + potentialMP; - uint256 vaultMP = initialMP + bonusMP; + vault.mpAccrued += _deltaMpTotal; + totalMPAccrued += _deltaMpTotal; - vault.mpAccrued += vaultMP; - totalMPAccrued += vaultMP; - - vault.maxMP += vaultMaxMP; - totalMaxMP += vaultMaxMP; + vault.maxMP += _deltaMPMax; + totalMaxMP += _deltaMPMax; vault.rewardIndex = rewardIndex; } @@ -238,33 +224,29 @@ contract RewardsStreamerMP is onlyRegisteredVault nonReentrant { - if (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD) { - revert StakingManager__InvalidLockingPeriod(); - } - VaultData storage vault = vaultData[msg.sender]; if (vault.lockUntil > 0) { revert StakingManager__AlreadyLocked(); } - if (vault.stakedBalance == 0) { - revert StakingManager__InsufficientBalance(); + if (lockPeriod == 0) { + revert StakingManager__LockingPeriodCannotBeZero(); } _updateGlobalState(); _updateVaultMP(msg.sender, true); + (uint256 deltaMp, uint256 newLockEnd) = + _calculateLock(vault.stakedBalance, vault.maxMP, vault.lockUntil, block.timestamp, lockPeriod); - uint256 additionalBonusMP = _calculateBonusMP(vault.stakedBalance, lockPeriod); - - // Update vault state - vault.lockUntil = block.timestamp + lockPeriod; - vault.mpAccrued += additionalBonusMP; - vault.maxMP += additionalBonusMP; + // Update account state + vault.lockUntil = newLockEnd; + vault.mpAccrued += deltaMp; + vault.maxMP += deltaMp; // Update global state - totalMPAccrued += additionalBonusMP; - totalMaxMP += additionalBonusMP; + totalMPAccrued += deltaMp; + totalMaxMP += deltaMp; vault.rewardIndex = rewardIndex; } @@ -277,13 +259,6 @@ contract RewardsStreamerMP is nonReentrant { VaultData storage vault = vaultData[msg.sender]; - if (amount > vault.stakedBalance) { - revert StakingManager__InsufficientBalance(); - } - - if (block.timestamp < vault.lockUntil) { - revert StakingManager__TokensAreLocked(); - } _unstake(amount, vault, msg.sender); } @@ -291,18 +266,15 @@ contract RewardsStreamerMP is _updateGlobalState(); _updateVaultMP(vaultAddress, true); - uint256 previousStakedBalance = vault.stakedBalance; - - // solhint-disable-next-line - uint256 mpToReduce = Math.mulDiv(vault.mpAccrued, amount, previousStakedBalance); - uint256 maxMPToReduce = Math.mulDiv(vault.maxMP, amount, previousStakedBalance); - + (uint256 _deltaMpTotal, uint256 _deltaMpMax) = _calculateUnstake( + vault.stakedBalance, vault.lockUntil, block.timestamp, vault.mpAccrued, vault.maxMP, amount + ); vault.stakedBalance -= amount; - vault.mpAccrued -= mpToReduce; - vault.maxMP -= maxMPToReduce; + vault.mpAccrued -= _deltaMpTotal; + vault.maxMP -= _deltaMpMax; vault.rewardIndex = rewardIndex; - totalMPAccrued -= mpToReduce; - totalMaxMP -= maxMPToReduce; + totalMPAccrued -= _deltaMpTotal; + totalMaxMP -= _deltaMpMax; totalStaked -= amount; } @@ -315,6 +287,8 @@ contract RewardsStreamerMP is VaultData storage vault = vaultData[msg.sender]; if (vault.stakedBalance > 0) { + //updates lockuntil to allow unstake early + vault.lockUntil = block.timestamp; // calling `_unstake` to update accounting accordingly _unstake(vault.stakedBalance, vault, msg.sender); @@ -358,7 +332,7 @@ contract RewardsStreamerMP is return (adjustedRewardIndex, totalMPAccrued); } - uint256 accruedMP = (timeDiff * totalStaked * MP_RATE_PER_YEAR) / YEAR; + uint256 accruedMP = _accrueMP(totalStaked, timeDiff); if (totalMPAccrued + accruedMP > totalMaxMP) { accruedMP = totalMaxMP - totalMPAccrued; } @@ -465,26 +439,19 @@ contract RewardsStreamerMP is return (accruedRewards, newRewardIndex); } - function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal pure returns (uint256) { - return Math.mulDiv(amount, lockPeriod, YEAR); - } - function _getVaultPendingMP(VaultData storage vault) internal view returns (uint256) { + if (block.timestamp == vault.lastMPUpdateTime) { + return 0; + } if (vault.maxMP == 0 || vault.stakedBalance == 0) { return 0; } - uint256 timeDiff = block.timestamp - vault.lastMPUpdateTime; - if (timeDiff == 0) { - return 0; - } + uint256 deltaMpTotal = _calculateAccrual( + vault.stakedBalance, vault.mpAccrued, vault.maxMP, vault.lastMPUpdateTime, block.timestamp + ); - uint256 accruedMP = Math.mulDiv(timeDiff * vault.stakedBalance, MP_RATE_PER_YEAR, YEAR); - - if (vault.mpAccrued + accruedMP > vault.maxMP) { - accruedMP = vault.maxMP - vault.mpAccrued; - } - return accruedMP; + return deltaMpTotal; } function _updateVaultMP(address vaultAddress, bool forceMPUpdate) internal { diff --git a/src/interfaces/IStakeConstants.sol b/src/interfaces/IStakeConstants.sol new file mode 100644 index 0000000..7c46c1f --- /dev/null +++ b/src/interfaces/IStakeConstants.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; + +/** + * @title IStakeConstants + * @author Ricardo Guilherme Schmidt + * @notice Interface for Stake Constants + * @dev This interface is necessary to linearize the inheritance of StakeMath and MultiplierPointMath + */ +interface IStakeConstants { + function MIN_LOCKUP_PERIOD() external view returns (uint256); + function MAX_LOCKUP_PERIOD() external view returns (uint256); + function MP_APY() external view returns (uint256); + function MAX_MULTIPLIER() external view returns (uint256); +} diff --git a/src/interfaces/IStakeManager.sol b/src/interfaces/IStakeManager.sol index 98c5629..c3e21fb 100644 --- a/src/interfaces/IStakeManager.sol +++ b/src/interfaces/IStakeManager.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.26; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; +import { IStakeConstants } from "./IStakeConstants.sol"; -interface IStakeManager is ITrustedCodehashAccess { +interface IStakeManager is ITrustedCodehashAccess, IStakeConstants { error StakingManager__FundsLocked(); error StakingManager__InvalidLockTime(); error StakingManager__InsufficientFunds(); @@ -24,8 +25,4 @@ interface IStakeManager is ITrustedCodehashAccess { function getStakedBalance(address _vault) external view returns (uint256 _balance); function STAKING_TOKEN() external view returns (IERC20); - function MIN_LOCKUP_PERIOD() external view returns (uint256); - function MAX_LOCKUP_PERIOD() external view returns (uint256); - function MP_RATE_PER_YEAR() external view returns (uint256); - function MAX_MULTIPLIER() external view returns (uint256); } diff --git a/src/math/MultiplierPointMath.sol b/src/math/MultiplierPointMath.sol new file mode 100644 index 0000000..d83846f --- /dev/null +++ b/src/math/MultiplierPointMath.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.26; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IStakeConstants } from "../interfaces/IStakeConstants.sol"; + +/** + * @title MultiplierPointMath + * @author Ricardo Guilherme Schmidt + * @notice Provides mathematical operations and utilities for managing multiplier points in the staking system. + */ +abstract contract MultiplierPointMath is IStakeConstants { + /// @notice One (mean) tropical year, in seconds. + uint256 public constant YEAR = 365 days; + /// @notice Accrued multiplier points maximum multiplier. + uint256 public constant MAX_MULTIPLIER = 4; + /// @notice Multiplier points annual percentage yield. + uint256 public constant MP_APY = 100; + /// @notice Multiplier points accrued maximum percentage yield. + uint256 public constant MP_MPY = MAX_MULTIPLIER * MP_APY; + /// @notice Multiplier points absolute maximum percentage yield. + uint256 public constant MP_MPY_ABSOLUTE = 100 + (2 * (MAX_MULTIPLIER * MP_APY)); + /// @notice The accrue rate period of time over which multiplier points are calculated. + uint256 public constant ACCRUE_RATE = 1 seconds; + /// @notice Minimal value to generate 1 multiplier point in the accrue rate period (rounded up). + uint256 public constant MIN_BALANCE = (((YEAR * 100) - 1) / (MP_APY * ACCRUE_RATE)) + 1; + /// @notice Maximum value to not overflow unsigned integer of 256 bits. + uint256 public constant MAX_BALANCE = type(uint256).max / (MP_APY * ACCRUE_RATE); + + /** + * @notice Calculates the accrued multiplier points (MPs) over a time period Δt, based on the account balance + * @param _balance Represents the current account balance + * @param _deltaTime The time difference or the duration over which the multiplier points are accrued, expressed in + * seconds + * @return accruedMP points accrued for given `_balance` and `_seconds` + */ + function _accrueMP(uint256 _balance, uint256 _deltaTime) internal pure returns (uint256 accruedMP) { + return Math.mulDiv(_balance, _deltaTime * MP_APY, YEAR * 100); + } + + /** + * @notice Calculates the bonus multiplier points (MPs) earned when a balance Δa is locked for a specified duration + * t_lock. + * It is equivalent to the accrued multiplier points function but specifically applied in the context of a locked + * balance. + * @param _balance quantity of tokens + * @param _lockedSeconds time in seconds locked + * @return bonusMP bonus multiplier points for given `_balance` and `_lockedSeconds` + */ + function _bonusMP(uint256 _balance, uint256 _lockedSeconds) internal pure returns (uint256 bonusMP) { + return _accrueMP(_balance, _lockedSeconds); + } + + /** + * @notice Calculates the initial multiplier points (MPs) based on the balance change Δa. The result is equal to + * the amount of balance added. + * @param _balance Represents the change in balance. + * @return initialMP Initial Multiplier Points + */ + function _initialMP(uint256 _balance) internal pure returns (uint256 initialMP) { + return _balance; + } + + /** + * @notice Calculates the reduction in multiplier points (MPs) when a portion of the balance Δa `_reducedAmount` is + * removed from the total balance a_bal `_balance`. + * The reduction is proportional to the ratio of the removed balance to the total balance, applied to the current + * multiplier points $mp$. + * @param _balance The total account balance before the removal of Δa `_reducedBalance` + * @param _mp Represents the current multiplier points + * @param _reducedAmount reduced balance + * @return reducedMP Multiplier points to reduce from `_mp` + */ + function _reduceMP( + uint256 _balance, + uint256 _mp, + uint256 _reducedAmount + ) + internal + pure + returns (uint256 reducedMP) + { + return Math.mulDiv(_mp, _reducedAmount, _balance); + } + + /** + * @notice Calculates maximum stake a given `_balance` can be generated with `MAX_MULTIPLIER` + * @param _balance quantity of tokens + * @return maxMPAccrued maximum quantity of muliplier points that can be generated for given `_balance` + */ + function _maxAccrueMP(uint256 _balance) internal pure returns (uint256 maxMPAccrued) { + return Math.mulDiv(_balance, MP_MPY, 100); + } + + /** + * @notice The maximum total multiplier points that can be generated for a determined amount of balance and lock + * duration. + * @param _balance Represents the current account balance + * @param _lockTime The time duration for which the balance is locked + * @return maxMP Maximum Multiplier Points that can be generated for given `_balance` and `_lockTime` + */ + function _maxTotalMP(uint256 _balance, uint256 _lockTime) internal pure returns (uint256 maxMP) { + return _balance + Math.mulDiv(_balance * MP_APY, (MAX_MULTIPLIER * YEAR) + _lockTime, YEAR * 100); + } + + /** + * @notice The absolute maximum total multiplier points that some balance could have, which is the sum of the + * maximum + * lockup time bonus possible and the maximum accrued multiplier points. + * @param _balance quantity of tokens + * @return maxMPAbsolute Absolute Maximum Multiplier Points + */ + function _maxAbsoluteTotalMP(uint256 _balance) internal pure returns (uint256 maxMPAbsolute) { + return Math.mulDiv(_balance, MP_MPY_ABSOLUTE, 100); + } + + /** + * @dev Caution: This value is estimated and can be incorrect due precision loss. + * @notice Calculates the remaining lock time available for a given `_mpMax` and `_balance` + * @param _balance Current balance used to calculate the maximum multiplier points. + * @param _mpMax Maximum multiplier points calculated from the current balance. + * @return lockTime Amount of lock time allowed to be increased + */ + function _lockTimeAvailable(uint256 _balance, uint256 _mpMax) internal pure returns (uint256 lockTime) { + return Math.mulDiv((_balance * MP_MPY_ABSOLUTE) - _mpMax, YEAR, _balance * 100); + } + + /** + * @notice Calculates the time required to accrue a specific multiplier point value. + * @param _balance The current balance. + * @param _mp The target multiplier points to accrue. + * @return timeToReachMaxMP The time required to reach the specified multiplier points, in seconds. + */ + function _timeToAccrueMP(uint256 _balance, uint256 _mp) internal pure returns (uint256 timeToReachMaxMP) { + return Math.mulDiv(_mp * 100, YEAR, _balance * MP_APY); + } + + /** + * @notice Calculates the bonus multiplier points based on the balance and maximum multiplier points. + * @param _balance The current balance. + * @param _maxMP The maximum multiplier points. + * @return bonusMP The calculated bonus multiplier points. + */ + function _retrieveBonusMP(uint256 _balance, uint256 _maxMP) internal pure returns (uint256 bonusMP) { + return _maxMP - (_balance + _maxAccrueMP(_balance)); + } + + /** + * @notice Retrieves the accrued multiplier points based on the total and maximum multiplier points. + * @param _balance The current balance. + * @param _totalMP The total multiplier points. + * @param _maxMP The maximum multiplier points. + * @return accruedMP The calculated accrued multiplier points. + */ + function _retrieveAccruedMP( + uint256 _balance, + uint256 _totalMP, + uint256 _maxMP + ) + internal + pure + returns (uint256 accruedMP) + { + return _totalMP + _maxAccrueMP(_balance) - _maxMP; + } +} diff --git a/src/math/StakeMath.sol b/src/math/StakeMath.sol new file mode 100644 index 0000000..1babdf8 --- /dev/null +++ b/src/math/StakeMath.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.26; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { MultiplierPointMath } from "./MultiplierPointMath.sol"; + +/** + * @title StakeMath + * @author Ricardo Guilherme Schmidt + * @notice Provides mathematical operations and utilities for managing staking operations. + */ +abstract contract StakeMath is MultiplierPointMath { + error StakeMath__FundsLocked(); + error StakeMath__InvalidLockingPeriod(); + error StakeMath__StakeIsTooLow(); + error StakeMath__InsufficientBalance(); + error StakeMath__AccrueTimeNotReached(); + error StakeMath__AbsoluteMaxMPOverflow(); + + event StakeMathTest(uint256 lockTime); + /// @notice Minimal lockup time + + uint256 public constant MIN_LOCKUP_PERIOD = 90 days; + /// @notice Maximum lockup period + uint256 public constant MAX_LOCKUP_PERIOD = MAX_MULTIPLIER * YEAR; + + /** + * @notice Calculates the bonus multiplier points earned when a balance Δa is increased an optionally locked for a + * specified duration + * @param _balance Account current balance + * @param _currentMaxMP Account current max multiplier points + * @param _currentLockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _increasedAmount Increased amount of balance + * @param _increasedLockSeconds Increased amount of seconds to lock + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _deltaMpMax Increased amount of max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function _calculateStake( + uint256 _balance, + uint256 _currentMaxMP, + uint256 _currentLockEndTime, + uint256 _processTime, + uint256 _increasedAmount, + uint256 _increasedLockSeconds + ) + internal + pure + returns (uint256 _deltaMpTotal, uint256 _deltaMpMax, uint256 _newLockEnd) + { + uint256 newBalance = _balance + _increasedAmount; + _newLockEnd = Math.max(_currentLockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + if (dt_lock != 0 && (dt_lock < MIN_LOCKUP_PERIOD || dt_lock > MAX_LOCKUP_PERIOD)) { + revert StakeMath__InvalidLockingPeriod(); + } + + uint256 deltaMpBonus; + if (dt_lock > 0) { + deltaMpBonus = _bonusMP(_increasedAmount, dt_lock); + } + + if (_balance > 0 && _increasedLockSeconds > 0) { + deltaMpBonus += _bonusMP(_balance, _increasedLockSeconds); + } + + _deltaMpTotal = _initialMP(_increasedAmount) + deltaMpBonus; + _deltaMpMax = _deltaMpTotal + _accrueMP(_increasedAmount, MAX_MULTIPLIER * YEAR); + + if (_deltaMpMax + _currentMaxMP > MP_MPY_ABSOLUTE * newBalance) { + revert StakeMath__AbsoluteMaxMPOverflow(); + } + } + + /** + * @notice Calculates the bonus multiplier points earned when a balance Δa is locked for a specified duration + * @param _balance Account current balance + * @param _currentMaxMP Account current max multiplier points + * @param _currentLockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _increasedLockSeconds Increased amount of seconds to lock + * @return _deltaMp Increased amount of total and max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function _calculateLock( + uint256 _balance, + uint256 _currentMaxMP, + uint256 _currentLockEndTime, + uint256 _processTime, + uint256 _increasedLockSeconds + ) + internal + pure + returns (uint256 _deltaMp, uint256 _newLockEnd) + { + if (_balance == 0) { + revert StakeMath__InsufficientBalance(); + } + + _newLockEnd = Math.max(_currentLockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + if (dt_lock != 0 && (dt_lock < MIN_LOCKUP_PERIOD || dt_lock > MAX_LOCKUP_PERIOD)) { + revert StakeMath__InvalidLockingPeriod(); + } + + _deltaMp = _bonusMP(_balance, _increasedLockSeconds); + + if (_deltaMp + _currentMaxMP > MP_MPY_ABSOLUTE * _balance) { + revert StakeMath__AbsoluteMaxMPOverflow(); + } + } + + /** + * + * @param _balance Account current balance + * @param _currentLockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _currentTotalMP Account current total multiplier points + * @param _currentMaxMP Account current max multiplier points + * @param _reducedAmount Reduced amount of balance + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _deltaMpMax Increased amount of max multiplier points + */ + function _calculateUnstake( + uint256 _balance, + uint256 _currentLockEndTime, + uint256 _processTime, + uint256 _currentTotalMP, + uint256 _currentMaxMP, + uint256 _reducedAmount + ) + internal + pure + returns (uint256 _deltaMpTotal, uint256 _deltaMpMax) + { + if (_reducedAmount > _balance) { + revert StakeMath__InsufficientBalance(); + } + if (_currentLockEndTime > _processTime) { + revert StakeMath__FundsLocked(); + } + _deltaMpTotal = _reduceMP(_balance, _currentTotalMP, _reducedAmount); + _deltaMpMax = _reduceMP(_balance, _currentMaxMP, _reducedAmount); + } + + /** + * @notice Calculates the accrued multiplier points for a given balance and seconds passed since last accrual + * @param _balance Account current balance + * @param _currentTotalMP Account current total multiplier points + * @param _currentMaxMP Account current max multiplier points + * @param _lastAccrualTime Account current last accrual timestamp + * @param _processTime Process current timestamp + * @return _deltaMpTotal Increased amount of total multiplier points + */ + function _calculateAccrual( + uint256 _balance, + uint256 _currentTotalMP, + uint256 _currentMaxMP, + uint256 _lastAccrualTime, + uint256 _processTime + ) + internal + pure + returns (uint256 _deltaMpTotal) + { + uint256 dt = _processTime - _lastAccrualTime; + if (_currentTotalMP < _currentMaxMP) { + _deltaMpTotal = Math.min(_accrueMP(_balance, dt), _currentMaxMP - _currentTotalMP); + } + } + + /** + * @dev Caution: This value is estimated and can be incorrect due precision loss. + * @notice Estimates the time an account set as locked time. + * @param _mpMax Maximum multiplier points calculated from the current balance. + * @param _balance Current balance used to calculate the maximum multiplier points. + */ + function _estimateLockTime(uint256 _mpMax, uint256 _balance) internal pure returns (uint256 _lockTime) { + return Math.mulDiv((_mpMax - _balance) * 100, YEAR, _balance * MP_APY, Math.Rounding.Ceil) - MAX_LOCKUP_PERIOD; + } +} diff --git a/test/RewardsStreamerMP.t.sol b/test/RewardsStreamerMP.t.sol index c14757a..b17ac94 100644 --- a/test/RewardsStreamerMP.t.sol +++ b/test/RewardsStreamerMP.t.sol @@ -9,13 +9,14 @@ import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol"; +import { StakeMath } from "../src/math/StakeMath.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"; import { StackOverflowStakeManager } from "./mocks/StackOverflowStakeManager.sol"; -contract RewardsStreamerMPTest is Test { +contract RewardsStreamerMPTest is StakeMath, Test { MockToken stakingToken; RewardsStreamerMP public streamer; @@ -133,47 +134,12 @@ contract RewardsStreamerMPTest is Test { vault.leave(account); } - function _calculeInitialMP(uint256 amount) internal pure returns (uint256) { - return amount; - } - - function _calculateMaxAccruedMP(uint256 amount) internal view returns (uint256) { - return amount * streamer.MAX_MULTIPLIER(); - } - - function _calculateAbsoluteMaxTotalMP(uint256 amount) internal view returns (uint256) { - return _calculeInitialMP(amount) + _calculateBonusMP(amount, streamer.MAX_LOCKUP_PERIOD()) - + _calculateMaxAccruedMP(amount); - } - - function _calculateMaxTotalMP(uint256 amount, uint256 lockPeriod) internal view returns (uint256 maxTotalMaxMP) { - uint256 bonusMP = 0; - if (lockPeriod != 0) { - bonusMP = _calculateBonusMP(amount, lockPeriod); - } - return _calculeInitialMP(amount) + bonusMP + _calculateMaxAccruedMP(amount); - } - - function _calculateBonusMP(uint256 amount, uint256 lockupTime) internal view returns (uint256) { - // solhint-disable-next-line - return Math.mulDiv(amount, lockupTime, 365 days); - } - - function _calculateAccuredMP(uint256 totalStaked, uint256 timeDiff) internal view returns (uint256) { - return Math.mulDiv(timeDiff * totalStaked, streamer.MP_RATE_PER_YEAR(), 365 days); - } - - function _calculateTimeToAccureMPLimit(uint256 amount) internal view returns (uint256) { + function _timeToAccrueMPLimit(uint256 amount) internal view returns (uint256) { uint256 maxMP = amount * streamer.MAX_MULTIPLIER(); - uint256 timeInSeconds = _calculateTimeToAccureMP(amount, maxMP); + uint256 timeInSeconds = _timeToAccrueMP(amount, maxMP); return timeInSeconds; } - function _calculateTimeToAccureMP(uint256 amount, uint256 target) internal view returns (uint256) { - uint256 mpPerYear = amount * streamer.MP_RATE_PER_YEAR(); - return target * 365 days / mpPerYear; - } - function _upgradeStakeManager() internal { UpgradeRewardsStreamerMPScript upgrade = new UpgradeRewardsStreamerMPScript(); upgrade.run(admin, IStakeManagerProxy(address(streamer))); @@ -181,53 +147,45 @@ contract RewardsStreamerMPTest is Test { } contract MathTest is RewardsStreamerMPTest { - function test_CalcInitialMP() public { - assertEq(_calculeInitialMP(1), 1, "wrong initial MP"); - assertEq(_calculeInitialMP(10e18), 10e18, "wrong initial MP"); - assertEq(_calculeInitialMP(20e18), 20e18, "wrong initial MP"); - assertEq(_calculeInitialMP(30e18), 30e18, "wrong initial MP"); + function test_CalcInitialMP() public pure { + assertEq(_initialMP(1), 1, "wrong initial MP"); + assertEq(_initialMP(10e18), 10e18, "wrong initial MP"); + assertEq(_initialMP(20e18), 20e18, "wrong initial MP"); + assertEq(_initialMP(30e18), 30e18, "wrong initial MP"); } - function test_CalcAccrueMP() public { - assertEq(_calculateAccuredMP(10e18, 0), 0, "wrong accrued MP"); - assertEq(_calculateAccuredMP(10e18, 365 days / 2), 5e18, "wrong accrued MP"); - assertEq(_calculateAccuredMP(10e18, 365 days), 10e18, "wrong accrued MP"); - assertEq(_calculateAccuredMP(10e18, 365 days * 2), 20e18, "wrong accrued MP"); - assertEq(_calculateAccuredMP(10e18, 365 days * 3), 30e18, "wrong accrued MP"); + function test_CalcAccrueMP() public pure { + assertEq(_accrueMP(10e18, 0), 0, "wrong accrued MP"); + assertEq(_accrueMP(10e18, 365 days / 2), 5e18, "wrong accrued MP"); + assertEq(_accrueMP(10e18, 365 days), 10e18, "wrong accrued MP"); + assertEq(_accrueMP(10e18, 365 days * 2), 20e18, "wrong accrued MP"); + assertEq(_accrueMP(10e18, 365 days * 3), 30e18, "wrong accrued MP"); } - function test_CalcBonusMP() public { - assertEq(_calculateBonusMP(10e18, 0), 0, "wrong bonus MP"); - assertEq(_calculateBonusMP(10e18, streamer.MIN_LOCKUP_PERIOD()), 2_465_753_424_657_534_246, "wrong bonus MP"); + function test_CalcBonusMP() public view { + assertEq(_bonusMP(10e18, 0), 0, "wrong bonus MP"); + assertEq(_bonusMP(10e18, streamer.MIN_LOCKUP_PERIOD()), 2_465_753_424_657_534_246, "wrong bonus MP"); + assertEq(_bonusMP(10e18, streamer.MIN_LOCKUP_PERIOD() + 13 days), 2_821_917_808_219_178_082, "wrong bonus MP"); + assertEq(_bonusMP(100e18, 0), 0, "wrong bonus MP"); + } + + function test_CalcMaxTotalMP() public view { + assertEq(_maxTotalMP(10e18, 0), 50e18, "wrong max total MP"); + assertEq(_maxTotalMP(10e18, streamer.MIN_LOCKUP_PERIOD()), 52_465_753_424_657_534_246, "wrong max total MP"); assertEq( - _calculateBonusMP(10e18, streamer.MIN_LOCKUP_PERIOD() + 13 days), - 2_821_917_808_219_178_082, - "wrong bonus MP" + _maxTotalMP(10e18, streamer.MIN_LOCKUP_PERIOD() + 13 days), 52_821_917_808_219_178_082, "wrong max total MP" ); - assertEq(_calculateBonusMP(100e18, 0), 0, "wrong bonus MP"); + assertEq(_maxTotalMP(100e18, 0), 500e18, "wrong max total MP"); } - function test_CalcMaxTotalMP() public { - assertEq(_calculateMaxTotalMP(10e18, 0), 50e18, "wrong max total MP"); - assertEq( - _calculateMaxTotalMP(10e18, streamer.MIN_LOCKUP_PERIOD()), 52_465_753_424_657_534_246, "wrong max total MP" - ); - assertEq( - _calculateMaxTotalMP(10e18, streamer.MIN_LOCKUP_PERIOD() + 13 days), - 52_821_917_808_219_178_082, - "wrong max total MP" - ); - assertEq(_calculateMaxTotalMP(100e18, 0), 500e18, "wrong max total MP"); + function test_CalcAbsoluteMaxTotalMP() public pure { + assertEq(_maxAbsoluteTotalMP(10e18), 90e18, "wrong absolute max total MP"); + assertEq(_maxAbsoluteTotalMP(100e18), 900e18, "wrong absolute max total MP"); } - function test_CalcAbsoluteMaxTotalMP() public { - assertEq(_calculateAbsoluteMaxTotalMP(10e18), 90e18, "wrong absolute max total MP"); - assertEq(_calculateAbsoluteMaxTotalMP(100e18), 900e18, "wrong absolute max total MP"); - } - - function test_CalcMaxAccruedMP() public { - assertEq(_calculateMaxAccruedMP(10e18), 40e18, "wrong max accrued MP"); - assertEq(_calculateMaxAccruedMP(100e18), 400e18, "wrong max accrued MP"); + function test_CalcMaxAccruedMP() public pure { + assertEq(_maxAccrueMP(10e18), 40e18, "wrong max accrued MP"); + assertEq(_maxAccrueMP(100e18), 400e18, "wrong max accrued MP"); } } @@ -372,7 +330,7 @@ contract IntegrationTest is RewardsStreamerMPTest { // T4 uint256 currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days / 2)); + vm.warp(currentTime + (YEAR / 2)); streamer.updateGlobalState(); checkStreamer( @@ -660,10 +618,10 @@ contract StakeTest is RewardsStreamerMPTest { function test_StakeOneAccountWithMinLockUp() public { uint256 stakeAmount = 10e18; uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD(); - uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod); + uint256 expectedBonusMP = _bonusMP(stakeAmount, lockUpPeriod); _stake(alice, stakeAmount, lockUpPeriod); - uint256 expectedMaxTotalMP = _calculateMaxTotalMP(stakeAmount, lockUpPeriod); + uint256 expectedMaxTotalMP = _maxTotalMP(stakeAmount, lockUpPeriod); checkStreamer( CheckStreamerParams({ @@ -681,7 +639,7 @@ contract StakeTest is RewardsStreamerMPTest { function test_StakeOneAccountWithMaxLockUp() public { uint256 stakeAmount = 10e18; uint256 lockUpPeriod = streamer.MAX_LOCKUP_PERIOD(); - uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod); + uint256 expectedBonusMP = _bonusMP(stakeAmount, lockUpPeriod); _stake(alice, stakeAmount, lockUpPeriod); @@ -701,10 +659,10 @@ contract StakeTest is RewardsStreamerMPTest { function test_StakeOneAccountWithRandomLockUp() public { uint256 stakeAmount = 10e18; uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD() + 13 days; - uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod); + uint256 expectedBonusMP = _bonusMP(stakeAmount, lockUpPeriod); _stake(alice, stakeAmount, lockUpPeriod); - uint256 expectedMaxTotalMP = _calculateMaxTotalMP(stakeAmount, lockUpPeriod); + uint256 expectedMaxTotalMP = _maxTotalMP(stakeAmount, lockUpPeriod); checkStreamer( CheckStreamerParams({ @@ -738,7 +696,7 @@ contract StakeTest is RewardsStreamerMPTest { ); uint256 currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days)); + vm.warp(currentTime + (YEAR)); streamer.updateGlobalState(); streamer.updateVaultMP(vaults[alice]); @@ -770,7 +728,7 @@ contract StakeTest is RewardsStreamerMPTest { ); currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days / 2)); + vm.warp(currentTime + (YEAR / 2)); streamer.updateGlobalState(); streamer.updateVaultMP(vaults[alice]); @@ -833,7 +791,7 @@ contract StakeTest is RewardsStreamerMPTest { ); uint256 currentTime = vm.getBlockTimestamp(); - uint256 timeToMaxMP = _calculateTimeToAccureMP(stakeAmount, totalMaxMP - totalMPAccrued); + uint256 timeToMaxMP = _timeToAccrueMP(stakeAmount, totalMaxMP - totalMPAccrued); vm.warp(currentTime + timeToMaxMP); streamer.updateGlobalState(); @@ -982,11 +940,11 @@ contract StakeTest is RewardsStreamerMPTest { function test_StakeMultipleAccountsWithMinLockUp() public { uint256 aliceStakeAmount = 10e18; uint256 aliceLockUpPeriod = streamer.MIN_LOCKUP_PERIOD(); - uint256 aliceExpectedBonusMP = _calculateBonusMP(aliceStakeAmount, aliceLockUpPeriod); + uint256 aliceExpectedBonusMP = _bonusMP(aliceStakeAmount, aliceLockUpPeriod); uint256 bobStakeAmount = 30e18; uint256 bobLockUpPeriod = 0; - uint256 bobExpectedBonusMP = _calculateBonusMP(bobStakeAmount, bobLockUpPeriod); + uint256 bobExpectedBonusMP = _bonusMP(bobStakeAmount, bobLockUpPeriod); // alice stakes with lockup period _stake(alice, aliceStakeAmount, aliceLockUpPeriod); @@ -996,8 +954,8 @@ contract StakeTest is RewardsStreamerMPTest { uint256 sumOfStakeAmount = aliceStakeAmount + bobStakeAmount; uint256 sumOfExpectedBonusMP = aliceExpectedBonusMP + bobExpectedBonusMP; - uint256 expectedMaxTotalMP = _calculateMaxTotalMP(aliceStakeAmount, aliceLockUpPeriod) - + _calculateMaxTotalMP(bobStakeAmount, bobLockUpPeriod); + uint256 expectedMaxTotalMP = + _maxTotalMP(aliceStakeAmount, aliceLockUpPeriod) + _maxTotalMP(bobStakeAmount, bobLockUpPeriod); checkStreamer( CheckStreamerParams({ totalStaked: sumOfStakeAmount, @@ -1013,11 +971,11 @@ contract StakeTest is RewardsStreamerMPTest { function test_StakeMultipleAccountsWithRandomLockUp() public { uint256 aliceStakeAmount = 10e18; uint256 aliceLockUpPeriod = streamer.MAX_LOCKUP_PERIOD() - 21 days; - uint256 aliceExpectedBonusMP = _calculateBonusMP(aliceStakeAmount, aliceLockUpPeriod); + uint256 aliceExpectedBonusMP = _bonusMP(aliceStakeAmount, aliceLockUpPeriod); uint256 bobStakeAmount = 30e18; uint256 bobLockUpPeriod = streamer.MIN_LOCKUP_PERIOD() + 43 days; - uint256 bobExpectedBonusMP = _calculateBonusMP(bobStakeAmount, bobLockUpPeriod); + uint256 bobExpectedBonusMP = _bonusMP(bobStakeAmount, bobLockUpPeriod); // alice stakes with lockup period _stake(alice, aliceStakeAmount, aliceLockUpPeriod); @@ -1027,8 +985,8 @@ contract StakeTest is RewardsStreamerMPTest { uint256 sumOfStakeAmount = aliceStakeAmount + bobStakeAmount; uint256 sumOfExpectedBonusMP = aliceExpectedBonusMP + bobExpectedBonusMP; - uint256 expectedMaxTotalMP = _calculateMaxTotalMP(aliceStakeAmount, aliceLockUpPeriod) - + _calculateMaxTotalMP(bobStakeAmount, bobLockUpPeriod); + uint256 expectedMaxTotalMP = + _maxTotalMP(aliceStakeAmount, aliceLockUpPeriod) + _maxTotalMP(bobStakeAmount, bobLockUpPeriod); checkStreamer( CheckStreamerParams({ @@ -1093,7 +1051,7 @@ contract StakeTest is RewardsStreamerMPTest { ); uint256 currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days)); + vm.warp(currentTime + (YEAR)); streamer.updateGlobalState(); streamer.updateVaultMP(vaults[alice]); @@ -1142,7 +1100,7 @@ contract StakeTest is RewardsStreamerMPTest { ); currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days / 2)); + vm.warp(currentTime + (YEAR / 2)); streamer.updateGlobalState(); streamer.updateVaultMP(vaults[alice]); @@ -1244,7 +1202,7 @@ contract UnstakeTest is StakeTest { // wait for 1 year uint256 currentTime = vm.getBlockTimestamp(); - vm.warp(currentTime + (365 days)); + vm.warp(currentTime + (YEAR)); streamer.updateGlobalState(); streamer.updateVaultMP(vaults[alice]); @@ -1281,7 +1239,7 @@ contract UnstakeTest is StakeTest { uint256 stakeAmount = 10e18; uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD(); // 10e18 is what's used in `test_StakeOneAccountWithMinLockUp` - uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod); + uint256 expectedBonusMP = _bonusMP(stakeAmount, lockUpPeriod); uint256 unstakeAmount = 5e18; uint256 warpLength = (365 days); // wait for 1 year @@ -1297,7 +1255,7 @@ contract UnstakeTest is StakeTest { totalStaked: stakeAmount, totalMPAccrued: (stakeAmount + expectedBonusMP) + stakeAmount, // we do `+ stakeAmount` we've accrued // `stakeAmount` after 1 year - totalMaxMP: _calculateMaxTotalMP(stakeAmount, lockUpPeriod), + totalMaxMP: _maxTotalMP(stakeAmount, lockUpPeriod), stakingBalance: 10e18, rewardBalance: 0, rewardIndex: 0 @@ -1307,13 +1265,13 @@ contract UnstakeTest is StakeTest { // unstake half of the tokens _unstake(alice, unstakeAmount); - uint256 expectedTotalMP = _calculeInitialMP(newBalance) + _calculateBonusMP(newBalance, lockUpPeriod) - + _calculateAccuredMP(newBalance, warpLength); + uint256 expectedTotalMP = + _initialMP(newBalance) + _bonusMP(newBalance, lockUpPeriod) + _accrueMP(newBalance, warpLength); checkStreamer( CheckStreamerParams({ totalStaked: newBalance, totalMPAccrued: expectedTotalMP, - totalMaxMP: _calculateMaxTotalMP(newBalance, lockUpPeriod), + totalMaxMP: _maxTotalMP(newBalance, lockUpPeriod), stakingBalance: newBalance, rewardBalance: 0, rewardIndex: 0 @@ -1355,7 +1313,7 @@ contract UnstakeTest is StakeTest { uint256 amountStaked = 10e18; uint256 secondsLocked = streamer.MIN_LOCKUP_PERIOD(); uint256 reducedStake = 5e18; - uint256 increasedTime = 365 days; + uint256 increasedTime = YEAR; //initialize memory placehodlders uint256[4] memory timestamp; @@ -1371,8 +1329,8 @@ contract UnstakeTest is StakeTest { { timestamp[stage] = block.timestamp; totalStaked[stage] = amountStaked; - predictedBonusMP[stage] = totalStaked[stage] + _calculateBonusMP(totalStaked[stage], secondsLocked); - predictedTotalMaxMP[stage] = _calculateMaxTotalMP(totalStaked[stage], secondsLocked); + predictedBonusMP[stage] = totalStaked[stage] + _bonusMP(totalStaked[stage], secondsLocked); + predictedTotalMaxMP[stage] = _maxTotalMP(totalStaked[stage], secondsLocked); increasedAccuredMP[stage] = 0; //no increased accured MP in first stage predictedAccuredMP[stage] = 0; //no accured MP in first stage predictedTotalMP[stage] = predictedBonusMP[stage] + predictedAccuredMP[stage]; @@ -1384,7 +1342,7 @@ contract UnstakeTest is StakeTest { predictedBonusMP[stage] = predictedBonusMP[stage - 1]; //no change in bonusMP in second stage predictedTotalMaxMP[stage] = predictedTotalMaxMP[stage - 1]; // solhint-disable-next-line max-line-length - increasedAccuredMP[stage] = _calculateAccuredMP(totalStaked[stage], timestamp[stage] - timestamp[stage - 1]); + increasedAccuredMP[stage] = _accrueMP(totalStaked[stage], timestamp[stage] - timestamp[stage - 1]); predictedAccuredMP[stage] = predictedAccuredMP[stage - 1] + increasedAccuredMP[stage]; predictedTotalMP[stage] = predictedBonusMP[stage] + predictedAccuredMP[stage]; } @@ -1604,8 +1562,8 @@ contract LockTest is RewardsStreamerMPTest { ); // Lock for 1 year - uint256 lockPeriod = 365 days; - uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockPeriod); + uint256 lockPeriod = YEAR; + uint256 expectedBonusMP = _bonusMP(stakeAmount, lockPeriod); _lock(alice, lockPeriod); @@ -1624,17 +1582,28 @@ contract LockTest is RewardsStreamerMPTest { } function test_LockFailsWithNoStake() public { - vm.expectRevert(RewardsStreamerMP.StakingManager__InsufficientBalance.selector); - _lock(alice, 365 days); + vm.expectRevert(StakeMath.StakeMath__InsufficientBalance.selector); + _lock(alice, YEAR); } - function test_LockFailsWithInvalidPeriod() public { + function test_LockFailsWithZero() public { _stake(alice, 10e18, 0); // Test with period = 0 - vm.expectRevert(RewardsStreamerMP.StakingManager__InvalidLockingPeriod.selector); + vm.expectRevert(RewardsStreamerMP.StakingManager__LockingPeriodCannotBeZero.selector); _lock(alice, 0); } + + function test_LockFailsWithInvalidPeriod(uint256 _lockPeriod) public { + vm.assume(_lockPeriod > 0); + vm.assume(_lockPeriod < MIN_LOCKUP_PERIOD || _lockPeriod > MAX_LOCKUP_PERIOD); + vm.assume(_lockPeriod < (type(uint256).max - block.timestamp)); //prevents arithmetic overflow + + _stake(alice, 10e18, 0); + + vm.expectRevert(StakeMath.StakeMath__InvalidLockingPeriod.selector); + _lock(alice, _lockPeriod); + } } contract EmergencyExitTest is RewardsStreamerMPTest { diff --git a/test/mocks/StackOverflowStakeManager.sol b/test/mocks/StackOverflowStakeManager.sol index 410476d..70b1464 100644 --- a/test/mocks/StackOverflowStakeManager.sol +++ b/test/mocks/StackOverflowStakeManager.sol @@ -6,6 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { TrustedCodehashAccess } from "./../../src/TrustedCodehashAccess.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { IStakeConstants } from "./../../src/interfaces/IStakeConstants.sol"; contract StackOverflowStakeManager is UUPSUpgradeable, @@ -16,7 +17,7 @@ contract StackOverflowStakeManager is IERC20 public STAKING_TOKEN; uint256 public constant SCALE_FACTOR = 1e18; - uint256 public constant MP_RATE_PER_YEAR = 1e18; + uint256 public constant MP_APY = 100; uint256 public constant MIN_LOCKUP_PERIOD = 90 days; uint256 public constant MAX_LOCKUP_PERIOD = 4 * 365 days;