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] 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); }