From 43b147dc09fe2a070f102ac1e2babf2814e16d04 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:50:13 -0300 Subject: [PATCH 01/13] fix(certora): exclude `stake()` from rule that checks account balance change The rule is failing since we've removed the `lockUntil > 0` check in `stake` and `processAccount` is no longer used in `stake()`. The rule requires `lockUntil > 0` so it will always fail there and can't find a non-reverting cases (which makes the rule pass). The reason it hasn't happened before was because: The rule required account.lockUntil > 0 Stake required lockUntil > 0 || account balance == 0 Also this commit adds an invariant: add invariant that account balance == 0 => accountMP == 0 --- certora/specs/StakeManager.spec | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index e99dc6f..075cd7c 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -94,8 +94,14 @@ invariant highEpochsAreNull(uint256 epochNumber) m -> !requiresPreviousManager(m) && !requiresNextManager(m) } -invariant accountBonusMPIsZeroIfBalanceIsZero(address addr) - to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountBonusMultiplierPoints(addr)) == 0 +invariant accountMPIsZeroIfBalanceIsZero(address addr) + to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountCurrentMultiplierPoints(addr)) == 0 + filtered { + f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector + } + +invariant InitialMPIsNeverSmallerThanBalance(address addr) + to_mathint(getAccountBonusMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) filtered { f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector } From a9fa5703d745ec946177a730e2c561703a96fa63 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:50:13 -0300 Subject: [PATCH 02/13] fix(specs): make `sumOfMultiplierPointsIsMultiplierpoints` work again This invariant failed as the prover started making wrong assumptions about the relationship between anyone's account's `totalMP` and its `balance`, as well as an account's `bonusMP` and its `balance`. This commit fixes it by adding the necessary invariants to proof the property. --- certora/specs/StakeManager.spec | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index 075cd7c..3bcb043 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -94,6 +94,12 @@ invariant highEpochsAreNull(uint256 epochNumber) m -> !requiresPreviousManager(m) && !requiresNextManager(m) } +invariant accountBonusMPIsZeroIfBalanceIsZero(address addr) + to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountBonusMultiplierPoints(addr)) == 0 + filtered { + f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector + } + invariant accountMPIsZeroIfBalanceIsZero(address addr) to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountCurrentMultiplierPoints(addr)) == 0 filtered { From 5d576825a3971c5730e6d56f744c89e3552fbd71 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Thu, 7 Nov 2024 10:50:13 -0300 Subject: [PATCH 03/13] refactor(StakeManager): extract interfaces and rename variables to merge functionalty with StakingRewardStreamer --- .gas-report | 83 ++-- .gas-snapshot | 90 ++-- certora/confs/StakeManager.conf | 2 +- certora/confs/StakeManagerProcess.conf | 2 +- certora/confs/StakeManagerStartMigration.conf | 2 +- certora/confs/StakeVault.conf | 2 +- certora/specs/StakeManager.spec | 11 +- certora/specs/StakeManagerProcessAccount.spec | 6 +- certora/specs/StakeManagerStartMigration.spec | 11 +- certora/specs/shared.spec | 3 +- contracts/IStakeManager.sol | 33 ++ contracts/MultiplierPointMath.sol | 51 +++ contracts/StakeManager.sol | 169 ++++---- contracts/StakeVault.sol | 29 +- contracts/VaultFactory.sol | 2 +- contracts/access/ITrustedCodehashAccess.sol | 29 ++ contracts/access/TrustedCodehashAccess.sol | 7 +- contracts/factory/SingletonFactory.sol | 59 +++ test/DynamicTest.t.sol | 390 ++++++++++++++++++ test/StakeManager.t.sol | 110 ++--- 20 files changed, 828 insertions(+), 263 deletions(-) create mode 100644 contracts/IStakeManager.sol create mode 100644 contracts/MultiplierPointMath.sol create mode 100644 contracts/access/ITrustedCodehashAccess.sol create mode 100644 contracts/factory/SingletonFactory.sol create mode 100644 test/DynamicTest.t.sol diff --git a/.gas-report b/.gas-report index cc34c99..2db355e 100644 --- a/.gas-report +++ b/.gas-report @@ -1,41 +1,42 @@ | contracts/StakeManager.sol:StakeManager contract | | | | | | |--------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2495816 | 13179 | | | | | +| 2512529 | 13257 | | | | | | Function Name | min | avg | median | max | # calls | -| EPOCH_SIZE | 263 | 263 | 263 | 263 | 1498 | -| MAX_BOOST | 264 | 264 | 264 | 264 | 637 | -| MAX_LOCKUP_PERIOD | 383 | 383 | 383 | 383 | 4 | +| EPOCH_SIZE | 285 | 285 | 285 | 285 | 1498 | +| MAX_LOCKUP_PERIOD | 361 | 361 | 361 | 361 | 4 | +| MAX_MULTIPLIER | 307 | 307 | 307 | 307 | 637 | | MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 12 | -| YEAR | 263 | 263 | 263 | 263 | 637 | -| accounts | 1616 | 1616 | 1616 | 1616 | 144285 | -| calculateMPToMint | 740 | 740 | 740 | 740 | 1276 | -| currentEpoch | 384 | 1050 | 384 | 2384 | 54 | -| epochEnd | 649 | 649 | 649 | 2649 | 23677 | +| YEAR | 307 | 307 | 307 | 307 | 637 | +| acceptUpdate | 23632 | 23632 | 23632 | 23632 | 1 | +| accounts | 1572 | 1572 | 1572 | 1572 | 144273 | +| calculateMP | 738 | 738 | 738 | 738 | 1276 | +| currentEpoch | 406 | 1072 | 406 | 2406 | 54 | +| epochEnd | 627 | 627 | 627 | 2627 | 23675 | | epochReward | 1381 | 2881 | 1381 | 5881 | 3 | -| executeAccount(address) | 149300 | 149300 | 149300 | 149300 | 2 | -| executeAccount(address,uint256) | 26562 | 72246 | 74122 | 200087 | 141872 | -| executeEpoch() | 23480 | 120708 | 121865 | 900380 | 23566 | -| executeEpoch(uint256) | 23861 | 24497 | 23861 | 26090 | 7 | -| expiredStakeStorage | 437 | 2346 | 2437 | 2437 | 22 | +| executeAccount(address) | 149349 | 149349 | 149349 | 149349 | 2 | +| executeAccount(address,uint256) | 26540 | 72264 | 74140 | 200389 | 141860 | +| executeEpoch() | 23458 | 120684 | 121843 | 900358 | 23564 | +| executeEpoch(uint256) | 23905 | 24541 | 23905 | 26134 | 7 | +| expiredStakeStorage | 394 | 2303 | 2394 | 2394 | 22 | | isTrustedCodehash | 541 | 949 | 541 | 2541 | 680 | -| lock | 23818 | 23818 | 23818 | 23818 | 1 | -| migrateTo | 23922 | 23928 | 23928 | 23934 | 2 | +| leave | 23631 | 23631 | 23631 | 23631 | 1 | +| lock | 23862 | 23862 | 23862 | 23862 | 1 | | migration | 417 | 1417 | 1417 | 2417 | 4 | | migrationInitialize | 24624 | 24624 | 24624 | 24624 | 1 | | newEpoch | 441 | 441 | 441 | 441 | 5 | -| owner | 2432 | 2432 | 2432 | 2432 | 13 | -| pendingMPToBeMinted | 363 | 363 | 363 | 363 | 46436 | +| owner | 2410 | 2410 | 2410 | 2410 | 13 | | pendingReward | 408 | 1442 | 2408 | 2408 | 29 | +| potentialMP | 408 | 408 | 408 | 408 | 46432 | | previousManager | 275 | 275 | 275 | 275 | 13 | +| rewardToken | 293 | 293 | 293 | 293 | 696 | | setTrustedCodehash | 47960 | 47960 | 47960 | 47960 | 139 | -| stake | 23983 | 23983 | 23983 | 23983 | 1 | -| stakedToken | 272 | 272 | 272 | 272 | 696 | -| startMigration | 103602 | 103610 | 103614 | 103614 | 3 | -| startTime | 306 | 306 | 306 | 306 | 21 | +| stake | 24005 | 24005 | 24005 | 24005 | 1 | +| startMigration | 103624 | 103632 | 103636 | 103636 | 3 | +| startTime | 264 | 264 | 264 | 264 | 21 | +| totalMP | 385 | 385 | 385 | 2385 | 46453 | +| totalStaked | 385 | 1785 | 2385 | 2385 | 20 | | totalSupply | 784 | 1965 | 2784 | 2784 | 22 | -| totalSupplyBalance | 407 | 1807 | 2407 | 2407 | 20 | -| totalSupplyMP | 384 | 384 | 384 | 2384 | 46457 | | unstake | 23841 | 23841 | 23841 | 23841 | 1 | @@ -44,13 +45,13 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| acceptMigration | 35311 | 35311 | 35311 | 35311 | 2 | -| leave | 35297 | 35297 | 35297 | 35297 | 1 | -| lock | 43285 | 90487 | 61938 | 180284 | 7 | -| owner | 362 | 362 | 362 | 362 | 679 | -| stake | 27265 | 282115 | 265681 | 351644 | 684 | -| stakedToken | 212 | 212 | 212 | 212 | 2 | -| unstake | 40180 | 96354 | 78700 | 229598 | 11 | +| acceptMigration | 35140 | 35140 | 35140 | 35140 | 2 | +| leave | 35152 | 35152 | 35152 | 35152 | 1 | +| lock | 43329 | 90544 | 61982 | 180383 | 7 | +| owner | 351 | 351 | 351 | 351 | 679 | +| stake | 27265 | 282111 | 265792 | 351743 | 684 | +| stakedToken | 215 | 215 | 215 | 215 | 2 | +| unstake | 40157 | 96345 | 78682 | 229644 | 11 | | contracts/VaultFactory.sol:VaultFactory contract | | | | | | @@ -58,7 +59,7 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| createVault | 696530 | 696530 | 696530 | 696530 | 683 | +| createVault | 682103 | 682103 | 682103 | 682103 | 683 | | setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | | stakeManager | 368 | 1868 | 2368 | 2368 | 4 | @@ -68,7 +69,7 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23727 | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23725 | | transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | @@ -77,24 +78,24 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46239 | 46199 | 46367 | 679 | -| balanceOf | 561 | 2107 | 2561 | 2561 | 30746 | +| approve | 46175 | 46241 | 46199 | 46367 | 679 | +| balanceOf | 561 | 2107 | 2561 | 2561 | 30744 | | script/Deploy.s.sol:Deploy contract | | | | | | |-------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6149062 | 29676 | | | | | +| 6149710 | 29676 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5343965 | 5343965 | 5343965 | 5343965 | 66 | +| run | 5343984 | 5343984 | 5343984 | 5343984 | 66 | | script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | |-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 3312594 | 16444 | | | | | +| 3329385 | 16522 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 2330294 | 2330294 | 2330294 | 2330294 | 19 | +| run | 2345854 | 2345854 | 2345854 | 2345854 | 19 | | script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | @@ -117,9 +118,9 @@ | test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | |------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 4833800 | 23474 | | | | | +| 4834448 | 23474 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 4183787 | 4183787 | 4183787 | 4183787 | 1 | +| run | 4183805 | 4183805 | 4183805 | 4183805 | 1 | diff --git a/.gas-snapshot b/.gas-snapshot index d90857a..cd6098a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ CreateVaultTest:testDeployment() (gas: 9774) -CreateVaultTest:test_createVault() (gas: 713999) +CreateVaultTest:test_createVault() (gas: 699564) ExecuteAccountTest:testDeployment() (gas: 28828) -ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1579141) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5295554) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1787120) -ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 321023887) +ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1564735) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5252076) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1772992) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 320738328) ExecuteEpochTest:testDeployment() (gas: 28829) ExecuteEpochTest:testNewDeployment() (gas: 30901) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1367865) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1385552) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1630963) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1395267) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1928353) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2511195) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1479072) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2520931) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1488764) -ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083687) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1353515) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1371040) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1616635) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1380975) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1914097) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2496675) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1464542) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2506631) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1474454) +ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083709) ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92344) -ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 256338) +ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 256303) ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 39028) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149748) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149770) LeaveTest:testDeployment() (gas: 28806) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1329731) -LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31995) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1315087) +LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31696) LockTest:testDeployment() (gas: 28806) -LockTest:test_NewLockupPeriod() (gas: 1328279) -LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1303028) -LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1543611) -LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31812) -LockTest:test_ShouldIncreaseBonusMP() (gas: 1310872) -LockTest:test_UpdateLockupPeriod() (gas: 1579753) +LockTest:test_NewLockupPeriod() (gas: 1313929) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1288722) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1529335) +LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31856) +LockTest:test_ShouldIncreaseBonusMP() (gas: 1296480) +LockTest:test_UpdateLockupPeriod() (gas: 1565467) MigrateTest:testDeployment() (gas: 28806) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1293753) -MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 32007) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1279254) +MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31738) MigrationInitializeTest:testDeployment() (gas: 28806) -MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5241726) +MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5275163) MigrationStakeManagerTest:testDeployment() (gas: 28806) MigrationStakeManagerTest:testNewDeployment() (gas: 30945) -MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149713) +MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149735) SetStakeManagerTest:testDeployment() (gas: 9774) SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 63105) SetStakeManagerTest:test_SetStakeManager() (gas: 41301) StakeManagerTest:testDeployment() (gas: 28578) StakeTest:testDeployment() (gas: 28784) -StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1078067) -StakeTest:test_RevertWhen_Restake() (gas: 1318488) -StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1322318) -StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32018) -StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 817324) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1061162) +StakeTest:test_RevertWhen_Restake() (gas: 1304182) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1308012) +StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32040) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 802919) StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 211363) -StakeTest:test_StakeWithLockBonusMP() (gas: 2356570) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1314730) -StakedTokenTest:testStakeToken() (gas: 7616) +StakeTest:test_StakeWithLockBonusMP() (gas: 2327824) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1300293) +StakedTokenTest:testStakeToken() (gas: 7619) UnstakeTest:testDeployment() (gas: 28828) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1299096) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1343662) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1284745) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1329288) UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31879) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6450026) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1321258) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1438734) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6438180) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1306895) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1424419) UserFlowsTest:testDeployment() (gas: 28806) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 130945931, ~: 130391929) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1479843) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2496411) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 130825949, ~: 130282915) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1463005) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2467653) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/certora/confs/StakeManager.conf b/certora/confs/StakeManager.conf index 9144da4..85d9f73 100644 --- a/certora/confs/StakeManager.conf +++ b/certora/confs/StakeManager.conf @@ -5,7 +5,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A", + "StakeManager:rewardToken=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager.sol", diff --git a/certora/confs/StakeManagerProcess.conf b/certora/confs/StakeManagerProcess.conf index 6b51c28..0c949c1 100644 --- a/certora/confs/StakeManagerProcess.conf +++ b/certora/confs/StakeManagerProcess.conf @@ -5,7 +5,7 @@ "certora/helpers/ExpiredStakeStorageA.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A", + "StakeManager:rewardToken=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager ProcessAccount", diff --git a/certora/confs/StakeManagerStartMigration.conf b/certora/confs/StakeManagerStartMigration.conf index 33a5e0e..12f382a 100644 --- a/certora/confs/StakeManagerStartMigration.conf +++ b/certora/confs/StakeManagerStartMigration.conf @@ -6,7 +6,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A", + "StakeManager:rewardToken=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", ], "msg": "Verifying StakeManager.sol", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index 2aa686c..3bf9483 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -7,7 +7,7 @@ ], "link" : [ "StakeVault:STAKED_TOKEN=ERC20A", - "StakeManager:stakedToken=ERC20A", + "StakeManager:rewardToken=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", "StakeVault:stakeManager=StakeManager" ], diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index 3bcb043..a3386e3 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -4,8 +4,8 @@ using ERC20A as staked; methods { function staked.balanceOf(address) external returns (uint256) envfree; - function totalSupplyBalance() external returns (uint256) envfree; - function totalSupplyMP() external returns (uint256) envfree; + function totalStaked() external returns (uint256) envfree; + function totalMP() external returns (uint256) envfree; function previousManager() external returns (address) envfree; function _.migrateFrom(address, bool, StakeManager.Account) external => NONDET; function _.increaseTotalMP(uint256) external => NONDET; @@ -22,7 +22,8 @@ function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { function isMigrationfunction(method f) returns bool { return - f.selector == sig:migrateTo(bool).selector || + f.selector == sig:acceptUpdate().selector || + f.selector == sig:leave().selector || f.selector == sig:transferNonPending().selector; } @@ -65,13 +66,13 @@ hook Sload uint256 newValue accounts[KEY address addr].totalMP { } invariant sumOfBalancesIsTotalSupplyBalance() - sumOfBalances == to_mathint(totalSupplyBalance()) + sumOfBalances == to_mathint(totalStaked()) filtered { m -> !requiresPreviousManager(m) && !requiresNextManager(m) } invariant sumOfMultipliersIsMultiplierSupply() - sumOfMultipliers == to_mathint(totalSupplyMP()) + sumOfMultipliers == to_mathint(totalMP()) filtered { m -> !requiresPreviousManager(m) && !requiresNextManager(m) } diff --git a/certora/specs/StakeManagerProcessAccount.spec b/certora/specs/StakeManagerProcessAccount.spec index d7d062c..5e20e67 100644 --- a/certora/specs/StakeManagerProcessAccount.spec +++ b/certora/specs/StakeManagerProcessAccount.spec @@ -4,14 +4,14 @@ using ERC20A as staked; methods { function staked.balanceOf(address) external returns (uint256) envfree; - function totalSupplyBalance() external returns (uint256) envfree; - function totalSupplyMP() external returns (uint256) envfree; + function totalStaked() external returns (uint256) envfree; + function totalMP() external returns (uint256) envfree; function totalMPPerEpoch() external returns (uint256) envfree; function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _processAccount(StakeManager.Account storage account, uint256 _limitEpoch) internal with(env e) => markAccountProccessed(e.msg.sender, _limitEpoch); function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => NONDET; - function pendingMPToBeMinted() external returns (uint256) envfree; + function potentialMP() external returns (uint256) envfree; } // keeps track of the last epoch an account was processed diff --git a/certora/specs/StakeManagerStartMigration.spec b/certora/specs/StakeManagerStartMigration.spec index a57b5d3..0ccb1b1 100644 --- a/certora/specs/StakeManagerStartMigration.spec +++ b/certora/specs/StakeManagerStartMigration.spec @@ -5,13 +5,13 @@ using StakeManagerNew as newStakeManager; methods { function staked.balanceOf(address) external returns (uint256) envfree; - function totalSupplyBalance() external returns (uint256) envfree; - function totalSupplyMP() external returns (uint256) envfree; + function totalStaked() external returns (uint256) envfree; + function totalMP() external returns (uint256) envfree; function previousManager() external returns (address) envfree; function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => DISPATCHER(true); - function StakeManagerNew.totalSupplyBalance() external returns (uint256) envfree; + function StakeManagerNew.totalStaked() external returns (uint256) envfree; } definition blockedWhenMigrating(method f) returns bool = ( @@ -25,7 +25,8 @@ definition blockedWhenMigrating(method f) returns bool = ( ); definition blockedWhenNotMigrating(method f) returns bool = ( - f.selector == sig:migrateTo(bool).selector || + f.selector == sig:acceptUpdate().selector || + f.selector == sig:leave().selector || f.selector == sig:transferNonPending().selector ); @@ -89,7 +90,7 @@ rule startMigrationCorrect { startMigration(e, newContract); assert currentContract.migration == newContract; - assert newStakeManager.totalSupplyBalance() == currentContract.totalSupplyBalance(); + assert newStakeManager.totalStaked() == currentContract.totalStaked(); } rule migrationLockedIn(method f) filtered { diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec index 053e710..884de62 100644 --- a/certora/specs/shared.spec +++ b/certora/specs/shared.spec @@ -7,7 +7,8 @@ definition requiresPreviousManager(method f) returns bool = ( ); definition requiresNextManager(method f) returns bool = ( - f.selector == sig:_stakeManager.migrateTo(bool).selector || + f.selector == sig:_stakeManager.acceptUpdate().selector || + f.selector == sig:_stakeManager.leave().selector || f.selector == sig:_stakeManager.transferNonPending().selector ); diff --git a/contracts/IStakeManager.sol b/contracts/IStakeManager.sol new file mode 100644 index 0000000..e2f9cf6 --- /dev/null +++ b/contracts/IStakeManager.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ITrustedCodehashAccess } from "./access/ITrustedCodehashAccess.sol"; + +interface IStakeManager is ITrustedCodehashAccess { + error StakeManager__FundsLocked(); + error StakeManager__InvalidLockTime(); + error StakeManager__InsufficientFunds(); + error StakeManager__StakeIsTooLow(); + + function MIN_LOCKUP_PERIOD() external pure returns (uint256); + function MAX_LOCKUP_PERIOD() external pure returns (uint256); + + function stake(uint256 _amount, uint256 _seconds) external; + function unstake(uint256 _amount) external; + function lock(uint256 _secondsIncrease) external; + + function acceptUpdate() external returns (IStakeManager _migrated); + function leave() external returns (bool _leaveAccepted); + + function totalStaked() external view returns (uint256 _totalStaked); + function getStakedBalance(address _vault) external view returns (uint256 _balance); + function potentialMP() external view returns (uint256 _potentialMP); + function totalMP() external view returns (uint256 _totalMP); + + function totalSupply() external view returns (uint256 _totalSupply); + function totalSupplyMinted() external view returns (uint256 _totalSupply); + function pendingReward() external view returns (uint256 _pendingReward); + + function calculateMP(uint256 _balance, uint256 _deltaTime) external pure returns (uint256); +} diff --git a/contracts/MultiplierPointMath.sol b/contracts/MultiplierPointMath.sol new file mode 100644 index 0000000..0907036 --- /dev/null +++ b/contracts/MultiplierPointMath.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.18; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +abstract contract MultiplierPointMath { + uint256 public constant YEAR = 365 days; + uint256 public constant MP_APY = 1; + uint256 public constant MAX_MULTIPLIER = 4; + + /** + * @notice Calculates multiplier points accurred for given `_amount` and `_seconds` time passed + * @param _amount quantity of tokens + * @param _seconds time in seconds + * @return _accuredMP points accured for given `_amount` and `_seconds` + */ + function _calculateAccuredMP(uint256 _amount, uint256 _seconds) internal pure returns (uint256 _accuredMP) { + return Math.mulDiv(_amount, _seconds, YEAR) * MP_APY; + } + + /** + * @notice Calculates bonus multiplier points for given `_amount` and `_lockedSeconds` + * @param _amount quantity of tokens + * @param _lockedSeconds time in seconds locked + * @return _bonusMP bonus multiplier points for given `_amount` and `_lockedSeconds` + */ + function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) internal pure returns (uint256 _bonusMP) { + _bonusMP = _amount; + if (_lockedSeconds > 0) { + _bonusMP += _calculateAccuredMP(_amount, _lockedSeconds); + } + } + + /** + * @notice Calculates minimum stake to genarate 1 multiplier points for given `_seconds` + * @param _seconds time in seconds + * @return _minimumStake minimum quantity of tokens + */ + function _calculateMinimumStake(uint256 _seconds) internal pure returns (uint256 _minimumStake) { + return YEAR / (_seconds * MP_APY); + } + + /** + * @notice Calculates maximum stake a given `_amount` can be generated with `MAX_MULTIPLIER` + * @param _amount quantity of tokens + * @return _maxMPAccured maximum quantity of muliplier points that can be generated for given `_amount` + */ + function _calculateMaxAccuredMP(uint256 _amount) internal pure returns (uint256 _maxMPAccured) { + return _calculateAccuredMP(_amount, MAX_MULTIPLIER * YEAR); + } +} diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index e184553..5179eae 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -2,16 +2,16 @@ pragma solidity ^0.8.18; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { TrustedCodehashAccess } from "./access/TrustedCodehashAccess.sol"; import { ExpiredStakeStorage } from "./storage/ExpiredStakeStorage.sol"; +import { IStakeManager } from "./IStakeManager.sol"; +import { MultiplierPointMath } from "./MultiplierPointMath.sol"; import { StakeVault } from "./StakeVault.sol"; -contract StakeManager is TrustedCodehashAccess { - error StakeManager__FundsLocked(); - error StakeManager__InvalidLockTime(); +contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAccess { error StakeManager__NoPendingMigration(); error StakeManager__PendingMigration(); error StakeManager__SenderIsNotPreviousStakeManager(); @@ -19,9 +19,7 @@ contract StakeManager is TrustedCodehashAccess { error StakeManager__AccountNotInitialized(); error StakeManager__InvalidMigration(); error StakeManager__AlreadyProcessedEpochs(); - error StakeManager__InsufficientFunds(); error StakeManager__AlreadyStaked(); - error StakeManager__StakeIsTooLow(); struct Account { address rewardAddress; @@ -37,15 +35,12 @@ contract StakeManager is TrustedCodehashAccess { struct Epoch { uint256 epochReward; uint256 totalSupply; - uint256 estimatedMP; + uint256 potentialMP; } uint256 public constant EPOCH_SIZE = 1 weeks; - uint256 public constant YEAR = 365 days; uint256 public constant MIN_LOCKUP_PERIOD = 2 weeks; uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR; // 4 years - uint256 public constant MP_APY = 1; - uint256 public constant MAX_BOOST = 4; mapping(address index => Account value) public accounts; mapping(uint256 index => Epoch value) public epochs; @@ -54,9 +49,9 @@ contract StakeManager is TrustedCodehashAccess { uint256 public pendingReward; uint256 public immutable startTime; - uint256 public pendingMPToBeMinted; - uint256 public totalSupplyMP; - uint256 public totalSupplyBalance; + uint256 public potentialMP; + uint256 public totalMP; + uint256 public totalStaked; uint256 public totalMPPerEpoch; ExpiredStakeStorage public expiredStakeStorage; @@ -65,7 +60,7 @@ contract StakeManager is TrustedCodehashAccess { StakeManager public migration; StakeManager public immutable previousManager; - ERC20 public immutable stakedToken; + IERC20 public immutable rewardToken; modifier onlyAccountInitialized(address account) { if (accounts[account].lockUntil == 0) { @@ -117,26 +112,26 @@ contract StakeManager is TrustedCodehashAccess { totalMPPerEpoch -= expiredMP; expiredStakeStorage.deleteExpiredMP(tempCurrentEpoch); } - uint256 epochEstimatedMP = totalMPPerEpoch; + uint256 epochPotentialMP = totalMPPerEpoch; if (tempCurrentEpoch == currentEpoch) { - epochEstimatedMP -= currentEpochTotalExpiredMP; + epochPotentialMP -= currentEpochTotalExpiredMP; currentEpochTotalExpiredMP = 0; thisEpoch.epochReward = epochReward(); pendingReward += thisEpoch.epochReward; } - pendingMPToBeMinted += epochEstimatedMP; - thisEpoch.estimatedMP = epochEstimatedMP; + potentialMP += epochPotentialMP; + thisEpoch.potentialMP = epochPotentialMP; thisEpoch.totalSupply = totalSupply(); tempCurrentEpoch++; } currentEpoch = tempCurrentEpoch; } - constructor(address _stakedToken, address _previousManager) { + constructor(address _REWARD_TOKEN, address _previousManager) { startTime = (_previousManager == address(0)) ? block.timestamp : StakeManager(_previousManager).startTime(); previousManager = StakeManager(_previousManager); - stakedToken = ERC20(_stakedToken); + rewardToken = IERC20(_REWARD_TOKEN); if (address(previousManager) != address(0)) { expiredStakeStorage = previousManager.expiredStakeStorage(); } else { @@ -147,36 +142,32 @@ contract StakeManager is TrustedCodehashAccess { /** * Increases balance of msg.sender; * @param _amount Amount of balance being staked. - * @param _secondsToLock Seconds of lockup time. 0 means no lockup. + * @param _seconds Seconds of lockup time. 0 means no lockup. * * @dev Reverts when resulting locked time is not in range of [MIN_LOCKUP_PERIOD, MAX_LOCKUP_PERIOD] * @dev Reverts when account has already staked funds. * @dev Reverts when amount staked results in less than 1 MP per epoch. */ - function stake(uint256 _amount, uint256 _secondsToLock) external onlyTrustedCodehash noPendingMigration { + function stake(uint256 _amount, uint256 _seconds) external onlyTrustedCodehash noPendingMigration { finalizeEpoch(newEpoch()); if (accounts[msg.sender].balance > 0) { revert StakeManager__AlreadyStaked(); } - if (_secondsToLock != 0 && (_secondsToLock < MIN_LOCKUP_PERIOD || _secondsToLock > MAX_LOCKUP_PERIOD)) { + if (_seconds != 0 && (_seconds < MIN_LOCKUP_PERIOD || _seconds > MAX_LOCKUP_PERIOD)) { revert StakeManager__InvalidLockTime(); } //mp estimation - uint256 mpPerEpoch = _getMPToMint(_amount, EPOCH_SIZE); + uint256 mpPerEpoch = _calculateAccuredMP(_amount, EPOCH_SIZE); if (mpPerEpoch < 1) { revert StakeManager__StakeIsTooLow(); } - uint256 currentEpochExpiredMP = mpPerEpoch - _getMPToMint(_amount, epochEnd() - block.timestamp); - uint256 maxMpToMint = _getMPToMint(_amount, MAX_BOOST * YEAR) + currentEpochExpiredMP; + uint256 currentEpochExpiredMP = mpPerEpoch - _calculateAccuredMP(_amount, epochEnd() - block.timestamp); + uint256 maxMpToMint = _calculateMaxAccuredMP(_amount) + currentEpochExpiredMP; uint256 epochAmountToReachMpLimit = (maxMpToMint) / mpPerEpoch; uint256 mpLimitEpoch = currentEpoch + epochAmountToReachMpLimit; uint256 lastEpochAmountToMint = ((mpPerEpoch * (epochAmountToReachMpLimit + 1)) - maxMpToMint); - uint256 bonusMP = _amount; - if (_secondsToLock > 0) { - //bonus for lock time - bonusMP += _getMPToMint(_amount, _secondsToLock); - } + uint256 bonusMP = _calculateBonusMP(_amount, _seconds); // account initialization accounts[msg.sender] = Account({ @@ -185,14 +176,14 @@ contract StakeManager is TrustedCodehashAccess { bonusMP: bonusMP, totalMP: bonusMP, lastMint: block.timestamp, - lockUntil: block.timestamp + _secondsToLock, + lockUntil: block.timestamp + _seconds, epoch: currentEpoch, mpLimitEpoch: mpLimitEpoch }); //update global storage - totalSupplyMP += bonusMP; - totalSupplyBalance += _amount; + totalMP += bonusMP; + totalStaked += _amount; currentEpochTotalExpiredMP += currentEpochExpiredMP; totalMPPerEpoch += mpPerEpoch; expiredStakeStorage.incrementExpiredMP(mpLimitEpoch, lastEpochAmountToMint); @@ -221,7 +212,7 @@ contract StakeManager is TrustedCodehashAccess { uint256 reducedMP = Math.mulDiv(_amount, account.totalMP, account.balance); uint256 reducedInitialMP = Math.mulDiv(_amount, account.bonusMP, account.balance); - uint256 mpPerEpoch = _getMPToMint(account.balance, EPOCH_SIZE); + uint256 mpPerEpoch = _calculateAccuredMP(account.balance, EPOCH_SIZE); expiredStakeStorage.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); if (account.mpLimitEpoch < currentEpoch) { totalMPPerEpoch -= mpPerEpoch; @@ -231,18 +222,18 @@ contract StakeManager is TrustedCodehashAccess { account.balance -= _amount; account.bonusMP -= reducedInitialMP; account.totalMP -= reducedMP; - totalSupplyBalance -= _amount; - totalSupplyMP -= reducedMP; + totalStaked -= _amount; + totalMP -= reducedMP; } /** * @notice Locks entire balance for more amount of time. - * @param _secondsToIncreaseLock Seconds to increase in locked time. If stake is unlocked, increases from + * @param _secondsIncrease Seconds to increase in locked time. If stake is unlocked, increases from * block.timestamp. * * @dev Reverts when resulting locked time is not in range of [MIN_LOCKUP_PERIOD, MAX_LOCKUP_PERIOD] */ - function lock(uint256 _secondsToIncreaseLock) + function lock(uint256 _secondsIncrease) external onlyTrustedCodehash onlyAccountInitialized(msg.sender) @@ -255,11 +246,11 @@ contract StakeManager is TrustedCodehashAccess { uint256 deltaTime; if (lockUntil < block.timestamp) { //if unlocked, increase from now - lockUntil = block.timestamp + _secondsToIncreaseLock; - deltaTime = _secondsToIncreaseLock; + lockUntil = block.timestamp + _secondsIncrease; + deltaTime = _secondsIncrease; } else { //if locked, increase from lock until - lockUntil += _secondsToIncreaseLock; + lockUntil += _secondsIncrease; deltaTime = lockUntil - block.timestamp; } //checks if the lock time is in range @@ -267,14 +258,14 @@ contract StakeManager is TrustedCodehashAccess { revert StakeManager__InvalidLockTime(); } //mints bonus multiplier points for seconds increased - uint256 bonusMP = _getMPToMint(account.balance, _secondsToIncreaseLock); + uint256 bonusMP = _calculateAccuredMP(account.balance, _secondsIncrease); //update account storage account.lockUntil = lockUntil; account.bonusMP += bonusMP; account.totalMP += bonusMP; //update global storage - totalSupplyMP += bonusMP; + totalMP += bonusMP; } /** @@ -331,16 +322,10 @@ contract StakeManager is TrustedCodehashAccess { revert StakeManager__InvalidMigration(); } migration = _migration; - stakedToken.transfer(address(migration), epochReward()); + rewardToken.transfer(address(migration), epochReward()); expiredStakeStorage.transferOwnership(address(_migration)); migration.migrationInitialize( - currentEpoch, - totalSupplyMP, - totalSupplyBalance, - startTime, - totalMPPerEpoch, - pendingMPToBeMinted, - currentEpochTotalExpiredMP + currentEpoch, totalMP, totalStaked, startTime, totalMPPerEpoch, potentialMP, currentEpochTotalExpiredMP ); } @@ -348,17 +333,17 @@ contract StakeManager is TrustedCodehashAccess { * @dev Callable automatically from old StakeManager.startMigration(address) * @notice Initilizes migration process * @param _currentEpoch epoch of old manager - * @param _totalSupplyMP MP supply on old manager - * @param _totalSupplyBalance stake supply on old manager + * @param _totalMP MP supply on old manager + * @param _totalStaked stake supply on old manager * @param _startTime start time of old manager */ function migrationInitialize( uint256 _currentEpoch, - uint256 _totalSupplyMP, - uint256 _totalSupplyBalance, + uint256 _totalMP, + uint256 _totalStaked, uint256 _startTime, uint256 _totalMPPerEpoch, - uint256 _pendingMPToBeMinted, + uint256 _potentialMP, uint256 _currentEpochExpiredMP ) external @@ -372,10 +357,10 @@ contract StakeManager is TrustedCodehashAccess { revert StakeManager__InvalidMigration(); } currentEpoch = _currentEpoch; - totalSupplyMP = _totalSupplyMP; - totalSupplyBalance = _totalSupplyBalance; + totalMP = _totalMP; + totalStaked = _totalStaked; totalMPPerEpoch = _totalMPPerEpoch; - pendingMPToBeMinted = _pendingMPToBeMinted; + potentialMP = _potentialMP; currentEpochTotalExpiredMP = _currentEpochExpiredMP; } @@ -383,7 +368,7 @@ contract StakeManager is TrustedCodehashAccess { * @notice Transfer current epoch funds for migrated manager */ function transferNonPending() external onlyPendingMigration { - stakedToken.transfer(address(migration), epochReward()); + rewardToken.transfer(address(migration), epochReward()); } /** @@ -391,7 +376,7 @@ contract StakeManager is TrustedCodehashAccess { * @param _acceptMigration true if wants to migrate, false if wants to leave */ function migrateTo(bool _acceptMigration) - external + internal onlyTrustedCodehash onlyAccountInitialized(msg.sender) onlyPendingMigration @@ -399,13 +384,30 @@ contract StakeManager is TrustedCodehashAccess { { _processAccount(accounts[msg.sender], currentEpoch); Account memory account = accounts[msg.sender]; - totalSupplyMP -= account.totalMP; - totalSupplyBalance -= account.balance; + totalMP -= account.totalMP; + totalStaked -= account.balance; delete accounts[msg.sender]; migration.migrateFrom(msg.sender, _acceptMigration, account); return migration; } + /** + * @notice Account accepts an update to new contract + * @return _migrated new manager + */ + function acceptUpdate() external returns (IStakeManager _migrated) { + return migrateTo(true); + } + + /** + * @notice Account leaves contract in case of a contract breach + * @return _leaveAccepted true if accepted + */ + function leave() external returns (bool _leaveAccepted) { + migrateTo(false); + return true; + } + /** * @dev Only callable from old manager. * @notice Migrate account from old manager @@ -417,8 +419,8 @@ contract StakeManager is TrustedCodehashAccess { if (_acceptMigration) { accounts[_vault] = _account; } else { - totalSupplyMP -= _account.totalMP; - totalSupplyBalance -= _account.balance; + totalMP -= _account.totalMP; + totalStaked -= _account.balance; } } @@ -428,7 +430,7 @@ contract StakeManager is TrustedCodehashAccess { * @param _amount amount MP increased on account after migration initialized */ function increaseTotalMP(uint256 _amount) external onlyPreviousManager { - totalSupplyMP += _amount; + totalMP += _amount; } /** @@ -461,7 +463,7 @@ contract StakeManager is TrustedCodehashAccess { account.epoch = userEpoch; if (userReward > 0) { pendingReward -= userReward; - stakedToken.transfer(account.rewardAddress, userReward); + rewardToken.transfer(account.rewardAddress, userReward); } if (address(migration) != address(0)) { mpDifference = account.totalMP - mpDifference; @@ -477,7 +479,7 @@ contract StakeManager is TrustedCodehashAccess { */ function _mintMP(Account storage account, uint256 processTime, Epoch storage epoch) private { uint256 mpToMint = _getMaxMPToMint( - _getMPToMint(account.balance, processTime - account.lastMint), + _calculateAccuredMP(account.balance, processTime - account.lastMint), account.balance, account.bonusMP, account.totalMP @@ -486,11 +488,11 @@ contract StakeManager is TrustedCodehashAccess { //update storage account.lastMint = processTime; account.totalMP += mpToMint; - totalSupplyMP += mpToMint; + totalMP += mpToMint; //mp estimation - epoch.estimatedMP -= mpToMint; - pendingMPToBeMinted -= mpToMint; + epoch.potentialMP -= mpToMint; + potentialMP -= mpToMint; } /** @@ -512,7 +514,7 @@ contract StakeManager is TrustedCodehashAccess { returns (uint256 _maxMpToMint) { // Maximum multiplier point for given balance - _maxMpToMint = _getMPToMint(_balance, MAX_BOOST * YEAR) + _bonusMP; + _maxMpToMint = _calculateMaxAccuredMP(_balance) + _bonusMP; if (_mpToMint + _totalMP > _maxMpToMint) { //reached cap when increasing MP return _maxMpToMint - _totalMP; //how much left to reach cap @@ -523,13 +525,12 @@ contract StakeManager is TrustedCodehashAccess { } /** - * @notice Calculates multiplier points to mint for given balance and time - * @param _balance balance of account - * @param _deltaTime time difference - * @return multiplier points to mint + * @notice Returns account balance + * @param _vault Account address + * @return _balance account balance */ - function _getMPToMint(uint256 _balance, uint256 _deltaTime) private pure returns (uint256) { - return Math.mulDiv(_balance, _deltaTime, YEAR) * MP_APY; + function getStakedBalance(address _vault) external view returns (uint256 _balance) { + return accounts[_vault].balance; } /* @@ -538,8 +539,8 @@ contract StakeManager is TrustedCodehashAccess { * @param _deltaTime time difference * @return multiplier points to mint */ - function calculateMPToMint(uint256 _balance, uint256 _deltaTime) public pure returns (uint256) { - return _getMPToMint(_balance, _deltaTime); + function calculateMP(uint256 _balance, uint256 _deltaTime) public pure returns (uint256) { + return _calculateAccuredMP(_balance, _deltaTime); } /** @@ -548,7 +549,7 @@ contract StakeManager is TrustedCodehashAccess { * @return _totalSupply current total supply */ function totalSupply() public view returns (uint256 _totalSupply) { - return totalSupplyMP + totalSupplyBalance + pendingMPToBeMinted; + return totalMP + totalStaked + potentialMP; } /** @@ -556,7 +557,7 @@ contract StakeManager is TrustedCodehashAccess { * @return _totalSupply current total supply */ function totalSupplyMinted() public view returns (uint256 _totalSupply) { - return totalSupplyMP + totalSupplyBalance; + return totalMP + totalStaked; } /** @@ -564,7 +565,7 @@ contract StakeManager is TrustedCodehashAccess { * @return _epochReward current epoch reward */ function epochReward() public view returns (uint256 _epochReward) { - return stakedToken.balanceOf(address(this)) - pendingReward; + return rewardToken.balanceOf(address(this)) - pendingReward; } /** diff --git a/contracts/StakeVault.sol b/contracts/StakeVault.sol index 8588d9e..f4603a1 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.18; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { StakeManager } from "./StakeManager.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IStakeManager } from "./IStakeManager.sol"; /** * @title StakeVault @@ -18,20 +18,20 @@ contract StakeVault is Ownable { error StakeVault__UnstakingFailed(); - StakeManager private stakeManager; + IStakeManager private stakeManager; - ERC20 public immutable STAKED_TOKEN; + IERC20 public immutable stakedToken; event Staked(address from, address to, uint256 _amount, uint256 time); - constructor(address _owner, ERC20 _stakedToken, StakeManager _stakeManager) { + constructor(address _owner, IERC20 _stakedToken, IStakeManager _stakeManager) { _transferOwnership(_owner); - STAKED_TOKEN = _stakedToken; + stakedToken = _stakedToken; stakeManager = _stakeManager; } function stake(uint256 _amount, uint256 _time) external onlyOwner { - bool success = STAKED_TOKEN.transferFrom(msg.sender, address(this), _amount); + bool success = stakedToken.transferFrom(msg.sender, address(this), _amount); if (!success) { revert StakeVault__StakingFailed(); } @@ -46,27 +46,24 @@ contract StakeVault is Ownable { function unstake(uint256 _amount) external onlyOwner { stakeManager.unstake(_amount); - bool success = STAKED_TOKEN.transfer(msg.sender, _amount); + bool success = stakedToken.transfer(msg.sender, _amount); if (!success) { revert StakeVault__UnstakingFailed(); } } function leave() external onlyOwner { - stakeManager.migrateTo(false); - STAKED_TOKEN.transferFrom(address(this), msg.sender, STAKED_TOKEN.balanceOf(address(this))); + if (stakeManager.leave()) { + stakedToken.transferFrom(address(this), msg.sender, stakedToken.balanceOf(address(this))); + } } /** - * @notice Opt-in migration to a new StakeManager contract. + * @notice Opt-in migration to a new IStakeManager contract. */ function acceptMigration() external onlyOwner { - StakeManager migrated = stakeManager.migrateTo(true); + IStakeManager migrated = stakeManager.acceptUpdate(); if (address(migrated) == address(0)) revert StakeVault__MigrationNotAvailable(); stakeManager = migrated; } - - function stakedToken() external view returns (ERC20) { - return STAKED_TOKEN; - } } diff --git a/contracts/VaultFactory.sol b/contracts/VaultFactory.sol index a4802c0..b165bad 100644 --- a/contracts/VaultFactory.sol +++ b/contracts/VaultFactory.sol @@ -57,7 +57,7 @@ contract VaultFactory is Ownable2Step { /// @dev Anyone can call this function. /// @dev Emits a {VaultCreated} event. function createVault() external returns (StakeVault) { - StakeVault vault = new StakeVault(msg.sender, stakeManager.stakedToken(), stakeManager); + StakeVault vault = new StakeVault(msg.sender, stakeManager.rewardToken(), stakeManager); emit VaultCreated(address(vault), msg.sender); return vault; } diff --git a/contracts/access/ITrustedCodehashAccess.sol b/contracts/access/ITrustedCodehashAccess.sol new file mode 100644 index 0000000..66ae6a5 --- /dev/null +++ b/contracts/access/ITrustedCodehashAccess.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +/** + * @title TrustedCodehashAccess + * @author Ricardo Guilherme Schmidt + * @notice Ensures that only specific contract bytecode hashes are trusted to + * interact with the functions using the `onlyTrustedCodehash` modifier. + */ +interface ITrustedCodehashAccess { + error TrustedCodehashAccess__UnauthorizedCodehash(); + + event TrustedCodehashUpdated(bytes32 indexed codehash, bool trusted); + + /** + * @notice Allows the owner to set or update the trust status for a contract's codehash. + * @dev Emits the `TrustedCodehashUpdated` event whenever a codehash is updated. + * @param _codehash The bytecode hash of the contract. + * @param _trusted Boolean flag to designate the contract as trusted or not. + */ + function setTrustedCodehash(bytes32 _codehash, bool _trusted) external; + + /** + * @notice Checks if a contract's codehash is trusted to interact with protected functions. + * @param _codehash The bytecode hash of the contract. + * @return bool True if the codehash is trusted, false otherwise. + */ + function isTrustedCodehash(bytes32 _codehash) external view returns (bool); +} diff --git a/contracts/access/TrustedCodehashAccess.sol b/contracts/access/TrustedCodehashAccess.sol index 3caac30..3d3c4f2 100644 --- a/contracts/access/TrustedCodehashAccess.sol +++ b/contracts/access/TrustedCodehashAccess.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; /** * @title TrustedCodehashAccess @@ -9,11 +10,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; * @notice Ensures that only specific contract bytecode hashes are trusted to * interact with the functions using the `onlyTrustedCodehash` modifier. */ -contract TrustedCodehashAccess is Ownable { - error TrustedCodehashAccess__UnauthorizedCodehash(); - - event TrustedCodehashUpdated(bytes32 indexed codehash, bool trusted); - +contract TrustedCodehashAccess is ITrustedCodehashAccess, Ownable { mapping(bytes32 codehash => bool permission) private trustedCodehashes; /** diff --git a/contracts/factory/SingletonFactory.sol b/contracts/factory/SingletonFactory.sol new file mode 100644 index 0000000..fc436ad --- /dev/null +++ b/contracts/factory/SingletonFactory.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.18; + +/** + * @title Singleton Factory (EIP-2470) + * @notice Exposes CREATE2 (EIP-1014) to deploy bytecode on deterministic addresses based on initialization code and + * salt. + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + */ +contract ERC2470 { + error ERC2470__CREATE2Failed(); + error ERC2470__CREATE2BadCall(); + + fallback(bytes calldata _initCode) external payable returns (bytes memory) { + return toBytes(deploy(_initCode, 0)); + } + + receive() external payable { + revert ERC2470__CREATE2BadCall(); + } + + function deploy(bytes memory _initCode, bytes32 _salt) public payable returns (address payable createdContract) { + assembly { + createdContract := create2(callvalue(), add(_initCode, 0x20), mload(_initCode), _salt) + } + if (createdContract == address(0)) { + revert ERC2470__CREATE2Failed(); + } + } + + function predict(bytes memory _initCode, bytes32 _salt) public view returns (address payable createdContract) { + createdContract = payable( + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), _salt, _initCode))))) + ); + } + + function predictFrom( + bytes memory _initCode, + bytes32 _salt, + address _factoryAddress + ) + public + pure + returns (address payable createdContract) + { + createdContract = payable( + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), _factoryAddress, _salt, _initCode))))) + ); + } + + function toBytes(address a) internal pure returns (bytes memory) { + return abi.encodePacked(a); + } + + function toAddress(bytes memory b) external pure returns (address addr) { + return abi.decode(b, (address)); + } +} diff --git a/test/DynamicTest.t.sol b/test/DynamicTest.t.sol new file mode 100644 index 0000000..ed607e4 --- /dev/null +++ b/test/DynamicTest.t.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import { Test, console } from "forge-std/Test.sol"; +import { Deploy } from "../script/Deploy.s.sol"; +import { DeployMigrationStakeManager } from "../script/DeployMigrationStakeManager.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { TrustedCodehashAccess, StakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; +import { MultiplierPointMath } from "../contracts/MultiplierPointMath.sol"; +import { StakeVault } from "../contracts/StakeVault.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; + +contract DynamicTest is MultiplierPointMath, Test { + DeploymentConfig internal deploymentConfig; + StakeManager internal stakeManager; + VaultFactory internal vaultFactory; + + address internal stakeToken; + address internal deployer; + address internal testUser = makeAddr("testUser"); + address internal testUser2 = makeAddr("testUser2"); + + function setUp() public virtual { + Deploy deployment = new Deploy(); + (vaultFactory, stakeManager, deploymentConfig) = deployment.run(); + (deployer, stakeToken) = deploymentConfig.activeNetworkConfig(); + } + + modifier withPrank(address _prankAddress) { + vm.startPrank(_prankAddress); + _; + vm.stopPrank(); + } + + modifier fuzz_stake(uint256 _amount) { + vm.assume(_amount > _calculateMinimumStake(stakeManager.EPOCH_SIZE())); + vm.assume(_amount < 1e20); + _; + } + + modifier fuzz_lock(uint256 _seconds) { + vm.assume(_seconds == 0 || _seconds > stakeManager.MIN_LOCKUP_PERIOD()); + vm.assume(_seconds == 0 || _seconds < stakeManager.MAX_LOCKUP_PERIOD()); + _; + } + + modifier fuzz_unstake(uint256 _staked, uint256 _unstaked) { + vm.assume(_unstaked > 0); + vm.assume(_unstaked < _staked); + _; + } + + function _setTrustedCodehash(StakeVault _vault, bool _trusted) internal withPrank(deployer) { + if (stakeManager.isTrustedCodehash(address(_vault).codehash) == _trusted) { + stakeManager.setTrustedCodehash(address(_vault).codehash, _trusted); + } + } + + function _createVault(address _account) internal withPrank(_account) returns (StakeVault vault) { + vm.prank(_account); + vault = vaultFactory.createVault(); + } + + function _initializeVault(address _account) internal returns (StakeVault vault) { + vault = _createVault(_account); + _setTrustedCodehash(vault, true); + } + + function _stake(StakeVault _vault, uint256 _amount, uint256 _lockedSeconds) internal withPrank(_vault.owner()) { + ERC20(stakeToken).approve(address(_vault), _amount); + _vault.stake(_amount, _lockedSeconds); + } + + function _unstake(StakeVault _vault, uint256 _amount) internal withPrank(_vault.owner()) { + _vault.unstake(_amount); + } + + function _lock(StakeVault _vault, uint256 _lockedSeconds) internal withPrank(_vault.owner()) { + _vault.unstake(_lockedSeconds); + } + + enum VaultMethod { + CREATE, + STAKE, + UNSTAKE, + LOCK + } + enum VMMethod { + VM_WARP + } + + struct StageActions { + VMAction[] vmActions; + VaultAction[] vaultActions; + } + + struct VaultAction { + StakeVault vault; + VaultMethod method; + uint256[] args; + } + + struct VMAction { + VMMethod method; + uint256[] args; + } + + struct StageState { + uint256 timestamp; + VaultState[] vaultStates; + } + + struct VaultState { + StakeVault vault; + uint256 increasedAccuredMP; + uint256 predictedBonusMP; + uint256 predictedAccuredMP; + uint256 stakeAmount; + } + + function _processStage( + StageState memory _input, + StageActions memory _action + ) + internal + pure + returns (StageState memory output) + { + output = _input; + for (uint256 i = 0; i < _action.vmActions.length; i++) { + output = _processStage_VMAction_StageResult(output, _action.vmActions[i]); + } + for (uint256 i = 0; i < _action.vaultActions.length; i++) { + output = _processStage_AccountAction_StageResult(output, _action.vaultActions[i]); + } + } + + function _processStage_VMAction_StageResult( + StageState memory _input, + VMAction memory _action + ) + internal + pure + returns (StageState memory output) + { + if (_action.method == VMMethod.VM_WARP) { + output.timestamp = _input.timestamp + _action.args[0]; + output.vaultStates = new VaultState[](_input.vaultStates.length); + for (uint256 i = 0; i < _input.vaultStates.length; i++) { + output.vaultStates[i] = _predict_VMAction_AccountState(_input.vaultStates[i], _action); + } + } + } + + function _processStage_AccountAction_StageResult( + StageState memory input, + VaultAction memory action + ) + internal + pure + returns (StageState memory output) + { + if (action.method == VaultMethod.CREATE) { + output.vaultStates = new VaultState[](input.vaultStates.length + 1); + } else { + output.vaultStates = new VaultState[](input.vaultStates.length); + } + for (uint256 i = 0; i < input.vaultStates.length; i++) { + output.vaultStates[i] = _predict_AccountAction_AccountState(input.vaultStates[i], action); + } + } + + function _predict_VMAction_AccountState( + VaultState memory input, + VMAction memory action + ) + internal + pure + returns (VaultState memory output) + { + if (action.method == VMMethod.VM_WARP) { + require(action.args.length == 1, "Incorrect number of arguments"); + output.stakeAmount = input.stakeAmount; + output.predictedBonusMP = input.predictedBonusMP; + output.increasedAccuredMP = _calculateAccuredMP(input.stakeAmount, action.args[0]); + output.predictedAccuredMP = input.predictedAccuredMP + output.increasedAccuredMP; + } + } + + function _predict_AccountAction_AccountState( + VaultState memory input, + VaultAction memory action + ) + internal + pure + returns (VaultState memory output) + { + if ( + action.method != VaultMethod.CREATE && action.vault != input.vault + || action.method == VaultMethod.CREATE && address(input.vault) != address(0) + ) { + return input; + } + output.vault = input.vault; + if (action.method == VaultMethod.CREATE) { + //output.vault = _createVault(address(uint160(action.args[0]))); + output.stakeAmount = 0; + output.predictedBonusMP = 0; + output.increasedAccuredMP = 0; + output.predictedAccuredMP = 0; + } else if (action.method == VaultMethod.STAKE) { + require(action.args.length == 2, "Incorrect number of arguments"); + output.stakeAmount = input.stakeAmount + action.args[0]; + output.predictedBonusMP = _calculateBonusMP(output.stakeAmount, action.args[1]); + output.increasedAccuredMP = input.increasedAccuredMP; + output.predictedAccuredMP = input.predictedAccuredMP; + } else if (action.method == VaultMethod.UNSTAKE) { + require(action.args.length == 1, "Incorrect number of arguments"); + output.stakeAmount = input.stakeAmount - action.args[0]; + output.predictedBonusMP = (output.stakeAmount * input.predictedBonusMP) / input.stakeAmount; + output.increasedAccuredMP = input.increasedAccuredMP; + output.predictedAccuredMP = (output.stakeAmount * input.predictedAccuredMP) / input.stakeAmount; + } else if (action.method == VaultMethod.LOCK) { + require(action.args.length == 1, "Incorrect number of arguments"); + output.stakeAmount = input.stakeAmount; + output.predictedBonusMP = _calculateBonusMP(output.stakeAmount, action.args[0]); + output.increasedAccuredMP = input.increasedAccuredMP; + output.predictedAccuredMP = input.predictedAccuredMP + output.increasedAccuredMP; + } + } + /* + function testFuzz_UnstakeBonusMPAndAccuredMP( + uint256 amountStaked, + uint256 secondsLocked, + uint256 reducedStake, + uint256 increasedTime + ) + public + fuzz_stake(amountStaked) + fuzz_unstake(amountStaked, reducedStake) + fuzz_lock(secondsLocked) + { + + //initialize memory placehodlders + uint totalStages = 4; + uint[totalStages] memory timestamp; + AccountState[totalStages] memory globalParams; + AccountState[totalStages][] memory userParams; + StageActions[totalStages] memory actions; + address[totalStages][] memory users; + + //stages variables setup + uint stage = 0; // first stage = initialization + { + actions[stage] = StageActions({ + timeIncrease: 0, + userActions: [ UserActions({ + stakeIncrease: amountStaked, + lockupIncrease: secondsLocked, + stakeDecrease: 0 + })] + }); + timestamp[stage] = block.timestamp; + users[stage] = [alice]; + userParams[stage] = new AccountState[](users[stage].length); + { + UserActions memory userActions = actions[stage].userActions[0]; + userParams[stage][0].stakeAmount = userActions.stakeIncrease; + userParams[stage][0].predictedBonusMP = _calculateBonusMP(userActions.stakeIncrease, + userActions.lockupIncrease); + userParams[stage][0].increasedAccuredMP = 0; //no increased accured MP in first stage + userParams[stage][0].predictedAccuredMP = 0; //no accured MP in first stage + } + } + + stage++; // second stage = progress in time + { + actions[stage] = StageActions({ + timeIncrease: increasedTime, + userActions: [UserActions({ + stakeIncrease: 0, + lockupIncrease: 0, + stakeDecrease: 0 + })] + }); + timestamp[stage] = timestamp[stage-1] + actions[stage].timeIncrease; + users[stage] = users[stage-1]; //no new users in second stage + userParams[stage] = new AccountState[](users[stage].length); + { + UserActions memory userActions = actions[stage].userActions[0]; + userParams[stage][0].stakeAmount = userParams[stage-1][0].stakeAmount; //no changes in stake at second stage + userParams[stage][0].predictedBonusMP = userParams[stage-1][0].predictedBonusMP; //no changes in bonusMP at + second stage + userParams[stage][0].increasedAccuredMP = _calculeAccuredMP(amountStaked, timestamp[stage] - + timestamp[stage-1]); + userParams[stage][0].predictedAccuredMP = userParams[stage-1][0].predictedAccuredMP + + userParams[stage][0].increasedAccuredMP; + } + } + + stage++; //third stage = reduce stake + { + timestamp[stage] = timestamp[stage-1]; //no time increased in third stage + users[stage] = users[stage-1]; //no new users in third stage + userParams[stage] = new AccountState[](users[stage].length); + { + userParams[stage][0].stakeAmount = userParams[stage-1][0].stakeAmount - reducedStake; + //bonusMP from this stage is a proportion from the difference of current stakeAmount and past stage stakeAmount + //if the account reduced 50% of its stake, the bonusMP should be reduced by 50% + userParams[stage][0].predictedBonusMP = (userParams[stage][0].stakeAmount * + userParams[stage-1][0].predictedBonusMP) / userParams[stage-1][0].stakeAmount; + userParams[stage][0].increasedAccuredMP = 0; //no accuredMP in third stage; + //total accuredMP from this stage is a proportion from the difference of current stakeAmount and past stage + stakeAmount + //if the account reduced 50% of its stake, the accuredMP should be reduced by 50% + userParams[stage][0].predictedAccuredMP = (userParams[stage][0].stakeAmount * predictedAccuredMP[stage-1]) / + userParams[stage-1][0].stakeAmount;; + } + } + + // stages execution + stage = 0; // first stage = initialization + { + _stake(amountStaked, secondsLocked); + for(uint i = 0; i < users[stage].length; i++) { + RewardsStreamerMP.UserInfo memory userInfo = streamer.getUserInfo(users[stage][i]); + assertEq(userInfo.stakedBalance, userParams[stage][i].stakeAmount, "wrong user staked balance"); + assertEq(userInfo.userMP, userParams[stage][i].predictedAccuredMP + userParams[stage][i].predictedBonusMP, + "wrong user MP"); + assertEq(userInfo.maxMP, userParams[stage][i].stakeAmount * MAX_MULTIPLIER + +userParams[stage][i].predictedBonusMP, "wrong user max MP"); + //sum all usersParams to globalParams + globalParams[stage].stakeAmount += userParams[stage][i].stakeAmount; + globalParams[stage].predictedBonusMP += userParams[stage][i].predictedBonusMP; + globalParams[stage].increasedAccuredMP += userParams[stage][i].increasedAccuredMP; + globalParams[stage].predictedAccuredMP += userParams[stage][i].predictedAccuredMP; + } + assertEq(streamer.totalStaked(), globalParams[stage].stakeAmount, "wrong total staked"); + assertEq(streamer.totalMP(), globalParams[stage].predictedBonusMP, "wrong total MP"); + assertEq(streamer.totalMaxMP(), globalParams[stage].stakeAmount * MAX_MULTIPLIER + + globalParams[stage].predictedBonusMP, "wrong totalMaxMP MP"); + } + + stage++; // second stage = progress in time + { + vm.warp(timestamp[stage]); + for(uint i = 0; i < users[stage].length; i++) { + RewardsStreamerMP.UserInfo memory userInfo = streamer.getUserInfo(users[stage][i]); + assertEq(userInfo.stakedBalance, userParams[stage][i].stakeAmount, "wrong user staked balance"); + assertEq(userInfo.userMP, userParams[stage][i].predictedAccuredMP + userParams[stage][i].predictedBonusMP, + "wrong user MP"); + assertEq(userInfo.maxMP, userParams[stage][i].stakeAmount * MAX_MULTIPLIER + +userParams[stage][i].predictedBonusMP, "wrong user max MP"); + //sum all usersParams to globalParams + globalParams[stage].stakeAmount += userParams[stage][i].stakeAmount; + globalParams[stage].predictedBonusMP += userParams[stage][i].predictedBonusMP; + globalParams[stage].increasedAccuredMP += userParams[stage][i].increasedAccuredMP; + globalParams[stage].predictedAccuredMP += userParams[stage][i].predictedAccuredMP; + } + assertEq(streamer.totalStaked(), globalParams[stage].stakeAmount, "wrong total staked"); + assertEq(streamer.totalMP(), globalParams[stage].predictedBonusMP, "wrong total MP"); + assertEq(streamer.totalMaxMP(), globalParams[stage].stakeAmount * MAX_MULTIPLIER + + globalParams[stage].predictedBonusMP, "wrong totalMaxMP MP"); + } + + stage++; // third stage = reduce stake + { + _unstake(reducedStake); + for(uint i = 0; i < users[stage].length; i++) { + RewardsStreamerMP.UserInfo memory userInfo = streamer.getUserInfo(users[stage][i]); + assertEq(userInfo.stakedBalance, userParams[stage][i].stakeAmount, "wrong user staked balance"); + assertEq(userInfo.userMP, userParams[stage][i].predictedAccuredMP + userParams[stage][i].predictedBonusMP, + "wrong user MP"); + assertEq(userInfo.maxMP, userParams[stage][i].stakeAmount * MAX_MULTIPLIER + + userParams[stage][i].predictedBonusMP, "wrong user max MP"); + //sum all usersParams to globalParams + globalParams[stage].stakeAmount += userParams[stage][i].stakeAmount; + globalParams[stage].predictedBonusMP += userParams[stage][i].predictedBonusMP; + globalParams[stage].increasedAccuredMP += userParams[stage][i].increasedAccuredMP; + globalParams[stage].predictedAccuredMP += userParams[stage][i].predictedAccuredMP; + } + assertEq(streamer.totalStaked(), globalParams[stage].stakeAmount, "wrong total staked"); + assertEq(streamer.totalMP(), globalParams[stage].predictedBonusMP, "wrong total MP"); + assertEq(streamer.totalMaxMP(), globalParams[stage].stakeAmount * MAX_MULTIPLIER + + globalParams[stage].predictedBonusMP, "wrong totalMaxMP MP"); + } + }*/ +} diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index 339234d..b72fdc3 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -7,7 +7,9 @@ import { Test, console } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; import { DeployMigrationStakeManager } from "../script/DeployMigrationStakeManager.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; -import { TrustedCodehashAccess, StakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; +import { StakeManager, IStakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; +import { ITrustedCodehashAccess } from "../contracts/access/ITrustedCodehashAccess.sol"; +import { MultiplierPointMath } from "../contracts/MultiplierPointMath.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; import { VaultFactory } from "../contracts/VaultFactory.sol"; @@ -31,9 +33,9 @@ contract StakeManagerTest is Test { assertEq(stakeManager.owner(), deployer); assertEq(stakeManager.currentEpoch(), 0); assertEq(stakeManager.pendingReward(), 0); - assertEq(stakeManager.totalSupplyMP(), 0); - assertEq(stakeManager.totalSupplyBalance(), 0); - assertEq(address(stakeManager.stakedToken()), stakeToken); + assertEq(stakeManager.totalMP(), 0); + assertEq(stakeManager.totalStaked(), 0); + assertEq(address(stakeManager.rewardToken()), stakeToken); assertEq(address(stakeManager.previousManager()), address(0)); assertEq(stakeManager.totalSupply(), 0); } @@ -83,7 +85,7 @@ contract StakeManagerTest is Test { contract StakeTest is StakeManagerTest { function test_RevertWhen_SenderIsNotVault() public { - vm.expectRevert(TrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); + vm.expectRevert(ITrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); stakeManager.stake(100, 1); } @@ -96,11 +98,13 @@ contract StakeTest is StakeManagerTest { (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "balance of user vault should be equal to stake amount after stake"); assertEq(bonusMP, stakeAmount, "bonusMP of user vault should be equal to stake amount after stake if no lock"); - assertEq(totalMP, bonusMP, "totalMP of user vault should be equal to bonusMP after stake if no epochs passed"); + assertEq( + totalMP, stakeAmount, "totalMP of user vault should be equal to stakeAmount after stake if no epochs passed" + ); vm.prank(testUser); userVault.lock(lockTime); - uint256 estimatedBonusMp = stakeAmount + stakeManager.calculateMPToMint(stakeAmount, lockTime); + uint256 estimatedBonusMp = stakeAmount + stakeManager.calculateMP(stakeAmount, lockTime); (, balance, bonusMP, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "balance of user vault should be equal to stake amount after lock"); @@ -128,18 +132,18 @@ contract StakeTest is StakeManagerTest { ERC20(stakeToken).approve(address(userVault), 100); uint256 lockTime = stakeManager.MIN_LOCKUP_PERIOD() - 1; - vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); + vm.expectRevert(IStakeManager.StakeManager__InvalidLockTime.selector); userVault.stake(100, lockTime); lockTime = stakeManager.MAX_LOCKUP_PERIOD() + 1; - vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); + vm.expectRevert(IStakeManager.StakeManager__InvalidLockTime.selector); userVault.stake(100, lockTime); } function test_RevertWhen_StakeIsTooLow() public { StakeVault userVault = _createTestVault(testUser); vm.startPrank(testUser); - vm.expectRevert(StakeManager.StakeManager__StakeIsTooLow.selector); + vm.expectRevert(IStakeManager.StakeManager__StakeIsTooLow.selector); userVault.stake(0, 0); } @@ -173,21 +177,21 @@ contract StakeTest is StakeManagerTest { StakeVault userVault = _createStakingAccount(testUser, stakeAmount, 0, stakeAmount); (,, uint256 totalMP,,,,,) = stakeManager.accounts(address(userVault)); - assertEq(stakeManager.totalSupplyMP(), stakeAmount, "total multiplier point supply"); + assertEq(stakeManager.totalMP(), stakeAmount, "total multiplier point supply"); assertEq(totalMP, stakeAmount, "user multiplier points"); vm.prank(testUser); userVault.unstake(stakeAmount); (,,, totalMP,,,,) = stakeManager.accounts(address(userVault)); - assertEq(stakeManager.totalSupplyMP(), 0, "totalSupplyMP burned after unstaking"); + assertEq(stakeManager.totalMP(), 0, "totalMP burned after unstaking"); assertEq(totalMP, 0, "userMP burned after unstaking"); } } contract UnstakeTest is StakeManagerTest { function test_RevertWhen_SenderIsNotVault() public { - vm.expectRevert(TrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); + vm.expectRevert(ITrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); stakeManager.unstake(1); } @@ -198,11 +202,11 @@ contract UnstakeTest is StakeManagerTest { StakeVault userVault = _createStakingAccount(testUser, stakeAmount, lockTime, mintAmount); vm.prank(testUser); - vm.expectRevert(StakeManager.StakeManager__FundsLocked.selector); + vm.expectRevert(IStakeManager.StakeManager__FundsLocked.selector); userVault.unstake(1); vm.prank(testUser); - vm.expectRevert(StakeManager.StakeManager__FundsLocked.selector); + vm.expectRevert(IStakeManager.StakeManager__FundsLocked.selector); userVault.unstake(stakeAmount); } @@ -216,7 +220,7 @@ contract UnstakeTest is StakeManagerTest { vm.prank(testUser); userVault.unstake(100); - assertEq(stakeManager.totalSupplyBalance(), 0); + assertEq(stakeManager.totalStaked(), 0); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); assertEq(ERC20(stakeToken).balanceOf(testUser), 1000); } @@ -233,7 +237,7 @@ contract UnstakeTest is StakeManagerTest { vm.prank(testUser); userVault.unstake(100); - assertEq(stakeManager.totalSupplyBalance(), 0); + assertEq(stakeManager.totalStaked(), 0); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); assertEq(ERC20(stakeToken).balanceOf(testUser), 1000); } @@ -245,14 +249,14 @@ contract UnstakeTest is StakeManagerTest { vm.startPrank(testUser); - assertEq(stakeManager.totalSupplyMP(), stakeAmount); + assertEq(stakeManager.totalMP(), stakeAmount); for (uint256 i = 0; i < 53; i++) { vm.warp(stakeManager.epochEnd()); stakeManager.executeAccount(address(userVault), i + 1); } (, uint256 balanceBefore, uint256 bonusMPBefore, uint256 totalMPBefore,,,,) = stakeManager.accounts(address(userVault)); - uint256 totalSupplyMPBefore = stakeManager.totalSupplyMP(); + uint256 totalSupplyMPBefore = stakeManager.totalMP(); uint256 unstakeAmount = stakeAmount * percentToBurn / 100; console.log("unstake", unstakeAmount); @@ -261,7 +265,7 @@ contract UnstakeTest is StakeManagerTest { (, uint256 balanceAfter, uint256 bonusMPAfter, uint256 totalMPAfter,,,,) = stakeManager.accounts(address(userVault)); - uint256 totalSupplyMPAfter = stakeManager.totalSupplyMP(); + uint256 totalSupplyMPAfter = stakeManager.totalMP(); console.log("totalSupplyMPBefore", totalSupplyMPBefore); console.log("totalSupplyMPAfter", totalSupplyMPAfter); console.log("balanceBefore", balanceBefore); @@ -282,14 +286,14 @@ contract UnstakeTest is StakeManagerTest { uint256 stakeAmount = 1000; StakeVault userVault = _createStakingAccount(testUser, stakeAmount); vm.startPrank(testUser); - vm.expectRevert(StakeManager.StakeManager__InsufficientFunds.selector); + vm.expectRevert(IStakeManager.StakeManager__InsufficientFunds.selector); userVault.unstake(stakeAmount + 1); } } contract LockTest is StakeManagerTest { function test_RevertWhen_SenderIsNotVault() public { - vm.expectRevert(TrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); + vm.expectRevert(ITrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); stakeManager.lock(100); } @@ -312,7 +316,7 @@ contract LockTest is StakeManagerTest { uint256 lockTime = stakeManager.MAX_LOCKUP_PERIOD() + 1; vm.startPrank(testUser); - vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); + vm.expectRevert(IStakeManager.StakeManager__InvalidLockTime.selector); userVault.lock(lockTime); } @@ -345,7 +349,7 @@ contract LockTest is StakeManagerTest { (,,,,, uint256 lockUntil,,) = stakeManager.accounts(address(userVault)); console.log(lockUntil); vm.startPrank(testUser); - vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); + vm.expectRevert(IStakeManager.StakeManager__InvalidLockTime.selector); userVault.lock(minLockup - 1); } @@ -354,15 +358,15 @@ contract LockTest is StakeManagerTest { uint256 lockTime = stakeManager.MAX_LOCKUP_PERIOD(); StakeVault userVault = _createStakingAccount(testUser, stakeAmount); (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); - uint256 totalSupplyMPBefore = stakeManager.totalSupplyMP(); + uint256 totalSupplyMPBefore = stakeManager.totalMP(); vm.startPrank(testUser); userVault.lock(lockTime); //solhint-disable-next-line max-line-length (, uint256 newBalance, uint256 newBonusMP, uint256 newCurrentMP,,,,) = stakeManager.accounts(address(userVault)); - uint256 totalSupplyMPAfter = stakeManager.totalSupplyMP(); - assertGt(totalSupplyMPAfter, totalSupplyMPBefore, "totalSupplyMP"); + uint256 totalSupplyMPAfter = stakeManager.totalMP(); + assertGt(totalSupplyMPAfter, totalSupplyMPBefore, "totalMP"); assertGt(newBonusMP, bonusMP, "bonusMP"); assertGt(newCurrentMP, totalMP, "totalMP"); assertEq(newBalance, balance, "balance"); @@ -371,8 +375,8 @@ contract LockTest is StakeManagerTest { contract LeaveTest is StakeManagerTest { function test_RevertWhen_SenderIsNotVault() public { - vm.expectRevert(TrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); - stakeManager.migrateTo(false); + vm.expectRevert(ITrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); + stakeManager.leave(); } function test_RevertWhen_NoPendingMigration() public { @@ -393,8 +397,8 @@ contract LeaveTest is StakeManagerTest { contract MigrateTest is StakeManagerTest { function test_RevertWhen_SenderIsNotVault() public { - vm.expectRevert(TrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); - stakeManager.migrateTo(true); + vm.expectRevert(ITrustedCodehashAccess.TrustedCodehashAccess__UnauthorizedCodehash.selector); + stakeManager.acceptUpdate(); } function test_RevertWhen_NoPendingMigration() public { @@ -431,8 +435,8 @@ contract MigrationInitializeTest is StakeManagerTest { secondStakeManager.startMigration(thirdStakeManager); uint256 currentEpoch = stakeManager.currentEpoch(); - uint256 totalMP = stakeManager.totalSupplyMP(); - uint256 totalBalance = stakeManager.totalSupplyBalance(); + uint256 totalMP = stakeManager.totalMP(); + uint256 totalBalance = stakeManager.totalStaked(); // `stakeManager` calling `migrationInitialize` while the new stake manager is // in migration itself, should revert @@ -514,7 +518,7 @@ contract ExecuteAccountTest is StakeManagerTest { //expected MP is, the starting totalMP + the calculatedMPToMint of user balance for one EPOCH_SIZE multiplied by // 2. - uint256 expectedMP = totalMP + (stakeManager.calculateMPToMint(stakeAmount, stakeManager.EPOCH_SIZE()) * 2); + uint256 expectedMP = totalMP + (stakeManager.calculateMP(stakeAmount, stakeManager.EPOCH_SIZE()) * 2); stakeManager.executeAccount(address(userVaults[0]), stakeManager.currentEpoch() + 1); (,,, totalMP, lastMint,, epoch,) = stakeManager.accounts(address(userVaults[0])); @@ -579,9 +583,9 @@ contract ExecuteAccountTest is StakeManagerTest { function test_ShouldNotMintMoreThanCap() public { uint256 stakeAmount = 10_000_000_000; - uint256 epochsAmountToReachCap = stakeManager.calculateMPToMint( - stakeAmount, stakeManager.MAX_BOOST() * stakeManager.YEAR() - ) / stakeManager.calculateMPToMint(stakeAmount, stakeManager.EPOCH_SIZE()); + uint256 epochsAmountToReachCap = stakeManager.calculateMP( + stakeAmount, stakeManager.MAX_MULTIPLIER() * stakeManager.YEAR() + ) / stakeManager.calculateMP(stakeAmount, stakeManager.EPOCH_SIZE()); deal(stakeToken, testUser, stakeAmount); @@ -673,17 +677,17 @@ contract UserFlowsTest is StakeManagerTest { assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 100); assertEq(ERC20(stakeToken).balanceOf(address(user2Vault)), 100); - assertEq(stakeManager.totalSupplyBalance(), 200); + assertEq(stakeManager.totalStaked(), 200); vm.startPrank(testUser); userVault.unstake(100); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); - assertEq(stakeManager.totalSupplyBalance(), 100); + assertEq(stakeManager.totalStaked(), 100); vm.startPrank(testUser2); user2Vault.unstake(100); assertEq(ERC20(stakeToken).balanceOf(address(user2Vault)), 0); - assertEq(stakeManager.totalSupplyBalance(), 0); + assertEq(stakeManager.totalStaked(), 0); } function test_StakeWithLockUpTimeLocksStake() public { @@ -694,7 +698,7 @@ contract UserFlowsTest is StakeManagerTest { vm.startPrank(testUser); // unstaking should fail as lockup time isn't over yet - vm.expectRevert(StakeManager.StakeManager__FundsLocked.selector); + vm.expectRevert(IStakeManager.StakeManager__FundsLocked.selector); userVault.unstake(100); // fast forward 12 weeks @@ -702,7 +706,7 @@ contract UserFlowsTest is StakeManagerTest { userVault.unstake(100); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); - assertEq(stakeManager.totalSupplyBalance(), 0); + assertEq(stakeManager.totalStaked(), 0); } function test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP( @@ -722,30 +726,30 @@ contract UserFlowsTest is StakeManagerTest { userVaults.push( _createStakingAccount(makeAddr(string(abi.encode(keccak256(abi.encode(accountNum))))), thisAccStake, 0) ); - uint256 thisAccReachCapIn = stakeManager.calculateMPToMint( - thisAccStake, stakeManager.MAX_BOOST() * stakeManager.YEAR() - ) / stakeManager.calculateMPToMint(thisAccStake, stakeManager.EPOCH_SIZE()); + uint256 thisAccReachCapIn = stakeManager.calculateMP( + thisAccStake, stakeManager.MAX_MULTIPLIER() * stakeManager.YEAR() + ) / stakeManager.calculateMP(thisAccStake, stakeManager.EPOCH_SIZE()); if (thisAccReachCapIn > epochsAmountToReachCap) { epochsAmountToReachCap = thisAccReachCapIn; //uses the amount to reach cap from the account that takes // longer to reach cap } } - //tests up to epochs to reach MAX_BOOST + 10 epochs + //tests up to epochs to reach MAX_MULTIPLIER + 10 epochs for (uint256 i = 0; i < epochsAmountToReachCap + 10; i++) { vm.warp(stakeManager.epochEnd()); stakeManager.executeEpoch(); - uint256 pendingMPToBeMintedBefore = stakeManager.pendingMPToBeMinted(); - uint256 totalSupplyMP = stakeManager.totalSupplyMP(); + uint256 pendingMPToBeMintedBefore = stakeManager.potentialMP(); + uint256 totalMP = stakeManager.totalMP(); for (uint256 j = 0; j < userVaults.length; j++) { (,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = stakeManager.accounts(address(userVaults[j])); stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); } - uint256 pendingMPToBeMintedAfter = stakeManager.pendingMPToBeMinted(); + uint256 pendingMPToBeMintedAfter = stakeManager.potentialMP(); - assertEq(pendingMPToBeMintedBefore + totalSupplyMP, stakeManager.totalSupplyMP()); + assertEq(pendingMPToBeMintedBefore + totalMP, stakeManager.totalMP()); assertEq(pendingMPToBeMintedAfter, 0); } } @@ -764,9 +768,9 @@ contract MigrationStakeManagerTest is StakeManagerTest { assertEq(newStakeManager.owner(), deployer); assertEq(newStakeManager.currentEpoch(), 0); assertEq(newStakeManager.pendingReward(), 0); - assertEq(newStakeManager.totalSupplyMP(), 0); - assertEq(newStakeManager.totalSupplyBalance(), 0); - assertEq(address(newStakeManager.stakedToken()), stakeToken); + assertEq(newStakeManager.totalMP(), 0); + assertEq(newStakeManager.totalStaked(), 0); + assertEq(address(newStakeManager.rewardToken()), stakeToken); assertEq(address(newStakeManager.previousManager()), address(stakeManager)); assertEq(newStakeManager.totalSupply(), 0); } From 4669a4fcf37b59982bce25d4308ba06c792e319a Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 8 Nov 2024 00:56:09 -0300 Subject: [PATCH 04/13] Update specs and interface --- .gas-report | 127 ------------------ .github/workflows/ci.yml | 2 +- .solhint.json | 2 +- certora/confs/MaxMPRule.conf | 2 +- certora/confs/StakeManager.conf | 2 +- certora/confs/StakeManagerProcess.conf | 2 +- certora/confs/StakeManagerStartMigration.conf | 2 +- certora/confs/StakeVault.conf | 2 +- certora/harness/StakeManagerNew.sol | 6 +- certora/helpers/ERC20A.sol | 4 +- certora/helpers/ExpiredStakeStorageA.sol | 2 +- contracts/IStakeManager.sol | 33 ----- contracts/StakeVault.sol | 19 +-- contracts/VaultFactory.sol | 4 +- contracts/access/TrustedCodehashAccess.sol | 4 +- contracts/factory/Create2FactoryLib.sol | 43 ++++++ contracts/factory/SingletonFactory.sol | 2 +- contracts/interfaces/IStakeManager.sol | 28 ++++ .../ITrustedCodehashAccess.sol | 2 +- contracts/storage/ExpiredStakeStorage.sol | 2 +- foundry.toml | 2 +- script/Base.s.sol | 2 +- script/Deploy.s.sol | 2 +- script/DeployMigrationStakeManager.s.sol | 4 +- script/DeploymentConfig.s.sol | 2 +- test/DynamicTest.t.sol | 2 +- test/StakeManager.t.sol | 6 +- test/StakeVault.t.sol | 4 +- test/VaultFactory.t.sol | 8 +- test/mocks/BrokenERC20.s.sol | 2 +- test/mocks/MockERC20.sol | 2 +- test/script/DeployBroken.s.sol | 2 +- 32 files changed, 120 insertions(+), 208 deletions(-) delete mode 100644 contracts/IStakeManager.sol create mode 100644 contracts/factory/Create2FactoryLib.sol create mode 100644 contracts/interfaces/IStakeManager.sol rename contracts/{access => interfaces}/ITrustedCodehashAccess.sol (97%) diff --git a/.gas-report b/.gas-report index 2db355e..e69de29 100644 --- a/.gas-report +++ b/.gas-report @@ -1,127 +0,0 @@ -| contracts/StakeManager.sol:StakeManager contract | | | | | | -|--------------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 2512529 | 13257 | | | | | -| Function Name | min | avg | median | max | # calls | -| EPOCH_SIZE | 285 | 285 | 285 | 285 | 1498 | -| MAX_LOCKUP_PERIOD | 361 | 361 | 361 | 361 | 4 | -| MAX_MULTIPLIER | 307 | 307 | 307 | 307 | 637 | -| MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 12 | -| YEAR | 307 | 307 | 307 | 307 | 637 | -| acceptUpdate | 23632 | 23632 | 23632 | 23632 | 1 | -| accounts | 1572 | 1572 | 1572 | 1572 | 144273 | -| calculateMP | 738 | 738 | 738 | 738 | 1276 | -| currentEpoch | 406 | 1072 | 406 | 2406 | 54 | -| epochEnd | 627 | 627 | 627 | 2627 | 23675 | -| epochReward | 1381 | 2881 | 1381 | 5881 | 3 | -| executeAccount(address) | 149349 | 149349 | 149349 | 149349 | 2 | -| executeAccount(address,uint256) | 26540 | 72264 | 74140 | 200389 | 141860 | -| executeEpoch() | 23458 | 120684 | 121843 | 900358 | 23564 | -| executeEpoch(uint256) | 23905 | 24541 | 23905 | 26134 | 7 | -| expiredStakeStorage | 394 | 2303 | 2394 | 2394 | 22 | -| isTrustedCodehash | 541 | 949 | 541 | 2541 | 680 | -| leave | 23631 | 23631 | 23631 | 23631 | 1 | -| lock | 23862 | 23862 | 23862 | 23862 | 1 | -| migration | 417 | 1417 | 1417 | 2417 | 4 | -| migrationInitialize | 24624 | 24624 | 24624 | 24624 | 1 | -| newEpoch | 441 | 441 | 441 | 441 | 5 | -| owner | 2410 | 2410 | 2410 | 2410 | 13 | -| pendingReward | 408 | 1442 | 2408 | 2408 | 29 | -| potentialMP | 408 | 408 | 408 | 408 | 46432 | -| previousManager | 275 | 275 | 275 | 275 | 13 | -| rewardToken | 293 | 293 | 293 | 293 | 696 | -| setTrustedCodehash | 47960 | 47960 | 47960 | 47960 | 139 | -| stake | 24005 | 24005 | 24005 | 24005 | 1 | -| startMigration | 103624 | 103632 | 103636 | 103636 | 3 | -| startTime | 264 | 264 | 264 | 264 | 21 | -| totalMP | 385 | 385 | 385 | 2385 | 46453 | -| totalStaked | 385 | 1785 | 2385 | 2385 | 20 | -| totalSupply | 784 | 1965 | 2784 | 2784 | 22 | -| unstake | 23841 | 23841 | 23841 | 23841 | 1 | - - -| contracts/StakeVault.sol:StakeVault contract | | | | | | -|----------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| acceptMigration | 35140 | 35140 | 35140 | 35140 | 2 | -| leave | 35152 | 35152 | 35152 | 35152 | 1 | -| lock | 43329 | 90544 | 61982 | 180383 | 7 | -| owner | 351 | 351 | 351 | 351 | 679 | -| stake | 27265 | 282111 | 265792 | 351743 | 684 | -| stakedToken | 215 | 215 | 215 | 215 | 2 | -| unstake | 40157 | 96345 | 78682 | 229644 | 11 | - - -| contracts/VaultFactory.sol:VaultFactory contract | | | | | | -|--------------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| createVault | 682103 | 682103 | 682103 | 682103 | 683 | -| setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | -| stakeManager | 368 | 1868 | 2368 | 2368 | 4 | - - -| contracts/storage/ExpiredStakeStorage.sol:ExpiredStakeStorage contract | | | | | | -|------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23725 | -| transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | - - -| lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol:ERC20 contract | | | | | | -|---------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46241 | 46199 | 46367 | 679 | -| balanceOf | 561 | 2107 | 2561 | 2561 | 30744 | - - -| script/Deploy.s.sol:Deploy contract | | | | | | -|-------------------------------------|-----------------|---------|---------|---------|---------| -| Deployment Cost | Deployment Size | | | | | -| 6149710 | 29676 | | | | | -| Function Name | min | avg | median | max | # calls | -| run | 5343984 | 5343984 | 5343984 | 5343984 | 66 | - - -| script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | -|-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| -| Deployment Cost | Deployment Size | | | | | -| 3329385 | 16522 | | | | | -| Function Name | min | avg | median | max | # calls | -| run | 2345854 | 2345854 | 2345854 | 2345854 | 19 | - - -| script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | -|---------------------------------------------------------|-----------------|-----|--------|-----|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| activeNetworkConfig | 455 | 455 | 455 | 455 | 132 | - - -| test/mocks/BrokenERC20.s.sol:BrokenERC20 contract | | | | | | -|---------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46175 | 46175 | 46175 | 1 | -| balanceOf | 561 | 1227 | 561 | 2561 | 3 | - - -| test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | -|------------------------------------------------------|-----------------|---------|---------|---------|---------| -| Deployment Cost | Deployment Size | | | | | -| 4834448 | 23474 | | | | | -| Function Name | min | avg | median | max | # calls | -| run | 4183805 | 4183805 | 4183805 | 4183805 | 1 | - - - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d874ba2..7339c6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,7 +140,7 @@ jobs: - name: Install Solidity run: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + wget https://github.com/ethereum/solidity/releases/download/v0.8.26/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc diff --git a/.solhint.json b/.solhint.json index ac7469e..25923e8 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,7 +2,7 @@ "extends": "solhint:recommended", "rules": { "code-complexity": ["error", 8], - "compiler-version": ["error", ">=0.8.19"], + "compiler-version": ["error", ">=0.8.27"], "func-name-mixedcase": "off", "func-visibility": ["error", { "ignoreConstructors": true }], "max-line-length": ["error", 120], diff --git a/certora/confs/MaxMPRule.conf b/certora/confs/MaxMPRule.conf index 73399f0..f37d5f3 100644 --- a/certora/confs/MaxMPRule.conf +++ b/certora/confs/MaxMPRule.conf @@ -6,7 +6,7 @@ ], "global_timeout": "7200", "link": [ - "StakeManager:stakedToken=ERC20A", + "StakeManager:STAKING_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", ], "loop_iter": "3", diff --git a/certora/confs/StakeManager.conf b/certora/confs/StakeManager.conf index 85d9f73..ce3f004 100644 --- a/certora/confs/StakeManager.conf +++ b/certora/confs/StakeManager.conf @@ -5,7 +5,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:rewardToken=ERC20A", + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager.sol", diff --git a/certora/confs/StakeManagerProcess.conf b/certora/confs/StakeManagerProcess.conf index 0c949c1..67b061d 100644 --- a/certora/confs/StakeManagerProcess.conf +++ b/certora/confs/StakeManagerProcess.conf @@ -5,7 +5,7 @@ "certora/helpers/ExpiredStakeStorageA.sol" ], "link" : [ - "StakeManager:rewardToken=ERC20A", + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager ProcessAccount", diff --git a/certora/confs/StakeManagerStartMigration.conf b/certora/confs/StakeManagerStartMigration.conf index 12f382a..e6e6761 100644 --- a/certora/confs/StakeManagerStartMigration.conf +++ b/certora/confs/StakeManagerStartMigration.conf @@ -6,7 +6,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:rewardToken=ERC20A", + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", ], "msg": "Verifying StakeManager.sol", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index 3bf9483..f78f918 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -7,7 +7,7 @@ ], "link" : [ "StakeVault:STAKED_TOKEN=ERC20A", - "StakeManager:rewardToken=ERC20A", + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", "StakeVault:stakeManager=StakeManager" ], diff --git a/certora/harness/StakeManagerNew.sol b/certora/harness/StakeManagerNew.sol index 1080a26..1899408 100644 --- a/certora/harness/StakeManagerNew.sol +++ b/certora/harness/StakeManagerNew.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; -import {StakeManager} from "../../contracts/StakeManager.sol"; +import { StakeManager } from "../../contracts/StakeManager.sol"; contract StakeManagerNew is StakeManager { - constructor(address token, address oldManager) StakeManager(token, oldManager) {} + constructor(address token, address oldManager) StakeManager(token, oldManager) { } } diff --git a/certora/helpers/ERC20A.sol b/certora/helpers/ERC20A.sol index cabfa08..0078928 100644 --- a/certora/helpers/ERC20A.sol +++ b/certora/helpers/ERC20A.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract ERC20A is ERC20 { - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { } } diff --git a/certora/helpers/ExpiredStakeStorageA.sol b/certora/helpers/ExpiredStakeStorageA.sol index 9787836..741fb68 100644 --- a/certora/helpers/ExpiredStakeStorageA.sol +++ b/certora/helpers/ExpiredStakeStorageA.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ExpiredStakeStorage } from "./../../contracts/storage/ExpiredStakeStorage.sol"; diff --git a/contracts/IStakeManager.sol b/contracts/IStakeManager.sol deleted file mode 100644 index e2f9cf6..0000000 --- a/contracts/IStakeManager.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ITrustedCodehashAccess } from "./access/ITrustedCodehashAccess.sol"; - -interface IStakeManager is ITrustedCodehashAccess { - error StakeManager__FundsLocked(); - error StakeManager__InvalidLockTime(); - error StakeManager__InsufficientFunds(); - error StakeManager__StakeIsTooLow(); - - function MIN_LOCKUP_PERIOD() external pure returns (uint256); - function MAX_LOCKUP_PERIOD() external pure returns (uint256); - - function stake(uint256 _amount, uint256 _seconds) external; - function unstake(uint256 _amount) external; - function lock(uint256 _secondsIncrease) external; - - function acceptUpdate() external returns (IStakeManager _migrated); - function leave() external returns (bool _leaveAccepted); - - function totalStaked() external view returns (uint256 _totalStaked); - function getStakedBalance(address _vault) external view returns (uint256 _balance); - function potentialMP() external view returns (uint256 _potentialMP); - function totalMP() external view returns (uint256 _totalMP); - - function totalSupply() external view returns (uint256 _totalSupply); - function totalSupplyMinted() external view returns (uint256 _totalSupply); - function pendingReward() external view returns (uint256 _pendingReward); - - function calculateMP(uint256 _balance, uint256 _deltaTime) external pure returns (uint256); -} diff --git a/contracts/StakeVault.sol b/contracts/StakeVault.sol index f4603a1..b62b229 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IStakeManager } from "./IStakeManager.sol"; +import { StakeManager } from "./StakeManager.sol"; /** * @title StakeVault @@ -20,18 +21,18 @@ contract StakeVault is Ownable { IStakeManager private stakeManager; - IERC20 public immutable stakedToken; + IERC20 public immutable STAKING_TOKEN; event Staked(address from, address to, uint256 _amount, uint256 time); - constructor(address _owner, IERC20 _stakedToken, IStakeManager _stakeManager) { + constructor(address _owner, IERC20 _STAKING_TOKEN, IStakeManager _stakeManager) { _transferOwnership(_owner); - stakedToken = _stakedToken; + STAKING_TOKEN = _STAKING_TOKEN; stakeManager = _stakeManager; } function stake(uint256 _amount, uint256 _time) external onlyOwner { - bool success = stakedToken.transferFrom(msg.sender, address(this), _amount); + bool success = STAKING_TOKEN.transferFrom(msg.sender, address(this), _amount); if (!success) { revert StakeVault__StakingFailed(); } @@ -46,15 +47,15 @@ contract StakeVault is Ownable { function unstake(uint256 _amount) external onlyOwner { stakeManager.unstake(_amount); - bool success = stakedToken.transfer(msg.sender, _amount); + bool success = STAKING_TOKEN.transfer(msg.sender, _amount); if (!success) { revert StakeVault__UnstakingFailed(); } } function leave() external onlyOwner { - if (stakeManager.leave()) { - stakedToken.transferFrom(address(this), msg.sender, stakedToken.balanceOf(address(this))); + if (StakeManager(stakeManager).leave()) { + STAKING_TOKEN.transferFrom(address(this), msg.sender, STAKING_TOKEN.balanceOf(address(this))); } } @@ -62,7 +63,7 @@ contract StakeVault is Ownable { * @notice Opt-in migration to a new IStakeManager contract. */ function acceptMigration() external onlyOwner { - IStakeManager migrated = stakeManager.acceptUpdate(); + IStakeManager migrated = StakeManager(stakeManager).acceptUpdate(); if (address(migrated) == address(0)) revert StakeVault__MigrationNotAvailable(); stakeManager = migrated; } diff --git a/contracts/VaultFactory.sol b/contracts/VaultFactory.sol index b165bad..f80880f 100644 --- a/contracts/VaultFactory.sol +++ b/contracts/VaultFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { StakeManager } from "./StakeManager.sol"; @@ -57,7 +57,7 @@ contract VaultFactory is Ownable2Step { /// @dev Anyone can call this function. /// @dev Emits a {VaultCreated} event. function createVault() external returns (StakeVault) { - StakeVault vault = new StakeVault(msg.sender, stakeManager.rewardToken(), stakeManager); + StakeVault vault = new StakeVault(msg.sender, stakeManager.REWARD_TOKEN(), stakeManager); emit VaultCreated(address(vault), msg.sender); return vault; } diff --git a/contracts/access/TrustedCodehashAccess.sol b/contracts/access/TrustedCodehashAccess.sol index 3d3c4f2..e5f5de6 100644 --- a/contracts/access/TrustedCodehashAccess.sol +++ b/contracts/access/TrustedCodehashAccess.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; +import { ITrustedCodehashAccess } from "../interfaces/ITrustedCodehashAccess.sol"; /** * @title TrustedCodehashAccess diff --git a/contracts/factory/Create2FactoryLib.sol b/contracts/factory/Create2FactoryLib.sol new file mode 100644 index 0000000..4039364 --- /dev/null +++ b/contracts/factory/Create2FactoryLib.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.26; + +/** + * @title Singleton Factory (EIP-2470) + * @notice Exposes CREATE2 (EIP-1014) to deploy bytecode on deterministic addresses based on initialization code and + * salt. + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + */ +library AddressLib { + error ERC2470__CREATE2Failed(); + error ERC2470__CREATE2BadCall(); + + function deploy(bytes memory _initCode, bytes32 _salt) internal returns (address payable createdContract) { + assembly { + createdContract := create2(callvalue(), add(_initCode, 0x20), mload(_initCode), _salt) + } + if (createdContract == address(0)) { + revert ERC2470__CREATE2Failed(); + } + } + + function computeAddress(bytes memory, bytes32 _salt) public view returns (address payable) { + return payable(hashToAddress(abi.encodePacked(bytes1(0xff), address(this), _salt, _initCode))); + } + + function computeAddress(address _deployer, bytes32 _salt, bytes memory _initCode) public pure returns (address) { + return hashToAddress(abi.encodePacked(bytes1(0xff), _deployer, _salt, _initCode)); + } + + function hashToAddress(bytes memory b) private pure returns (address addr) { + return address(uint160(uint256(keccak256(b)))); + } + + function addressToBytes(address a) internal pure returns (bytes memory) { + return abi.encodePacked(a); + } + + function bytesToAddress(bytes memory b) external pure returns (address addr) { + return abi.decode(b, (address)); + } +} diff --git a/contracts/factory/SingletonFactory.sol b/contracts/factory/SingletonFactory.sol index fc436ad..6882ef6 100644 --- a/contracts/factory/SingletonFactory.sol +++ b/contracts/factory/SingletonFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; /** * @title Singleton Factory (EIP-2470) diff --git a/contracts/interfaces/IStakeManager.sol b/contracts/interfaces/IStakeManager.sol new file mode 100644 index 0000000..ac75683 --- /dev/null +++ b/contracts/interfaces/IStakeManager.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; + +interface IStakeManager is ITrustedCodehashAccess { + error StakeManager__FundsLocked(); + error StakeManager__InvalidLockTime(); + error StakeManager__InsufficientFunds(); + error StakeManager__StakeIsTooLow(); + + function stake(uint256 _amount, uint256 _seconds) external; + function lock(uint256 _seconds) external; + function unstake(uint256 _amount) external; + + function totalStaked() external view returns (uint256); + function totalMP() external view returns (uint256); + function totalMaxMP() external view returns (uint256); + function getStakedBalance(address _vault) external view returns (uint256 _balance); + + function STAKING_TOKEN() external view returns (IERC20); + function REWARD_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/contracts/access/ITrustedCodehashAccess.sol b/contracts/interfaces/ITrustedCodehashAccess.sol similarity index 97% rename from contracts/access/ITrustedCodehashAccess.sol rename to contracts/interfaces/ITrustedCodehashAccess.sol index 66ae6a5..303b002 100644 --- a/contracts/access/ITrustedCodehashAccess.sol +++ b/contracts/interfaces/ITrustedCodehashAccess.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; /** * @title TrustedCodehashAccess diff --git a/contracts/storage/ExpiredStakeStorage.sol b/contracts/storage/ExpiredStakeStorage.sol index 656effa..a544ff4 100644 --- a/contracts/storage/ExpiredStakeStorage.sol +++ b/contracts/storage/ExpiredStakeStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/foundry.toml b/foundry.toml index ab51352..6caf35c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,7 +13,7 @@ optimizer_runs = 10_000 out = "out" script = "script" - solc = "0.8.20" + solc = "0.8.26" src = "contracts" test = "test" diff --git a/script/Base.s.sol b/script/Base.s.sol index 2a106d7..ad9234c 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.19 <=0.9.0; +pragma solidity >=0.8.26 <=0.9.0; import { Script } from "forge-std/Script.sol"; diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5130227..15417e5 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <=0.9.0; +pragma solidity >=0.8.26 <=0.9.0; import { BaseScript } from "./Base.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol"; diff --git a/script/DeployMigrationStakeManager.s.sol b/script/DeployMigrationStakeManager.s.sol index 1b2dfd8..60b42bb 100644 --- a/script/DeployMigrationStakeManager.s.sol +++ b/script/DeployMigrationStakeManager.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <=0.9.0; +pragma solidity >=0.8.26 <=0.9.0; import { BaseScript } from "./Base.s.sol"; import { StakeManager } from "../contracts/StakeManager.sol"; @@ -18,7 +18,7 @@ contract DeployMigrationStakeManager is BaseScript { function run() public returns (StakeManager) { prevStakeManager = vm.envOr({ name: "PREV_STAKE_MANAGER", defaultValue: prevStakeManager }); - stakeToken = vm.envOr({ name: "STAKE_TOKEN_ADDRESS", defaultValue: stakeToken }); + stakeToken = vm.envOr({ name: "STAKING_TOKEN_ADDRESS", defaultValue: stakeToken }); if (stakeToken == address(0)) { revert DeployMigrationStakeManager_InvalidStakeTokenAddress(); diff --git a/script/DeploymentConfig.s.sol b/script/DeploymentConfig.s.sol index 68a22f2..6012c1e 100644 --- a/script/DeploymentConfig.s.sol +++ b/script/DeploymentConfig.s.sol @@ -1,6 +1,6 @@ //// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <=0.9.0; +pragma solidity >=0.8.26 <=0.9.0; import { Script } from "forge-std/Script.sol"; import { MockERC20 } from "../test/mocks/MockERC20.sol"; diff --git a/test/DynamicTest.t.sol b/test/DynamicTest.t.sol index ed607e4..983ba45 100644 --- a/test/DynamicTest.t.sol +++ b/test/DynamicTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index b72fdc3..802406d 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -35,7 +35,7 @@ contract StakeManagerTest is Test { assertEq(stakeManager.pendingReward(), 0); assertEq(stakeManager.totalMP(), 0); assertEq(stakeManager.totalStaked(), 0); - assertEq(address(stakeManager.rewardToken()), stakeToken); + assertEq(address(stakeManager.REWARD_TOKEN()), stakeToken); assertEq(address(stakeManager.previousManager()), address(0)); assertEq(stakeManager.totalSupply(), 0); } @@ -770,7 +770,7 @@ contract MigrationStakeManagerTest is StakeManagerTest { assertEq(newStakeManager.pendingReward(), 0); assertEq(newStakeManager.totalMP(), 0); assertEq(newStakeManager.totalStaked(), 0); - assertEq(address(newStakeManager.rewardToken()), stakeToken); + assertEq(address(newStakeManager.REWARD_TOKEN()), stakeToken); assertEq(address(newStakeManager.previousManager()), address(stakeManager)); assertEq(newStakeManager.totalSupply(), 0); } diff --git a/test/StakeVault.t.sol b/test/StakeVault.t.sol index bd7841b..38270d5 100644 --- a/test/StakeVault.t.sol +++ b/test/StakeVault.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -42,7 +42,7 @@ contract StakedTokenTest is StakeVaultTest { } function testStakeToken() public { - assertEq(address(stakeVault.stakedToken()), stakeToken); + assertEq(address(stakeVault.STAKING_TOKEN()), stakeToken); } } diff --git a/test/VaultFactory.t.sol b/test/VaultFactory.t.sol index 2a0a333..48550d9 100644 --- a/test/VaultFactory.t.sol +++ b/test/VaultFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; @@ -18,14 +18,14 @@ contract VaultFactoryTest is Test { address internal deployer; - address internal stakedToken; + address internal STAKING_TOKEN; address internal testUser = makeAddr("testUser"); function setUp() public virtual { Deploy deployment = new Deploy(); (vaultFactory, stakeManager, deploymentConfig) = deployment.run(); - (deployer, stakedToken) = deploymentConfig.activeNetworkConfig(); + (deployer, STAKING_TOKEN) = deploymentConfig.activeNetworkConfig(); } function testDeployment() public { @@ -67,6 +67,6 @@ contract CreateVaultTest is VaultFactoryTest { emit VaultCreated(makeAddr("some address"), testUser); StakeVault vault = vaultFactory.createVault(); assertEq(vault.owner(), testUser); - assertEq(address(vault.stakedToken()), address(stakedToken)); + assertEq(address(vault.STAKING_TOKEN()), address(STAKING_TOKEN)); } } diff --git a/test/mocks/BrokenERC20.s.sol b/test/mocks/BrokenERC20.s.sol index 4b31604..6d11323 100644 --- a/test/mocks/BrokenERC20.s.sol +++ b/test/mocks/BrokenERC20.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol index dcff6c5..81927ee 100644 --- a/test/mocks/MockERC20.sol +++ b/test/mocks/MockERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/script/DeployBroken.s.sol b/test/script/DeployBroken.s.sol index e2105e1..17b4b76 100644 --- a/test/script/DeployBroken.s.sol +++ b/test/script/DeployBroken.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +pragma solidity ^0.8.26; import { BaseScript } from "../../script/Base.s.sol"; import { StakeManager } from "../../contracts/StakeManager.sol"; From 9be84c707ac7c5046d89c7bc93e0e9b4c87a255a Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 8 Nov 2024 00:57:26 -0300 Subject: [PATCH 05/13] update math --- contracts/MultiplierPointMath.sol | 128 ++++++++++++++++++++----- contracts/StakeMath.sol | 153 ++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 contracts/StakeMath.sol diff --git a/contracts/MultiplierPointMath.sol b/contracts/MultiplierPointMath.sol index 0907036..4de8a07 100644 --- a/contracts/MultiplierPointMath.sol +++ b/contracts/MultiplierPointMath.sol @@ -1,51 +1,135 @@ // SPDX-License-Identifier: MIT-1.0 -pragma solidity ^0.8.18; +pragma solidity ^0.8.26; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; abstract contract MultiplierPointMath { - uint256 public constant YEAR = 365 days; - uint256 public constant MP_APY = 1; + /// @notice One (mean) tropical year, in seconds. + uint256 public constant YEAR = 365 days + 5 hours + 48 minutes + 45 seconds; + /// @notice Multiplier points annual percentage yield. + uint256 public constant MP_APY = 100; + /// @notice Accrued multiplier points maximum multiplier. uint256 public constant MAX_MULTIPLIER = 4; + /// @notice The accrue rate period of time over which multiplier points are calculated. + uint256 public constant ACCURE_RATE = 1 weeks; + /// @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 * ACCURE_RATE)) + 1; + /// @notice Multiplier points absolute maximum multiplier + uint256 public constant MAX_MULTIPLIER_ABSOLUTE = 1 + (2 * (MAX_MULTIPLIER * MP_APY) / 100); + /// @notice Maximum lockup period + uint256 public constant MAX_LOCKUP_PERIOD = MAX_MULTIPLIER * YEAR; /** - * @notice Calculates multiplier points accurred for given `_amount` and `_seconds` time passed - * @param _amount quantity of tokens - * @param _seconds time in seconds + * @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 _accuredMP points accured for given `_amount` and `_seconds` + * 51584438 + * 10000000 */ - function _calculateAccuredMP(uint256 _amount, uint256 _seconds) internal pure returns (uint256 _accuredMP) { - return Math.mulDiv(_amount, _seconds, YEAR) * MP_APY; + function _calculateAccuredMP(uint256 _balance, uint256 _deltaTime) public pure returns (uint256 _accuredMP) { + return Math.mulDiv(_balance, _deltaTime * MP_APY, YEAR * 100); } /** - * @notice Calculates bonus multiplier points for given `_amount` and `_lockedSeconds` + * @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 _amount quantity of tokens * @param _lockedSeconds time in seconds locked * @return _bonusMP bonus multiplier points for given `_amount` and `_lockedSeconds` */ - function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) internal pure returns (uint256 _bonusMP) { - _bonusMP = _amount; - if (_lockedSeconds > 0) { - _bonusMP += _calculateAccuredMP(_amount, _lockedSeconds); - } + function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) public pure returns (uint256 _bonusMP) { + return _calculateAccuredMP(_amount, _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 _amount Represents the change in balance. + */ + function _calculateInitialMP(uint256 _amount) public pure returns (uint256 _initialMP) { + return _amount; } /** - * @notice Calculates minimum stake to genarate 1 multiplier points for given `_seconds` - * @param _seconds time in seconds - * @return _minimumStake minimum quantity of tokens + * @notice Calculates the reduction in multiplier points (MPs) when a portion of the balance Δa `_reducedAmount` is + * removed from the total balance a_bal `_currentBalance`. + * The reduction is proportional to the ratio of the removed balance to the total balance, applied to the current + * multiplier points $mp$. + * @param _mp Represents the current multiplier points + * @param _currentBalance The total account balance before the removal of Δa `_reducedBalance` + * @param _reducedAmount reduced balance + * @return _reducedMP Multiplier points to reduce from `_mp` */ - function _calculateMinimumStake(uint256 _seconds) internal pure returns (uint256 _minimumStake) { - return YEAR / (_seconds * MP_APY); + function _calculateReducedMP( + uint256 _mp, + uint256 _currentBalance, + uint256 _reducedAmount + ) + public + pure + returns (uint256 _reducedMP) + { + return Math.mulDiv(_mp, _currentBalance, _reducedAmount); } /** * @notice Calculates maximum stake a given `_amount` can be generated with `MAX_MULTIPLIER` - * @param _amount quantity of tokens + * @param _balance quantity of tokens * @return _maxMPAccured maximum quantity of muliplier points that can be generated for given `_amount` */ - function _calculateMaxAccuredMP(uint256 _amount) internal pure returns (uint256 _maxMPAccured) { - return _calculateAccuredMP(_amount, MAX_MULTIPLIER * YEAR); + function _calculateMaxAccuredMP(uint256 _balance) public pure returns (uint256 _maxMPAccured) { + return Math.mulDiv(_balance, MAX_MULTIPLIER * MP_APY, 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 _calculateMaxMP(uint256 _balance, uint256 _lockTime) public pure returns (uint256 _maxMP) { + return _balance + Math.mulDiv(_balance * MP_APY, (MAX_MULTIPLIER * YEAR) + _lockTime, YEAR * 100); + } + + /** + * @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 _currentBalance Current balance used to calculate the maximum multiplier points. + */ + function _estimateLockTime(uint256 _mpMax, uint256 _currentBalance) public pure returns (uint256 _lockTime) { + return Math.mulDiv((_mpMax - _currentBalance) * 100, YEAR, _currentBalance * MP_APY, Math.Rounding.Ceil) + - MAX_LOCKUP_PERIOD; + } + + /** + * @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 `_currentBalance` + * @param _mpMax Maximum multiplier points calculated from the current balance. + * @param _currentBalance Current balance used to calculate the maximum multiplier points. + */ + function _remainingLockTimeAvailable( + uint256 _mpMax, + uint256 _currentBalance + ) + public + pure + returns (uint256 _lockTime) + { + return Math.mulDiv((_currentBalance * MAX_MULTIPLIER_ABSOLUTE) - _mpMax, YEAR, _currentBalance); + } + + /** + * @notice Calculates the lock time for a given bonus multiplier points and current balance. + * @param _bonusMP bonus multiplier points intended to be generated + * @param _currentBalance current balance + */ + function _calculateLockTime(uint256 _bonusMP, uint256 _currentBalance) public pure returns (uint256 _lockTime) { + return Math.mulDiv(_bonusMP * 100, YEAR, _currentBalance * MP_APY); } } diff --git a/contracts/StakeMath.sol b/contracts/StakeMath.sol new file mode 100644 index 0000000..d427bbc --- /dev/null +++ b/contracts/StakeMath.sol @@ -0,0 +1,153 @@ +// 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"; + +abstract contract StakeMath is MultiplierPointMath { + /// @notice Minimal lockup time + uint256 public constant MIN_LOCKUP_TIME = 1 weeks; + + /** + * @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 _maxMP Account current max multiplier points + * @param _lockEndTime 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 _newMaxMP Account new max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function _calculateStake( + uint256 _balance, + uint256 _maxMP, + uint256 _lockEndTime, + uint256 _processTime, + uint256 _increasedAmount, + uint256 _increasedLockSeconds + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) + { + uint256 newBalance = _balance + _increasedAmount; + require(newBalance >= MIN_BALANCE, "StakeMath: balance too low"); + _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); + + uint256 deltaMpBonus; + if (dt_lock > 0) { + deltaMpBonus = _calculateBonusMP(_increasedAmount, dt_lock); + } + + if (_balance > 0 && _increasedLockSeconds > 0) { + deltaMpBonus += _calculateBonusMP(_balance, _increasedLockSeconds); + } + + _deltaMpTotal = _calculateInitialMP(_increasedAmount) + deltaMpBonus; + _newMaxMP = _maxMP + _deltaMpTotal + _calculateAccuredMP(_balance, MAX_MULTIPLIER * YEAR); + + require( + _newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance + _increasedAmount), "StakeMath: max multiplier exceeded" + ); + } + + /** + * @notice Calculates the bonus multiplier points earned when a balance Δa is locked for a specified duration + * @param _balance Account current balance + * @param _maxMP Account current max multiplier points + * @param _lockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _increasedLockSeconds Increased amount of seconds to lock + * @return _deltaMpTotal Increased amount of total multiplier points + * @return _newMaxMP Account new max multiplier points + * @return _newLockEnd Account new lock end timestamp + */ + function calculateLock( + uint256 _balance, + uint256 _maxMP, + uint256 _lockEndTime, + uint256 _processTime, + uint256 _increasedLockSeconds + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) + { + require(_balance > 0); + require(_increasedLockSeconds > 0); + + _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; + uint256 dt_lock = _newLockEnd - _processTime; + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); + + _deltaMpTotal += _calculateBonusMP(_balance, _increasedLockSeconds); + _newMaxMP = _maxMP + _deltaMpTotal; + + require(_newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance), "StakeMath: max multiplier exceeded"); + } + + /** + * + * @param _balance Account current balance + * @param _lockEndTime Account current lock end timestamp + * @param _processTime Process current timestamp + * @param _totalMP Account current total multiplier points + * @param _maxMP 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 _lockEndTime, + uint256 _processTime, + uint256 _totalMP, + uint256 _maxMP, + uint256 _reducedAmount + ) + public + pure + returns (uint256 _deltaMpTotal, uint256 _deltaMpMax) + { + require(_lockEndTime <= _processTime, "StakeMath: lockup not ended"); + require(_balance >= _reducedAmount, "StakeMath: balance too low"); + uint256 newBalance = _balance - _reducedAmount; + require(newBalance == 0 || newBalance >= MIN_BALANCE, "StakeMath: balance too low"); + _deltaMpTotal = _calculateReducedMP(_totalMP, _balance, _reducedAmount); + _deltaMpMax = _calculateReducedMP(_maxMP, _balance, _reducedAmount); + } + + /** + * @notice Calculates the accrued multiplier points for a given balance and seconds passed since last accrual + * @param _balance Account current balance + * @param _totalMP Account current total multiplier points + * @param _maxMP 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 _totalMP, + uint256 _maxMP, + uint256 _lastAccrualTime, + uint256 _processTime + ) + public + pure + returns (uint256 _deltaMpTotal) + { + uint256 dt = _processTime - _lastAccrualTime; + require(dt >= ACCURE_RATE, "StakeMath: no enough time passed"); + if (_totalMP <= _maxMP) { + _deltaMpTotal = Math.min(_calculateAccuredMP(_balance, dt), _maxMP - _totalMP); + } + } +} From 0aa3f2e3d2477c524d3b20bf0d92b73ba985ac23 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Mon, 11 Nov 2024 19:30:50 -0300 Subject: [PATCH 06/13] chore: update interface and update Solidity version to 0.8.27 across all files --- .gas-report | 127 ++++++++++++++++++ .gas-snapshot | 122 ++++++++--------- .github/workflows/ci.yml | 2 +- certora/harness/StakeManagerNew.sol | 2 +- certora/helpers/ERC20A.sol | 2 +- certora/helpers/ExpiredStakeStorageA.sol | 2 +- contracts/MultiplierPointMath.sol | 48 +++---- contracts/StakeManager.sol | 37 +++-- contracts/StakeMath.sol | 35 +++-- contracts/StakeVault.sol | 8 +- contracts/VaultFactory.sol | 2 +- contracts/access/TrustedCodehashAccess.sol | 2 +- contracts/factory/Create2FactoryLib.sol | 6 +- contracts/factory/SingletonFactory.sol | 2 +- contracts/interfaces/IStakeConstants.sol | 12 ++ contracts/interfaces/IStakeManager.sol | 17 +-- .../interfaces/ITrustedCodehashAccess.sol | 2 +- contracts/storage/ExpiredStakeStorage.sol | 2 +- foundry.toml | 2 +- script/Base.s.sol | 2 +- script/Deploy.s.sol | 2 +- script/DeployMigrationStakeManager.s.sol | 2 +- script/DeploymentConfig.s.sol | 2 +- test/DynamicTest.t.sol | 8 +- test/StakeManager.t.sol | 49 +++---- test/StakeVault.t.sol | 2 +- test/VaultFactory.t.sol | 2 +- test/mocks/BrokenERC20.s.sol | 2 +- test/mocks/MockERC20.sol | 2 +- test/script/DeployBroken.s.sol | 2 +- 30 files changed, 319 insertions(+), 188 deletions(-) create mode 100644 contracts/interfaces/IStakeConstants.sol diff --git a/.gas-report b/.gas-report index e69de29..5f7b853 100644 --- a/.gas-report +++ b/.gas-report @@ -0,0 +1,127 @@ +| contracts/StakeManager.sol:StakeManager contract | | | | | | +|--------------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 2600451 | 13644 | | | | | +| Function Name | min | avg | median | max | # calls | +| ACCURE_RATE | 328 | 328 | 328 | 328 | 1546 | +| MAX_LOCKUP_PERIOD | 405 | 405 | 405 | 405 | 4 | +| MAX_MULTIPLIER | 307 | 307 | 307 | 307 | 685 | +| MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 12 | +| REWARD_TOKEN | 317 | 317 | 317 | 317 | 744 | +| YEAR | 308 | 308 | 308 | 308 | 685 | +| acceptUpdate | 23633 | 23633 | 23633 | 23633 | 1 | +| accounts | 1572 | 1572 | 1572 | 1572 | 154869 | +| calculateMP | 798 | 798 | 798 | 798 | 1372 | +| currentEpoch | 384 | 1050 | 384 | 2384 | 54 | +| epochEnd | 627 | 627 | 627 | 2627 | 25441 | +| epochReward | 1381 | 2881 | 1381 | 5881 | 3 | +| executeAccount(address) | 33469 | 110665 | 149264 | 149264 | 3 | +| executeAccount(address,uint256) | 26562 | 72195 | 74101 | 199919 | 152455 | +| executeEpoch() | 23435 | 120639 | 121820 | 900335 | 25330 | +| executeEpoch(uint256) | 23861 | 24497 | 23861 | 26090 | 7 | +| expiredStakeStorage | 416 | 2325 | 2416 | 2416 | 22 | +| isTrustedCodehash | 541 | 944 | 541 | 2541 | 728 | +| leave | 23675 | 23675 | 23675 | 23675 | 1 | +| lock | 23840 | 23840 | 23840 | 23840 | 1 | +| migration | 417 | 1417 | 1417 | 2417 | 4 | +| migrationInitialize | 24646 | 24646 | 24646 | 24646 | 1 | +| newEpoch | 463 | 463 | 463 | 463 | 5 | +| owner | 2432 | 2432 | 2432 | 2432 | 13 | +| pendingReward | 408 | 1442 | 2408 | 2408 | 29 | +| potentialMP | 408 | 408 | 408 | 408 | 49964 | +| previousManager | 297 | 297 | 297 | 297 | 13 | +| setTrustedCodehash | 47982 | 47982 | 47982 | 47982 | 147 | +| stake | 24047 | 24047 | 24047 | 24047 | 1 | +| startMigration | 103624 | 103632 | 103636 | 103636 | 3 | +| startTime | 306 | 306 | 306 | 306 | 21 | +| totalMP | 385 | 385 | 385 | 2385 | 49985 | +| totalStaked | 386 | 1786 | 2386 | 2386 | 20 | +| totalSupply | 784 | 1965 | 2784 | 2784 | 22 | +| unstake | 23841 | 23841 | 23841 | 23841 | 1 | + + +| contracts/StakeVault.sol:StakeVault contract | | | | | | +|----------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| STAKING_TOKEN | 193 | 193 | 193 | 193 | 2 | +| acceptMigration | 35141 | 35141 | 35141 | 35141 | 2 | +| leave | 35196 | 35196 | 35196 | 35196 | 1 | +| lock | 43196 | 71076 | 62042 | 162843 | 7 | +| owner | 318 | 318 | 318 | 318 | 727 | +| stake | 27291 | 282802 | 266296 | 351880 | 732 | +| unstake | 40179 | 88236 | 78765 | 184769 | 11 | + + +| contracts/VaultFactory.sol:VaultFactory contract | | | | | | +|--------------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| createVault | 682927 | 682927 | 682927 | 682927 | 731 | +| setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | +| stakeManager | 368 | 1868 | 2368 | 2368 | 4 | + + +| contracts/storage/ExpiredStakeStorage.sol:ExpiredStakeStorage contract | | | | | | +|------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25486 | +| transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | + + +| script/Deploy.s.sol:Deploy contract | | | | | | +|-------------------------------------|-----------------|---------|---------|---------|---------| +| Deployment Cost | Deployment Size | | | | | +| 6220738 | 29931 | | | | | +| Function Name | min | avg | median | max | # calls | +| run | 5420174 | 5420174 | 5420174 | 5420174 | 66 | + + +| script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | +|-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| +| Deployment Cost | Deployment Size | | | | | +| 3412741 | 16827 | | | | | +| Function Name | min | avg | median | max | # calls | +| run | 2427736 | 2427736 | 2427736 | 2427736 | 19 | + + +| script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | +|---------------------------------------------------------|-----------------|-----|--------|-----|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| activeNetworkConfig | 455 | 455 | 455 | 455 | 132 | + + +| test/mocks/BrokenERC20.s.sol:BrokenERC20 contract | | | | | | +|---------------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| approve | 46175 | 46175 | 46175 | 46175 | 1 | +| balanceOf | 561 | 1227 | 561 | 2561 | 3 | + + +| test/mocks/MockERC20.sol:MockERC20 contract | | | | | | +|---------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| approve | 46175 | 46235 | 46199 | 46367 | 727 | +| balanceOf | 561 | 2127 | 2561 | 2561 | 32612 | + + +| test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | +|------------------------------------------------------|-----------------|---------|---------|---------|---------| +| Deployment Cost | Deployment Size | | | | | +| 4918894 | 23867 | | | | | +| Function Name | min | avg | median | max | # calls | +| run | 4266410 | 4266410 | 4266410 | 4266410 | 1 | + + + + diff --git a/.gas-snapshot b/.gas-snapshot index cd6098a..1b5a51d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ CreateVaultTest:testDeployment() (gas: 9774) -CreateVaultTest:test_createVault() (gas: 699564) -ExecuteAccountTest:testDeployment() (gas: 28828) -ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1564735) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5252076) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1772992) -ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 320738328) -ExecuteEpochTest:testDeployment() (gas: 28829) -ExecuteEpochTest:testNewDeployment() (gas: 30901) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1353515) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1371040) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1616635) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1380975) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1914097) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2496675) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1464542) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2506631) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1474454) -ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083709) -ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92344) -ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 256303) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 39028) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149770) -LeaveTest:testDeployment() (gas: 28806) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1315087) -LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31696) -LockTest:testDeployment() (gas: 28806) -LockTest:test_NewLockupPeriod() (gas: 1313929) -LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1288722) -LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1529335) -LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31856) -LockTest:test_ShouldIncreaseBonusMP() (gas: 1296480) -LockTest:test_UpdateLockupPeriod() (gas: 1565467) -MigrateTest:testDeployment() (gas: 28806) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1279254) -MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31738) -MigrationInitializeTest:testDeployment() (gas: 28806) -MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5275163) -MigrationStakeManagerTest:testDeployment() (gas: 28806) -MigrationStakeManagerTest:testNewDeployment() (gas: 30945) -MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149735) +CreateVaultTest:test_createVault() (gas: 700333) +ExecuteAccountTest:testDeployment() (gas: 28875) +ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1559514) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5232040) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1769214) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 320225995) +ExecuteEpochTest:testDeployment() (gas: 28876) +ExecuteEpochTest:testNewDeployment() (gas: 30948) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1350443) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1367228) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1613317) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1377593) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1910717) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2492323) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1460710) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2502709) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1471052) +ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083615) +ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92277) +ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 252659) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 38961) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149703) +LeaveTest:testDeployment() (gas: 28853) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1311974) +LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31740) +LockTest:testDeployment() (gas: 28853) +LockTest:test_NewLockupPeriod() (gas: 1310978) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1285586) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1406860) +LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31834) +LockTest:test_ShouldIncreaseBonusMP() (gas: 1293620) +LockTest:test_UpdateLockupPeriod() (gas: 1470415) +MigrateTest:testDeployment() (gas: 28853) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1276097) +MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31739) +MigrationInitializeTest:testDeployment() (gas: 28853) +MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5451158) +MigrationStakeManagerTest:testDeployment() (gas: 28853) +MigrationStakeManagerTest:testNewDeployment() (gas: 30992) +MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149668) SetStakeManagerTest:testDeployment() (gas: 9774) SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 63105) SetStakeManagerTest:test_SetStakeManager() (gas: 41301) -StakeManagerTest:testDeployment() (gas: 28578) -StakeTest:testDeployment() (gas: 28784) -StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1061162) -StakeTest:test_RevertWhen_Restake() (gas: 1304182) -StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1308012) -StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32040) -StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 802919) -StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 211363) -StakeTest:test_StakeWithLockBonusMP() (gas: 2327824) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1300293) -StakedTokenTest:testStakeToken() (gas: 7619) -UnstakeTest:testDeployment() (gas: 28828) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1284745) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1329288) +StakeManagerTest:testDeployment() (gas: 28625) +StakeTest:testDeployment() (gas: 28831) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1057656) +StakeTest:test_RevertWhen_Restake() (gas: 1301092) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1304525) +StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32082) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803915) +StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 207758) +StakeTest:test_StakeWithLockBonusMP() (gas: 2323783) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1297379) +StakedTokenTest:testStakeToken() (gas: 7597) +UnstakeTest:testDeployment() (gas: 28875) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1281609) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1325777) UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31879) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6438180) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1306895) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1424419) -UserFlowsTest:testDeployment() (gas: 28806) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 130825949, ~: 130282915) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1463005) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2467653) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6432067) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1303800) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1387208) +UserFlowsTest:testDeployment() (gas: 28853) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 114, μ: 130857603, ~: 130246926) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1425816) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2461484) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7339c6d..077557b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,7 +140,7 @@ jobs: - name: Install Solidity run: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.26/solc-static-linux + wget https://github.com/ethereum/solidity/releases/download/v0.8.27/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc diff --git a/certora/harness/StakeManagerNew.sol b/certora/harness/StakeManagerNew.sol index 1899408..e3fa354 100644 --- a/certora/harness/StakeManagerNew.sol +++ b/certora/harness/StakeManagerNew.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { StakeManager } from "../../contracts/StakeManager.sol"; diff --git a/certora/helpers/ERC20A.sol b/certora/helpers/ERC20A.sol index 0078928..efcb59a 100644 --- a/certora/helpers/ERC20A.sol +++ b/certora/helpers/ERC20A.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/certora/helpers/ExpiredStakeStorageA.sol b/certora/helpers/ExpiredStakeStorageA.sol index 741fb68..37b9a78 100644 --- a/certora/helpers/ExpiredStakeStorageA.sol +++ b/certora/helpers/ExpiredStakeStorageA.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ExpiredStakeStorage } from "./../../contracts/storage/ExpiredStakeStorage.sol"; diff --git a/contracts/MultiplierPointMath.sol b/contracts/MultiplierPointMath.sol index 4de8a07..3a83b48 100644 --- a/contracts/MultiplierPointMath.sol +++ b/contracts/MultiplierPointMath.sol @@ -1,23 +1,24 @@ // SPDX-License-Identifier: MIT-1.0 -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IStakeConstants } from "./interfaces/IStakeConstants.sol"; -abstract contract MultiplierPointMath { +abstract contract MultiplierPointMath is IStakeConstants { /// @notice One (mean) tropical year, in seconds. uint256 public constant YEAR = 365 days + 5 hours + 48 minutes + 45 seconds; - /// @notice Multiplier points annual percentage yield. - uint256 public constant MP_APY = 100; /// @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 ACCURE_RATE = 1 weeks; /// @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 * ACCURE_RATE)) + 1; - /// @notice Multiplier points absolute maximum multiplier - uint256 public constant MAX_MULTIPLIER_ABSOLUTE = 1 + (2 * (MAX_MULTIPLIER * MP_APY) / 100); - /// @notice Maximum lockup period - uint256 public constant MAX_LOCKUP_PERIOD = MAX_MULTIPLIER * YEAR; /** * @notice Calculates the accrued multiplier points (MPs) over a time period Δt, based on the account balance @@ -25,10 +26,8 @@ abstract contract MultiplierPointMath { * @param _deltaTime The time difference or the duration over which the multiplier points are accrued, expressed in * seconds * @return _accuredMP points accured for given `_amount` and `_seconds` - * 51584438 - * 10000000 */ - function _calculateAccuredMP(uint256 _balance, uint256 _deltaTime) public pure returns (uint256 _accuredMP) { + function _calculateAccuredMP(uint256 _balance, uint256 _deltaTime) internal pure returns (uint256 _accuredMP) { return Math.mulDiv(_balance, _deltaTime * MP_APY, YEAR * 100); } @@ -41,7 +40,7 @@ abstract contract MultiplierPointMath { * @param _lockedSeconds time in seconds locked * @return _bonusMP bonus multiplier points for given `_amount` and `_lockedSeconds` */ - function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) public pure returns (uint256 _bonusMP) { + function _calculateBonusMP(uint256 _amount, uint256 _lockedSeconds) internal pure returns (uint256 _bonusMP) { return _calculateAccuredMP(_amount, _lockedSeconds); } @@ -50,7 +49,7 @@ abstract contract MultiplierPointMath { * the amount of balance added. * @param _amount Represents the change in balance. */ - function _calculateInitialMP(uint256 _amount) public pure returns (uint256 _initialMP) { + function _calculateInitialMP(uint256 _amount) internal pure returns (uint256 _initialMP) { return _amount; } @@ -81,8 +80,8 @@ abstract contract MultiplierPointMath { * @param _balance quantity of tokens * @return _maxMPAccured maximum quantity of muliplier points that can be generated for given `_amount` */ - function _calculateMaxAccuredMP(uint256 _balance) public pure returns (uint256 _maxMPAccured) { - return Math.mulDiv(_balance, MAX_MULTIPLIER * MP_APY, 100); + function _calculateMaxAccuredMP(uint256 _balance) internal pure returns (uint256 _maxMPAccured) { + return Math.mulDiv(_balance, MP_MPY, 100); } /** @@ -92,21 +91,10 @@ abstract contract MultiplierPointMath { * @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 _calculateMaxMP(uint256 _balance, uint256 _lockTime) public pure returns (uint256 _maxMP) { + function _calculateMaxMP(uint256 _balance, uint256 _lockTime) internal pure returns (uint256 _maxMP) { return _balance + Math.mulDiv(_balance * MP_APY, (MAX_MULTIPLIER * YEAR) + _lockTime, YEAR * 100); } - /** - * @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 _currentBalance Current balance used to calculate the maximum multiplier points. - */ - function _estimateLockTime(uint256 _mpMax, uint256 _currentBalance) public pure returns (uint256 _lockTime) { - return Math.mulDiv((_mpMax - _currentBalance) * 100, YEAR, _currentBalance * MP_APY, Math.Rounding.Ceil) - - MAX_LOCKUP_PERIOD; - } - /** * @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 `_currentBalance` @@ -121,15 +109,15 @@ abstract contract MultiplierPointMath { pure returns (uint256 _lockTime) { - return Math.mulDiv((_currentBalance * MAX_MULTIPLIER_ABSOLUTE) - _mpMax, YEAR, _currentBalance); + return Math.mulDiv((_currentBalance * MP_MPY_ABSOLUTE) - _mpMax, YEAR, _currentBalance * 100); } /** - * @notice Calculates the lock time for a given bonus multiplier points and current balance. + * @notice Calculates the lock time for a given bonus multiplier points\ and current balance. * @param _bonusMP bonus multiplier points intended to be generated * @param _currentBalance current balance */ - function _calculateLockTime(uint256 _bonusMP, uint256 _currentBalance) public pure returns (uint256 _lockTime) { + function _calculateLockTime(uint256 _bonusMP, uint256 _currentBalance) internal pure returns (uint256 _lockTime) { return Math.mulDiv(_bonusMP * 100, YEAR, _currentBalance * MP_APY); } } diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index 5179eae..9428ea7 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.27; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { TrustedCodehashAccess } from "./access/TrustedCodehashAccess.sol"; import { ExpiredStakeStorage } from "./storage/ExpiredStakeStorage.sol"; -import { IStakeManager } from "./IStakeManager.sol"; +import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { MultiplierPointMath } from "./MultiplierPointMath.sol"; +import { StakeMath } from "./StakeMath.sol"; import { StakeVault } from "./StakeVault.sol"; -contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAccess { +contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { error StakeManager__NoPendingMigration(); error StakeManager__PendingMigration(); error StakeManager__SenderIsNotPreviousStakeManager(); @@ -38,10 +39,6 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce uint256 potentialMP; } - uint256 public constant EPOCH_SIZE = 1 weeks; - uint256 public constant MIN_LOCKUP_PERIOD = 2 weeks; - uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR; // 4 years - mapping(address index => Account value) public accounts; mapping(uint256 index => Epoch value) public epochs; @@ -60,7 +57,8 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce StakeManager public migration; StakeManager public immutable previousManager; - IERC20 public immutable rewardToken; + IERC20 public immutable REWARD_TOKEN; + IERC20 public immutable STAKING_TOKEN; modifier onlyAccountInitialized(address account) { if (accounts[account].lockUntil == 0) { @@ -131,7 +129,8 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce constructor(address _REWARD_TOKEN, address _previousManager) { startTime = (_previousManager == address(0)) ? block.timestamp : StakeManager(_previousManager).startTime(); previousManager = StakeManager(_previousManager); - rewardToken = IERC20(_REWARD_TOKEN); + REWARD_TOKEN = IERC20(_REWARD_TOKEN); + STAKING_TOKEN = IERC20(_REWARD_TOKEN); if (address(previousManager) != address(0)) { expiredStakeStorage = previousManager.expiredStakeStorage(); } else { @@ -158,7 +157,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce } //mp estimation - uint256 mpPerEpoch = _calculateAccuredMP(_amount, EPOCH_SIZE); + uint256 mpPerEpoch = _calculateAccuredMP(_amount, ACCURE_RATE); if (mpPerEpoch < 1) { revert StakeManager__StakeIsTooLow(); } @@ -167,7 +166,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce uint256 epochAmountToReachMpLimit = (maxMpToMint) / mpPerEpoch; uint256 mpLimitEpoch = currentEpoch + epochAmountToReachMpLimit; uint256 lastEpochAmountToMint = ((mpPerEpoch * (epochAmountToReachMpLimit + 1)) - maxMpToMint); - uint256 bonusMP = _calculateBonusMP(_amount, _seconds); + uint256 bonusMP = _calculateInitialMP(_amount) + _calculateBonusMP(_amount, _seconds); // account initialization accounts[msg.sender] = Account({ @@ -212,7 +211,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce uint256 reducedMP = Math.mulDiv(_amount, account.totalMP, account.balance); uint256 reducedInitialMP = Math.mulDiv(_amount, account.bonusMP, account.balance); - uint256 mpPerEpoch = _calculateAccuredMP(account.balance, EPOCH_SIZE); + uint256 mpPerEpoch = _calculateAccuredMP(account.balance, ACCURE_RATE); expiredStakeStorage.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); if (account.mpLimitEpoch < currentEpoch) { totalMPPerEpoch -= mpPerEpoch; @@ -322,7 +321,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce revert StakeManager__InvalidMigration(); } migration = _migration; - rewardToken.transfer(address(migration), epochReward()); + REWARD_TOKEN.transfer(address(migration), epochReward()); expiredStakeStorage.transferOwnership(address(_migration)); migration.migrationInitialize( currentEpoch, totalMP, totalStaked, startTime, totalMPPerEpoch, potentialMP, currentEpochTotalExpiredMP @@ -368,7 +367,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce * @notice Transfer current epoch funds for migrated manager */ function transferNonPending() external onlyPendingMigration { - rewardToken.transfer(address(migration), epochReward()); + REWARD_TOKEN.transfer(address(migration), epochReward()); } /** @@ -448,7 +447,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce while (userEpoch < _limitEpoch) { Epoch storage iEpoch = epochs[userEpoch]; //mint multiplier points to that epoch - _mintMP(account, startTime + (EPOCH_SIZE * (userEpoch + 1)), iEpoch); + _mintMP(account, startTime + (ACCURE_RATE * (userEpoch + 1)), iEpoch); uint256 userSupply = account.balance + account.totalMP; uint256 userEpochReward = Math.mulDiv(userSupply, iEpoch.epochReward, iEpoch.totalSupply); userReward += userEpochReward; @@ -463,7 +462,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce account.epoch = userEpoch; if (userReward > 0) { pendingReward -= userReward; - rewardToken.transfer(account.rewardAddress, userReward); + REWARD_TOKEN.transfer(account.rewardAddress, userReward); } if (address(migration) != address(0)) { mpDifference = account.totalMP - mpDifference; @@ -565,7 +564,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce * @return _epochReward current epoch reward */ function epochReward() public view returns (uint256 _epochReward) { - return rewardToken.balanceOf(address(this)) - pendingReward; + return REWARD_TOKEN.balanceOf(address(this)) - pendingReward; } /** @@ -573,7 +572,7 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce * @return _epochEnd end time of current epoch */ function epochEnd() public view returns (uint256 _epochEnd) { - return startTime + (EPOCH_SIZE * (currentEpoch + 1)); + return startTime + (ACCURE_RATE * (currentEpoch + 1)); } /** @@ -581,6 +580,6 @@ contract StakeManager is IStakeManager, MultiplierPointMath, TrustedCodehashAcce * @return _newEpoch the number of the epoch after all epochs that can be processed */ function newEpoch() public view returns (uint256 _newEpoch) { - _newEpoch = (block.timestamp - startTime) / EPOCH_SIZE; + _newEpoch = (block.timestamp - startTime) / ACCURE_RATE; } } diff --git a/contracts/StakeMath.sol b/contracts/StakeMath.sol index d427bbc..939cd8c 100644 --- a/contracts/StakeMath.sol +++ b/contracts/StakeMath.sol @@ -1,12 +1,14 @@ // SPDX-License-Identifier: MIT-1.0 -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { MultiplierPointMath } from "./MultiplierPointMath.sol"; abstract contract StakeMath is MultiplierPointMath { /// @notice Minimal lockup time - uint256 public constant MIN_LOCKUP_TIME = 1 weeks; + uint256 public constant MIN_LOCKUP_PERIOD = 1 weeks; + /// @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 @@ -29,7 +31,7 @@ abstract contract StakeMath is MultiplierPointMath { uint256 _increasedAmount, uint256 _increasedLockSeconds ) - public + internal pure returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) { @@ -37,7 +39,7 @@ abstract contract StakeMath is MultiplierPointMath { require(newBalance >= MIN_BALANCE, "StakeMath: balance too low"); _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; uint256 dt_lock = _newLockEnd - _processTime; - require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_PERIOD, "StakeMath: lockup time too low"); require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); uint256 deltaMpBonus; @@ -52,9 +54,7 @@ abstract contract StakeMath is MultiplierPointMath { _deltaMpTotal = _calculateInitialMP(_increasedAmount) + deltaMpBonus; _newMaxMP = _maxMP + _deltaMpTotal + _calculateAccuredMP(_balance, MAX_MULTIPLIER * YEAR); - require( - _newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance + _increasedAmount), "StakeMath: max multiplier exceeded" - ); + require(_newMaxMP <= MP_MPY_ABSOLUTE * (_balance + _increasedAmount), "StakeMath: max multiplier exceeded"); } /** @@ -75,7 +75,7 @@ abstract contract StakeMath is MultiplierPointMath { uint256 _processTime, uint256 _increasedLockSeconds ) - public + internal pure returns (uint256 _deltaMpTotal, uint256 _newMaxMP, uint256 _newLockEnd) { @@ -84,13 +84,13 @@ abstract contract StakeMath is MultiplierPointMath { _newLockEnd = Math.max(_lockEndTime, _processTime) + _increasedLockSeconds; uint256 dt_lock = _newLockEnd - _processTime; - require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_TIME, "StakeMath: lockup time too low"); + require(dt_lock == 0 || dt_lock >= MIN_LOCKUP_PERIOD, "StakeMath: lockup time too low"); require(dt_lock <= MAX_LOCKUP_PERIOD, "StakeMath: lockup time too high"); _deltaMpTotal += _calculateBonusMP(_balance, _increasedLockSeconds); _newMaxMP = _maxMP + _deltaMpTotal; - require(_newMaxMP <= MAX_MULTIPLIER_ABSOLUTE * (_balance), "StakeMath: max multiplier exceeded"); + require(_newMaxMP <= MP_MPY_ABSOLUTE * (_balance), "StakeMath: max multiplier exceeded"); } /** @@ -112,7 +112,7 @@ abstract contract StakeMath is MultiplierPointMath { uint256 _maxMP, uint256 _reducedAmount ) - public + internal pure returns (uint256 _deltaMpTotal, uint256 _deltaMpMax) { @@ -140,7 +140,7 @@ abstract contract StakeMath is MultiplierPointMath { uint256 _lastAccrualTime, uint256 _processTime ) - public + internal pure returns (uint256 _deltaMpTotal) { @@ -150,4 +150,15 @@ abstract contract StakeMath is MultiplierPointMath { _deltaMpTotal = Math.min(_calculateAccuredMP(_balance, dt), _maxMP - _totalMP); } } + + /** + * @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 _currentBalance Current balance used to calculate the maximum multiplier points. + */ + function _estimateLockTime(uint256 _mpMax, uint256 _currentBalance) internal pure returns (uint256 _lockTime) { + return Math.mulDiv((_mpMax - _currentBalance) * 100, YEAR, _currentBalance * MP_APY, Math.Rounding.Up) + - MAX_LOCKUP_PERIOD; + } } diff --git a/contracts/StakeVault.sol b/contracts/StakeVault.sol index b62b229..3d7c8db 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IStakeManager } from "./IStakeManager.sol"; +import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { StakeManager } from "./StakeManager.sol"; /** @@ -54,7 +54,7 @@ contract StakeVault is Ownable { } function leave() external onlyOwner { - if (StakeManager(stakeManager).leave()) { + if (StakeManager(address(stakeManager)).leave()) { STAKING_TOKEN.transferFrom(address(this), msg.sender, STAKING_TOKEN.balanceOf(address(this))); } } @@ -63,7 +63,7 @@ contract StakeVault is Ownable { * @notice Opt-in migration to a new IStakeManager contract. */ function acceptMigration() external onlyOwner { - IStakeManager migrated = StakeManager(stakeManager).acceptUpdate(); + IStakeManager migrated = StakeManager(address(stakeManager)).acceptUpdate(); if (address(migrated) == address(0)) revert StakeVault__MigrationNotAvailable(); stakeManager = migrated; } diff --git a/contracts/VaultFactory.sol b/contracts/VaultFactory.sol index f80880f..643877f 100644 --- a/contracts/VaultFactory.sol +++ b/contracts/VaultFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { StakeManager } from "./StakeManager.sol"; diff --git a/contracts/access/TrustedCodehashAccess.sol b/contracts/access/TrustedCodehashAccess.sol index e5f5de6..b489189 100644 --- a/contracts/access/TrustedCodehashAccess.sol +++ b/contracts/access/TrustedCodehashAccess.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ITrustedCodehashAccess } from "../interfaces/ITrustedCodehashAccess.sol"; diff --git a/contracts/factory/Create2FactoryLib.sol b/contracts/factory/Create2FactoryLib.sol index 4039364..b41c445 100644 --- a/contracts/factory/Create2FactoryLib.sol +++ b/contracts/factory/Create2FactoryLib.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; /** * @title Singleton Factory (EIP-2470) @@ -21,10 +21,6 @@ library AddressLib { } } - function computeAddress(bytes memory, bytes32 _salt) public view returns (address payable) { - return payable(hashToAddress(abi.encodePacked(bytes1(0xff), address(this), _salt, _initCode))); - } - function computeAddress(address _deployer, bytes32 _salt, bytes memory _initCode) public pure returns (address) { return hashToAddress(abi.encodePacked(bytes1(0xff), _deployer, _salt, _initCode)); } diff --git a/contracts/factory/SingletonFactory.sol b/contracts/factory/SingletonFactory.sol index 6882ef6..23365e5 100644 --- a/contracts/factory/SingletonFactory.sol +++ b/contracts/factory/SingletonFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; /** * @title Singleton Factory (EIP-2470) diff --git a/contracts/interfaces/IStakeConstants.sol b/contracts/interfaces/IStakeConstants.sol new file mode 100644 index 0000000..8052b75 --- /dev/null +++ b/contracts/interfaces/IStakeConstants.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol"; + +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/contracts/interfaces/IStakeManager.sol b/contracts/interfaces/IStakeManager.sol index ac75683..6144263 100644 --- a/contracts/interfaces/IStakeManager.sol +++ b/contracts/interfaces/IStakeManager.sol @@ -1,28 +1,25 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; 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 IStakeConstants, ITrustedCodehashAccess { error StakeManager__FundsLocked(); error StakeManager__InvalidLockTime(); error StakeManager__InsufficientFunds(); error StakeManager__StakeIsTooLow(); + function STAKING_TOKEN() external view returns (IERC20); + function REWARD_TOKEN() external view returns (IERC20); + function stake(uint256 _amount, uint256 _seconds) external; function lock(uint256 _seconds) external; function unstake(uint256 _amount) external; function totalStaked() external view returns (uint256); function totalMP() external view returns (uint256); - function totalMaxMP() external view returns (uint256); + //function totalMaxMP() external view returns (uint256); function getStakedBalance(address _vault) external view returns (uint256 _balance); - - function STAKING_TOKEN() external view returns (IERC20); - function REWARD_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/contracts/interfaces/ITrustedCodehashAccess.sol b/contracts/interfaces/ITrustedCodehashAccess.sol index 303b002..42d225c 100644 --- a/contracts/interfaces/ITrustedCodehashAccess.sol +++ b/contracts/interfaces/ITrustedCodehashAccess.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; /** * @title TrustedCodehashAccess diff --git a/contracts/storage/ExpiredStakeStorage.sol b/contracts/storage/ExpiredStakeStorage.sol index a544ff4..9689d4e 100644 --- a/contracts/storage/ExpiredStakeStorage.sol +++ b/contracts/storage/ExpiredStakeStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/foundry.toml b/foundry.toml index 6caf35c..8a1d3dc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,7 +13,7 @@ optimizer_runs = 10_000 out = "out" script = "script" - solc = "0.8.26" + solc = "0.8.27" src = "contracts" test = "test" diff --git a/script/Base.s.sol b/script/Base.s.sol index ad9234c..f7b2eea 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.26 <=0.9.0; +pragma solidity >=0.8.27 <=0.9.0; import { Script } from "forge-std/Script.sol"; diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 15417e5..8335eb6 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26 <=0.9.0; +pragma solidity >=0.8.27 <=0.9.0; import { BaseScript } from "./Base.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol"; diff --git a/script/DeployMigrationStakeManager.s.sol b/script/DeployMigrationStakeManager.s.sol index 60b42bb..f908d26 100644 --- a/script/DeployMigrationStakeManager.s.sol +++ b/script/DeployMigrationStakeManager.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26 <=0.9.0; +pragma solidity >=0.8.27 <=0.9.0; import { BaseScript } from "./Base.s.sol"; import { StakeManager } from "../contracts/StakeManager.sol"; diff --git a/script/DeploymentConfig.s.sol b/script/DeploymentConfig.s.sol index 6012c1e..dea9f01 100644 --- a/script/DeploymentConfig.s.sol +++ b/script/DeploymentConfig.s.sol @@ -1,6 +1,6 @@ //// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26 <=0.9.0; +pragma solidity >=0.8.27 <=0.9.0; import { Script } from "forge-std/Script.sol"; import { MockERC20 } from "../test/mocks/MockERC20.sol"; diff --git a/test/DynamicTest.t.sol b/test/DynamicTest.t.sol index 983ba45..3c08637 100644 --- a/test/DynamicTest.t.sol +++ b/test/DynamicTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -8,11 +8,11 @@ import { Deploy } from "../script/Deploy.s.sol"; import { DeployMigrationStakeManager } from "../script/DeployMigrationStakeManager.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { TrustedCodehashAccess, StakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; -import { MultiplierPointMath } from "../contracts/MultiplierPointMath.sol"; +import { StakeMath } from "../contracts/StakeMath.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; import { VaultFactory } from "../contracts/VaultFactory.sol"; -contract DynamicTest is MultiplierPointMath, Test { +contract DynamicTest is StakeMath, Test { DeploymentConfig internal deploymentConfig; StakeManager internal stakeManager; VaultFactory internal vaultFactory; @@ -35,7 +35,7 @@ contract DynamicTest is MultiplierPointMath, Test { } modifier fuzz_stake(uint256 _amount) { - vm.assume(_amount > _calculateMinimumStake(stakeManager.EPOCH_SIZE())); + vm.assume(_amount > stakeManager.MIN_BALANCE()); vm.assume(_amount < 1e20); _; } diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index 802406d..d84ffae 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -8,7 +8,7 @@ import { Deploy } from "../script/Deploy.s.sol"; import { DeployMigrationStakeManager } from "../script/DeployMigrationStakeManager.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { StakeManager, IStakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; -import { ITrustedCodehashAccess } from "../contracts/access/ITrustedCodehashAccess.sol"; +import { ITrustedCodehashAccess } from "../contracts/interfaces/ITrustedCodehashAccess.sol"; import { MultiplierPointMath } from "../contracts/MultiplierPointMath.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; import { VaultFactory } from "../contracts/VaultFactory.sol"; @@ -325,7 +325,7 @@ contract LockTest is StakeManagerTest { StakeVault userVault = _createStakingAccount(testUser, 1000, minLockup, 1000); vm.warp(block.timestamp + stakeManager.MIN_LOCKUP_PERIOD() - 1); - stakeManager.executeAccount(address(userVault), 1); + stakeManager.executeAccount(address(userVault)); (, uint256 balance, uint256 bonusMP, uint256 totalMP,, uint256 lockUntil,,) = stakeManager.accounts(address(userVault)); @@ -485,7 +485,7 @@ contract ExecuteAccountTest is StakeManagerTest { vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeAccount(address(userVault), currentEpoch + 1); - vm.warp(stakeManager.epochEnd() + stakeManager.EPOCH_SIZE() - 1); + vm.warp(stakeManager.epochEnd() + stakeManager.ACCURE_RATE() - 1); vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeEpoch(currentEpoch + 2); @@ -516,9 +516,10 @@ contract ExecuteAccountTest is StakeManagerTest { stakeManager.executeEpoch(); vm.warp(stakeManager.epochEnd()); - //expected MP is, the starting totalMP + the calculatedMPToMint of user balance for one EPOCH_SIZE multiplied by + //expected MP is, the starting totalMP + the calculatedMPToMint of user balance for one ACCURE_RATE multiplied + // by // 2. - uint256 expectedMP = totalMP + (stakeManager.calculateMP(stakeAmount, stakeManager.EPOCH_SIZE()) * 2); + uint256 expectedMP = totalMP + (stakeManager.calculateMP(stakeAmount, stakeManager.ACCURE_RATE()) * 2); stakeManager.executeAccount(address(userVaults[0]), stakeManager.currentEpoch() + 1); (,,, totalMP, lastMint,, epoch,) = stakeManager.accounts(address(userVaults[0])); @@ -569,7 +570,7 @@ contract ExecuteAccountTest is StakeManagerTest { console.log("--=======#======="); console.log("--# TOTAL_SUPPLY", stakeManager.totalSupply()); console.log("--# PND_REWARDS", stakeManager.pendingReward()); - assertEq(lastMint, lastMintBefore + stakeManager.EPOCH_SIZE(), "must increaase lastMint"); + assertEq(lastMint, lastMintBefore + stakeManager.ACCURE_RATE(), "must increaase lastMint"); assertEq(epoch, epochBefore + 1, "must increase epoch"); assertGt(totalMP, totalMPBefore, "must increase MPs"); assertGt(rewards, rewardsBefore, "must increase rewards"); @@ -585,25 +586,25 @@ contract ExecuteAccountTest is StakeManagerTest { uint256 epochsAmountToReachCap = stakeManager.calculateMP( stakeAmount, stakeManager.MAX_MULTIPLIER() * stakeManager.YEAR() - ) / stakeManager.calculateMP(stakeAmount, stakeManager.EPOCH_SIZE()); + ) / stakeManager.calculateMP(stakeAmount, stakeManager.ACCURE_RATE()); deal(stakeToken, testUser, stakeAmount); userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 1)); + vm.warp(stakeManager.epochEnd() - (stakeManager.ACCURE_RATE() - 1)); userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 2)); + vm.warp(stakeManager.epochEnd() - (stakeManager.ACCURE_RATE() - 2)); userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 3)); + vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 3)); userVaults.push(_createStakingAccount(makeAddr("testUser4"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 2)); + vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 2)); userVaults.push(_createStakingAccount(makeAddr("testUser5"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 1)); + vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 1)); userVaults.push(_createStakingAccount(makeAddr("testUser6"), stakeAmount, 0)); vm.warp(stakeManager.epochEnd() - 2); @@ -647,7 +648,7 @@ contract ExecuteAccountTest is StakeManagerTest { //solhint-disable-next-line max-line-length (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch,) = stakeManager.accounts(address(userVaults[j])); uint256 rewards = ERC20(stakeToken).balanceOf(rewardAddress); - assertEq(lastMint, lastMintBefore + stakeManager.EPOCH_SIZE(), "must increaase lastMint"); + assertEq(lastMint, lastMintBefore + stakeManager.ACCURE_RATE(), "must increaase lastMint"); assertEq(epoch, epochBefore + 1, "must increase epoch"); // MPs will still be minted in mpLimitEpoch + 1 when accounts // started staking at any point *inside* of an epoch, so we @@ -728,7 +729,7 @@ contract UserFlowsTest is StakeManagerTest { ); uint256 thisAccReachCapIn = stakeManager.calculateMP( thisAccStake, stakeManager.MAX_MULTIPLIER() * stakeManager.YEAR() - ) / stakeManager.calculateMP(thisAccStake, stakeManager.EPOCH_SIZE()); + ) / stakeManager.calculateMP(thisAccStake, stakeManager.ACCURE_RATE()); if (thisAccReachCapIn > epochsAmountToReachCap) { epochsAmountToReachCap = thisAccReachCapIn; //uses the amount to reach cap from the account that takes // longer to reach cap @@ -813,7 +814,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 1, "Current epoch should increase if 1 second after epochend of 1"); - vm.warp(firstEpochEnd + (stakeManager.EPOCH_SIZE() * 99)); + vm.warp(firstEpochEnd + (stakeManager.ACCURE_RATE() * 99)); assertEq(stakeManager.newEpoch(), 100); stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 100); @@ -822,7 +823,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochExecuteEpochAfterEnd() public { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 2)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 2)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); vm.warp(stakeManager.epochEnd()); @@ -830,7 +831,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() * 2)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -868,7 +869,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 10 - i)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); stakeManager.executeEpoch(); } stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -878,7 +879,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 10 - i)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); } stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -888,7 +889,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 10 - i)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); } stakeManager.executeAccount(address(userVault)); } @@ -896,13 +897,13 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochExecuteAccountAfterEpochEnd() public { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 2)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 2)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); vm.warp(stakeManager.epochEnd()); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() * 2)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -910,7 +911,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.EPOCH_SIZE() / 10 - i)); + vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } } diff --git a/test/StakeVault.t.sol b/test/StakeVault.t.sol index 38270d5..3e00cb4 100644 --- a/test/StakeVault.t.sol +++ b/test/StakeVault.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/VaultFactory.t.sol b/test/VaultFactory.t.sol index 48550d9..0cca62c 100644 --- a/test/VaultFactory.t.sol +++ b/test/VaultFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { Test } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; diff --git a/test/mocks/BrokenERC20.s.sol b/test/mocks/BrokenERC20.s.sol index 6d11323..510fa42 100644 --- a/test/mocks/BrokenERC20.s.sol +++ b/test/mocks/BrokenERC20.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol index 81927ee..87d6d16 100644 --- a/test/mocks/MockERC20.sol +++ b/test/mocks/MockERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/test/script/DeployBroken.s.sol b/test/script/DeployBroken.s.sol index 17b4b76..054a9fd 100644 --- a/test/script/DeployBroken.s.sol +++ b/test/script/DeployBroken.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; +pragma solidity ^0.8.27; import { BaseScript } from "../../script/Base.s.sol"; import { StakeManager } from "../../contracts/StakeManager.sol"; From 93062570b2982d120f0814e478ba179d4b4e05a3 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Tue, 12 Nov 2024 10:10:21 -0300 Subject: [PATCH 07/13] refactor(StakeManager): Use maxMP instead of totalMP --- .gas-report | 42 +++++++------- .gas-snapshot | 112 ++++++++++++++++++------------------- contracts/StakeManager.sol | 65 ++++++--------------- test/StakeManager.t.sol | 68 ++++++++++++---------- 4 files changed, 131 insertions(+), 156 deletions(-) diff --git a/.gas-report b/.gas-report index 5f7b853..ebaf607 100644 --- a/.gas-report +++ b/.gas-report @@ -1,7 +1,7 @@ | contracts/StakeManager.sol:StakeManager contract | | | | | | |--------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2600451 | 13644 | | | | | +| 2605863 | 13669 | | | | | | Function Name | min | avg | median | max | # calls | | ACCURE_RATE | 328 | 328 | 328 | 328 | 1546 | | MAX_LOCKUP_PERIOD | 405 | 405 | 405 | 405 | 4 | @@ -10,14 +10,14 @@ | REWARD_TOKEN | 317 | 317 | 317 | 317 | 744 | | YEAR | 308 | 308 | 308 | 308 | 685 | | acceptUpdate | 23633 | 23633 | 23633 | 23633 | 1 | -| accounts | 1572 | 1572 | 1572 | 1572 | 154869 | -| calculateMP | 798 | 798 | 798 | 798 | 1372 | +| accounts | 1572 | 1572 | 1572 | 1572 | 155271 | +| calculateMP | 798 | 798 | 798 | 798 | 1371 | | currentEpoch | 384 | 1050 | 384 | 2384 | 54 | -| epochEnd | 627 | 627 | 627 | 2627 | 25441 | +| epochEnd | 627 | 627 | 627 | 2627 | 25508 | | epochReward | 1381 | 2881 | 1381 | 5881 | 3 | -| executeAccount(address) | 33469 | 110665 | 149264 | 149264 | 3 | -| executeAccount(address,uint256) | 26562 | 72195 | 74101 | 199919 | 152455 | -| executeEpoch() | 23435 | 120639 | 121820 | 900335 | 25330 | +| executeAccount(address) | 33469 | 110371 | 148822 | 148822 | 3 | +| executeAccount(address,uint256) | 26562 | 71632 | 73548 | 195495 | 152857 | +| executeEpoch() | 23435 | 120644 | 121820 | 900335 | 25397 | | executeEpoch(uint256) | 23861 | 24497 | 23861 | 26090 | 7 | | expiredStakeStorage | 416 | 2325 | 2416 | 2416 | 22 | | isTrustedCodehash | 541 | 944 | 541 | 2541 | 728 | @@ -28,13 +28,13 @@ | newEpoch | 463 | 463 | 463 | 463 | 5 | | owner | 2432 | 2432 | 2432 | 2432 | 13 | | pendingReward | 408 | 1442 | 2408 | 2408 | 29 | -| potentialMP | 408 | 408 | 408 | 408 | 49964 | +| potentialMP | 408 | 408 | 408 | 408 | 50098 | | previousManager | 297 | 297 | 297 | 297 | 13 | | setTrustedCodehash | 47982 | 47982 | 47982 | 47982 | 147 | | stake | 24047 | 24047 | 24047 | 24047 | 1 | | startMigration | 103624 | 103632 | 103636 | 103636 | 3 | | startTime | 306 | 306 | 306 | 306 | 21 | -| totalMP | 385 | 385 | 385 | 2385 | 49985 | +| totalMP | 385 | 385 | 385 | 2385 | 50119 | | totalStaked | 386 | 1786 | 2386 | 2386 | 20 | | totalSupply | 784 | 1965 | 2784 | 2784 | 22 | | unstake | 23841 | 23841 | 23841 | 23841 | 1 | @@ -48,10 +48,10 @@ | STAKING_TOKEN | 193 | 193 | 193 | 193 | 2 | | acceptMigration | 35141 | 35141 | 35141 | 35141 | 2 | | leave | 35196 | 35196 | 35196 | 35196 | 1 | -| lock | 43196 | 71076 | 62042 | 162843 | 7 | +| lock | 43196 | 71013 | 62042 | 162400 | 7 | | owner | 318 | 318 | 318 | 318 | 727 | -| stake | 27291 | 282802 | 266296 | 351880 | 732 | -| unstake | 40179 | 88236 | 78765 | 184769 | 11 | +| stake | 27291 | 283772 | 266970 | 352542 | 732 | +| unstake | 40179 | 88155 | 78765 | 184327 | 11 | | contracts/VaultFactory.sol:VaultFactory contract | | | | | | @@ -69,24 +69,24 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25486 | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25553 | | transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | | script/Deploy.s.sol:Deploy contract | | | | | | |-------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6220738 | 29931 | | | | | +| 6226159 | 29956 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5420174 | 5420174 | 5420174 | 5420174 | 66 | +| run | 5425181 | 5425181 | 5425181 | 5425181 | 66 | | script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | |-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 3412741 | 16827 | | | | | +| 3418160 | 16852 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 2427736 | 2427736 | 2427736 | 2427736 | 19 | +| run | 2432743 | 2432743 | 2432743 | 2432743 | 19 | | script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | @@ -111,16 +111,16 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46235 | 46199 | 46367 | 727 | -| balanceOf | 561 | 2127 | 2561 | 2561 | 32612 | +| approve | 46175 | 46233 | 46199 | 46367 | 727 | +| balanceOf | 561 | 2128 | 2561 | 2561 | 32679 | | test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | |------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 4918894 | 23867 | | | | | +| 4924318 | 23892 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 4266410 | 4266410 | 4266410 | 4266410 | 1 | +| run | 4271418 | 4271418 | 4271418 | 4271418 | 1 | diff --git a/.gas-snapshot b/.gas-snapshot index 1b5a51d..5bf219f 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ CreateVaultTest:testDeployment() (gas: 9774) CreateVaultTest:test_createVault() (gas: 700333) -ExecuteAccountTest:testDeployment() (gas: 28875) -ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1559514) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5232040) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1769214) -ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 320225995) -ExecuteEpochTest:testDeployment() (gas: 28876) -ExecuteEpochTest:testNewDeployment() (gas: 30948) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1350443) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1367228) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1613317) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1377593) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1910717) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2492323) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1460710) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2502709) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1471052) +ExecuteAccountTest:testDeployment() (gas: 28853) +ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1559565) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5229838) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1769243) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 319099087) +ExecuteEpochTest:testDeployment() (gas: 28898) +ExecuteEpochTest:testNewDeployment() (gas: 31015) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1351146) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1367601) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1613976) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1377966) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1909295) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2489731) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1460839) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2500073) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1471203) ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083615) -ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92277) -ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 252659) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 38961) +ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92233) +ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 252720) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 38962) ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149703) -LeaveTest:testDeployment() (gas: 28853) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1311974) -LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31740) -LockTest:testDeployment() (gas: 28853) -LockTest:test_NewLockupPeriod() (gas: 1310978) -LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1285586) -LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1406860) -LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31834) -LockTest:test_ShouldIncreaseBonusMP() (gas: 1293620) -LockTest:test_UpdateLockupPeriod() (gas: 1470415) -MigrateTest:testDeployment() (gas: 28853) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1276097) -MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31739) -MigrationInitializeTest:testDeployment() (gas: 28853) -MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5451158) -MigrationStakeManagerTest:testDeployment() (gas: 28853) -MigrationStakeManagerTest:testNewDeployment() (gas: 30992) -MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149668) +LeaveTest:testDeployment() (gas: 28831) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1312647) +LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31763) +LockTest:testDeployment() (gas: 28831) +LockTest:test_NewLockupPeriod() (gas: 1311618) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1286248) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1407167) +LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31857) +LockTest:test_ShouldIncreaseMaxMP() (gas: 1294282) +LockTest:test_UpdateLockupPeriod() (gas: 1470744) +MigrateTest:testDeployment() (gas: 28831) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1276770) +MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31762) +MigrationInitializeTest:testDeployment() (gas: 28831) +MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5462037) +MigrationStakeManagerTest:testDeployment() (gas: 28831) +MigrationStakeManagerTest:testNewDeployment() (gas: 30948) +MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149713) SetStakeManagerTest:testDeployment() (gas: 9774) SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 63105) SetStakeManagerTest:test_SetStakeManager() (gas: 41301) -StakeManagerTest:testDeployment() (gas: 28625) +StakeManagerTest:testDeployment() (gas: 28603) StakeTest:testDeployment() (gas: 28831) -StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1057656) -StakeTest:test_RevertWhen_Restake() (gas: 1301092) -StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1304525) -StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32082) -StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803915) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1057645) +StakeTest:test_RevertWhen_Restake() (gas: 1301832) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1305220) +StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32104) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803959) StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 207758) -StakeTest:test_StakeWithLockBonusMP() (gas: 2323783) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1297379) +StakeTest:test_StakeWithLockMaxMP() (gas: 2325472) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1298474) StakedTokenTest:testStakeToken() (gas: 7597) -UnstakeTest:testDeployment() (gas: 28875) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1281609) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1325777) -UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31879) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6432067) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1303800) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1387208) -UserFlowsTest:testDeployment() (gas: 28853) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 114, μ: 130857603, ~: 130246926) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1425816) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2461484) +UnstakeTest:testDeployment() (gas: 28853) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1282314) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1326462) +UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31857) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6413830) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1304463) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1387562) +UserFlowsTest:testDeployment() (gas: 28831) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 114, μ: 130456714, ~: 129540928) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1426181) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2462875) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index 9428ea7..8d72e26 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -25,7 +25,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { struct Account { address rewardAddress; uint256 balance; - uint256 bonusMP; + uint256 maxMP; uint256 totalMP; uint256 lastMint; uint256 lockUntil; @@ -167,12 +167,13 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { uint256 mpLimitEpoch = currentEpoch + epochAmountToReachMpLimit; uint256 lastEpochAmountToMint = ((mpPerEpoch * (epochAmountToReachMpLimit + 1)) - maxMpToMint); uint256 bonusMP = _calculateInitialMP(_amount) + _calculateBonusMP(_amount, _seconds); + uint256 maxMP = _calculateMaxMP(_amount, _seconds); // account initialization accounts[msg.sender] = Account({ rewardAddress: StakeVault(msg.sender).owner(), balance: _amount, - bonusMP: bonusMP, + maxMP: maxMP, totalMP: bonusMP, lastMint: block.timestamp, lockUntil: block.timestamp + _seconds, @@ -208,8 +209,8 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { } _processAccount(account, currentEpoch); - uint256 reducedMP = Math.mulDiv(_amount, account.totalMP, account.balance); - uint256 reducedInitialMP = Math.mulDiv(_amount, account.bonusMP, account.balance); + uint256 reducedTotalMP = Math.mulDiv(_amount, account.totalMP, account.balance); + uint256 reducedMaxMP = Math.mulDiv(_amount, account.maxMP, account.balance); uint256 mpPerEpoch = _calculateAccuredMP(account.balance, ACCURE_RATE); expiredStakeStorage.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); @@ -219,10 +220,10 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { //update storage account.balance -= _amount; - account.bonusMP -= reducedInitialMP; - account.totalMP -= reducedMP; + account.maxMP -= reducedMaxMP; + account.totalMP -= reducedTotalMP; totalStaked -= _amount; - totalMP -= reducedMP; + totalMP -= reducedTotalMP; } /** @@ -261,7 +262,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { //update account storage account.lockUntil = lockUntil; - account.bonusMP += bonusMP; + account.maxMP += bonusMP; account.totalMP += bonusMP; //update global storage totalMP += bonusMP; @@ -477,50 +478,18 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { * @param epoch Epoch to increment total supply */ function _mintMP(Account storage account, uint256 processTime, Epoch storage epoch) private { - uint256 mpToMint = _getMaxMPToMint( - _calculateAccuredMP(account.balance, processTime - account.lastMint), - account.balance, - account.bonusMP, - account.totalMP - ); - + uint256 accruedMP = _calculateAccuredMP(account.balance, processTime - account.lastMint); + if (accruedMP + account.totalMP > account.maxMP) { + accruedMP = account.maxMP - account.totalMP; //how much left to reach cap + } //update storage account.lastMint = processTime; - account.totalMP += mpToMint; - totalMP += mpToMint; + account.totalMP += accruedMP; + totalMP += accruedMP; //mp estimation - epoch.potentialMP -= mpToMint; - potentialMP -= mpToMint; - } - - /** - * @notice Calculates maximum multiplier point increase for given balance - * @param _mpToMint tested value - * @param _balance balance of account - * @param _totalMP total multiplier point of the account - * @param _bonusMP bonus multiplier point of the account - * @return _maxMpToMint maximum multiplier points to mint - */ - function _getMaxMPToMint( - uint256 _mpToMint, - uint256 _balance, - uint256 _bonusMP, - uint256 _totalMP - ) - private - pure - returns (uint256 _maxMpToMint) - { - // Maximum multiplier point for given balance - _maxMpToMint = _calculateMaxAccuredMP(_balance) + _bonusMP; - if (_mpToMint + _totalMP > _maxMpToMint) { - //reached cap when increasing MP - return _maxMpToMint - _totalMP; //how much left to reach cap - } else { - //not reached capw hen increasing MP - return _mpToMint; //just return tested value - } + epoch.potentialMP -= accruedMP; + potentialMP -= accruedMP; } /** diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index d84ffae..f2046ea 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -9,11 +9,11 @@ import { DeployMigrationStakeManager } from "../script/DeployMigrationStakeManag import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { StakeManager, IStakeManager, ExpiredStakeStorage } from "../contracts/StakeManager.sol"; import { ITrustedCodehashAccess } from "../contracts/interfaces/ITrustedCodehashAccess.sol"; -import { MultiplierPointMath } from "../contracts/MultiplierPointMath.sol"; +import { StakeMath } from "../contracts/StakeMath.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; import { VaultFactory } from "../contracts/VaultFactory.sol"; -contract StakeManagerTest is Test { +contract StakeManagerTest is Test, StakeMath { DeploymentConfig internal deploymentConfig; StakeManager internal stakeManager; VaultFactory internal vaultFactory; @@ -89,37 +89,43 @@ contract StakeTest is StakeManagerTest { stakeManager.stake(100, 1); } - function test_StakeWithLockBonusMP() public { + function test_StakeWithLockMaxMP() public { uint256 stakeAmount = 10_000; uint256 lockTime = stakeManager.MIN_LOCKUP_PERIOD(); StakeVault userVault = _createStakingAccount(testUser, stakeAmount, 0, stakeAmount); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance, uint256 maxMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "balance of user vault should be equal to stake amount after stake"); - assertEq(bonusMP, stakeAmount, "bonusMP of user vault should be equal to stake amount after stake if no lock"); + assertEq( + maxMP, + _calculateMaxMP(stakeAmount, 0), + "maxMP of user vault should be equal to stake amount after stake if no lock" + ); assertEq( totalMP, stakeAmount, "totalMP of user vault should be equal to stakeAmount after stake if no epochs passed" ); vm.prank(testUser); userVault.lock(lockTime); - uint256 estimatedBonusMp = stakeAmount + stakeManager.calculateMP(stakeAmount, lockTime); - - (, balance, bonusMP, totalMP,,,,) = stakeManager.accounts(address(userVault)); + uint256 estimatedMaxMP = maxMP + _calculateAccuredMP(stakeAmount, lockTime); + uint256 estimatedTotalMP = _calculateInitialMP(stakeAmount) + _calculateBonusMP(stakeAmount, lockTime); + (, balance, maxMP, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "balance of user vault should be equal to stake amount after lock"); - assertEq(bonusMP, estimatedBonusMp, "bonusMP of user vault should be equal to estimated bonusMP after lock"); - assertEq(totalMP, bonusMP, "totalMP of user vault should be equal to bonusMP after lock if no epochs passed"); + assertEq(maxMP, estimatedMaxMP, "maxMP of user vault should be equal to estimated maxMP after lock"); + assertEq( + totalMP, estimatedTotalMP, "totalMP of user vault should be equal to maxMP after lock if no epochs passed" + ); StakeVault userVault2 = _createStakingAccount(testUser, stakeAmount, lockTime, stakeAmount); - (, balance, bonusMP, totalMP,,,,) = stakeManager.accounts(address(userVault2)); + (, balance, maxMP, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount, "balance of user vault should be equal to stake amount after stake locked"); + assertEq(maxMP, estimatedMaxMP, "maxMP of user vault should be equal to estimated maxMP after stake locked"); assertEq( - bonusMP, estimatedBonusMp, "bonusMP of user vault should be equal to estimated bonusMP after stake locked" - ); - assertEq( - totalMP, bonusMP, "totalMP of user vault should be equal to bonusMP after stake locked if no epochs passed" + totalMP, + estimatedTotalMP, + "totalMP of user vault should be equal to maxMP after stake locked if no epochs passed" ); } @@ -173,10 +179,10 @@ contract StakeTest is StakeManagerTest { } function test_StakeWithoutLockUpTimeMintsMultiplierPoints() public { - uint256 stakeAmount = 54; + uint256 stakeAmount = MIN_BALANCE; StakeVault userVault = _createStakingAccount(testUser, stakeAmount, 0, stakeAmount); - (,, uint256 totalMP,,,,,) = stakeManager.accounts(address(userVault)); + (,,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(stakeManager.totalMP(), stakeAmount, "total multiplier point supply"); assertEq(totalMP, stakeAmount, "user multiplier points"); @@ -254,7 +260,7 @@ contract UnstakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); stakeManager.executeAccount(address(userVault), i + 1); } - (, uint256 balanceBefore, uint256 bonusMPBefore, uint256 totalMPBefore,,,,) = + (, uint256 balanceBefore, uint256 maxMPBefore, uint256 totalMPBefore,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPBefore = stakeManager.totalMP(); uint256 unstakeAmount = stakeAmount * percentToBurn / 100; @@ -262,7 +268,7 @@ contract UnstakeTest is StakeManagerTest { assertEq(ERC20(stakeToken).balanceOf(testUser), 0); userVault.unstake(unstakeAmount); - (, uint256 balanceAfter, uint256 bonusMPAfter, uint256 totalMPAfter,,,,) = + (, uint256 balanceAfter, uint256 maxMPAfter, uint256 totalMPAfter,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPAfter = stakeManager.totalMP(); @@ -270,13 +276,13 @@ contract UnstakeTest is StakeManagerTest { console.log("totalSupplyMPAfter", totalSupplyMPAfter); console.log("balanceBefore", balanceBefore); console.log("balanceAfter", balanceAfter); - console.log("bonusMPBefore", bonusMPBefore); - console.log("bonusMPAfter", bonusMPAfter); + console.log("maxMPBefore", maxMPBefore); + console.log("maxMPAfter", maxMPAfter); console.log("totalMPBefore", totalMPBefore); console.log("totalMPAfter", totalMPAfter); assertEq(balanceAfter, balanceBefore - (balanceBefore * percentToBurn / 100)); - assertEq(bonusMPAfter, bonusMPBefore - (bonusMPBefore * percentToBurn / 100)); + assertEq(maxMPAfter, maxMPBefore - (maxMPBefore * percentToBurn / 100)); assertEq(totalMPAfter, totalMPBefore - (totalMPBefore * percentToBurn / 100)); assertEq(totalSupplyMPAfter, totalSupplyMPBefore - (totalMPBefore * percentToBurn / 100)); assertEq(ERC20(stakeToken).balanceOf(testUser), unstakeAmount); @@ -304,10 +310,10 @@ contract LockTest is StakeManagerTest { vm.startPrank(testUser); userVault.lock(lockTime); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance, uint256 maxMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); console.log("balance", balance); - console.log("bonusMP", bonusMP); + console.log("maxMP", maxMP); console.log("totalMP", totalMP); } @@ -326,12 +332,12 @@ contract LockTest is StakeManagerTest { vm.warp(block.timestamp + stakeManager.MIN_LOCKUP_PERIOD() - 1); stakeManager.executeAccount(address(userVault)); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,, uint256 lockUntil,,) = + (, uint256 balance, uint256 maxMP, uint256 totalMP,, uint256 lockUntil,,) = stakeManager.accounts(address(userVault)); vm.startPrank(testUser); userVault.lock(minLockup - 1); - (, balance, bonusMP, totalMP,, lockUntil,,) = stakeManager.accounts(address(userVault)); + (, balance, maxMP, totalMP,, lockUntil,,) = stakeManager.accounts(address(userVault)); assertEq(lockUntil, block.timestamp + minLockup); @@ -353,21 +359,21 @@ contract LockTest is StakeManagerTest { userVault.lock(minLockup - 1); } - function test_ShouldIncreaseBonusMP() public { + function test_ShouldIncreaseMaxMP() public { uint256 stakeAmount = 100; uint256 lockTime = stakeManager.MAX_LOCKUP_PERIOD(); StakeVault userVault = _createStakingAccount(testUser, stakeAmount); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance, uint256 maxMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPBefore = stakeManager.totalMP(); vm.startPrank(testUser); userVault.lock(lockTime); //solhint-disable-next-line max-line-length - (, uint256 newBalance, uint256 newBonusMP, uint256 newCurrentMP,,,,) = stakeManager.accounts(address(userVault)); + (, uint256 newBalance, uint256 newMaxMP, uint256 newCurrentMP,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPAfter = stakeManager.totalMP(); assertGt(totalSupplyMPAfter, totalSupplyMPBefore, "totalMP"); - assertGt(newBonusMP, bonusMP, "bonusMP"); + assertGt(newMaxMP, maxMP, "maxMP"); assertGt(newCurrentMP, totalMP, "totalMP"); assertEq(newBalance, balance, "balance"); } @@ -717,7 +723,7 @@ contract UserFlowsTest is StakeManagerTest { public { uint8 accountNum = 5; - uint256 minimumPossibleStake = 53; //less than this the stake per epoch of the account would be 0 + uint256 minimumPossibleStake = MIN_BALANCE; //less than this the stake per epoch of the account would be 0 uint256 baseStakeAmount = (minimumPossibleStake * (uint256(randomStakeMultiplier) + 1)) + uint256(randomStakeAddition); uint256 epochsAmountToReachCap = 0; From df3f1d28b792b2cc21efaaaa6841518d4e6194b3 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 13 Nov 2024 12:12:49 -0300 Subject: [PATCH 08/13] feat(EpochMath): add EpochMath contract and integrate with StakeManager --- .gas-report | 52 +++++++++--------- .gas-snapshot | 106 ++++++++++++++++++------------------ contracts/EpochMath.sol | 32 +++++++++++ contracts/StakeManager.sol | 107 ++++++++++++++++++++++++------------- test/StakeManager.t.sol | 102 ++++++++++++++++++++--------------- 5 files changed, 240 insertions(+), 159 deletions(-) create mode 100644 contracts/EpochMath.sol diff --git a/.gas-report b/.gas-report index ebaf607..890c504 100644 --- a/.gas-report +++ b/.gas-report @@ -1,25 +1,25 @@ | contracts/StakeManager.sol:StakeManager contract | | | | | | |--------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2605863 | 13669 | | | | | +| 2893352 | 15010 | | | | | | Function Name | min | avg | median | max | # calls | -| ACCURE_RATE | 328 | 328 | 328 | 328 | 1546 | +| ACCURE_RATE | 306 | 306 | 306 | 306 | 1546 | | MAX_LOCKUP_PERIOD | 405 | 405 | 405 | 405 | 4 | -| MAX_MULTIPLIER | 307 | 307 | 307 | 307 | 685 | +| MAX_MULTIPLIER | 285 | 285 | 285 | 285 | 685 | | MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 12 | | REWARD_TOKEN | 317 | 317 | 317 | 317 | 744 | | YEAR | 308 | 308 | 308 | 308 | 685 | | acceptUpdate | 23633 | 23633 | 23633 | 23633 | 1 | -| accounts | 1572 | 1572 | 1572 | 1572 | 155271 | -| calculateMP | 798 | 798 | 798 | 798 | 1371 | -| currentEpoch | 384 | 1050 | 384 | 2384 | 54 | -| epochEnd | 627 | 627 | 627 | 2627 | 25508 | +| accounts | 1616 | 1616 | 1616 | 1616 | 154911 | +| calculateMP | 790 | 790 | 790 | 790 | 1371 | +| currentEpoch | 384 | 385 | 384 | 2384 | 25502 | | epochReward | 1381 | 2881 | 1381 | 5881 | 3 | -| executeAccount(address) | 33469 | 110371 | 148822 | 148822 | 3 | -| executeAccount(address,uint256) | 26562 | 71632 | 73548 | 195495 | 152857 | -| executeEpoch() | 23435 | 120644 | 121820 | 900335 | 25397 | +| executeAccount(address) | 33469 | 110369 | 148819 | 148819 | 3 | +| executeAccount(address,uint256) | 26562 | 71648 | 73544 | 195463 | 152497 | +| executeEpoch() | 23435 | 120637 | 121820 | 900335 | 25337 | | executeEpoch(uint256) | 23861 | 24497 | 23861 | 26090 | 7 | -| expiredStakeStorage | 416 | 2325 | 2416 | 2416 | 22 | +| expiredStakeStorage | 394 | 2303 | 2394 | 2394 | 22 | +| getEpochStartTime | 525 | 525 | 525 | 525 | 25448 | | isTrustedCodehash | 541 | 944 | 541 | 2541 | 728 | | leave | 23675 | 23675 | 23675 | 23675 | 1 | | lock | 23840 | 23840 | 23840 | 23840 | 1 | @@ -28,13 +28,13 @@ | newEpoch | 463 | 463 | 463 | 463 | 5 | | owner | 2432 | 2432 | 2432 | 2432 | 13 | | pendingReward | 408 | 1442 | 2408 | 2408 | 29 | -| potentialMP | 408 | 408 | 408 | 408 | 50098 | +| potentialMP | 408 | 408 | 408 | 408 | 49978 | | previousManager | 297 | 297 | 297 | 297 | 13 | | setTrustedCodehash | 47982 | 47982 | 47982 | 47982 | 147 | | stake | 24047 | 24047 | 24047 | 24047 | 1 | -| startMigration | 103624 | 103632 | 103636 | 103636 | 3 | +| startMigration | 103602 | 103610 | 103614 | 103614 | 3 | | startTime | 306 | 306 | 306 | 306 | 21 | -| totalMP | 385 | 385 | 385 | 2385 | 50119 | +| totalMP | 363 | 363 | 363 | 2363 | 49999 | | totalStaked | 386 | 1786 | 2386 | 2386 | 20 | | totalSupply | 784 | 1965 | 2784 | 2784 | 22 | | unstake | 23841 | 23841 | 23841 | 23841 | 1 | @@ -48,10 +48,10 @@ | STAKING_TOKEN | 193 | 193 | 193 | 193 | 2 | | acceptMigration | 35141 | 35141 | 35141 | 35141 | 2 | | leave | 35196 | 35196 | 35196 | 35196 | 1 | -| lock | 43196 | 71013 | 62042 | 162400 | 7 | +| lock | 43196 | 71010 | 62038 | 162394 | 7 | | owner | 318 | 318 | 318 | 318 | 727 | -| stake | 27291 | 283772 | 266970 | 352542 | 732 | -| unstake | 40179 | 88155 | 78765 | 184327 | 11 | +| stake | 27291 | 264286 | 247573 | 342739 | 732 | +| unstake | 40179 | 92967 | 80797 | 190882 | 11 | | contracts/VaultFactory.sol:VaultFactory contract | | | | | | @@ -69,24 +69,24 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25553 | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25493 | | transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | | script/Deploy.s.sol:Deploy contract | | | | | | |-------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6226159 | 29956 | | | | | +| 6513756 | 31297 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5425181 | 5425181 | 5425181 | 5425181 | 66 | +| run | 5694023 | 5694023 | 5694023 | 5694023 | 66 | | script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | |-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 3418160 | 16852 | | | | | +| 3705689 | 18193 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 2432743 | 2432743 | 2432743 | 2432743 | 19 | +| run | 2701563 | 2701563 | 2701563 | 2701563 | 19 | | script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | @@ -111,16 +111,16 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46233 | 46199 | 46367 | 727 | -| balanceOf | 561 | 2128 | 2561 | 2561 | 32679 | +| approve | 46175 | 46235 | 46199 | 46367 | 727 | +| balanceOf | 561 | 2127 | 2561 | 2561 | 32619 | | test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | |------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 4924318 | 23892 | | | | | +| 5211884 | 25233 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 4271418 | 4271418 | 4271418 | 4271418 | 1 | +| run | 4540259 | 4540259 | 4540259 | 4540259 | 1 | diff --git a/.gas-snapshot b/.gas-snapshot index 5bf219f..893fa66 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ CreateVaultTest:testDeployment() (gas: 9774) CreateVaultTest:test_createVault() (gas: 700333) -ExecuteAccountTest:testDeployment() (gas: 28853) -ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1559565) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5229838) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1769243) -ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 319099087) -ExecuteEpochTest:testDeployment() (gas: 28898) -ExecuteEpochTest:testNewDeployment() (gas: 31015) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1351146) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1367601) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1613976) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1377966) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1909295) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2489731) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1460839) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2500073) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1471203) -ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1083615) -ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92233) -ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 252720) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 38962) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149703) -LeaveTest:testDeployment() (gas: 28831) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1312647) +ExecuteAccountTest:testDeployment() (gas: 28837) +ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1541694) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5174438) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1748244) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 319449153) +ExecuteEpochTest:testDeployment() (gas: 28882) +ExecuteEpochTest:testNewDeployment() (gas: 30999) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1333866) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1355485) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1601493) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1365421) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1892001) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2477599) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1448727) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2487512) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1458662) +ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1084321) +ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92967) +ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 253302) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 39660) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 150415) +LeaveTest:testDeployment() (gas: 28809) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1289510) LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31763) -LockTest:testDeployment() (gas: 28831) -LockTest:test_NewLockupPeriod() (gas: 1311618) -LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1286248) -LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1407167) +LockTest:testDeployment() (gas: 28809) +LockTest:test_NewLockupPeriod() (gas: 1292261) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1266851) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1387812) LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31857) -LockTest:test_ShouldIncreaseMaxMP() (gas: 1294282) -LockTest:test_UpdateLockupPeriod() (gas: 1470744) -MigrateTest:testDeployment() (gas: 28831) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1276770) +LockTest:test_ShouldIncreaseMaxMP() (gas: 1271185) +LockTest:test_UpdateLockupPeriod() (gas: 1451427) +MigrateTest:testDeployment() (gas: 28809) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1253633) MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31762) -MigrationInitializeTest:testDeployment() (gas: 28831) -MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5462037) -MigrationStakeManagerTest:testDeployment() (gas: 28831) -MigrationStakeManagerTest:testNewDeployment() (gas: 30948) -MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149713) +MigrationInitializeTest:testDeployment() (gas: 28809) +MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 6037401) +MigrationStakeManagerTest:testDeployment() (gas: 28809) +MigrationStakeManagerTest:testNewDeployment() (gas: 30926) +MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 150413) SetStakeManagerTest:testDeployment() (gas: 9774) SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 63105) SetStakeManagerTest:test_SetStakeManager() (gas: 41301) -StakeManagerTest:testDeployment() (gas: 28603) -StakeTest:testDeployment() (gas: 28831) +StakeManagerTest:testDeployment() (gas: 28581) +StakeTest:testDeployment() (gas: 28809) StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1057645) -StakeTest:test_RevertWhen_Restake() (gas: 1301832) -StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1305220) +StakeTest:test_RevertWhen_Restake() (gas: 1278695) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1282083) StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32104) -StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803959) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803932) StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 207758) -StakeTest:test_StakeWithLockMaxMP() (gas: 2325472) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1298474) +StakeTest:test_StakeWithLockMaxMP() (gas: 2286806) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1276306) StakedTokenTest:testStakeToken() (gas: 7597) -UnstakeTest:testDeployment() (gas: 28853) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1282314) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1326462) +UnstakeTest:testDeployment() (gas: 28831) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1262917) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1303325) UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31857) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6413830) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1304463) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1387562) -UserFlowsTest:testDeployment() (gas: 28831) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 114, μ: 130456714, ~: 129540928) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1426181) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2462875) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6472842) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1310464) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1393559) +UserFlowsTest:testDeployment() (gas: 28809) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 114, μ: 130275882, ~: 129634570) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1403965) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2419067) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/contracts/EpochMath.sol b/contracts/EpochMath.sol new file mode 100644 index 0000000..55b4cbc --- /dev/null +++ b/contracts/EpochMath.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.27; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { MultiplierPointMath } from "./MultiplierPointMath.sol"; + +abstract contract EpochMath is MultiplierPointMath { + function getEpochStartTime(uint256 _epochNum) public view virtual returns (uint256); + + function _calculateMPPrediction( + uint256 _amount, + uint256 _currentEpoch, + uint256 _deltaTime + ) + internal + pure + returns (uint256 mpRate, uint256 mpFractional, uint256 epochTarget1, uint256 epochTarget2, uint256 mpRemainder) + { + mpRate = _calculateAccuredMP(_amount, ACCURE_RATE); + mpFractional = mpRate - _calculateAccuredMP(_amount, _deltaTime); + + uint256 mpTarget = _calculateMaxAccuredMP(_amount) + mpFractional; + uint256 deltaEpochTarget1 = mpTarget / mpRate; + uint256 deltaEpochTarget2 = mpTarget % mpRate; + + epochTarget1 = _currentEpoch + deltaEpochTarget1; + if (deltaEpochTarget2 > 0) { + epochTarget2 = epochTarget1 + 1; + mpRemainder = (mpRate * (epochTarget1 + 1)) - mpTarget; + } + } +} diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index 8d72e26..d1bf071 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -9,10 +9,11 @@ import { TrustedCodehashAccess } from "./access/TrustedCodehashAccess.sol"; import { ExpiredStakeStorage } from "./storage/ExpiredStakeStorage.sol"; import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { MultiplierPointMath } from "./MultiplierPointMath.sol"; +import { EpochMath } from "./EpochMath.sol"; import { StakeMath } from "./StakeMath.sol"; import { StakeVault } from "./StakeVault.sol"; -contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { +contract StakeManager is StakeMath, EpochMath, TrustedCodehashAccess, IStakeManager { error StakeManager__NoPendingMigration(); error StakeManager__PendingMigration(); error StakeManager__SenderIsNotPreviousStakeManager(); @@ -30,7 +31,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { uint256 lastMint; uint256 lockUntil; uint256 epoch; - uint256 mpLimitEpoch; + uint256 startEpoch; } struct Epoch { @@ -49,7 +50,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { uint256 public potentialMP; uint256 public totalMP; uint256 public totalStaked; - uint256 public totalMPPerEpoch; + uint256 public totalMPRate; ExpiredStakeStorage public expiredStakeStorage; @@ -107,10 +108,10 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { Epoch storage thisEpoch = epochs[tempCurrentEpoch]; uint256 expiredMP = expiredStakeStorage.getExpiredMP(tempCurrentEpoch); if (expiredMP > 0) { - totalMPPerEpoch -= expiredMP; + totalMPRate -= expiredMP; expiredStakeStorage.deleteExpiredMP(tempCurrentEpoch); } - uint256 epochPotentialMP = totalMPPerEpoch; + uint256 epochPotentialMP = totalMPRate; if (tempCurrentEpoch == currentEpoch) { epochPotentialMP -= currentEpochTotalExpiredMP; currentEpochTotalExpiredMP = 0; @@ -155,39 +156,39 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { if (_seconds != 0 && (_seconds < MIN_LOCKUP_PERIOD || _seconds > MAX_LOCKUP_PERIOD)) { revert StakeManager__InvalidLockTime(); } - - //mp estimation - uint256 mpPerEpoch = _calculateAccuredMP(_amount, ACCURE_RATE); - if (mpPerEpoch < 1) { + if (_amount < MIN_BALANCE) { revert StakeManager__StakeIsTooLow(); } - uint256 currentEpochExpiredMP = mpPerEpoch - _calculateAccuredMP(_amount, epochEnd() - block.timestamp); - uint256 maxMpToMint = _calculateMaxAccuredMP(_amount) + currentEpochExpiredMP; - uint256 epochAmountToReachMpLimit = (maxMpToMint) / mpPerEpoch; - uint256 mpLimitEpoch = currentEpoch + epochAmountToReachMpLimit; - uint256 lastEpochAmountToMint = ((mpPerEpoch * (epochAmountToReachMpLimit + 1)) - maxMpToMint); - uint256 bonusMP = _calculateInitialMP(_amount) + _calculateBonusMP(_amount, _seconds); - uint256 maxMP = _calculateMaxMP(_amount, _seconds); + + uint256 deltaTotalMP = _calculateInitialMP(_amount) + _calculateBonusMP(_amount, _seconds); + uint256 deltaMaxMP = _calculateMaxMP(_amount, _seconds); // account initialization accounts[msg.sender] = Account({ rewardAddress: StakeVault(msg.sender).owner(), balance: _amount, - maxMP: maxMP, - totalMP: bonusMP, + maxMP: deltaMaxMP, + totalMP: deltaTotalMP, lastMint: block.timestamp, lockUntil: block.timestamp + _seconds, epoch: currentEpoch, - mpLimitEpoch: mpLimitEpoch + startEpoch: currentEpoch }); + (uint256 mpRate, uint256 mpFractional, uint256 epochTarget1, uint256 epochTarget2, uint256 mpRemainder) = + _calculateMPPrediction(_amount, currentEpoch, getEpochStartTime(currentEpoch + 1) - block.timestamp); + //update global storage - totalMP += bonusMP; + totalMP += deltaTotalMP; totalStaked += _amount; - currentEpochTotalExpiredMP += currentEpochExpiredMP; - totalMPPerEpoch += mpPerEpoch; - expiredStakeStorage.incrementExpiredMP(mpLimitEpoch, lastEpochAmountToMint); - expiredStakeStorage.incrementExpiredMP(mpLimitEpoch + 1, mpPerEpoch - lastEpochAmountToMint); + if (mpRemainder > 0) { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRemainder); + expiredStakeStorage.incrementExpiredMP(epochTarget2, mpRate - mpRemainder); + } else { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRate); + } + currentEpochTotalExpiredMP += mpFractional; + totalMPRate += mpRate; } /** @@ -207,21 +208,59 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { if (account.lockUntil > block.timestamp) { revert StakeManager__FundsLocked(); } + if (account.startEpoch == currentEpoch) { + //revert StakeManager__FundsLocked(); + } + uint256 newBalance = account.balance - _amount; + if (newBalance > 0 && newBalance < MIN_BALANCE) { + revert StakeManager__StakeIsTooLow(); + } + _processAccount(account, currentEpoch); uint256 reducedTotalMP = Math.mulDiv(_amount, account.totalMP, account.balance); uint256 reducedMaxMP = Math.mulDiv(_amount, account.maxMP, account.balance); + (uint256 mpRate,, uint256 epochTarget1, uint256 epochTarget2, uint256 mpRemainder) = + _calculateMPPrediction(account.balance, account.startEpoch, ACCURE_RATE); - uint256 mpPerEpoch = _calculateAccuredMP(account.balance, ACCURE_RATE); - expiredStakeStorage.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); - if (account.mpLimitEpoch < currentEpoch) { - totalMPPerEpoch -= mpPerEpoch; + if (mpRemainder > 0) { + expiredStakeStorage.decrementExpiredMP(epochTarget1, mpRemainder); + expiredStakeStorage.decrementExpiredMP(epochTarget2, mpRate - mpRemainder); + + if (epochTarget1 < currentEpoch) { + totalMPRate -= mpRemainder; + } + + if (epochTarget2 < currentEpoch) { + totalMPRate -= mpRate - mpRemainder; + } + } else { + expiredStakeStorage.decrementExpiredMP(epochTarget1, mpRate); + if (epochTarget1 < currentEpoch) { + totalMPRate -= mpRate; + } } //update storage account.balance -= _amount; account.maxMP -= reducedMaxMP; account.totalMP -= reducedTotalMP; + if (account.balance > 0 && account.totalMP < account.maxMP) { + (mpRate,, epochTarget1, epochTarget2, mpRemainder) = + _calculateMPPrediction(account.balance, account.startEpoch, ACCURE_RATE); + if (mpRemainder > 0) { + if (currentEpoch > epochTarget1) { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRemainder); + } + if (currentEpoch > epochTarget2) { + expiredStakeStorage.incrementExpiredMP(epochTarget2, mpRate - mpRemainder); + } + } else { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRate); + } + totalMPRate += Math.min(mpRate, account.maxMP - account.totalMP); + } + totalStaked -= _amount; totalMP -= reducedTotalMP; } @@ -325,7 +364,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { REWARD_TOKEN.transfer(address(migration), epochReward()); expiredStakeStorage.transferOwnership(address(_migration)); migration.migrationInitialize( - currentEpoch, totalMP, totalStaked, startTime, totalMPPerEpoch, potentialMP, currentEpochTotalExpiredMP + currentEpoch, totalMP, totalStaked, startTime, totalMPRate, potentialMP, currentEpochTotalExpiredMP ); } @@ -359,7 +398,7 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { currentEpoch = _currentEpoch; totalMP = _totalMP; totalStaked = _totalStaked; - totalMPPerEpoch = _totalMPPerEpoch; + totalMPRate = _totalMPPerEpoch; potentialMP = _potentialMP; currentEpochTotalExpiredMP = _currentEpochExpiredMP; } @@ -536,12 +575,8 @@ contract StakeManager is StakeMath, TrustedCodehashAccess, IStakeManager { return REWARD_TOKEN.balanceOf(address(this)) - pendingReward; } - /** - * @notice Returns end time of current epoch - * @return _epochEnd end time of current epoch - */ - function epochEnd() public view returns (uint256 _epochEnd) { - return startTime + (ACCURE_RATE * (currentEpoch + 1)); + function getEpochStartTime(uint256 _epochNum) public view override returns (uint256 _epochEnd) { + return startTime + (ACCURE_RATE * (_epochNum)); } /** diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index f2046ea..107ba6b 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -218,46 +218,46 @@ contract UnstakeTest is StakeManagerTest { function test_UnstakeShouldReturnFund_NoLockUp() public { uint256 lockTime = 0; - uint256 stakeAmount = 100; + uint256 stakeAmount = 100 * MIN_BALANCE; uint256 mintAmount = stakeAmount * 10; StakeVault userVault = _createStakingAccount(testUser, stakeAmount, lockTime, mintAmount); - assertEq(ERC20(stakeToken).balanceOf(testUser), 900); + assertEq(ERC20(stakeToken).balanceOf(testUser), mintAmount - stakeAmount); vm.prank(testUser); - userVault.unstake(100); + userVault.unstake(stakeAmount); assertEq(stakeManager.totalStaked(), 0); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); - assertEq(ERC20(stakeToken).balanceOf(testUser), 1000); + assertEq(ERC20(stakeToken).balanceOf(testUser), mintAmount); } function test_UnstakeShouldReturnFund_WithLockUp() public { uint256 lockTime = stakeManager.MIN_LOCKUP_PERIOD(); - uint256 stakeAmount = 100; + uint256 stakeAmount = 100 * MIN_BALANCE; uint256 mintAmount = stakeAmount * 10; StakeVault userVault = _createStakingAccount(testUser, stakeAmount, lockTime, mintAmount); - assertEq(ERC20(stakeToken).balanceOf(testUser), 900); + assertEq(ERC20(stakeToken).balanceOf(testUser), mintAmount - stakeAmount); vm.warp(block.timestamp + lockTime + 1); vm.prank(testUser); - userVault.unstake(100); + userVault.unstake(stakeAmount); assertEq(stakeManager.totalStaked(), 0); assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); - assertEq(ERC20(stakeToken).balanceOf(testUser), 1000); + assertEq(ERC20(stakeToken).balanceOf(testUser), mintAmount); } function test_UnstakeShouldBurnMultiplierPoints() public { uint256 percentToBurn = 90; - uint256 stakeAmount = 100; + uint256 stakeAmount = 100 * MIN_BALANCE; StakeVault userVault = _createStakingAccount(testUser, stakeAmount); vm.startPrank(testUser); assertEq(stakeManager.totalMP(), stakeAmount); for (uint256 i = 0; i < 53; i++) { - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeAccount(address(userVault), i + 1); } (, uint256 balanceBefore, uint256 maxMPBefore, uint256 totalMPBefore,,,,) = @@ -470,7 +470,7 @@ contract ExecuteAccountTest is StakeManagerTest { vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeAccount(address(userVault), currentEpoch + 1); - vm.warp(stakeManager.epochEnd() - 1); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - 1); vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeEpoch(currentEpoch + 1); @@ -478,7 +478,7 @@ contract ExecuteAccountTest is StakeManagerTest { vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeAccount(address(userVault), currentEpoch + 1); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeAccount(address(userVault), currentEpoch + 1); stakeManager.executeEpoch(currentEpoch + 1); @@ -491,7 +491,7 @@ contract ExecuteAccountTest is StakeManagerTest { vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeAccount(address(userVault), currentEpoch + 1); - vm.warp(stakeManager.epochEnd() + stakeManager.ACCURE_RATE() - 1); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + stakeManager.ACCURE_RATE() - 1); vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeEpoch(currentEpoch + 2); @@ -518,9 +518,9 @@ contract ExecuteAccountTest is StakeManagerTest { userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch,) = stakeManager.accounts(address(userVaults[0])); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); //expected MP is, the starting totalMP + the calculatedMPToMint of user balance for one ACCURE_RATE multiplied // by @@ -546,7 +546,7 @@ contract ExecuteAccountTest is StakeManagerTest { for (uint256 i = 0; i < 3; i++) { deal(stakeToken, address(stakeManager), 100 ether); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); console.log("######### NOW", block.timestamp); stakeManager.executeEpoch(); console.log("##### NEW EPOCH", stakeManager.currentEpoch()); @@ -598,30 +598,36 @@ contract ExecuteAccountTest is StakeManagerTest { userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.ACCURE_RATE() - 1)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - (stakeManager.ACCURE_RATE() - 1)); userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.ACCURE_RATE() - 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - (stakeManager.ACCURE_RATE() - 2)); userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 3)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - ((stakeManager.ACCURE_RATE() / 4) * 3) + ); userVaults.push(_createStakingAccount(makeAddr("testUser4"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 2)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - ((stakeManager.ACCURE_RATE() / 4) * 2) + ); userVaults.push(_createStakingAccount(makeAddr("testUser5"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.ACCURE_RATE() / 4) * 1)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - ((stakeManager.ACCURE_RATE() / 4) * 1) + ); userVaults.push(_createStakingAccount(makeAddr("testUser6"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - 2); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - 2); userVaults.push(_createStakingAccount(makeAddr("testUser7"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - 1); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - 1); userVaults.push(_createStakingAccount(makeAddr("testUser8"), stakeAmount, 0)); for (uint256 i = 0; i <= epochsAmountToReachCap; i++) { deal(stakeToken, address(stakeManager), 100 ether); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); for (uint256 j = 0; j < userVaults.length; j++) { (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = @@ -643,7 +649,7 @@ contract ExecuteAccountTest is StakeManagerTest { for (uint256 i = 0; i < 100; i++) { deal(stakeToken, address(stakeManager), 100 ether); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); for (uint256 j = 0; j < userVaults.length; j++) { (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = @@ -744,7 +750,7 @@ contract UserFlowsTest is StakeManagerTest { //tests up to epochs to reach MAX_MULTIPLIER + 10 epochs for (uint256 i = 0; i < epochsAmountToReachCap + 10; i++) { - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); uint256 pendingMPToBeMintedBefore = stakeManager.potentialMP(); uint256 totalMP = stakeManager.totalMP(); @@ -790,7 +796,7 @@ contract MigrationStakeManagerTest is StakeManagerTest { stakeManager.startMigration(newStakeManager); assertEq(address(stakeManager.migration()), address(newStakeManager)); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); vm.expectRevert(StakeManager.StakeManager__PendingMigration.selector); stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 0); @@ -799,7 +805,7 @@ contract MigrationStakeManagerTest is StakeManagerTest { contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochNewEpoch() public { - uint256 firstEpochEnd = stakeManager.epochEnd(); + uint256 firstEpochEnd = stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1); assertEq(stakeManager.currentEpoch(), 0, "Epoch not 0 at start of test"); assertEq(stakeManager.newEpoch(), 0, "New epoch not 0 at start of test"); stakeManager.executeEpoch(); @@ -829,15 +835,15 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochExecuteEpochAfterEnd() public { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 2)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() * 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -846,7 +852,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); } stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -856,7 +862,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); } stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -866,7 +872,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); } stakeManager.executeAccount(address(userVault)); } @@ -875,7 +881,9 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); stakeManager.executeEpoch(); } stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -885,7 +893,9 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); } stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -895,7 +905,9 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); } stakeManager.executeAccount(address(userVault)); } @@ -903,13 +915,13 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochExecuteAccountAfterEpochEnd() public { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 2)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() * 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -917,7 +929,9 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { StakeVault userVault = _createStakingAccount(makeAddr("testUser"), 100_000, 0); for (uint256 i = 0; i < 10; i++) { - vm.warp(stakeManager.epochEnd() + (stakeManager.ACCURE_RATE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } } @@ -925,7 +939,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() public { assertEq(stakeManager.currentEpoch(), 0); - vm.warp(stakeManager.epochEnd() - 1); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - 1); stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 0); } @@ -933,7 +947,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { function test_ExecuteEpochShouldIncreaseEpoch() public { assertEq(stakeManager.currentEpoch(), 0); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 1); } @@ -945,7 +959,7 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { deal(stakeToken, address(stakeManager), 1); assertEq(stakeManager.pendingReward(), 0); assertEq(stakeManager.epochReward(), 1); - vm.warp(stakeManager.epochEnd()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); stakeManager.executeEpoch(); assertEq(stakeManager.pendingReward(), 1); assertEq(stakeManager.epochReward(), 0); From 14baaffc7c7615175f31874bf881f78c3f794eb8 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 13 Nov 2024 16:46:30 -0300 Subject: [PATCH 09/13] chore(cerotra/spec): fix rules --- certora/confs/StakeVault.conf | 2 +- certora/specs/MaxMPRule.spec | 6 +-- certora/specs/StakeManager.spec | 44 +++++++------------ certora/specs/StakeManagerProcessAccount.spec | 2 +- certora/specs/shared.spec | 17 +++---- contracts/StakeManager.sol | 4 +- 6 files changed, 32 insertions(+), 43 deletions(-) diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index f78f918..e981f8c 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -6,7 +6,7 @@ "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeVault:STAKED_TOKEN=ERC20A", + "StakeVault:STAKING_TOKEN=ERC20A", "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", "StakeVault:stakeManager=StakeManager" diff --git a/certora/specs/MaxMPRule.spec b/certora/specs/MaxMPRule.spec index b13b890..fb26269 100644 --- a/certora/specs/MaxMPRule.spec +++ b/certora/specs/MaxMPRule.spec @@ -5,13 +5,13 @@ methods { } invariant MPcantBeGreaterThanMaxMP(address addr) - to_mathint(getAccountCurrentMultiplierPoints(addr)) <= (getAccountBalance(addr) * 8) + getAccountBonusMultiplierPoints(addr) + to_mathint(getAccountCurrentMultiplierPoints(addr)) <= to_mathint(getAccountMaxMultiplierPoints(addr)) filtered { f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector } { preserved { - requireInvariant InitialMPIsNeverSmallerThanBalance(addr); - requireInvariant CurrentMPIsNeverSmallerThanInitialMP(addr); + requireInvariant MaxMPIsNeverSmallerThanBalance(addr); + requireInvariant CurrentMPIsNeverSmallerThanBalance(addr); } } diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index a3386e3..960668f 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -78,7 +78,7 @@ invariant sumOfMultipliersIsMultiplierSupply() } { preserved with (env e){ requireInvariant accountMPIsZeroIfBalanceIsZero(e.msg.sender); - requireInvariant accountBonusMPIsZeroIfBalanceIsZero(e.msg.sender); + requireInvariant accountMaxMPIsZeroIfBalanceIsZero(e.msg.sender); } } @@ -95,20 +95,8 @@ invariant highEpochsAreNull(uint256 epochNumber) m -> !requiresPreviousManager(m) && !requiresNextManager(m) } -invariant accountBonusMPIsZeroIfBalanceIsZero(address addr) - to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountBonusMultiplierPoints(addr)) == 0 - filtered { - f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector - } - -invariant accountMPIsZeroIfBalanceIsZero(address addr) - to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountCurrentMultiplierPoints(addr)) == 0 - filtered { - f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector - } - -invariant InitialMPIsNeverSmallerThanBalance(address addr) - to_mathint(getAccountBonusMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) +invariant accountMaxMPIsZeroIfBalanceIsZero(address addr) + to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountMaxMultiplierPoints(addr)) == 0 filtered { f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector } @@ -135,18 +123,18 @@ rule stakingMintsMultiplierPoints1To1Ratio { uint256 multiplierPointsBefore; uint256 multiplierPointsAfter; - requireInvariant InitialMPIsNeverSmallerThanBalance(e.msg.sender); - requireInvariant CurrentMPIsNeverSmallerThanInitialMP(e.msg.sender); + requireInvariant MaxMPIsNeverSmallerThanBalance(e.msg.sender); + requireInvariant CurrentMPIsNeverSmallerThanBalance(e.msg.sender); requireInvariant accountMPIsZeroIfBalanceIsZero(e.msg.sender); require getAccountLockUntil(e.msg.sender) <= e.block.timestamp; - multiplierPointsBefore = getAccountBonusMultiplierPoints(e.msg.sender); + multiplierPointsBefore = getAccountMaxMultiplierPoints(e.msg.sender); stake(e, amount, lockupTime); - multiplierPointsAfter = getAccountBonusMultiplierPoints(e.msg.sender); - - assert lockupTime == 0 => to_mathint(multiplierPointsAfter) == multiplierPointsBefore + amount; - assert to_mathint(multiplierPointsAfter) >= multiplierPointsBefore + amount; + multiplierPointsAfter = getAccountMaxMultiplierPoints(e.msg.sender); +// + assert lockupTime == 0 => to_mathint(multiplierPointsAfter) == amount * 5; + assert to_mathint(multiplierPointsAfter) == to_mathint(amount + ((amount * 100) * ((4 * 31556925) + lockupTime)) / (31556925 * 100)); } rule stakingGreaterLockupTimeMeansGreaterMPs { @@ -155,19 +143,19 @@ rule stakingGreaterLockupTimeMeansGreaterMPs { uint256 amount; uint256 lockupTime1; uint256 lockupTime2; - uint256 multiplierPointsAfter1; - uint256 multiplierPointsAfter2; + uint256 maxMPAfter1; + uint256 maxMPAfter2; storage initalStorage = lastStorage; stake(e, amount, lockupTime1); - multiplierPointsAfter1 = getAccountBonusMultiplierPoints(e.msg.sender); + maxMPAfter1 = getAccountMaxMultiplierPoints(e.msg.sender); stake(e, amount, lockupTime2) at initalStorage; - multiplierPointsAfter2 = getAccountBonusMultiplierPoints(e.msg.sender); + maxMPAfter2 = getAccountMaxMultiplierPoints(e.msg.sender); - assert lockupTime1 >= lockupTime2 => to_mathint(multiplierPointsAfter1) >= to_mathint(multiplierPointsAfter2); - satisfy to_mathint(multiplierPointsAfter1) > to_mathint(multiplierPointsAfter2); + assert lockupTime1 >= lockupTime2 => to_mathint(maxMPAfter1) >= to_mathint(maxMPAfter2); + satisfy to_mathint(maxMPAfter1) > to_mathint(maxMPAfter2); } /** diff --git a/certora/specs/StakeManagerProcessAccount.spec b/certora/specs/StakeManagerProcessAccount.spec index 5e20e67..32b3de5 100644 --- a/certora/specs/StakeManagerProcessAccount.spec +++ b/certora/specs/StakeManagerProcessAccount.spec @@ -6,7 +6,7 @@ methods { function staked.balanceOf(address) external returns (uint256) envfree; function totalStaked() external returns (uint256) envfree; function totalMP() external returns (uint256) envfree; - function totalMPPerEpoch() external returns (uint256) envfree; + function totalMPRate() external returns (uint256) envfree; function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _processAccount(StakeManager.Account storage account, uint256 _limitEpoch) internal with(env e) => markAccountProccessed(e.msg.sender, _limitEpoch); diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec index 884de62..7851964 100644 --- a/certora/specs/shared.spec +++ b/certora/specs/shared.spec @@ -19,11 +19,11 @@ function getAccountBalance(address addr) returns uint256 { return balance; } -function getAccountBonusMultiplierPoints(address addr) returns uint256 { - uint256 bonusMP; - _, _, bonusMP, _, _, _, _, _ = _stakeManager.accounts(addr); +function getAccountMaxMultiplierPoints(address addr) returns uint256 { + uint256 maxMP; + _, _, maxMP, _, _, _, _, _ = _stakeManager.accounts(addr); - return bonusMP; + return maxMP; } function getAccountCurrentMultiplierPoints(address addr) returns uint256 { @@ -40,14 +40,15 @@ function getAccountLockUntil(address addr) returns uint256 { return lockUntil; } -invariant InitialMPIsNeverSmallerThanBalance(address addr) - to_mathint(getAccountBonusMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) + +invariant MaxMPIsNeverSmallerThanBalance(address addr) + to_mathint(getAccountMaxMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) filtered { f -> f.selector != sig:_stakeManager.migrateFrom(address,bool,StakeManager.Account).selector } -invariant CurrentMPIsNeverSmallerThanInitialMP(address addr) - to_mathint(getAccountCurrentMultiplierPoints(addr)) >= to_mathint(getAccountBonusMultiplierPoints(addr)) +invariant CurrentMPIsNeverSmallerThanBalance(address addr) + to_mathint(getAccountCurrentMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) filtered { f -> f.selector != sig:_stakeManager.migrateFrom(address,bool,StakeManager.Account).selector } diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index d1bf071..c67f91f 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -381,7 +381,7 @@ contract StakeManager is StakeMath, EpochMath, TrustedCodehashAccess, IStakeMana uint256 _totalMP, uint256 _totalStaked, uint256 _startTime, - uint256 _totalMPPerEpoch, + uint256 _totalMPRate, uint256 _potentialMP, uint256 _currentEpochExpiredMP ) @@ -398,7 +398,7 @@ contract StakeManager is StakeMath, EpochMath, TrustedCodehashAccess, IStakeMana currentEpoch = _currentEpoch; totalMP = _totalMP; totalStaked = _totalStaked; - totalMPRate = _totalMPPerEpoch; + totalMPRate = _totalMPRate; potentialMP = _potentialMP; currentEpochTotalExpiredMP = _currentEpochExpiredMP; } From 4ebd4184b093dace382fad0cdb2a638f517ea9e9 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 13 Nov 2024 18:09:51 -0300 Subject: [PATCH 10/13] chore(ci.yml): update certora to 7.17.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 077557b..55cbe90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: with: { java-version: "11", java-package: jre } - name: Install Certora CLI - run: pip3 install certora-cli==7.10.2 + run: pip3 install certora-cli==7.17.2 - name: Install Solidity run: | From ca5d9eb0c86eadc89c0911f23897d81f5b92645d Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 13 Nov 2024 18:27:32 -0300 Subject: [PATCH 11/13] choe(MaxMPRule.conf): remove args and settings --- certora/confs/MaxMPRule.conf | 48 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/certora/confs/MaxMPRule.conf b/certora/confs/MaxMPRule.conf index f37d5f3..315d95f 100644 --- a/certora/confs/MaxMPRule.conf +++ b/certora/confs/MaxMPRule.conf @@ -1,32 +1,20 @@ { - "files": [ - "contracts/StakeManager.sol", + "files": [ + "contracts/StakeManager.sol", "certora/helpers/ExpiredStakeStorageA.sol", - "certora/helpers/ERC20A.sol" - ], - "global_timeout": "7200", - "link": [ - "StakeManager:STAKING_TOKEN=ERC20A", - "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", - ], - "loop_iter": "3", - "msg": "Z3 random seeds", - "optimistic_loop": true, - "packages": [ - "forge-std=lib/forge-std/src", - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]" - ], - "prover_version": "master", - "rule": [ - "MPcantBeGreaterThanMaxMP" - ], - "rule_sanity": "none", - "smt_timeout": "7200", - "verify": "StakeManager:certora/specs/MaxMPRule.spec" -} - - + "certora/helpers/ERC20A.sol" + ], + "link": [ + "StakeManager:REWARD_TOKEN=ERC20A", + "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" + ], + "msg": "Verifying StakeManager.sol maxMP rule", + "rule_sanity": "basic", + "verify": "StakeManager:certora/specs/MaxMPRule.spec", + "optimistic_loop": true, + "loop_iter": "3", + "packages": [ + "forge-std=lib/forge-std/src", + "@openzeppelin=lib/openzeppelin-contracts" + ] +} \ No newline at end of file From d0374a5ef004d3bfa861f1d92ba00a3e459c4765 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 13 Nov 2024 18:29:11 -0300 Subject: [PATCH 12/13] chore(certora/confs): lint files --- certora/confs/StakeManager.conf | 10 +++++----- certora/confs/StakeManagerProcess.conf | 12 ++++++------ certora/confs/StakeManagerStartMigration.conf | 16 ++++++++-------- certora/confs/StakeVault.conf | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/certora/confs/StakeManager.conf b/certora/confs/StakeManager.conf index ce3f004..6fc3a00 100644 --- a/certora/confs/StakeManager.conf +++ b/certora/confs/StakeManager.conf @@ -1,10 +1,10 @@ { - "files": - ["contracts/StakeManager.sol", + "files": [ + "contracts/StakeManager.sol", "certora/helpers/ExpiredStakeStorageA.sol", - "certora/helpers/ERC20A.sol" - ], - "link" : [ + "certora/helpers/ERC20A.sol" + ], + "link": [ "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], diff --git a/certora/confs/StakeManagerProcess.conf b/certora/confs/StakeManagerProcess.conf index 67b061d..1c45e5b 100644 --- a/certora/confs/StakeManagerProcess.conf +++ b/certora/confs/StakeManagerProcess.conf @@ -1,10 +1,10 @@ { - "files": - ["contracts/StakeManager.sol", - "certora/helpers/ERC20A.sol", - "certora/helpers/ExpiredStakeStorageA.sol" - ], - "link" : [ + "files": [ + "contracts/StakeManager.sol", + "certora/helpers/ExpiredStakeStorageA.sol", + "certora/helpers/ERC20A.sol" + ], + "link": [ "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], diff --git a/certora/confs/StakeManagerStartMigration.conf b/certora/confs/StakeManagerStartMigration.conf index e6e6761..bafb372 100644 --- a/certora/confs/StakeManagerStartMigration.conf +++ b/certora/confs/StakeManagerStartMigration.conf @@ -1,13 +1,13 @@ { - "files": - [ "contracts/StakeManager.sol", - "certora/harness/StakeManagerNew.sol", - "certora/helpers/ExpiredStakeStorageA.sol", - "certora/helpers/ERC20A.sol" - ], - "link" : [ + "files": [ + "contracts/StakeManager.sol", + "certora/harness/StakeManagerNew.sol", + "certora/helpers/ExpiredStakeStorageA.sol", + "certora/helpers/ERC20A.sol" + ], + "link": [ "StakeManager:REWARD_TOKEN=ERC20A", - "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", + "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager.sol", "rule_sanity": "basic", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index e981f8c..04b5f84 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -5,7 +5,7 @@ "certora/helpers/ExpiredStakeStorageA.sol", "certora/helpers/ERC20A.sol" ], - "link" : [ + "link": [ "StakeVault:STAKING_TOKEN=ERC20A", "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", From c3e89a1a756d6c96d71a763c82b28573cd3f4aae Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 15 Nov 2024 02:22:56 -0300 Subject: [PATCH 13/13] fix(certora/specs): Make rules work again --- certora/specs/MaxMPRule.spec | 31 +++++++++++++++++-- certora/specs/StakeManager.spec | 7 ----- certora/specs/StakeManagerProcessAccount.spec | 1 - certora/specs/StakeManagerStartMigration.spec | 1 - certora/specs/StakeVault.spec | 7 ----- certora/specs/shared.spec | 15 +++++++++ 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/certora/specs/MaxMPRule.spec b/certora/specs/MaxMPRule.spec index fb26269..6ae51f1 100644 --- a/certora/specs/MaxMPRule.spec +++ b/certora/specs/MaxMPRule.spec @@ -1,15 +1,42 @@ import "./shared.spec"; methods { - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; + function startTime() external returns (uint256) envfree; + function currentEpoch() external returns (uint256) envfree; } +function simplifyEpochProcessing(env e){ + require e.block.timestamp == _stakeManager.startTime(); + require _stakeManager.currentEpoch() == 0; +} + +/* TODO: very usage of CONSTANT with Math.mulDiv or simplify mulDiv somehow, and replace simplifyEpochProcessing with reduceEpochProcessing + +function reduceEpochProcessing(env e, uint256 maxEpochs) { + require e.block.timestamp >= _stakeManager.startTime(); + uint256 currentEpoch = _stakeManager.currentEpoch(); + uint256 newEpoch = _stakeManager.newEpoch(e); + require currentEpoch <= newEpoch; + require currentEpoch - newEpoch <= maxEpochs; +} + +function reduceAccountProcessing(env e, address addr, uint256 maxEpochs) { + uint256 currentEpoch = _stakeManager.currentEpoch(); + uint256 accountEpoch = getAccountEpoch(addr); + require accountEpoch <= currentEpoch; + require accountEpoch >= currentEpoch - maxEpochs; +} +*/ + invariant MPcantBeGreaterThanMaxMP(address addr) to_mathint(getAccountCurrentMultiplierPoints(addr)) <= to_mathint(getAccountMaxMultiplierPoints(addr)) filtered { f -> f.selector != sig:migrateFrom(address,bool,StakeManager.Account).selector } - { preserved { + { preserved with (env e) { + simplifyEpochProcessing(e); + /*reduceEpochProcessing(e, 3); + reduceAccountProcessing(e, addr, 3);*/ requireInvariant MaxMPIsNeverSmallerThanBalance(addr); requireInvariant CurrentMPIsNeverSmallerThanBalance(addr); } diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index 960668f..e6dbfef 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -10,16 +10,9 @@ methods { function _.migrateFrom(address, bool, StakeManager.Account) external => NONDET; function _.increaseTotalMP(uint256) external => NONDET; function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => NONDET; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; - function Math.mulDiv(uint256 a, uint256 b, uint256 c) internal returns uint256 => mulDivSummary(a,b,c); function _._ external => DISPATCH [] default NONDET; } -function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { - require c != 0; - return require_uint256(a*b/c); -} - function isMigrationfunction(method f) returns bool { return f.selector == sig:acceptUpdate().selector || diff --git a/certora/specs/StakeManagerProcessAccount.spec b/certora/specs/StakeManagerProcessAccount.spec index 32b3de5..30bde55 100644 --- a/certora/specs/StakeManagerProcessAccount.spec +++ b/certora/specs/StakeManagerProcessAccount.spec @@ -7,7 +7,6 @@ methods { function totalStaked() external returns (uint256) envfree; function totalMP() external returns (uint256) envfree; function totalMPRate() external returns (uint256) envfree; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _processAccount(StakeManager.Account storage account, uint256 _limitEpoch) internal with(env e) => markAccountProccessed(e.msg.sender, _limitEpoch); function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => NONDET; diff --git a/certora/specs/StakeManagerStartMigration.spec b/certora/specs/StakeManagerStartMigration.spec index 0ccb1b1..0a15f02 100644 --- a/certora/specs/StakeManagerStartMigration.spec +++ b/certora/specs/StakeManagerStartMigration.spec @@ -8,7 +8,6 @@ methods { function totalStaked() external returns (uint256) envfree; function totalMP() external returns (uint256) envfree; function previousManager() external returns (address) envfree; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => DISPATCHER(true); function StakeManagerNew.totalStaked() external returns (uint256) envfree; diff --git a/certora/specs/StakeVault.spec b/certora/specs/StakeVault.spec index c1c02b1..446b193 100644 --- a/certora/specs/StakeVault.spec +++ b/certora/specs/StakeVault.spec @@ -7,16 +7,9 @@ methods { function ERC20A.balanceOf(address) external returns (uint256) envfree; function ERC20A.allowance(address, address) external returns(uint256) envfree; function ERC20A.totalSupply() external returns(uint256) envfree; - function StakeManager.accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; function _.migrateFrom(address, bool, StakeManager.Account) external => DISPATCHER(true); function _.increaseTotalMP(uint256) external => DISPATCHER(true); function _.owner() external => DISPATCHER(true); - function Math.mulDiv(uint256 a, uint256 b, uint256 c) internal returns uint256 => mulDivSummary(a,b,c); -} - -function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { - require c != 0; - return require_uint256(a*b/c); } definition isMigrationFunction(method f) returns bool = ( diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec index 7851964..fe39382 100644 --- a/certora/specs/shared.spec +++ b/certora/specs/shared.spec @@ -1,5 +1,15 @@ using StakeManager as _stakeManager; +methods { + function StakeManager.accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; + function Math.mulDiv(uint256 a, uint256 b, uint256 c) internal returns uint256 => mulDivSummary(a, b, c); +} + +function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { + require c != 0; + return require_uint256(a*b/c); +} + definition requiresPreviousManager(method f) returns bool = ( f.selector == sig:_stakeManager.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256).selector || f.selector == sig:_stakeManager.migrateFrom(address,bool,StakeManager.Account).selector || @@ -40,6 +50,11 @@ function getAccountLockUntil(address addr) returns uint256 { return lockUntil; } +function getAccountEpoch(address addr) returns uint256 { + uint256 epoch; + _, _, _, _, _, _, epoch, _ = _stakeManager.accounts(addr); + return epoch; +} invariant MaxMPIsNeverSmallerThanBalance(address addr) to_mathint(getAccountMaxMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr))