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