diff --git a/.gas-report b/.gas-report index cc34c99..890c504 100644 --- a/.gas-report +++ b/.gas-report @@ -1,41 +1,42 @@ | contracts/StakeManager.sol:StakeManager contract | | | | | | |--------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2495816 | 13179 | | | | | +| 2893352 | 15010 | | | | | | 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 | +| ACCURE_RATE | 306 | 306 | 306 | 306 | 1546 | +| MAX_LOCKUP_PERIOD | 405 | 405 | 405 | 405 | 4 | +| MAX_MULTIPLIER | 285 | 285 | 285 | 285 | 685 | | 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 | +| REWARD_TOKEN | 317 | 317 | 317 | 317 | 744 | +| YEAR | 308 | 308 | 308 | 308 | 685 | +| acceptUpdate | 23633 | 23633 | 23633 | 23633 | 1 | +| 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) | 149300 | 149300 | 149300 | 149300 | 2 | -| executeAccount(address,uint256) | 26562 | 72246 | 74122 | 200087 | 141872 | -| executeEpoch() | 23480 | 120708 | 121865 | 900380 | 23566 | +| 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 | 437 | 2346 | 2437 | 2437 | 22 | -| isTrustedCodehash | 541 | 949 | 541 | 2541 | 680 | -| lock | 23818 | 23818 | 23818 | 23818 | 1 | -| migrateTo | 23922 | 23928 | 23928 | 23934 | 2 | +| 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 | | migration | 417 | 1417 | 1417 | 2417 | 4 | -| migrationInitialize | 24624 | 24624 | 24624 | 24624 | 1 | -| newEpoch | 441 | 441 | 441 | 441 | 5 | +| migrationInitialize | 24646 | 24646 | 24646 | 24646 | 1 | +| newEpoch | 463 | 463 | 463 | 463 | 5 | | owner | 2432 | 2432 | 2432 | 2432 | 13 | -| pendingMPToBeMinted | 363 | 363 | 363 | 363 | 46436 | | pendingReward | 408 | 1442 | 2408 | 2408 | 29 | -| previousManager | 275 | 275 | 275 | 275 | 13 | -| setTrustedCodehash | 47960 | 47960 | 47960 | 47960 | 139 | -| stake | 23983 | 23983 | 23983 | 23983 | 1 | -| stakedToken | 272 | 272 | 272 | 272 | 696 | +| 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 | 103602 | 103610 | 103614 | 103614 | 3 | | startTime | 306 | 306 | 306 | 306 | 21 | +| totalMP | 363 | 363 | 363 | 2363 | 49999 | +| totalStaked | 386 | 1786 | 2386 | 2386 | 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 | +| STAKING_TOKEN | 193 | 193 | 193 | 193 | 2 | +| acceptMigration | 35141 | 35141 | 35141 | 35141 | 2 | +| leave | 35196 | 35196 | 35196 | 35196 | 1 | +| lock | 43196 | 71010 | 62038 | 162394 | 7 | +| owner | 318 | 318 | 318 | 318 | 727 | +| stake | 27291 | 264286 | 247573 | 342739 | 732 | +| unstake | 40179 | 92967 | 80797 | 190882 | 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 | 682927 | 682927 | 682927 | 682927 | 731 | | setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | | stakeManager | 368 | 1868 | 2368 | 2368 | 4 | @@ -68,33 +69,24 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23727 | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 25493 | | 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 | 46239 | 46199 | 46367 | 679 | -| balanceOf | 561 | 2107 | 2561 | 2561 | 30746 | - - | script/Deploy.s.sol:Deploy contract | | | | | | |-------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6149062 | 29676 | | | | | +| 6513756 | 31297 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5343965 | 5343965 | 5343965 | 5343965 | 66 | +| run | 5694023 | 5694023 | 5694023 | 5694023 | 66 | | script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | |-------------------------------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 3312594 | 16444 | | | | | +| 3705689 | 18193 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 2330294 | 2330294 | 2330294 | 2330294 | 19 | +| run | 2701563 | 2701563 | 2701563 | 2701563 | 19 | | script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | @@ -114,12 +106,21 @@ | 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 | 32619 | + + | test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | |------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 4833800 | 23474 | | | | | +| 5211884 | 25233 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 4183787 | 4183787 | 4183787 | 4183787 | 1 | +| run | 4540259 | 4540259 | 4540259 | 4540259 | 1 | diff --git a/.gas-snapshot b/.gas-snapshot index d90857a..893fa66 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ CreateVaultTest:testDeployment() (gas: 9774) -CreateVaultTest:test_createVault() (gas: 713999) -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) -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_ExecuteEpochShouldIncreaseEpoch() (gas: 92344) -ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 256338) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 39028) -ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149748) -LeaveTest:testDeployment() (gas: 28806) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1329731) -LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31995) -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) -MigrateTest:testDeployment() (gas: 28806) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1293753) -MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 32007) -MigrationInitializeTest:testDeployment() (gas: 28806) -MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5241726) -MigrationStakeManagerTest:testDeployment() (gas: 28806) -MigrationStakeManagerTest:testNewDeployment() (gas: 30945) -MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149713) +CreateVaultTest:test_createVault() (gas: 700333) +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: 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: 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: 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: 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_StakeTokenTransferFails() (gas: 211363) -StakeTest:test_StakeWithLockBonusMP() (gas: 2356570) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1314730) -StakedTokenTest:testStakeToken() (gas: 7616) -UnstakeTest:testDeployment() (gas: 28828) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1299096) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1343662) -UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31879) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6450026) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1321258) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1438734) -UserFlowsTest:testDeployment() (gas: 28806) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 130945931, ~: 130391929) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1479843) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2496411) +StakeManagerTest:testDeployment() (gas: 28581) +StakeTest:testDeployment() (gas: 28809) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1057645) +StakeTest:test_RevertWhen_Restake() (gas: 1278695) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1282083) +StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32104) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 803932) +StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 207758) +StakeTest:test_StakeWithLockMaxMP() (gas: 2286806) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1276306) +StakedTokenTest:testStakeToken() (gas: 7597) +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: 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index d874ba2..55cbe90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,11 +136,11 @@ 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: | - wget https://github.com/ethereum/solidity/releases/download/v0.8.19/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/.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..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:stakedToken=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 diff --git a/certora/confs/StakeManager.conf b/certora/confs/StakeManager.conf index 9144da4..6fc3a00 100644 --- a/certora/confs/StakeManager.conf +++ b/certora/confs/StakeManager.conf @@ -1,11 +1,11 @@ { - "files": - ["contracts/StakeManager.sol", + "files": [ + "contracts/StakeManager.sol", "certora/helpers/ExpiredStakeStorageA.sol", - "certora/helpers/ERC20A.sol" - ], - "link" : [ - "StakeManager:stakedToken=ERC20A", + "certora/helpers/ERC20A.sol" + ], + "link": [ + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager.sol", diff --git a/certora/confs/StakeManagerProcess.conf b/certora/confs/StakeManagerProcess.conf index 6b51c28..1c45e5b 100644 --- a/certora/confs/StakeManagerProcess.conf +++ b/certora/confs/StakeManagerProcess.conf @@ -1,11 +1,11 @@ { - "files": - ["contracts/StakeManager.sol", - "certora/helpers/ERC20A.sol", - "certora/helpers/ExpiredStakeStorageA.sol" - ], - "link" : [ - "StakeManager:stakedToken=ERC20A", + "files": [ + "contracts/StakeManager.sol", + "certora/helpers/ExpiredStakeStorageA.sol", + "certora/helpers/ERC20A.sol" + ], + "link": [ + "StakeManager:REWARD_TOKEN=ERC20A", "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager ProcessAccount", diff --git a/certora/confs/StakeManagerStartMigration.conf b/certora/confs/StakeManagerStartMigration.conf index 33a5e0e..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" : [ - "StakeManager:stakedToken=ERC20A", - "StakeManager:expiredStakeStorage=ExpiredStakeStorageA", + "files": [ + "contracts/StakeManager.sol", + "certora/harness/StakeManagerNew.sol", + "certora/helpers/ExpiredStakeStorageA.sol", + "certora/helpers/ERC20A.sol" + ], + "link": [ + "StakeManager:REWARD_TOKEN=ERC20A", + "StakeManager:expiredStakeStorage=ExpiredStakeStorageA" ], "msg": "Verifying StakeManager.sol", "rule_sanity": "basic", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index 2aa686c..04b5f84 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -5,9 +5,9 @@ "certora/helpers/ExpiredStakeStorageA.sol", "certora/helpers/ERC20A.sol" ], - "link" : [ - "StakeVault:STAKED_TOKEN=ERC20A", - "StakeManager:stakedToken=ERC20A", + "link": [ + "StakeVault:STAKING_TOKEN=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..e3fa354 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.27; -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..efcb59a 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.27; 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..37b9a78 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.27; import { ExpiredStakeStorage } from "./../../contracts/storage/ExpiredStakeStorage.sol"; diff --git a/certora/specs/MaxMPRule.spec b/certora/specs/MaxMPRule.spec index b13b890..6ae51f1 100644 --- a/certora/specs/MaxMPRule.spec +++ b/certora/specs/MaxMPRule.spec @@ -1,17 +1,44 @@ 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)) <= (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); + { 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 e99dc6f..e6dbfef 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -4,25 +4,19 @@ 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; 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:migrateTo(bool).selector || + f.selector == sig:acceptUpdate().selector || + f.selector == sig:leave().selector || f.selector == sig:transferNonPending().selector; } @@ -65,19 +59,19 @@ 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) } { preserved with (env e){ requireInvariant accountMPIsZeroIfBalanceIsZero(e.msg.sender); - requireInvariant accountBonusMPIsZeroIfBalanceIsZero(e.msg.sender); + requireInvariant accountMaxMPIsZeroIfBalanceIsZero(e.msg.sender); } } @@ -94,8 +88,8 @@ invariant highEpochsAreNull(uint256 epochNumber) m -> !requiresPreviousManager(m) && !requiresNextManager(m) } -invariant accountBonusMPIsZeroIfBalanceIsZero(address addr) - to_mathint(getAccountBalance(addr)) == 0 => to_mathint(getAccountBonusMultiplierPoints(addr)) == 0 +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 } @@ -122,18 +116,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 { @@ -142,19 +136,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 d7d062c..30bde55 100644 --- a/certora/specs/StakeManagerProcessAccount.spec +++ b/certora/specs/StakeManagerProcessAccount.spec @@ -4,14 +4,13 @@ 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 totalMPPerEpoch() external returns (uint256) envfree; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; + function totalStaked() external returns (uint256) envfree; + function totalMP() external returns (uint256) envfree; + function totalMPRate() external returns (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..0a15f02 100644 --- a/certora/specs/StakeManagerStartMigration.spec +++ b/certora/specs/StakeManagerStartMigration.spec @@ -5,13 +5,12 @@ 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 +24,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 +89,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/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 053e710..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 || @@ -7,7 +17,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 ); @@ -18,11 +29,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 { @@ -39,14 +50,20 @@ function getAccountLockUntil(address addr) returns uint256 { return lockUntil; } -invariant InitialMPIsNeverSmallerThanBalance(address addr) - to_mathint(getAccountBonusMultiplierPoints(addr)) >= to_mathint(getAccountBalance(addr)) +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)) 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/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/MultiplierPointMath.sol b/contracts/MultiplierPointMath.sol new file mode 100644 index 0000000..3a83b48 --- /dev/null +++ b/contracts/MultiplierPointMath.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT-1.0 +pragma solidity ^0.8.27; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IStakeConstants } from "./interfaces/IStakeConstants.sol"; + +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 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 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` + */ + function _calculateAccuredMP(uint256 _balance, uint256 _deltaTime) internal pure returns (uint256 _accuredMP) { + return Math.mulDiv(_balance, _deltaTime * MP_APY, YEAR * 100); + } + + /** + * @notice Calculates the bonus multiplier points (MPs) earned when a balance Δa is locked for a specified duration + * t_lock. + * It is equivalent to the accrued multiplier points function but specifically applied in the context of a locked + * balance. + * @param _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) { + 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) internal pure returns (uint256 _initialMP) { + return _amount; + } + + /** + * @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 _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 _balance quantity of tokens + * @return _maxMPAccured maximum quantity of muliplier points that can be generated for given `_amount` + */ + function _calculateMaxAccuredMP(uint256 _balance) internal pure returns (uint256 _maxMPAccured) { + return Math.mulDiv(_balance, MP_MPY, 100); + } + + /** + * @notice The maximum total multiplier points that can be generated for a determined amount of balance and lock + * duration. + * @param _balance Represents the current account balance + * @param _lockTime The time duration for which the balance is locked + * @return _maxMP Maximum multiplier points that can be generated for given `_balance` and `_lockTime` + */ + function _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 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 * MP_MPY_ABSOLUTE) - _mpMax, YEAR, _currentBalance * 100); + } + + /** + * @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) 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 e184553..c67f91f 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -1,17 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.27; -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 "./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 TrustedCodehashAccess { - error StakeManager__FundsLocked(); - error StakeManager__InvalidLockTime(); +contract StakeManager is StakeMath, EpochMath, TrustedCodehashAccess, IStakeManager { error StakeManager__NoPendingMigration(); error StakeManager__PendingMigration(); error StakeManager__SenderIsNotPreviousStakeManager(); @@ -19,34 +21,25 @@ 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; uint256 balance; - uint256 bonusMP; + uint256 maxMP; uint256 totalMP; uint256 lastMint; uint256 lockUntil; uint256 epoch; - uint256 mpLimitEpoch; + uint256 startEpoch; } 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,10 +47,10 @@ contract StakeManager is TrustedCodehashAccess { uint256 public pendingReward; uint256 public immutable startTime; - uint256 public pendingMPToBeMinted; - uint256 public totalSupplyMP; - uint256 public totalSupplyBalance; - uint256 public totalMPPerEpoch; + uint256 public potentialMP; + uint256 public totalMP; + uint256 public totalStaked; + uint256 public totalMPRate; ExpiredStakeStorage public expiredStakeStorage; @@ -65,7 +58,8 @@ contract StakeManager is TrustedCodehashAccess { StakeManager public migration; StakeManager public immutable previousManager; - ERC20 public immutable stakedToken; + IERC20 public immutable REWARD_TOKEN; + IERC20 public immutable STAKING_TOKEN; modifier onlyAccountInitialized(address account) { if (accounts[account].lockUntil == 0) { @@ -114,29 +108,30 @@ contract StakeManager is TrustedCodehashAccess { Epoch storage thisEpoch = epochs[tempCurrentEpoch]; uint256 expiredMP = expiredStakeStorage.getExpiredMP(tempCurrentEpoch); if (expiredMP > 0) { - totalMPPerEpoch -= expiredMP; + totalMPRate -= expiredMP; expiredStakeStorage.deleteExpiredMP(tempCurrentEpoch); } - uint256 epochEstimatedMP = totalMPPerEpoch; + uint256 epochPotentialMP = totalMPRate; 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); + REWARD_TOKEN = IERC20(_REWARD_TOKEN); + STAKING_TOKEN = IERC20(_REWARD_TOKEN); if (address(previousManager) != address(0)) { expiredStakeStorage = previousManager.expiredStakeStorage(); } else { @@ -147,56 +142,53 @@ 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); - if (mpPerEpoch < 1) { + if (_amount < MIN_BALANCE) { revert StakeManager__StakeIsTooLow(); } - uint256 currentEpochExpiredMP = mpPerEpoch - _getMPToMint(_amount, epochEnd() - block.timestamp); - uint256 maxMpToMint = _getMPToMint(_amount, MAX_BOOST * YEAR) + 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 deltaTotalMP = _calculateInitialMP(_amount) + _calculateBonusMP(_amount, _seconds); + uint256 deltaMaxMP = _calculateMaxMP(_amount, _seconds); // account initialization accounts[msg.sender] = Account({ rewardAddress: StakeVault(msg.sender).owner(), balance: _amount, - bonusMP: bonusMP, - totalMP: bonusMP, + maxMP: deltaMaxMP, + totalMP: deltaTotalMP, lastMint: block.timestamp, - lockUntil: block.timestamp + _secondsToLock, + 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 - totalSupplyMP += bonusMP; - totalSupplyBalance += _amount; - currentEpochTotalExpiredMP += currentEpochExpiredMP; - totalMPPerEpoch += mpPerEpoch; - expiredStakeStorage.incrementExpiredMP(mpLimitEpoch, lastEpochAmountToMint); - expiredStakeStorage.incrementExpiredMP(mpLimitEpoch + 1, mpPerEpoch - lastEpochAmountToMint); + totalMP += deltaTotalMP; + totalStaked += _amount; + if (mpRemainder > 0) { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRemainder); + expiredStakeStorage.incrementExpiredMP(epochTarget2, mpRate - mpRemainder); + } else { + expiredStakeStorage.incrementExpiredMP(epochTarget1, mpRate); + } + currentEpochTotalExpiredMP += mpFractional; + totalMPRate += mpRate; } /** @@ -216,33 +208,71 @@ contract StakeManager is TrustedCodehashAccess { 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 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 mpRate,, uint256 epochTarget1, uint256 epochTarget2, uint256 mpRemainder) = + _calculateMPPrediction(account.balance, account.startEpoch, ACCURE_RATE); + + if (mpRemainder > 0) { + expiredStakeStorage.decrementExpiredMP(epochTarget1, mpRemainder); + expiredStakeStorage.decrementExpiredMP(epochTarget2, mpRate - mpRemainder); - uint256 mpPerEpoch = _getMPToMint(account.balance, EPOCH_SIZE); - expiredStakeStorage.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); - if (account.mpLimitEpoch < currentEpoch) { - totalMPPerEpoch -= mpPerEpoch; + 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.bonusMP -= reducedInitialMP; - account.totalMP -= reducedMP; - totalSupplyBalance -= _amount; - totalSupplyMP -= reducedMP; + 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; } /** * @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 +285,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 +297,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.maxMP += bonusMP; account.totalMP += bonusMP; //update global storage - totalSupplyMP += bonusMP; + totalMP += bonusMP; } /** @@ -331,16 +361,10 @@ contract StakeManager is TrustedCodehashAccess { revert StakeManager__InvalidMigration(); } migration = _migration; - stakedToken.transfer(address(migration), epochReward()); + REWARD_TOKEN.transfer(address(migration), epochReward()); expiredStakeStorage.transferOwnership(address(_migration)); migration.migrationInitialize( - currentEpoch, - totalSupplyMP, - totalSupplyBalance, - startTime, - totalMPPerEpoch, - pendingMPToBeMinted, - currentEpochTotalExpiredMP + currentEpoch, totalMP, totalStaked, startTime, totalMPRate, potentialMP, currentEpochTotalExpiredMP ); } @@ -348,17 +372,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 _totalMPRate, + uint256 _potentialMP, uint256 _currentEpochExpiredMP ) external @@ -372,10 +396,10 @@ contract StakeManager is TrustedCodehashAccess { revert StakeManager__InvalidMigration(); } currentEpoch = _currentEpoch; - totalSupplyMP = _totalSupplyMP; - totalSupplyBalance = _totalSupplyBalance; - totalMPPerEpoch = _totalMPPerEpoch; - pendingMPToBeMinted = _pendingMPToBeMinted; + totalMP = _totalMP; + totalStaked = _totalStaked; + totalMPRate = _totalMPRate; + potentialMP = _potentialMP; currentEpochTotalExpiredMP = _currentEpochExpiredMP; } @@ -383,7 +407,7 @@ contract StakeManager is TrustedCodehashAccess { * @notice Transfer current epoch funds for migrated manager */ function transferNonPending() external onlyPendingMigration { - stakedToken.transfer(address(migration), epochReward()); + REWARD_TOKEN.transfer(address(migration), epochReward()); } /** @@ -391,7 +415,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 +423,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 +458,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 +469,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; } /** @@ -446,7 +487,7 @@ contract StakeManager is TrustedCodehashAccess { 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; @@ -461,7 +502,7 @@ contract StakeManager is TrustedCodehashAccess { account.epoch = userEpoch; if (userReward > 0) { pendingReward -= userReward; - stakedToken.transfer(account.rewardAddress, userReward); + REWARD_TOKEN.transfer(account.rewardAddress, userReward); } if (address(migration) != address(0)) { mpDifference = account.totalMP - mpDifference; @@ -476,60 +517,27 @@ contract StakeManager is TrustedCodehashAccess { * @param epoch Epoch to increment total supply */ function _mintMP(Account storage account, uint256 processTime, Epoch storage epoch) private { - uint256 mpToMint = _getMaxMPToMint( - _getMPToMint(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; - totalSupplyMP += mpToMint; + account.totalMP += accruedMP; + totalMP += accruedMP; //mp estimation - epoch.estimatedMP -= mpToMint; - pendingMPToBeMinted -= 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 = _getMPToMint(_balance, MAX_BOOST * YEAR) + _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; } /** - * @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 +546,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 +556,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 +564,7 @@ contract StakeManager is TrustedCodehashAccess { * @return _totalSupply current total supply */ function totalSupplyMinted() public view returns (uint256 _totalSupply) { - return totalSupplyMP + totalSupplyBalance; + return totalMP + totalStaked; } /** @@ -564,15 +572,11 @@ contract StakeManager is TrustedCodehashAccess { * @return _epochReward current epoch reward */ function epochReward() public view returns (uint256 _epochReward) { - return stakedToken.balanceOf(address(this)) - pendingReward; + 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 + (EPOCH_SIZE * (currentEpoch + 1)); + function getEpochStartTime(uint256 _epochNum) public view override returns (uint256 _epochEnd) { + return startTime + (ACCURE_RATE * (_epochNum)); } /** @@ -580,6 +584,6 @@ contract StakeManager is TrustedCodehashAccess { * @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 new file mode 100644 index 0000000..939cd8c --- /dev/null +++ b/contracts/StakeMath.sol @@ -0,0 +1,164 @@ +// 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 StakeMath is MultiplierPointMath { + /// @notice Minimal lockup time + 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 + * 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 + ) + internal + 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_PERIOD, "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 <= MP_MPY_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 + ) + internal + 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_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 <= MP_MPY_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 + ) + internal + 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 + ) + internal + 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); + } + } + + /** + * @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 8588d9e..3d7c8db 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { StakeManager } from "./StakeManager.sol"; /** @@ -18,20 +19,20 @@ contract StakeVault is Ownable { error StakeVault__UnstakingFailed(); - StakeManager private stakeManager; + IStakeManager private stakeManager; - ERC20 public immutable STAKED_TOKEN; + IERC20 public immutable STAKING_TOKEN; event Staked(address from, address to, uint256 _amount, uint256 time); - constructor(address _owner, ERC20 _stakedToken, StakeManager _stakeManager) { + constructor(address _owner, IERC20 _STAKING_TOKEN, IStakeManager _stakeManager) { _transferOwnership(_owner); - STAKED_TOKEN = _stakedToken; + STAKING_TOKEN = _STAKING_TOKEN; stakeManager = _stakeManager; } function stake(uint256 _amount, uint256 _time) external onlyOwner { - bool success = STAKED_TOKEN.transferFrom(msg.sender, address(this), _amount); + bool success = STAKING_TOKEN.transferFrom(msg.sender, address(this), _amount); if (!success) { revert StakeVault__StakingFailed(); } @@ -46,27 +47,24 @@ contract StakeVault is Ownable { function unstake(uint256 _amount) external onlyOwner { stakeManager.unstake(_amount); - bool success = STAKED_TOKEN.transfer(msg.sender, _amount); + bool success = STAKING_TOKEN.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(address(stakeManager)).leave()) { + STAKING_TOKEN.transferFrom(address(this), msg.sender, STAKING_TOKEN.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(address(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..643877f 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.27; 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.stakedToken(), 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 3caac30..b489189 100644 --- a/contracts/access/TrustedCodehashAccess.sol +++ b/contracts/access/TrustedCodehashAccess.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ITrustedCodehashAccess } from "../interfaces/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/Create2FactoryLib.sol b/contracts/factory/Create2FactoryLib.sol new file mode 100644 index 0000000..b41c445 --- /dev/null +++ b/contracts/factory/Create2FactoryLib.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.27; + +/** + * @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(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 new file mode 100644 index 0000000..23365e5 --- /dev/null +++ b/contracts/factory/SingletonFactory.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.27; + +/** + * @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/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 new file mode 100644 index 0000000..6144263 --- /dev/null +++ b/contracts/interfaces/IStakeManager.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +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 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 getStakedBalance(address _vault) external view returns (uint256 _balance); +} diff --git a/contracts/interfaces/ITrustedCodehashAccess.sol b/contracts/interfaces/ITrustedCodehashAccess.sol new file mode 100644 index 0000000..42d225c --- /dev/null +++ b/contracts/interfaces/ITrustedCodehashAccess.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @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/storage/ExpiredStakeStorage.sol b/contracts/storage/ExpiredStakeStorage.sol index 656effa..9689d4e 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.27; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/foundry.toml b/foundry.toml index ab51352..8a1d3dc 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.27" src = "contracts" test = "test" diff --git a/script/Base.s.sol b/script/Base.s.sol index 2a106d7..f7b2eea 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.27 <=0.9.0; import { Script } from "forge-std/Script.sol"; diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5130227..8335eb6 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.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 1b2dfd8..f908d26 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.27 <=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..dea9f01 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.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 new file mode 100644 index 0000000..3c08637 --- /dev/null +++ b/test/DynamicTest.t.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.27; + +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 { StakeMath } from "../contracts/StakeMath.sol"; +import { StakeVault } from "../contracts/StakeVault.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; + +contract DynamicTest is StakeMath, 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 > stakeManager.MIN_BALANCE()); + 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..107ba6b 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.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -7,11 +7,13 @@ 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/interfaces/ITrustedCodehashAccess.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; @@ -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.REWARD_TOKEN()), stakeToken); assertEq(address(stakeManager.previousManager()), address(0)); assertEq(stakeManager.totalSupply(), 0); } @@ -83,39 +85,47 @@ 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); } - 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(totalMP, bonusMP, "totalMP of user vault should be equal to bonusMP after stake if no epochs passed"); + 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.calculateMPToMint(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" ); } @@ -128,18 +138,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); } @@ -169,25 +179,25 @@ 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)); - assertEq(stakeManager.totalSupplyMP(), stakeAmount, "total multiplier point supply"); + (,,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); + 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,81 +208,81 @@ 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); } 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.totalSupplyBalance(), 0); + 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.totalSupplyBalance(), 0); + 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.totalSupplyMP(), stakeAmount); + 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 bonusMPBefore, uint256 totalMPBefore,,,,) = + (, uint256 balanceBefore, uint256 maxMPBefore, uint256 totalMPBefore,,,,) = stakeManager.accounts(address(userVault)); - uint256 totalSupplyMPBefore = stakeManager.totalSupplyMP(); + uint256 totalSupplyMPBefore = stakeManager.totalMP(); uint256 unstakeAmount = stakeAmount * percentToBurn / 100; console.log("unstake", unstakeAmount); 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.totalSupplyMP(); + uint256 totalSupplyMPAfter = stakeManager.totalMP(); console.log("totalSupplyMPBefore", totalSupplyMPBefore); 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); @@ -282,14 +292,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); } @@ -300,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); } @@ -312,7 +322,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); } @@ -321,13 +331,13 @@ 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); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,, uint256 lockUntil,,) = + stakeManager.executeAccount(address(userVault)); + (, 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); @@ -345,25 +355,25 @@ 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); } - 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 totalSupplyMPBefore = stakeManager.totalSupplyMP(); + (, 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 totalSupplyMPAfter = stakeManager.totalSupplyMP(); - assertGt(totalSupplyMPAfter, totalSupplyMPBefore, "totalSupplyMP"); - assertGt(newBonusMP, bonusMP, "bonusMP"); + (, uint256 newBalance, uint256 newMaxMP, uint256 newCurrentMP,,,,) = stakeManager.accounts(address(userVault)); + uint256 totalSupplyMPAfter = stakeManager.totalMP(); + assertGt(totalSupplyMPAfter, totalSupplyMPBefore, "totalMP"); + assertGt(newMaxMP, maxMP, "maxMP"); assertGt(newCurrentMP, totalMP, "totalMP"); assertEq(newBalance, balance, "balance"); } @@ -371,8 +381,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 +403,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 +441,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 @@ -460,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); @@ -468,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); @@ -481,7 +491,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.getEpochStartTime(stakeManager.currentEpoch() + 1) + stakeManager.ACCURE_RATE() - 1); vm.expectRevert(StakeManager.StakeManager__InvalidLimitEpoch.selector); stakeManager.executeEpoch(currentEpoch + 2); @@ -508,13 +518,14 @@ 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 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.calculateMPToMint(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])); @@ -535,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()); @@ -565,7 +576,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"); @@ -579,38 +590,44 @@ 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.ACCURE_RATE()); deal(stakeToken, testUser, stakeAmount); userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 1)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - (stakeManager.ACCURE_RATE() - 1)); userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) - (stakeManager.ACCURE_RATE() - 2)); userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); - vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 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.EPOCH_SIZE() / 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.EPOCH_SIZE() / 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,) = @@ -632,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,) = @@ -643,7 +660,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 @@ -673,17 +690,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 +711,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 +719,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( @@ -712,7 +729,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; @@ -722,30 +739,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.ACCURE_RATE()); 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()); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1)); 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 +781,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.REWARD_TOKEN()), stakeToken); assertEq(address(newStakeManager.previousManager()), address(stakeManager)); assertEq(newStakeManager.totalSupply(), 0); } @@ -779,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); @@ -788,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(); @@ -809,7 +826,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); @@ -818,15 +835,15 @@ 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.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.EPOCH_SIZE() * 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -835,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()); @@ -845,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()); @@ -855,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)); } @@ -864,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.EPOCH_SIZE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); stakeManager.executeEpoch(); } stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -874,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.EPOCH_SIZE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); } stakeManager.executeEpoch(); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); @@ -884,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.EPOCH_SIZE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); } stakeManager.executeAccount(address(userVault)); } @@ -892,13 +915,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.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.EPOCH_SIZE() * 2)); + vm.warp(stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() * 2)); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } @@ -906,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.EPOCH_SIZE() / 10 - i)); + vm.warp( + stakeManager.getEpochStartTime(stakeManager.currentEpoch() + 1) + (stakeManager.ACCURE_RATE() / 10 - i) + ); stakeManager.executeAccount(address(userVault), stakeManager.currentEpoch()); } } @@ -914,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); } @@ -922,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); } @@ -934,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); diff --git a/test/StakeVault.t.sol b/test/StakeVault.t.sol index bd7841b..3e00cb4 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.27; 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..0cca62c 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.27; 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..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.19; +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 dcff6c5..87d6d16 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.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 e2105e1..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.19; +pragma solidity ^0.8.27; import { BaseScript } from "../../script/Base.s.sol"; import { StakeManager } from "../../contracts/StakeManager.sol";