From 44d16c4b00ba0ee0d99656486df60d0182badc9d Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:54:45 +0200 Subject: [PATCH] feat(StakeManager): implement multiplier points estimation This commit introduces the internal accounting logic for accrueing multiplier points, that will later be used to determine how many experience points an account is eligible to. The majority of the work here was done by @3esmit. --- .gas-report | 98 --------- .gas-snapshot | 67 ++++-- certora/confs/StakeManager.conf | 7 +- certora/confs/StakeManagerProcess.conf | 9 +- certora/confs/StakeManagerStartMigration.conf | 4 +- certora/confs/StakeVault.conf | 2 + certora/helpers/StakeRewardEstimateA.sol | 8 + certora/specs/StakeManager.spec | 15 +- certora/specs/StakeManagerProcessAccount.spec | 11 +- certora/specs/StakeManagerStartMigration.spec | 10 +- certora/specs/StakeVault.spec | 6 +- contracts/StakeManager.sol | 205 +++++++++++++----- test/StakeManager.t.sol | 166 +++++++++----- 13 files changed, 350 insertions(+), 258 deletions(-) create mode 100644 certora/helpers/StakeRewardEstimateA.sol diff --git a/.gas-report b/.gas-report index dcf8f68..e69de29 100644 --- a/.gas-report +++ b/.gas-report @@ -1,98 +0,0 @@ -| contracts/StakeManager.sol:StakeManager contract | | | | | | -|--------------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 2058079 | 10495 | | | | | -| Function Name | min | avg | median | max | # calls | -| EPOCH_SIZE | 285 | 285 | 285 | 285 | 9 | -| MAX_LOCKUP_PERIOD | 405 | 405 | 405 | 405 | 2 | -| MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 3 | -| accounts | 1406 | 1406 | 1406 | 1406 | 22 | -| currentEpoch | 341 | 1571 | 2341 | 2341 | 13 | -| epochEnd | 627 | 627 | 627 | 627 | 56 | -| executeAccount | 1311 | 54054 | 58730 | 104630 | 63 | -| executeEpoch | 87833 | 95166 | 87833 | 109833 | 3 | -| isVault | 517 | 2117 | 2517 | 2517 | 15 | -| lock | 2614 | 2614 | 2614 | 2614 | 1 | -| migrateTo | 1041 | 1713 | 1041 | 2721 | 5 | -| oldManager | 240 | 240 | 240 | 240 | 8 | -| owner | 2341 | 2341 | 2341 | 2341 | 8 | -| pendingReward | 386 | 1243 | 386 | 2386 | 21 | -| setVault | 22606 | 22606 | 22606 | 22606 | 12 | -| stake | 2638 | 136864 | 188409 | 189128 | 17 | -| stakedToken | 260 | 260 | 260 | 260 | 26 | -| totalSupply | 561 | 561 | 561 | 561 | 17 | -| totalSupplyBalance | 362 | 1592 | 2362 | 2362 | 13 | -| totalSupplyMP | 384 | 1614 | 2384 | 2384 | 13 | -| unstake | 1730 | 20008 | 8086 | 127550 | 9 | - - -| contracts/StakeVault.sol:StakeVault contract | | | | | | -|----------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 635445 | 3370 | | | | | -| Function Name | min | avg | median | max | # calls | -| acceptMigration | 1726 | 1726 | 1726 | 1726 | 2 | -| leave | 1712 | 1712 | 1712 | 1712 | 1 | -| owner | 362 | 362 | 362 | 362 | 14 | -| stake | 3433 | 165544 | 219184 | 219903 | 17 | -| stakedToken | 212 | 212 | 212 | 212 | 2 | -| unstake | 2588 | 28122 | 14407 | 131871 | 8 | - - -| contracts/VaultFactory.sol:VaultFactory contract | | | | | | -|--------------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 1043406 | 5305 | | | | | -| Function Name | min | avg | median | max | # calls | -| createVault | 670954 | 674204 | 675454 | 675454 | 18 | -| setStakeManager | 2518 | 5317 | 4644 | 8790 | 3 | -| stakeManager | 368 | 1868 | 2368 | 2368 | 4 | - - -| lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol:ERC20 contract | | | | | | -|---------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 649818 | 3562 | | | | | -| Function Name | min | avg | median | max | # calls | -| approve | 24603 | 24603 | 24603 | 24603 | 15 | -| balanceOf | 561 | 819 | 561 | 2561 | 139 | -| transfer | 3034 | 8340 | 3034 | 22934 | 15 | -| transferFrom | 27530 | 27530 | 27530 | 27530 | 16 | - - -| script/Deploy.s.sol:Deploy contract | | | | | | -|-------------------------------------|-----------------|---------|---------|---------|---------| -| Deployment Cost | Deployment Size | | | | | -| 5142589 | 26992 | | | | | -| Function Name | min | avg | median | max | # calls | -| run | 4837698 | 4837698 | 4837698 | 4837698 | 32 | - - -| script/DeploymentConfig.s.sol:DeploymentConfig contract | | | | | | -|---------------------------------------------------------|-----------------|-----|--------|-----|---------| -| Deployment Cost | Deployment Size | | | | | -| 1634091 | 8548 | | | | | -| Function Name | min | avg | median | max | # calls | -| activeNetworkConfig | 455 | 455 | 455 | 455 | 64 | - - -| test/mocks/BrokenERC20.s.sol:BrokenERC20 contract | | | | | | -|---------------------------------------------------|-----------------|-------|--------|-------|---------| -| Deployment Cost | Deployment Size | | | | | -| 475642 | 2660 | | | | | -| Function Name | min | avg | median | max | # calls | -| approve | 24603 | 24603 | 24603 | 24603 | 1 | -| balanceOf | 561 | 1227 | 561 | 2561 | 3 | -| transferFrom | 511 | 511 | 511 | 511 | 1 | - - -| test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | -|------------------------------------------------------|-----------------|---------|---------|---------|---------| -| Deployment Cost | Deployment Size | | | | | -| 3915327 | 20790 | | | | | -| Function Name | min | avg | median | max | # calls | -| run | 3677521 | 3677521 | 3677521 | 3677521 | 1 | - - - - diff --git a/.gas-snapshot b/.gas-snapshot index 73d50f9..764a568 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,33 +1,52 @@ CreateVaultTest:testDeployment() (gas: 9774) -CreateVaultTest:test_createVault() (gas: 692923) -ExecuteAccountTest:testDeployment() (gas: 26335) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 3633587) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1051631) -LeaveTest:testDeployment() (gas: 26335) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1052239) -LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 10794) -LockTest:testDeployment() (gas: 26335) -LockTest:test_RevertWhen_InvalidLockupPeriod() (gas: 865463) +CreateVaultTest:test_createVault() (gas: 692936) +ExecuteAccountTest:testDeployment() (gas: 28720) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 3856669) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1154868) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 78782687) +ExecuteEpochTest:testDeployment() (gas: 28720) +ExecuteEpochTest:testNewDeployment() (gas: 30815) +ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 94832) +ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 253059) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 17994) +ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 105720) +LeaveTest:testDeployment() (gas: 28720) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1154740) +LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 10750) +LockTest:testDeployment() (gas: 28720) +LockTest:test_NewLockupPeriod() (gas: 1143590) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1135184) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1231816) LockTest:test_RevertWhen_SenderIsNotVault() (gas: 10630) -MigrateTest:testDeployment() (gas: 26335) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1049846) -MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 10794) +LockTest:test_ShouldIncreaseBonusMP() (gas: 1123712) +LockTest:test_UpdateLockupPeriod() (gas: 1281226) +MigrateTest:testDeployment() (gas: 28720) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1152379) +MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 10750) +MigrationInitializeTest:testDeployment() (gas: 28720) +MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5719768) +MigrationStakeManagerTest:testDeployment() (gas: 28720) +MigrationStakeManagerTest:testNewDeployment() (gas: 30859) +MigrationStakeManagerTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 105708) SetStakeManagerTest:testDeployment() (gas: 9774) SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 20481) SetStakeManagerTest:test_SetStakeManager() (gas: 19869) -StakeManagerTest:testDeployment() (gas: 26107) -StakeTest:testDeployment() (gas: 26335) -StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 883366) -StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10650) +StakeManagerTest:testDeployment() (gas: 28492) +StakeTest:testDeployment() (gas: 28720) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 892157) +StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10680) StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 175040) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 948728) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1029276) StakedTokenTest:testStakeToken() (gas: 7616) -UnstakeTest:testDeployment() (gas: 26357) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1051813) +UnstakeTest:testDeployment() (gas: 28742) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1132994) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1158740) UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10653) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 3573382) -UnstakeTest:test_UnstakeShouldReturnFunds() (gas: 946825) -UserFlowsTest:testDeployment() (gas: 26335) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1046480) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 1825625) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 5498441) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1026676) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1115802) +UserFlowsTest:testDeployment() (gas: 28720) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8) (runs: 1001, μ: 67853584, ~: 29218023) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1116690) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 1951132) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/certora/confs/StakeManager.conf b/certora/confs/StakeManager.conf index 3291acf..6fd4b76 100644 --- a/certora/confs/StakeManager.conf +++ b/certora/confs/StakeManager.conf @@ -1,10 +1,12 @@ { - "files": + "files": ["contracts/StakeManager.sol", + "certora/helpers/StakeRewardEstimateA.sol", "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A" + "StakeManager:stakedToken=ERC20A", + "StakeManager:stakeRewardEstimate=StakeRewardEstimateA" ], "msg": "Verifying StakeManager.sol", "rule_sanity": "basic", @@ -12,6 +14,7 @@ "optimistic_loop": true, "loop_iter": "3", "packages": [ + "forge-std=lib/forge-std/src", "@openzeppelin=lib/openzeppelin-contracts" ] } diff --git a/certora/confs/StakeManagerProcess.conf b/certora/confs/StakeManagerProcess.conf index 35cede7..c070703 100644 --- a/certora/confs/StakeManagerProcess.conf +++ b/certora/confs/StakeManagerProcess.conf @@ -1,10 +1,12 @@ { - "files": + "files": ["contracts/StakeManager.sol", - "certora/helpers/ERC20A.sol" + "certora/helpers/ERC20A.sol", + "certora/helpers/StakeRewardEstimateA.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A" + "StakeManager:stakedToken=ERC20A", + "StakeManager:stakeRewardEstimate=StakeRewardEstimateA" ], "msg": "Verifying StakeManager ProcessAccount", "rule_sanity": "basic", @@ -12,6 +14,7 @@ "optimistic_loop": true, "loop_iter": "3", "packages": [ + "forge-std=lib/forge-std/src", "@openzeppelin=lib/openzeppelin-contracts" ] } diff --git a/certora/confs/StakeManagerStartMigration.conf b/certora/confs/StakeManagerStartMigration.conf index e12c8fb..ba1524d 100644 --- a/certora/confs/StakeManagerStartMigration.conf +++ b/certora/confs/StakeManagerStartMigration.conf @@ -2,10 +2,12 @@ "files": [ "contracts/StakeManager.sol", "certora/harness/StakeManagerNew.sol", + "certora/helpers/StakeRewardEstimateA.sol", "certora/helpers/ERC20A.sol" ], "link" : [ - "StakeManager:stakedToken=ERC20A" + "StakeManager:stakedToken=ERC20A", + "StakeManager:stakeRewardEstimate=StakeRewardEstimateA", ], "msg": "Verifying StakeManager.sol", "rule_sanity": "basic", diff --git a/certora/confs/StakeVault.conf b/certora/confs/StakeVault.conf index c70040e..088e1f2 100644 --- a/certora/confs/StakeVault.conf +++ b/certora/confs/StakeVault.conf @@ -2,11 +2,13 @@ "files": [ "contracts/StakeManager.sol", "contracts/StakeVault.sol", + "certora/helpers/StakeRewardEstimateA.sol", "certora/helpers/ERC20A.sol" ], "link" : [ "StakeVault:STAKED_TOKEN=ERC20A", "StakeManager:stakedToken=ERC20A", + "StakeManager:stakeRewardEstimate=StakeRewardEstimateA", "StakeVault:stakeManager=StakeManager" ], "msg": "Verifying StakeVault.sol", diff --git a/certora/helpers/StakeRewardEstimateA.sol b/certora/helpers/StakeRewardEstimateA.sol new file mode 100644 index 0000000..981a646 --- /dev/null +++ b/certora/helpers/StakeRewardEstimateA.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { StakeRewardEstimate } from "./../../contracts/StakeManager.sol"; + +contract StakeRewardEstimateA is StakeRewardEstimate {} + diff --git a/certora/specs/StakeManager.spec b/certora/specs/StakeManager.spec index 6d24226..81cce58 100644 --- a/certora/specs/StakeManager.spec +++ b/certora/specs/StakeManager.spec @@ -6,9 +6,10 @@ methods { 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) external => NONDET; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256) envfree; + 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 { @@ -18,28 +19,28 @@ function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { function getAccountBalance(address addr) returns uint256 { uint256 balance; - _, balance, _, _, _, _, _ = accounts(addr); + _, balance, _, _, _, _, _, _ = accounts(addr); return balance; } function getAccountBonusMultiplierPoints(address addr) returns uint256 { uint256 bonusMP; - _, _, bonusMP, _, _, _, _ = accounts(addr); + _, _, bonusMP, _, _, _, _, _ = accounts(addr); return bonusMP; } function getAccountCurrentMultiplierPoints(address addr) returns uint256 { uint256 totalMP; - _, _, _, totalMP, _, _, _ = accounts(addr); + _, _, _, totalMP, _, _, _, _ = accounts(addr); return totalMP; } function getAccountLockUntil(address addr) returns uint256 { uint256 lockUntil; - _, _, _, _, _, lockUntil, _ = accounts(addr); + _, _, _, _, _, lockUntil, _, _ = accounts(addr); return lockUntil; } @@ -59,7 +60,7 @@ function simplification(env e) { } definition requiresPreviousManager(method f) returns bool = ( - f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256).selector || + f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256).selector || f.selector == sig:migrateFrom(address,bool,StakeManager.Account).selector || f.selector == sig:increaseTotalMP(uint256).selector ); diff --git a/certora/specs/StakeManagerProcessAccount.spec b/certora/specs/StakeManagerProcessAccount.spec index a9c7ae7..c121520 100644 --- a/certora/specs/StakeManagerProcessAccount.spec +++ b/certora/specs/StakeManagerProcessAccount.spec @@ -2,10 +2,13 @@ using ERC20A as staked; methods { function staked.balanceOf(address) external returns (uint256) envfree; function totalSupplyBalance() external returns (uint256) envfree; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, 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 _processAccount(StakeManager.Account storage account, uint256 _limitEpoch) internal with(env e) => markAccountProccessed(e.msg.sender, _limitEpoch); - function _.migrationInitialize(uint256,uint256,uint256,uint256) external => NONDET; + function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => NONDET; + function pendingMPToBeMinted() external returns (uint256) envfree; } // keeps track of the last epoch an account was processed @@ -19,7 +22,7 @@ function markAccountProccessed(address account, uint256 _limitEpoch) { function getAccountLockUntil(address addr) returns uint256 { uint256 lockUntil; - _, _, _, _, _, lockUntil, _ = accounts(addr); + _, _, _, _, _, lockUntil, _, _ = accounts(addr); return lockUntil; } @@ -29,7 +32,7 @@ hook Sstore accounts[KEY address addr].balance uint256 newValue (uint256 oldValu } definition requiresPreviousManager(method f) returns bool = ( - f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256).selector || + f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256).selector || f.selector == sig:migrateFrom(address,bool,StakeManager.Account).selector || f.selector == sig:increaseTotalMP(uint256).selector ); diff --git a/certora/specs/StakeManagerStartMigration.spec b/certora/specs/StakeManagerStartMigration.spec index 0355cd1..c469914 100644 --- a/certora/specs/StakeManagerStartMigration.spec +++ b/certora/specs/StakeManagerStartMigration.spec @@ -6,23 +6,23 @@ methods { function totalSupplyBalance() external returns (uint256) envfree; function totalSupplyMP() external returns (uint256) envfree; function previousManager() external returns (address) envfree; - function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256) envfree; + function accounts(address) external returns(address, uint256, uint256, uint256, uint256, uint256, uint256, uint256) envfree; - function _.migrationInitialize(uint256,uint256,uint256,uint256) external => DISPATCHER(true); + function _.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256) external => DISPATCHER(true); function StakeManagerNew.totalSupplyBalance() external returns (uint256) envfree; } function getAccountMultiplierPoints(address addr) returns uint256 { uint256 multiplierPoints; - _, _, _, multiplierPoints, _, _, _ = accounts(addr); + _, _, _, multiplierPoints, _, _, _, _ = accounts(addr); return multiplierPoints; } function getAccountBalance(address addr) returns uint256 { uint256 balance; - _, balance, _, _, _, _, _ = accounts(addr); + _, balance, _, _, _, _, _, _ = accounts(addr); return balance; } @@ -33,7 +33,7 @@ definition blockedWhenMigrating(method f) returns bool = ( f.selector == sig:lock(uint256).selector || f.selector == sig:executeEpoch().selector || f.selector == sig:startMigration(address).selector || - f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256).selector + f.selector == sig:migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256).selector ); definition blockedWhenNotMigrating(method f) returns bool = ( diff --git a/certora/specs/StakeVault.spec b/certora/specs/StakeVault.spec index 6efc393..cadb9c4 100644 --- a/certora/specs/StakeVault.spec +++ b/certora/specs/StakeVault.spec @@ -5,7 +5,7 @@ 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) 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); @@ -19,13 +19,13 @@ function mulDivSummary(uint256 a, uint256 b, uint256 c) returns uint256 { function getAccountBalance(address addr) returns uint256 { uint256 balance; - _, balance, _, _, _, _, _ = stakeManager.accounts(addr); + _, balance, _, _, _, _, _, _ = stakeManager.accounts(addr); return balance; } definition isMigrationFunction(method f) returns bool = ( - f.selector == sig:stakeManager.migrationInitialize(uint256,uint256,uint256,uint256).selector || + f.selector == sig:stakeManager.migrationInitialize(uint256,uint256,uint256,uint256,uint256,uint256,uint256).selector || f.selector == sig:stakeManager.migrateFrom(address,bool,StakeManager.Account).selector || f.selector == sig:stakeManager.increaseTotalMP(uint256).selector || f.selector == sig:stakeManager.startMigration(address).selector diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index 2c44342..3ea8d86 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -8,6 +8,26 @@ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { StakeVault } from "./StakeVault.sol"; +contract StakeRewardEstimate is Ownable { + mapping(uint256 epochId => uint256 balance) public expiredMPPerEpoch; + + function getExpiredMP(uint256 epochId) public view returns (uint256) { + return expiredMPPerEpoch[epochId]; + } + + function incrementExpiredMP(uint256 epochId, uint256 amount) public onlyOwner { + expiredMPPerEpoch[epochId] += amount; + } + + function decrementExpiredMP(uint256 epochId, uint256 amount) public onlyOwner { + expiredMPPerEpoch[epochId] -= amount; + } + + function deleteExpiredMP(uint256 epochId) public onlyOwner { + delete expiredMPPerEpoch[epochId]; + } +} + contract StakeManager is Ownable { error StakeManager__SenderIsNotVault(); error StakeManager__FundsLocked(); @@ -20,6 +40,8 @@ contract StakeManager is Ownable { error StakeManager__InvalidMigration(); error StakeManager__AlreadyProcessedEpochs(); error StakeManager__InsufficientFunds(); + error StakeManager__AlreadyStaked(); + error StakeManager__StakeIsTooLow(); struct Account { address rewardAddress; @@ -29,12 +51,14 @@ contract StakeManager is Ownable { uint256 lastMint; uint256 lockUntil; uint256 epoch; + uint256 mpLimitEpoch; } struct Epoch { uint256 startTime; uint256 epochReward; uint256 totalSupply; + uint256 estimatedMP; } uint256 public constant EPOCH_SIZE = 1 weeks; @@ -50,8 +74,16 @@ contract StakeManager is Ownable { uint256 public currentEpoch; uint256 public pendingReward; + + uint256 public pendingMPToBeMinted; uint256 public totalSupplyMP; uint256 public totalSupplyBalance; + uint256 public totalMPPerEpoch; + + StakeRewardEstimate public stakeRewardEstimate; + + uint256 public currentEpochTotalExpiredMP; + StakeManager public migration; StakeManager public immutable previousManager; ERC20 public immutable stakedToken; @@ -108,10 +140,20 @@ contract StakeManager is Ownable { */ modifier finalizeEpoch() { if (block.timestamp >= epochEnd() && address(migration) == address(0)) { + uint256 expiredMP = stakeRewardEstimate.getExpiredMP(currentEpoch); + if (expiredMP > 0) { + totalMPPerEpoch -= expiredMP; + stakeRewardEstimate.deleteExpiredMP(currentEpoch); + } + epochs[currentEpoch].estimatedMP = totalMPPerEpoch - currentEpochTotalExpiredMP; + delete currentEpochTotalExpiredMP; + pendingMPToBeMinted += epochs[currentEpoch].estimatedMP; + //finalize current epoch epochs[currentEpoch].epochReward = epochReward(); epochs[currentEpoch].totalSupply = totalSupply(); pendingReward += epochs[currentEpoch].epochReward; + //create new epoch currentEpoch++; epochs[currentEpoch].startTime = block.timestamp; @@ -123,54 +165,62 @@ contract StakeManager is Ownable { epochs[0].startTime = block.timestamp; previousManager = StakeManager(_previousManager); stakedToken = ERC20(_stakedToken); + if (address(previousManager) != address(0)) { + stakeRewardEstimate = previousManager.stakeRewardEstimate(); + } else { + stakeRewardEstimate = new StakeRewardEstimate(); + } } /** * Increases balance of msg.sender; - * @param _amount Amount of balance to be decreased. - * @param _timeToIncrease Seconds to increase in locked time. If stake is unlocked, increases from block.timestamp. + * @param _amount Amount of balance being staked. + * @param _secondsToLock 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 _timeToIncrease) external onlyVault noPendingMigration finalizeEpoch { + function stake(uint256 _amount, uint256 _secondsToLock) external onlyVault noPendingMigration finalizeEpoch { Account storage account = accounts[msg.sender]; - - if (account.lockUntil == 0) { - // account not initialized - account.lockUntil = block.timestamp; - account.epoch = currentEpoch; //starts in current epoch - account.rewardAddress = StakeVault(msg.sender).owner(); - } else { - _processAccount(account, currentEpoch); + if (account.balance > 0 || account.lockUntil != 0) { + revert StakeManager__AlreadyStaked(); } - - uint256 deltaTime = 0; - - if (_timeToIncrease > 0) { - uint256 lockUntil = account.lockUntil + _timeToIncrease; - if (lockUntil < block.timestamp) { - revert StakeManager__InvalidLockTime(); - } - - deltaTime = lockUntil - block.timestamp; - if (deltaTime < MIN_LOCKUP_PERIOD || deltaTime > MAX_LOCKUP_PERIOD) { - revert StakeManager__InvalidLockTime(); - } + if (_secondsToLock != 0 && (_secondsToLock < MIN_LOCKUP_PERIOD || _secondsToLock > MAX_LOCKUP_PERIOD)) { + revert StakeManager__InvalidLockTime(); } - _mintBonusMP(account, deltaTime, _amount); - //update storage + //mp estimation + uint256 mpPerEpoch = _getMPToMint(_amount, EPOCH_SIZE); + if (mpPerEpoch < 1) { + revert StakeManager__StakeIsTooLow(); + } + uint256 currentEpochExpiredMP = mpPerEpoch - _getMPToMint(_amount, epochEnd() - block.timestamp); + uint256 maxMpToMint = _getMPToMint(_amount, MAX_BOOST * YEAR) + currentEpochExpiredMP; + uint256 epochAmountToReachMpLimit = (maxMpToMint) / mpPerEpoch; + uint256 mpLimitEpoch = currentEpoch + epochAmountToReachMpLimit; + uint256 lastEpochAmountToMint = ((mpPerEpoch * (epochAmountToReachMpLimit + 1)) - maxMpToMint); + + // account initialization + account.lockUntil = block.timestamp + _secondsToLock; + account.epoch = currentEpoch; //starts in current epoch + account.rewardAddress = StakeVault(msg.sender).owner(); + account.balance = _amount; + account.mpLimitEpoch = mpLimitEpoch; + _mintBonusMP(account, _secondsToLock, _amount); + + //update global storage totalSupplyBalance += _amount; - account.balance += _amount; - account.lockUntil += _timeToIncrease; + currentEpochTotalExpiredMP += currentEpochExpiredMP; + totalMPPerEpoch += mpPerEpoch; + stakeRewardEstimate.incrementExpiredMP(mpLimitEpoch, lastEpochAmountToMint); + stakeRewardEstimate.incrementExpiredMP(mpLimitEpoch + 1, mpPerEpoch - lastEpochAmountToMint); } /** * leaves the staking pool and withdraws all funds; */ - function unstake( - uint256 _amount - ) + function unstake(uint256 _amount) external onlyVault onlyAccountInitialized(msg.sender) @@ -189,6 +239,13 @@ contract StakeManager is Ownable { uint256 reducedMP = Math.mulDiv(_amount, account.totalMP, account.balance); uint256 reducedInitialMP = Math.mulDiv(_amount, account.bonusMP, account.balance); + uint256 mpPerEpoch = _getMPToMint(account.balance, EPOCH_SIZE); + + stakeRewardEstimate.decrementExpiredMP(account.mpLimitEpoch, mpPerEpoch); + if (account.mpLimitEpoch < currentEpoch) { + totalMPPerEpoch -= mpPerEpoch; + } + //update storage account.balance -= _amount; account.bonusMP -= reducedInitialMP; @@ -199,13 +256,12 @@ contract StakeManager is Ownable { /** * @notice Locks entire balance for more amount of time. - * @param _timeToIncrease Seconds to increase in locked time. If stake is unlocked, increases from block.timestamp. + * @param _secondsToIncreaseLock 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 _timeToIncrease - ) + function lock(uint256 _secondsToIncreaseLock) external onlyVault onlyAccountInitialized(msg.sender) @@ -217,16 +273,16 @@ contract StakeManager is Ownable { uint256 lockUntil = account.lockUntil; uint256 deltaTime; if (lockUntil < block.timestamp) { - lockUntil = block.timestamp + _timeToIncrease; - deltaTime = _timeToIncrease; + lockUntil = block.timestamp + _secondsToIncreaseLock; + deltaTime = _secondsToIncreaseLock; } else { - lockUntil += _timeToIncrease; + lockUntil += _secondsToIncreaseLock; deltaTime = lockUntil - block.timestamp; } if (deltaTime < MIN_LOCKUP_PERIOD || deltaTime > MAX_LOCKUP_PERIOD) { revert StakeManager__InvalidLockTime(); } - _mintBonusMP(account, _timeToIncrease, 0); + _mintBonusMP(account, _secondsToIncreaseLock, 0); //update account storage account.lockUntil = lockUntil; } @@ -273,7 +329,16 @@ contract StakeManager is Ownable { } migration = _migration; stakedToken.transfer(address(migration), epochReward()); - migration.migrationInitialize(currentEpoch, totalSupplyMP, totalSupplyBalance, epochs[currentEpoch].startTime); + stakeRewardEstimate.transferOwnership(address(_migration)); + migration.migrationInitialize( + currentEpoch, + totalSupplyMP, + totalSupplyBalance, + epochs[currentEpoch].startTime, + totalMPPerEpoch, + pendingMPToBeMinted, + currentEpochTotalExpiredMP + ); } /** @@ -288,7 +353,10 @@ contract StakeManager is Ownable { uint256 _currentEpoch, uint256 _totalSupplyMP, uint256 _totalSupplyBalance, - uint256 _epochStartTime + uint256 _epochStartTime, + uint256 _totalMPPerEpoch, + uint256 _pendingMPToBeMinted, + uint256 _currentEpochExpiredMP ) external onlyPreviousManager @@ -303,6 +371,9 @@ contract StakeManager is Ownable { totalSupplyMP = _totalSupplyMP; totalSupplyBalance = _totalSupplyBalance; epochs[currentEpoch].startTime = _epochStartTime; + totalMPPerEpoch = _totalMPPerEpoch; + pendingMPToBeMinted = _pendingMPToBeMinted; + currentEpochTotalExpiredMP = _currentEpochExpiredMP; } /** @@ -316,9 +387,7 @@ contract StakeManager is Ownable { * @notice Migrate account to new manager. * @param _acceptMigration true if wants to migrate, false if wants to leave */ - function migrateTo( - bool _acceptMigration - ) + function migrateTo(bool _acceptMigration) external onlyVault onlyAccountInitialized(msg.sender) @@ -377,21 +446,19 @@ contract StakeManager is Ownable { _mintMP(account, iEpoch.startTime + EPOCH_SIZE, iEpoch); uint256 userSupply = account.balance + account.totalMP; uint256 userEpochReward = Math.mulDiv(userSupply, iEpoch.epochReward, iEpoch.totalSupply); - userReward += userEpochReward; iEpoch.epochReward -= userEpochReward; iEpoch.totalSupply -= userSupply; + //TODO: remove epoch when iEpoch.totalSupply reaches zero } account.epoch = userEpoch; if (userReward > 0) { pendingReward -= userReward; stakedToken.transfer(account.rewardAddress, userReward); } - mpDifference = account.totalMP - mpDifference; + mpDifference = account.totalMP - mpDifference; //TODO: optimize, this only needed for migration if (address(migration) != address(0)) { migration.increaseTotalMP(mpDifference); - } else if (userEpoch == currentEpoch) { - _mintMP(account, block.timestamp, epochs[currentEpoch]); } } @@ -431,7 +498,7 @@ contract StakeManager is Ownable { * @param epoch Epoch to increment total supply */ function _mintMP(Account storage account, uint256 processTime, Epoch storage epoch) private { - uint256 increasedMP = _getMaxMPToMint( //check for MAX_BOOST + uint256 mpToMint = _getMaxMPToMint( _getMPToMint(account.balance, processTime - account.lastMint), account.balance, account.bonusMP, @@ -440,9 +507,12 @@ contract StakeManager is Ownable { //update storage account.lastMint = processTime; - account.totalMP += increasedMP; - totalSupplyMP += increasedMP; - epoch.totalSupply += increasedMP; + account.totalMP += mpToMint; + totalSupplyMP += mpToMint; + + //mp estimation + epoch.estimatedMP -= mpToMint; + pendingMPToBeMinted -= mpToMint; } /** @@ -451,7 +521,7 @@ contract StakeManager is Ownable { * @param _balance balance of account * @param _totalMP total multiplier point of the account * @param _bonusMP bonus multiplier point of the account - * @return _maxToIncrease maximum multiplier point increase + * @return _maxMpToMint maximum multiplier points to mint */ function _getMaxMPToMint( uint256 _mpToMint, @@ -461,13 +531,13 @@ contract StakeManager is Ownable { ) private pure - returns (uint256 _maxToIncrease) + returns (uint256 _maxMpToMint) { // Maximum multiplier point for given balance - _maxToIncrease = _getMPToMint(_balance, MAX_BOOST * YEAR) + _bonusMP; - if (_mpToMint + _totalMP > _maxToIncrease) { + _maxMpToMint = _getMPToMint(_balance, MAX_BOOST * YEAR) + _bonusMP; + if (_mpToMint + _totalMP > _maxMpToMint) { //reached cap when increasing MP - return _maxToIncrease - _totalMP; //how much left to reach cap + return _maxMpToMint - _totalMP; //how much left to reach cap } else { //not reached capw hen increasing MP return _mpToMint; //just return tested value @@ -484,11 +554,30 @@ contract StakeManager is Ownable { return Math.mulDiv(_balance, _deltaTime, YEAR) * MP_APY; } + /* + * @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 + */ + function calculateMPToMint(uint256 _balance, uint256 _deltaTime) public pure returns (uint256) { + return _getMPToMint(_balance, _deltaTime); + } + /** - * @notice Returns total of multiplier points and balance + * @notice Returns total of multiplier points and balance, + * and the pending MPs that would be minted if all accounts were processed * @return _totalSupply current total supply */ function totalSupply() public view returns (uint256 _totalSupply) { + return totalSupplyMP + totalSupplyBalance + pendingMPToBeMinted; + } + + /** + * @notice Returns total of multiplier points and balance + * @return _totalSupply current total supply + */ + function totalSupplyMinted() public view returns (uint256 _totalSupply) { return totalSupplyMP + totalSupplyBalance; } diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index 681ae16..a6cd658 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -7,7 +7,7 @@ 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 { StakeManager } from "../contracts/StakeManager.sol"; +import { StakeManager, StakeRewardEstimate } from "../contracts/StakeManager.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; import { VaultFactory } from "../contracts/VaultFactory.sol"; @@ -108,19 +108,19 @@ contract StakeTest is StakeManagerTest { uint256 stakeAmount = 100; StakeVault userVault = _createStakingAccount(testUser, stakeAmount, 0, stakeAmount * 10); - (,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); + (,, uint256 totalMP,,,,,) = stakeManager.accounts(address(userVault)); assertEq(stakeManager.totalSupplyMP(), stakeAmount, "total multiplier point supply"); assertEq(totalMP, stakeAmount, "user multiplier points"); vm.prank(testUser); userVault.unstake(stakeAmount); - (,,, totalMP,,,) = stakeManager.accounts(address(userVault)); + (,,, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(stakeManager.totalSupplyMP(), 0, "totalSupplyMP burned after unstaking"); assertEq(totalMP, 0, "userMP burned after unstaking"); } - function test_restakeOnLocked() public { + function _test_restakeOnLocked() public { uint256 lockToIncrease = stakeManager.MIN_LOCKUP_PERIOD(); uint256 stakeAmount = 100; uint256 stakeAmount2 = 200; @@ -131,7 +131,7 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser); userVault.stake(stakeAmount2, 0); - (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); assertGt(totalMP, stakeAmount + stakeAmount2, "account MP"); @@ -140,12 +140,12 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser); userVault.stake(stakeAmount3, 0); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount3, "account balance 2"); assertGt(totalMP, stakeAmount + stakeAmount2 + stakeAmount3, "account MP 2"); } - function test_restakeJustStake() public { + function _test_restakeJustStake() public { uint256 stakeAmount = 100; uint256 stakeAmount2 = 50; uint256 mintAmount = stakeAmount * 10; @@ -158,10 +158,10 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(stakeAmount2, 0); - (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); assertEq(totalMP, stakeAmount + stakeAmount2, "account MP"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount + stakeAmount2, "account 2 balance"); assertGt(totalMP, stakeAmount + stakeAmount2, "account 2 MP"); @@ -172,15 +172,15 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(stakeAmount2, 0); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account balance 2"); assertGt(totalMP, stakeAmount + stakeAmount2 + stakeAmount2, "account MP 2"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account 2 balance 2"); assertGt(totalMP, stakeAmount + stakeAmount2 + stakeAmount2, "account 2 MP 2"); } - function test_restakeJustLock() public { + function _test_restakeJustLock() public { uint256 lockToIncrease = stakeManager.MIN_LOCKUP_PERIOD(); uint256 stakeAmount = 100; uint256 mintAmount = stakeAmount * 10; @@ -191,10 +191,10 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(0, lockToIncrease); - (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "account balance"); assertGt(totalMP, stakeAmount, "account MP"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount, "account 2 balance"); assertGt(totalMP, stakeAmount, "account 2 MP"); @@ -205,15 +205,15 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(0, lockToIncrease); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "account balance 2"); assertGt(totalMP, stakeAmount, "account MP 2"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount, "account 2 balance 2"); assertGt(totalMP, stakeAmount, "account 2 MP 2"); } - function test_restakeStakeAndLock() public { + function _test_restakeStakeAndLock() public { uint256 lockToIncrease = stakeManager.MIN_LOCKUP_PERIOD(); uint256 stakeAmount = 100; uint256 stakeAmount2 = 50; @@ -226,10 +226,10 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(stakeAmount2, lockToIncrease); - (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance,, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); assertGt(totalMP, stakeAmount + stakeAmount2, "account MP"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount + stakeAmount2, "account 2 balance"); assertGt(totalMP, stakeAmount + stakeAmount2, "account 2 MP"); @@ -240,10 +240,10 @@ contract StakeTest is StakeManagerTest { vm.prank(testUser2); userVault2.stake(stakeAmount2, lockToIncrease); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account balance 2"); assertGt(totalMP, stakeAmount + stakeAmount2 + stakeAmount2, "account MP 2"); - (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault2)); + (, balance,, totalMP,,,,) = stakeManager.accounts(address(userVault2)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account 2 balance 2"); assertGt(totalMP, stakeAmount + stakeAmount2 + stakeAmount2, "account 2 MP 2"); } @@ -314,7 +314,7 @@ contract UnstakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); stakeManager.executeAccount(address(userVault), i + 1); } - (, uint256 balanceBefore, uint256 bonusMPBefore, uint256 totalMPBefore,,,) = + (, uint256 balanceBefore, uint256 bonusMPBefore, uint256 totalMPBefore,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPBefore = stakeManager.totalSupplyMP(); uint256 unstakeAmount = stakeAmount * percentToBurn / 100; @@ -322,7 +322,7 @@ contract UnstakeTest is StakeManagerTest { assertEq(ERC20(stakeToken).balanceOf(testUser), 0); userVault.unstake(unstakeAmount); - (, uint256 balanceAfter, uint256 bonusMPAfter, uint256 totalMPAfter,,,) = + (, uint256 balanceAfter, uint256 bonusMPAfter, uint256 totalMPAfter,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPAfter = stakeManager.totalSupplyMP(); @@ -343,7 +343,7 @@ contract UnstakeTest is StakeManagerTest { } function test_RevertWhen_AmountMoreThanBalance() public { - uint256 stakeAmount = 100; + uint256 stakeAmount = 1000; StakeVault userVault = _createStakingAccount(testUser, stakeAmount); vm.startPrank(testUser); vm.expectRevert(StakeManager.StakeManager__InsufficientFunds.selector); @@ -364,7 +364,7 @@ contract LockTest is StakeManagerTest { vm.startPrank(testUser); userVault.lock(lockTime); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); console.log("balance", balance); console.log("bonusMP", bonusMP); @@ -386,12 +386,12 @@ contract LockTest is StakeManagerTest { vm.warp(block.timestamp + stakeManager.MIN_LOCKUP_PERIOD() - 1); stakeManager.executeAccount(address(userVault), 1); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,, uint256 lockUntil,) = + (, uint256 balance, uint256 bonusMP, uint256 totalMP,, uint256 lockUntil,,) = stakeManager.accounts(address(userVault)); vm.startPrank(testUser); userVault.lock(minLockup - 1); - (, balance, bonusMP, totalMP,, lockUntil,) = stakeManager.accounts(address(userVault)); + (, balance, bonusMP, totalMP,, lockUntil,,) = stakeManager.accounts(address(userVault)); assertEq(lockUntil, block.timestamp + minLockup); @@ -406,7 +406,7 @@ contract LockTest is StakeManagerTest { vm.warp(block.timestamp + stakeManager.MIN_LOCKUP_PERIOD()); stakeManager.executeAccount(address(userVault), 1); - (,,,,, uint256 lockUntil,) = stakeManager.accounts(address(userVault)); + (,,,,, uint256 lockUntil,,) = stakeManager.accounts(address(userVault)); console.log(lockUntil); vm.startPrank(testUser); vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); @@ -417,13 +417,14 @@ contract LockTest is StakeManagerTest { uint256 stakeAmount = 100; uint256 lockTime = stakeManager.MAX_LOCKUP_PERIOD(); StakeVault userVault = _createStakingAccount(testUser, stakeAmount); - (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); + (, uint256 balance, uint256 bonusMP, uint256 totalMP,,,,) = stakeManager.accounts(address(userVault)); uint256 totalSupplyMPBefore = stakeManager.totalSupplyMP(); vm.startPrank(testUser); userVault.lock(lockTime); - (, uint256 newBalance, uint256 newBonusMP, uint256 newCurrentMP,,,) = stakeManager.accounts(address(userVault)); + //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"); @@ -471,8 +472,6 @@ contract MigrateTest is StakeManagerTest { userVault.acceptMigration(); vm.stopPrank(); } - - function increaseEpoch(uint256 epochNumber) internal { } } contract MigrationInitializeTest is StakeManagerTest { @@ -485,10 +484,15 @@ contract MigrationInitializeTest is StakeManagerTest { vm.startPrank(deployer); StakeManager secondStakeManager = new StakeManager(stakeToken, address(stakeManager)); StakeManager thirdStakeManager = new StakeManager(stakeToken, address(secondStakeManager)); + vm.stopPrank(); // first, ensure `secondStakeManager` is in migration mode itself + StakeRewardEstimate db = stakeManager.stakeRewardEstimate(); + vm.prank(address(stakeManager)); + db.transferOwnership(address(secondStakeManager)); + + vm.prank(address(deployer)); secondStakeManager.startMigration(thirdStakeManager); - vm.stopPrank(); uint256 currentEpoch = stakeManager.currentEpoch(); uint256 totalMP = stakeManager.totalSupplyMP(); @@ -498,7 +502,7 @@ contract MigrationInitializeTest is StakeManagerTest { // in migration itself, should revert vm.prank(address(stakeManager)); vm.expectRevert(StakeManager.StakeManager__PendingMigration.selector); - secondStakeManager.migrationInitialize(currentEpoch, totalMP, totalBalance, 0); + secondStakeManager.migrationInitialize(currentEpoch, totalMP, totalBalance, 0, 0, 0, 0); } } @@ -539,7 +543,8 @@ contract ExecuteAccountTest is StakeManagerTest { console.log("# PND_REWARDS", stakeManager.pendingReward()); for (uint256 j = 0; j < userVaults.length; j++) { - (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore) = + //solhint-disable-next-line max-line-length + (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = stakeManager.accounts(address(userVaults[j])); uint256 rewardsBefore = ERC20(stakeToken).balanceOf(rewardAddress); console.log("-Vault number", j); @@ -550,7 +555,8 @@ contract ExecuteAccountTest is StakeManagerTest { console.log("---##### rewards :", rewardsBefore); console.log("--=====AFTER======"); stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); - (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch) = stakeManager.accounts(address(userVaults[j])); + //solhint-disable-next-line max-line-length + (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch,) = stakeManager.accounts(address(userVaults[j])); uint256 rewards = ERC20(stakeToken).balanceOf(rewardAddress); console.log("---### deltaTime :", lastMint - lastMintBefore); console.log("---### totalMP :", totalMP); @@ -572,26 +578,50 @@ contract ExecuteAccountTest is StakeManagerTest { } function test_ShouldNotMintMoreThanCap() public { - uint256 stakeAmount = 10_000_000; + uint256 stakeAmount = 10_000_000_000; + + uint256 epochsAmountToReachCap = stakeManager.calculateMPToMint( + stakeAmount, stakeManager.MAX_BOOST() * stakeManager.YEAR() + ) / stakeManager.calculateMPToMint(stakeAmount, stakeManager.EPOCH_SIZE()); + deal(stakeToken, testUser, stakeAmount); userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); - userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, stakeManager.MAX_LOCKUP_PERIOD())); - userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, stakeManager.MIN_LOCKUP_PERIOD())); - for (uint256 i = 0; i < 209; i++) { + vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 1)); + userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - (stakeManager.EPOCH_SIZE() - 2)); + userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 3)); + userVaults.push(_createStakingAccount(makeAddr("testUser4"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 2)); + userVaults.push(_createStakingAccount(makeAddr("testUser5"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - ((stakeManager.EPOCH_SIZE() / 4) * 1)); + userVaults.push(_createStakingAccount(makeAddr("testUser6"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - 2); + userVaults.push(_createStakingAccount(makeAddr("testUser7"), stakeAmount, 0)); + + vm.warp(stakeManager.epochEnd() - 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()); stakeManager.executeEpoch(); for (uint256 j = 0; j < userVaults.length; j++) { - (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore) = + (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = stakeManager.accounts(address(userVaults[j])); uint256 rewardsBefore = ERC20(stakeToken).balanceOf(rewardAddress); stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); - (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch) = stakeManager.accounts(address(userVaults[j])); + //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(epoch, epochBefore + 1, "must increase epoch"); assertGt(totalMP, totalMPBefore, "must increase MPs"); assertGt(rewards, rewardsBefore, "must increase rewards"); @@ -606,16 +636,22 @@ contract ExecuteAccountTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); stakeManager.executeEpoch(); for (uint256 j = 0; j < userVaults.length; j++) { - (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore) = + (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore,) = stakeManager.accounts(address(userVaults[j])); uint256 rewardsBefore = ERC20(stakeToken).balanceOf(rewardAddress); stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); - (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch) = stakeManager.accounts(address(userVaults[j])); + //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(epoch, epochBefore + 1, "must increase epoch"); - assertEq(totalMP, totalMPBefore, "must NOT increase MPs"); + // MPs will still be minted in mpLimitEpoch + 1 when accounts + // started staking at any point *inside* of an epoch, so we + // only perform the assert below one epoch after *that* + if (i > 0) { + assertEq(totalMP, totalMPBefore, "must NOT increase MPs"); + } assertGt(rewards, rewardsBefore, "must increase rewards"); lastMintBefore = lastMint; epochBefore = epoch; @@ -623,14 +659,11 @@ contract ExecuteAccountTest is StakeManagerTest { } } } - - function test_UpdateEpoch() public { } - function test_PayRewards() public { } - - function test_MintMPLimit() public { } } contract UserFlowsTest is StakeManagerTest { + StakeVault[] private userVaults; + function test_StakedSupplyShouldIncreaseAndDecreaseAgain() public { uint256 lockTime = 0; uint256 stakeAmount = 100; @@ -672,6 +705,35 @@ contract UserFlowsTest is StakeManagerTest { assertEq(ERC20(stakeToken).balanceOf(address(userVault)), 0); assertEq(stakeManager.totalSupplyBalance(), 0); } + + function test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8 accountNum) public { + uint256 stakeAmount = 10_000_000; + + for (uint256 i = 0; i <= accountNum; i++) { + userVaults.push( + _createStakingAccount(makeAddr(string(abi.encode(keccak256(abi.encode(accountNum))))), stakeAmount, 0) + ); + } + + uint256 epochsAmountToReachCap = 1; + + for (uint256 i = 0; i < epochsAmountToReachCap; i++) { + vm.warp(stakeManager.epochEnd()); + stakeManager.executeEpoch(); + uint256 pendingMPToBeMintedBefore = stakeManager.pendingMPToBeMinted(); + uint256 totalSupplyMP = stakeManager.totalSupplyMP(); + 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(); + + assertEq(pendingMPToBeMintedBefore + totalSupplyMP, stakeManager.totalSupplyMP()); + assertEq(pendingMPToBeMintedAfter, 0); + } + } } contract MigrationStakeManagerTest is StakeManagerTest { @@ -710,7 +772,6 @@ contract MigrationStakeManagerTest is StakeManagerTest { } contract ExecuteEpochTest is MigrationStakeManagerTest { - //currentEpoch can only increase if time stakeManager.epochEnd(). function test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() public { assertEq(stakeManager.currentEpoch(), 0); @@ -718,7 +779,6 @@ contract ExecuteEpochTest is MigrationStakeManagerTest { stakeManager.executeEpoch(); assertEq(stakeManager.currentEpoch(), 0); } - //currentEpoch can only increase. function test_ExecuteEpochShouldIncreaseEpoch() public { assertEq(stakeManager.currentEpoch(), 0);