diff --git a/.gas-report b/.gas-report index 72eb99d..6203e52 100644 --- a/.gas-report +++ b/.gas-report @@ -8,14 +8,14 @@ | MAX_LOCKUP_PERIOD | 361 | 361 | 361 | 361 | 4 | | MIN_LOCKUP_PERIOD | 264 | 264 | 264 | 264 | 12 | | YEAR | 263 | 263 | 263 | 263 | 637 | -| accounts | 1597 | 1597 | 1597 | 1597 | 144273 | +| accounts | 1597 | 1597 | 1597 | 1597 | 144393 | | calculateMPToMint | 740 | 740 | 740 | 740 | 1276 | | currentEpoch | 384 | 1050 | 384 | 2384 | 54 | -| epochEnd | 627 | 627 | 627 | 2627 | 23675 | +| epochEnd | 627 | 627 | 627 | 2627 | 23695 | | epochReward | 1425 | 2925 | 1425 | 5925 | 3 | | executeAccount(address) | 151152 | 151152 | 151152 | 151152 | 2 | -| executeAccount(address,uint256) | 26562 | 72616 | 74122 | 217879 | 141860 | -| executeEpoch() | 23458 | 120800 | 121956 | 938985 | 23564 | +| executeAccount(address,uint256) | 26562 | 72609 | 74122 | 217879 | 141980 | +| executeEpoch() | 23458 | 120804 | 121956 | 938985 | 23584 | | executeEpoch(uint256) | 23861 | 24497 | 23861 | 26090 | 7 | | isVault | 540 | 948 | 540 | 2540 | 680 | | lock | 23862 | 23862 | 23862 | 23862 | 1 | @@ -24,7 +24,7 @@ | migrationInitialize | 24602 | 24602 | 24602 | 24602 | 1 | | newEpoch | 441 | 441 | 441 | 441 | 5 | | owner | 2432 | 2432 | 2432 | 2432 | 13 | -| pendingMPToBeMinted | 364 | 364 | 364 | 364 | 46432 | +| pendingMPToBeMinted | 364 | 364 | 364 | 364 | 46472 | | pendingReward | 386 | 1420 | 2386 | 2386 | 29 | | previousManager | 275 | 275 | 275 | 275 | 13 | | setVault | 46239 | 46239 | 46239 | 46239 | 139 | @@ -35,7 +35,7 @@ | startTime | 306 | 306 | 306 | 306 | 21 | | totalSupply | 762 | 1943 | 2762 | 2762 | 22 | | totalSupplyBalance | 407 | 1807 | 2407 | 2407 | 20 | -| totalSupplyMP | 362 | 362 | 362 | 2362 | 46453 | +| totalSupplyMP | 362 | 362 | 362 | 2362 | 46493 | | unstake | 23819 | 23819 | 23819 | 23819 | 1 | @@ -44,7 +44,7 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23725 | +| getExpiredMP | 2427 | 2427 | 2427 | 2427 | 23745 | | transferOwnership | 28533 | 28533 | 28533 | 28533 | 1 | @@ -53,23 +53,23 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| acceptMigration | 35280 | 35280 | 35280 | 35280 | 2 | -| leave | 35266 | 35266 | 35266 | 35266 | 1 | -| lock | 43329 | 96146 | 64421 | 204745 | 7 | -| owner | 362 | 362 | 362 | 362 | 679 | -| stake | 27265 | 282079 | 265719 | 351866 | 684 | -| stakedToken | 212 | 212 | 212 | 212 | 2 | -| unstake | 40167 | 95818 | 78688 | 233549 | 11 | +| acceptMigration | 35303 | 35303 | 35303 | 35303 | 2 | +| leave | 35327 | 35327 | 35327 | 35327 | 1 | +| lock | 43352 | 96169 | 64444 | 204768 | 7 | +| owner | 364 | 364 | 364 | 364 | 679 | +| stake | 49545 | 304906 | 288231 | 374378 | 684 | +| stakedToken | 257 | 257 | 257 | 257 | 2 | +| unstake | 40209 | 98514 | 82820 | 237681 | 11 | -| contracts/VaultFactory.sol:VaultFactory contract | | | | | | -|--------------------------------------------------|-----------------|--------|--------|--------|---------| -| Deployment Cost | Deployment Size | | | | | -| 0 | 0 | | | | | -| Function Name | min | avg | median | max | # calls | -| createVault | 696553 | 696553 | 696553 | 696553 | 683 | -| setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | -| stakeManager | 368 | 1868 | 2368 | 2368 | 4 | +| contracts/VaultFactory.sol:VaultFactory contract | | | | | | +|--------------------------------------------------|-----------------|---------|---------|---------|---------| +| Deployment Cost | Deployment Size | | | | | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| createVault | 1127500 | 1127500 | 1127500 | 1127500 | 683 | +| setStakeManager | 23710 | 26669 | 26076 | 30222 | 3 | +| stakeManager | 368 | 1868 | 2368 | 2368 | 4 | | lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol:ERC20 contract | | | | | | @@ -77,16 +77,16 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| approve | 46175 | 46239 | 46199 | 46367 | 679 | -| balanceOf | 561 | 2107 | 2561 | 2561 | 30744 | +| approve | 46175 | 46235 | 46199 | 46367 | 679 | +| balanceOf | 561 | 2108 | 2561 | 2561 | 30764 | | script/Deploy.s.sol:Deploy contract | | | | | | |-------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 6118940 | 29538 | | | | | +| 6587720 | 31730 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 5316305 | 5316305 | 5316305 | 5316305 | 66 | +| run | 5755371 | 5755371 | 5755371 | 5755371 | 66 | | script/DeployMigrationStakeManager.s.sol:DeployMigrationStakeManager contract | | | | | | @@ -117,9 +117,9 @@ | test/script/DeployBroken.s.sol:DeployBroken contract | | | | | | |------------------------------------------------------|-----------------|---------|---------|---------|---------| | Deployment Cost | Deployment Size | | | | | -| 4803693 | 23336 | | | | | +| 5272431 | 25528 | | | | | | Function Name | min | avg | median | max | # calls | -| run | 4156127 | 4156127 | 4156127 | 4156127 | 1 | +| run | 4595193 | 4595193 | 4595193 | 4595193 | 1 | diff --git a/.gas-snapshot b/.gas-snapshot index b013b9b..08d78fd 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,38 +1,38 @@ CreateVaultTest:testDeployment() (gas: 9774) -CreateVaultTest:test_createVault() (gas: 714022) +CreateVaultTest:test_createVault() (gas: 1145016) ExecuteAccountTest:testDeployment() (gas: 28785) -ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 1580255) -ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 5297777) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 1788412) -ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 321397553) +ExecuteAccountTest:test_ExecuteAccountLimit() (gas: 2033714) +ExecuteAccountTest:test_ExecuteAccountMintMP() (gas: 6658154) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 2241871) +ExecuteAccountTest:test_ShouldNotMintMoreThanCap() (gas: 325025225) ExecuteEpochTest:testDeployment() (gas: 28786) ExecuteEpochTest:testNewDeployment() (gas: 30858) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1366211) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1385045) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 1629507) -ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1395200) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 1936210) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2523553) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1479491) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2533729) -ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1489623) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterEpochEnd() (gas: 1819670) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1838504) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2082966) +ExecuteEpochTest:test_ExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1848659) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochAfterEnd() (gas: 2389669) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochs() (gas: 2977012) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsJumoMany() (gas: 1932950) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTime() (gas: 2987188) +ExecuteEpochTest:test_ExecuteEpochExecuteEpochExecuteAccountAfterManyEpochsWithBrokenTimeJumpMany() (gas: 1943082) ExecuteEpochTest:test_ExecuteEpochNewEpoch() (gas: 1122339) ExecuteEpochTest:test_ExecuteEpochShouldIncreaseEpoch() (gas: 92413) ExecuteEpochTest:test_ExecuteEpochShouldIncreasePendingReward() (gas: 256446) ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochBeforeEnd() (gas: 38984) ExecuteEpochTest:test_ExecuteEpochShouldNotIncreaseEpochInMigration() (gas: 149638) LeaveTest:testDeployment() (gas: 28763) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1327993) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 1781536) LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 31964) LockTest:testDeployment() (gas: 28763) -LockTest:test_NewLockupPeriod() (gas: 1329045) -LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1301374) -LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1545884) +LockTest:test_NewLockupPeriod() (gas: 1782527) +LockTest:test_RevertWhen_InvalidNewLockupPeriod() (gas: 1754856) +LockTest:test_RevertWhen_InvalidUpdateLockupPeriod() (gas: 1999366) LockTest:test_RevertWhen_SenderIsNotVault() (gas: 31856) -LockTest:test_ShouldIncreaseBonusMP() (gas: 1311575) -LockTest:test_UpdateLockupPeriod() (gas: 1634540) +LockTest:test_ShouldIncreaseBonusMP() (gas: 1765057) +LockTest:test_UpdateLockupPeriod() (gas: 2088045) MigrateTest:testDeployment() (gas: 28763) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1292046) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 1745528) MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 31976) MigrationInitializeTest:testDeployment() (gas: 28763) MigrationInitializeTest:test_RevertWhen_MigrationPending() (gas: 5181388) @@ -44,24 +44,24 @@ SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 63105) SetStakeManagerTest:test_SetStakeManager() (gas: 41301) StakeManagerTest:testDeployment() (gas: 28535) StakeTest:testDeployment() (gas: 28741) -StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1080635) -StakeTest:test_RevertWhen_Restake() (gas: 1316847) -StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1320861) +StakeTest:test_RevertWhen_InvalidLockupPeriod() (gas: 1556142) +StakeTest:test_RevertWhen_Restake() (gas: 1775486) +StakeTest:test_RevertWhen_RestakeWithLock() (gas: 1779500) StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 32018) -StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 817762) -StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 211363) -StakeTest:test_StakeWithLockBonusMP() (gas: 2357564) -StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1322003) -StakedTokenTest:testStakeToken() (gas: 7616) +StakeTest:test_RevertWhen_StakeIsTooLow() (gas: 1251089) +StakeTest:test_RevertWhen_StakeTokenTransferFails() (gas: 229187) +StakeTest:test_StakeWithLockBonusMP() (gas: 3264505) +StakeTest:test_StakeWithoutLockUpTimeMintsMultiplierPoints() (gas: 1778561) +StakedTokenTest:testStakeToken() (gas: 7661) UnstakeTest:testDeployment() (gas: 28785) -UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1297407) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1342144) +UnstakeTest:test_RevertWhen_AmountMoreThanBalance() (gas: 1750908) +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 1795687) UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 31857) -UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6526945) -UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1319573) -UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1440205) +UnstakeTest:test_UnstakeShouldBurnMultiplierPoints() (gas: 6893844) +UnstakeTest:test_UnstakeShouldReturnFund_NoLockUp() (gas: 1776131) +UnstakeTest:test_UnstakeShouldReturnFund_WithLockUp() (gas: 1896763) UserFlowsTest:testDeployment() (gas: 28763) -UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 131402731, ~: 130856521) -UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1481301) -UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 2494771) +UserFlowsTest:test_PendingMPToBeMintedCannotBeGreaterThanTotalSupplyMP(uint8,uint128) (runs: 106, μ: 134225064, ~: 133577563) +UserFlowsTest:test_StakeWithLockUpTimeLocksStake() (gas: 1937901) +UserFlowsTest:test_StakedSupplyShouldIncreaseAndDecreaseAgain() (gas: 3406943) VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/contracts/StakeVault.sol b/contracts/StakeVault.sol index 8588d9e..1335a8a 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -9,56 +9,164 @@ import { StakeManager } from "./StakeManager.sol"; /** * @title StakeVault * @author Ricardo Guilherme Schmidt - * @notice Secures user stake + * @notice A contract to secure user stakes and manage staking with StakeManager. + * @dev This contract is owned by the user and allows staking, unstaking, and withdrawing tokens. */ contract StakeVault is Ownable { + error StakeVault__NoEnoughAvailableBalance(); + error StakeVault__InvalidDestinationAddress(); error StakeVault__MigrationNotAvailable(); - error StakeVault__StakingFailed(); - error StakeVault__UnstakingFailed(); + //STAKED_TOKEN must be kept as an immutable, otherwise, StakeManager would accept StakeVaults with any token + //if is needed that STAKED_TOKEN to be a variable, StakeManager should be changed to check codehash and + //StakeVault(msg.sender).stakedToken() + ERC20 public immutable STAKED_TOKEN; StakeManager private stakeManager; + uint256 public amountStaked = 0; - ERC20 public immutable STAKED_TOKEN; + /** + * @dev Emitted when tokens are staked. + * @param from The address from which tokens are transferred. + * @param to The address receiving the staked tokens (this contract). + * @param amount The amount of tokens staked. + * @param time The time period for which tokens are staked. + */ + event Staked(address indexed from, address indexed to, uint256 amount, uint256 time); - event Staked(address from, address to, uint256 _amount, uint256 time); + modifier validDestination(address _destination) { + if (_destination == address(0)) { + revert StakeVault__InvalidDestinationAddress(); + } + _; + } + /** + * @notice Initializes the contract with the owner, staked token, and stake manager. + * @param _owner The address of the owner. + * @param _stakedToken The ERC20 token to be staked. + * @param _stakeManager The address of the StakeManager contract. + */ constructor(address _owner, ERC20 _stakedToken, StakeManager _stakeManager) { _transferOwnership(_owner); STAKED_TOKEN = _stakedToken; stakeManager = _stakeManager; } + /** + * @notice Stake tokens for a specified time. + * @param _amount The amount of tokens to stake. + * @param _time The time period to stake for. + */ function stake(uint256 _amount, uint256 _time) external onlyOwner { - bool success = STAKED_TOKEN.transferFrom(msg.sender, address(this), _amount); - if (!success) { - revert StakeVault__StakingFailed(); - } - stakeManager.stake(_amount, _time); + _stake(_amount, _time, msg.sender); + } - emit Staked(msg.sender, address(this), _amount, _time); + /** + * @notice Stake tokens from a specified address for a specified time. + * @param _amount The amount of tokens to stake. + * @param _time The time period to stake for. + * @param _from The address from which tokens will be transferred. + */ + function stake(uint256 _amount, uint256 _time, address _from) external onlyOwner { + _stake(_amount, _time, _from); } + /** + * @notice Extends the lock time of the stake. + * @param _time The additional time to lock the stake. + */ function lock(uint256 _time) external onlyOwner { stakeManager.lock(_time); } + /** + * @notice Unstake a specified amount of tokens and send to the owner. + * @param _amount The amount of tokens to unstake. + */ function unstake(uint256 _amount) external onlyOwner { - stakeManager.unstake(_amount); - bool success = STAKED_TOKEN.transfer(msg.sender, _amount); - if (!success) { - revert StakeVault__UnstakingFailed(); - } + _unstake(_amount, msg.sender); } + /** + * @notice Unstake a specified amount of tokens and send to a destination address. + * @param _amount The amount of tokens to unstake. + * @param _destination The address to receive the unstaked tokens. + */ + function unstake(uint256 _amount, address _destination) external onlyOwner validDestination(_destination) { + _unstake(_amount, _destination); + } + + /** + * @notice Withdraw tokens from the contract. + * @param _token The ERC20 token to withdraw. + * @param _amount The amount of tokens to withdraw. + */ + function withdraw(ERC20 _token, uint256 _amount) external onlyOwner { + _withdraw(_token, _amount, msg.sender); + } + + /** + * @notice Withdraw tokens from the contract to a destination address. + * @param _token The ERC20 token to withdraw. + * @param _amount The amount of tokens to withdraw. + * @param _destination The address to receive the tokens. + */ + function withdraw( + ERC20 _token, + uint256 _amount, + address _destination + ) + external + onlyOwner + validDestination(_destination) + { + _withdraw(_token, _amount, _destination); + } + + /** + * @notice Withdraw Ether from the contract to the owner address. + * @param _amount The amount of Ether to withdraw. + */ + function withdraw(uint256 _amount) external onlyOwner { + _withdraw(_amount, payable(msg.sender)); + } + + /** + * @notice Withdraw Ether from the contract to a destination address. + * @param _amount The amount of Ether to withdraw. + * @param _destination The address to receive the Ether. + */ + function withdraw( + uint256 _amount, + address payable _destination + ) + external + onlyOwner + validDestination(_destination) + { + _withdraw(_amount, _destination); + } + + /** + * @notice Reject migration, leave staking contract and withdraw all tokens to the owner. + */ function leave() external onlyOwner { - stakeManager.migrateTo(false); - STAKED_TOKEN.transferFrom(address(this), msg.sender, STAKED_TOKEN.balanceOf(address(this))); + _leave(msg.sender); + } + + /** + * @notice Reject migration, leave staking contract and withdraw all tokens to a destination address. + * @param _destination The address to receive the tokens. + */ + function leave(address _destination) external onlyOwner validDestination(_destination) { + _leave(_destination); } /** * @notice Opt-in migration to a new StakeManager contract. + * @dev Updates the stakeManager to the migrated contract. */ function acceptMigration() external onlyOwner { StakeManager migrated = stakeManager.migrateTo(true); @@ -66,7 +174,61 @@ contract StakeVault is Ownable { stakeManager = migrated; } + /** + * @notice Returns the staked token. + * @return The ERC20 staked token. + */ function stakedToken() external view returns (ERC20) { return STAKED_TOKEN; } + + /** + * @notice Returns the available amount of a token that can be withdrawn. + * @param _token The ERC20 token to check. + * @return The amount of token available for withdrawal. + */ + function availableWithdraw(ERC20 _token) external view returns (uint256) { + if (_token == STAKED_TOKEN) { + return STAKED_TOKEN.balanceOf(address(this)) - amountStaked; + } + return _token.balanceOf(address(this)); + } + + function _stake(uint256 _amount, uint256 _time, address _source) internal { + amountStaked += _amount; + bool success = STAKED_TOKEN.transferFrom(_source, address(this), _amount); + if (!success) { + revert StakeVault__StakingFailed(); + } + + stakeManager.stake(_amount, _time); + + emit Staked(_source, address(this), _amount, _time); + } + + function _unstake(uint256 _amount, address _destination) internal { + stakeManager.unstake(_amount); + bool success = STAKED_TOKEN.transfer(_destination, _amount); + amountStaked -= _amount; + if (!success) { + revert StakeVault__UnstakingFailed(); + } + } + + function _leave(address _destination) internal { + stakeManager.migrateTo(false); + STAKED_TOKEN.transferFrom(address(this), _destination, STAKED_TOKEN.balanceOf(address(this))); + amountStaked = 0; + } + + function _withdraw(ERC20 _token, uint256 _amount, address _destination) internal { + if (_token == STAKED_TOKEN && STAKED_TOKEN.balanceOf(address(this)) - amountStaked < _amount) { + revert StakeVault__NoEnoughAvailableBalance(); + } + _token.transfer(_destination, _amount); + } + + function _withdraw(uint256 _amount, address payable _destination) internal { + _destination.transfer(_amount); + } }