From a484cf40faf4405767660fe1ebf9ff8d26707aeb Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 11 Nov 2024 04:21:27 -0500 Subject: [PATCH 01/16] fixed withdraw function in OperatorStakingPool --- contracts/core/interfaces/IStakingRewardsPool.sol | 2 ++ contracts/core/test/LSTMock.sol | 9 +++++++-- contracts/linkStaking/OperatorStakingPool.sol | 4 +++- test/linkStaking/operator-staking-pool.test.ts | 5 +++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/contracts/core/interfaces/IStakingRewardsPool.sol b/contracts/core/interfaces/IStakingRewardsPool.sol index b48c6ae3..8cf08f4a 100644 --- a/contracts/core/interfaces/IStakingRewardsPool.sol +++ b/contracts/core/interfaces/IStakingRewardsPool.sol @@ -28,4 +28,6 @@ interface IStakingRewardsPool is IERC677 { function totalShares() external view returns (uint256); function totalSupply() external view returns (uint256); + + function transferShares(address _recipient, uint256 _sharesAmount) external returns (bool); } diff --git a/contracts/core/test/LSTMock.sol b/contracts/core/test/LSTMock.sol index a87b4e8d..0c60fbb6 100644 --- a/contracts/core/test/LSTMock.sol +++ b/contracts/core/test/LSTMock.sol @@ -18,15 +18,20 @@ contract LSTMock is ERC677 { return (super.balanceOf(_account) * mulitplierBasisPoints) / 10000; } - function getSharesByStake(uint256 _amount) external view returns (uint256) { + function getSharesByStake(uint256 _amount) public view returns (uint256) { return (_amount * 10000) / mulitplierBasisPoints; } - function getStakeByShares(uint256 _sharesAmount) external view returns (uint256) { + function getStakeByShares(uint256 _sharesAmount) public view returns (uint256) { return (_sharesAmount * mulitplierBasisPoints) / 10000; } function setMultiplierBasisPoints(uint256 _multiplierBasisPoints) external { mulitplierBasisPoints = _multiplierBasisPoints; } + + function transferShares(address _recipient, uint256 _sharesAmount) external returns (bool) { + _transfer(msg.sender, _recipient, getStakeByShares(_sharesAmount)); + return true; + } } diff --git a/contracts/linkStaking/OperatorStakingPool.sol b/contracts/linkStaking/OperatorStakingPool.sol index 5909ae4d..5f5117a6 100644 --- a/contracts/linkStaking/OperatorStakingPool.sol +++ b/contracts/linkStaking/OperatorStakingPool.sol @@ -193,7 +193,7 @@ contract OperatorStakingPool is Initializable, UUPSUpgradeable, OwnableUpgradeab /** * @notice Withdraws tokens - * @param _operator address of operator with withdraw for + * @param _operator address of operator to withdraw for * @param _amount amount to withdraw **/ function _withdraw(address _operator, uint256 _amount) private { @@ -201,6 +201,8 @@ contract OperatorStakingPool is Initializable, UUPSUpgradeable, OwnableUpgradeab shareBalances[_operator] -= sharesAmount; totalShares -= sharesAmount; + lst.transferShares(_operator, sharesAmount); + emit Withdraw(_operator, _amount, sharesAmount); } diff --git a/test/linkStaking/operator-staking-pool.test.ts b/test/linkStaking/operator-staking-pool.test.ts index 10a3e5a7..d6f82462 100644 --- a/test/linkStaking/operator-staking-pool.test.ts +++ b/test/linkStaking/operator-staking-pool.test.ts @@ -87,15 +87,16 @@ describe('OperatorStakingPool', () => { assert.equal(fromEther(await opPool.getOperatorStaked(accounts[1])), 300) assert.equal(fromEther(await opPool.getTotalPrincipal()), 300) assert.equal(fromEther(await opPool.getTotalStaked()), 300) + assert.equal(fromEther(await lst.balanceOf(opPool.target)), 300) - await lst.setMultiplierBasisPoints(20000) - await opPool.connect(signers[1]).withdraw(toEther(500)) + await opPool.connect(signers[1]).withdraw(toEther(200)) assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[0])), 0) assert.equal(fromEther(await opPool.getOperatorStaked(accounts[0])), 0) assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[1])), 100) assert.equal(fromEther(await opPool.getOperatorStaked(accounts[1])), 100) assert.equal(fromEther(await opPool.getTotalPrincipal()), 100) assert.equal(fromEther(await opPool.getTotalStaked()), 100) + assert.equal(fromEther(await lst.balanceOf(opPool.target)), 100) }) it('addOperators should work correctly', async () => { From 2c84477535cd496ff9ba4f8181e0b9a4289467a2 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 11 Nov 2024 04:35:59 -0500 Subject: [PATCH 02/16] fixed removeSplitter --- .../core/lstRewardsSplitter/LSTRewardsSplitterController.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/core/lstRewardsSplitter/LSTRewardsSplitterController.sol b/contracts/core/lstRewardsSplitter/LSTRewardsSplitterController.sol index 3d2dabae..175c363f 100644 --- a/contracts/core/lstRewardsSplitter/LSTRewardsSplitterController.sol +++ b/contracts/core/lstRewardsSplitter/LSTRewardsSplitterController.sol @@ -135,7 +135,7 @@ contract LSTRewardsSplitterController is Ownable { uint256 principalDeposits = splitter.principalDeposits(); if (balance != 0) { if (balance != principalDeposits) splitter.splitRewards(); - splitter.withdraw(balance, _account); + splitter.withdraw(splitter.principalDeposits(), _account); } delete splitters[_account]; From c5391b7cf0aebd68ceb875d2253d2f976e6e0681 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 11 Nov 2024 04:36:36 -0500 Subject: [PATCH 03/16] update vaultMapping in removeVault --- contracts/linkStaking/OperatorVCS.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/linkStaking/OperatorVCS.sol b/contracts/linkStaking/OperatorVCS.sol index 56aa2a0a..765d22ab 100644 --- a/contracts/linkStaking/OperatorVCS.sol +++ b/contracts/linkStaking/OperatorVCS.sol @@ -325,6 +325,7 @@ contract OperatorVCS is VaultControllerStrategy { vaults[i] = vaults[i + 1]; } vaults.pop(); + delete vaultMapping[vault]; token.safeTransfer(address(stakingPool), token.balanceOf(address(this))); } From 29c58e9e55e2dfeb7a720c18cea08840b6bdaba5 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 19 Nov 2024 07:15:09 -0500 Subject: [PATCH 04/16] updated pp docs --- contracts/core/priorityPool/PriorityPool.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/core/priorityPool/PriorityPool.sol b/contracts/core/priorityPool/PriorityPool.sol index ab5104d0..b36e6241 100644 --- a/contracts/core/priorityPool/PriorityPool.sol +++ b/contracts/core/priorityPool/PriorityPool.sol @@ -36,9 +36,9 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl // address of oracle contract that handles LST distribution address public distributionOracle; - // min amount of tokens that can be deposited into the staking pool in a single tx + // min amount of tokens that can be deposited into the staking pool strategies in a single tx uint128 public queueDepositMin; - // max amount of tokens that can be deposited into the staking pool in a single tx + // max amount of tokens that can be deposited into the staking pool strategies in a single tx uint128 public queueDepositMax; // current status of the pool PoolStatus public poolStatus; @@ -52,9 +52,9 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl // total number of tokens queued for deposit into the staking pool uint256 public totalQueued; - // total number of tokens deposited into the staking pool since the last distribution + // total number of tokens deposited into the staking pool or swapped for LSTs since the last distribution uint256 public depositsSinceLastUpdate; - // total number of shares received for tokens deposited into the staking pool since the last distribution + // total number of shares received for depositSinceLastUpdate uint256 private sharesSinceLastUpdate; // list of all accounts that have ever queued tokens @@ -440,8 +440,8 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl } /** - * @notice Returns the amount of new deposits into the staking pool since the last call to - * updateDistribution and the amount of shares received for those deposits + * @notice Returns the total number of tokens deposited into the staking pool or swapped for LSTs since + * the last call to updateDistribution and the amount of shares received for those tokens * @return amount of deposits * @return amount of shares */ From b9b2a08444459b9b4750f552ba5ff7cb80f812ad Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 19 Nov 2024 09:19:40 -0500 Subject: [PATCH 05/16] disallow staking pool donation when pool is empty --- contracts/core/StakingPool.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/core/StakingPool.sol b/contracts/core/StakingPool.sol index bb00acec..759ffb5d 100644 --- a/contracts/core/StakingPool.sol +++ b/contracts/core/StakingPool.sol @@ -48,6 +48,7 @@ contract StakingPool is StakingRewardsPool { error SenderNotAuthorized(); error InvalidDeposit(); + error NothingStaked(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -431,6 +432,7 @@ contract StakingPool is StakingRewardsPool { * @param _amount amount to deposit **/ function donateTokens(uint256 _amount) external { + if (totalStaked == 0) revert NothingStaked(); token.safeTransferFrom(msg.sender, address(this), _amount); totalStaked += _amount; emit DonateTokens(msg.sender, _amount); From 4e1c698d419227de7232ea3f8e7d92a4af5a650e Mon Sep 17 00:00:00 2001 From: BkChoy Date: Wed, 20 Nov 2024 08:13:00 -0500 Subject: [PATCH 06/16] allow owner to manually unbond a vault --- contracts/linkStaking/OperatorVCS.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/linkStaking/OperatorVCS.sol b/contracts/linkStaking/OperatorVCS.sol index 765d22ab..c6d054a7 100644 --- a/contracts/linkStaking/OperatorVCS.sol +++ b/contracts/linkStaking/OperatorVCS.sol @@ -330,6 +330,18 @@ contract OperatorVCS is VaultControllerStrategy { token.safeTransfer(address(stakingPool), token.balanceOf(address(this))); } + /** + * @notice Manually unbonds a vault + * @dev a vault can only be manually unbonded if the operator has been removed from the + * Chainlink staking contract + * @param _index index of vault + */ + function unbondVault(uint256 _index) external onlyOwner { + IVault vault = vaults[_index]; + if (!IVault(vault).isRemoved()) revert OperatorNotRemoved(); + vaults[_index].unbond(); + } + /** * @notice Updates accounting for any number of vault groups * @dev used to correct minor accounting errors that result from the removal or slashing From 7e8d573f39502ab7ed6e54a71e2eada53cf596e9 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Wed, 20 Nov 2024 11:41:11 -0500 Subject: [PATCH 07/16] added forceWithdraw function to WithdrawalPool --- .../core/priorityPool/WithdrawalPool.sol | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/contracts/core/priorityPool/WithdrawalPool.sol b/contracts/core/priorityPool/WithdrawalPool.sol index dd6e807f..282d2289 100644 --- a/contracts/core/priorityPool/WithdrawalPool.sol +++ b/contracts/core/priorityPool/WithdrawalPool.sol @@ -294,6 +294,39 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { emit Withdraw(owner, amountToWithdraw); } + /** + * @notice Executes a group of fully finalized withdrawals + * @dev used by owner in the case that withdrawalBatchIdCutoff cannot be updated due to + * outstanding withdrawals + * @param _withdrawalIds list of withdrawal ids to execute + * @param _batchIds list of batch ids corresponding to withdrawal ids + */ + function forceWithdraw( + uint256[] calldata _withdrawalIds, + uint256[] calldata _batchIds + ) external onlyOwner { + for (uint256 i = 0; i < _withdrawalIds.length; ++i) { + uint256 withdrawalId = _withdrawalIds[i]; + Withdrawal memory withdrawal = queuedWithdrawals[_withdrawalIds[i]]; + uint256 batchId = _batchIds[i]; + WithdrawalBatch memory batch = withdrawalBatches[batchId]; + address owner = withdrawalOwners[withdrawalId]; + + if (withdrawalId <= withdrawalBatches[batchId - 1].indexOfLastWithdrawal) + revert InvalidWithdrawalId(); + if (withdrawalId > batch.indexOfLastWithdrawal) revert InvalidWithdrawalId(); + + uint256 amountToWithdraw = withdrawal.partiallyWithdrawableAmount + + (uint256(batch.stakePerShares) * uint256(withdrawal.sharesRemaining)) / + 1e18; + delete queuedWithdrawals[withdrawalId]; + delete withdrawalOwners[withdrawalId]; + + token.safeTransfer(owner, amountToWithdraw); + emit Withdraw(owner, amountToWithdraw); + } + } + /** * @notice Queues a withdrawal of liquid staking tokens for an account * @param _account address of account From 8217090110465fcb16f8e15abd6b3498def6e2e4 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sat, 23 Nov 2024 04:46:49 -0500 Subject: [PATCH 08/16] fixed updateWithdrawalBatchIdCutoff --- contracts/core/priorityPool/WithdrawalPool.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/core/priorityPool/WithdrawalPool.sol b/contracts/core/priorityPool/WithdrawalPool.sol index 282d2289..c2c503b4 100644 --- a/contracts/core/priorityPool/WithdrawalPool.sol +++ b/contracts/core/priorityPool/WithdrawalPool.sol @@ -419,11 +419,11 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { // find the last batch where all withdrawals have no funds remaining for (uint256 i = newWithdrawalBatchIdCutoff; i < numBatches; ++i) { + newWithdrawalBatchIdCutoff = i; + if (withdrawalBatches[i].indexOfLastWithdrawal >= newWithdrawalIdCutoff) { break; } - - newWithdrawalBatchIdCutoff = i; } withdrawalIdCutoff = uint128(newWithdrawalIdCutoff); From a483da1491693911d240f078e8771d96dd3e11d5 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sat, 23 Nov 2024 05:15:26 -0500 Subject: [PATCH 09/16] call updateStrategyRewards before modifying fees --- contracts/core/StakingPool.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/core/StakingPool.sol b/contracts/core/StakingPool.sol index 759ffb5d..5370731a 100644 --- a/contracts/core/StakingPool.sol +++ b/contracts/core/StakingPool.sol @@ -346,6 +346,12 @@ contract StakingPool is StakingRewardsPool { * @param _feeBasisPoints fee in basis points **/ function addFee(address _receiver, uint256 _feeBasisPoints) external onlyOwner { + uint256[] memory strategyIdxs = new uint256[](strategies.length); + for (uint256 i = 0; i < strategyIdxs.length; ++i) { + strategyIdxs[i] = i; + } + _updateStrategyRewards(strategyIdxs, ""); + fees.push(Fee(_receiver, _feeBasisPoints)); require(_totalFeesBasisPoints() <= 4000, "Total fees must be <= 40%"); } @@ -363,6 +369,12 @@ contract StakingPool is StakingRewardsPool { ) external onlyOwner { require(_index < fees.length, "Fee does not exist"); + uint256[] memory strategyIdxs = new uint256[](strategies.length); + for (uint256 i = 0; i < strategyIdxs.length; ++i) { + strategyIdxs[i] = i; + } + _updateStrategyRewards(strategyIdxs, ""); + if (_feeBasisPoints == 0) { fees[_index] = fees[fees.length - 1]; fees.pop(); From 2c4eade329446a432121872742ed1f1c9e3bc778 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sat, 23 Nov 2024 05:36:22 -0500 Subject: [PATCH 10/16] incremented OperatorVCS reinitializer --- contracts/linkStaking/OperatorVCS.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/linkStaking/OperatorVCS.sol b/contracts/linkStaking/OperatorVCS.sol index c6d054a7..7327ac36 100644 --- a/contracts/linkStaking/OperatorVCS.sol +++ b/contracts/linkStaking/OperatorVCS.sol @@ -59,7 +59,7 @@ contract OperatorVCS is VaultControllerStrategy { uint256 _vaultMaxDeposits, uint256 _operatorRewardPercentage, address _vaultDepositController - ) public reinitializer(3) { + ) public reinitializer(4) { if (address(token) == address(0)) { __VaultControllerStrategy_init( _token, From 7500506fcccc49b25cf32bafc764ff34ef61ae6b Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sat, 23 Nov 2024 06:37:08 -0500 Subject: [PATCH 11/16] skip depositing into removed vaults --- contracts/linkStaking/base/VaultControllerStrategy.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/linkStaking/base/VaultControllerStrategy.sol b/contracts/linkStaking/base/VaultControllerStrategy.sol index 080fcd5c..ce3697c7 100644 --- a/contracts/linkStaking/base/VaultControllerStrategy.sol +++ b/contracts/linkStaking/base/VaultControllerStrategy.sol @@ -266,6 +266,11 @@ contract VaultDepositController is Strategy { uint256 deposits = vault.getPrincipalDeposits(); uint256 canDeposit = _maxDeposits - deposits; + if (vault.isRemoved()) { + ++i; + continue; + } + // cannot leave a vault with less than minimum deposits if (deposits < _minDeposits && toDeposit < (_minDeposits - deposits)) { break; From 5b870defb46137ec622e63353096a9f65b6c28fa Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sat, 23 Nov 2024 06:43:26 -0500 Subject: [PATCH 12/16] incremented CommunityVCS reinitializer --- contracts/linkStaking/CommunityVCS.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/linkStaking/CommunityVCS.sol b/contracts/linkStaking/CommunityVCS.sol index 33208871..033f8014 100644 --- a/contracts/linkStaking/CommunityVCS.sol +++ b/contracts/linkStaking/CommunityVCS.sol @@ -49,7 +49,7 @@ contract CommunityVCS is VaultControllerStrategy { uint128 _vaultDeploymentThreshold, uint128 _vaultDeploymentAmount, address _vaultDepositController - ) public initializer { + ) public reinitializer(2) { if (address(token) == address(0)) { __VaultControllerStrategy_init( _token, From 72f7f2c823dd320e61deb92ba8ba302c5f56569b Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 25 Nov 2024 02:36:04 -0500 Subject: [PATCH 13/16] fix depositToVaults --- contracts/linkStaking/base/VaultControllerStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/linkStaking/base/VaultControllerStrategy.sol b/contracts/linkStaking/base/VaultControllerStrategy.sol index ce3697c7..79d917e0 100644 --- a/contracts/linkStaking/base/VaultControllerStrategy.sol +++ b/contracts/linkStaking/base/VaultControllerStrategy.sol @@ -201,7 +201,7 @@ contract VaultDepositController is Strategy { // if vault is empty and equal to withdrawal index, increment withdrawal index to the next vault in the group if (deposits == 0 && vaultIndex == group.withdrawalIndex) { group.withdrawalIndex += uint64(globalState.numVaultGroups); - if (group.withdrawalIndex > globalState.depositIndex) { + if (group.withdrawalIndex >= globalState.depositIndex) { group.withdrawalIndex = uint64(groupIndex); } } From ec3a1fc122bc054e35251ef20562a638ebb6a395 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 25 Nov 2024 02:47:02 -0500 Subject: [PATCH 14/16] fix getTotalUnbonded --- contracts/linkStaking/FundFlowController.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/linkStaking/FundFlowController.sol b/contracts/linkStaking/FundFlowController.sol index f5a210d6..3591f761 100644 --- a/contracts/linkStaking/FundFlowController.sol +++ b/contracts/linkStaking/FundFlowController.sol @@ -220,7 +220,12 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable { totalDepositRoom[i] = depositRoom; if (_vaultGroups[i] == curUnbondedVaultGroup) { - totalUnbonded = _getTotalUnbonded(vaults, numVaultGroups, _vaultGroups[i]); + totalUnbonded = _getTotalUnbonded( + vaults, + numVaultGroups, + _vaultGroups[i], + depositIndex + ); } } @@ -384,7 +389,8 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable { uint256 nextGroupTotalUnbonded = _getTotalUnbonded( vaults, numVaultGroups, - _nextUnbondedVaultGroup + _nextUnbondedVaultGroup, + depositIndex ); return (curGroupVaultsToUnbond, curGroupTotalDepositRoom, nextGroupTotalUnbonded); @@ -396,6 +402,7 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable { * @param _numVaultGroups total number of vault groups * @param _vaultGroup index of vault group * @param _vaultMaxDeposits max deposits per vault + * @param _depositIndex global deposit index * @return total deposit room * @return list of non-empty vaults */ @@ -434,16 +441,18 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable { * @param _vaults list of all vaults * @param _numVaultGroups total number of vault groups * @param _vaultGroup index of vault group + * @param _depositIndex global deposit index * @return total unbonded */ function _getTotalUnbonded( address[] memory _vaults, uint256 _numVaultGroups, - uint256 _vaultGroup + uint256 _vaultGroup, + uint256 _depositIndex ) internal view returns (uint256) { uint256 totalUnbonded; - for (uint256 i = _vaultGroup; i < _vaults.length; i += _numVaultGroups) { + for (uint256 i = _vaultGroup; i < _depositIndex; i += _numVaultGroups) { if (!IVault(_vaults[i]).claimPeriodActive() || IVault(_vaults[i]).isRemoved()) continue; totalUnbonded += IVault(_vaults[i]).getPrincipalDeposits(); From 302148de38f742058a8c6f614b102a368923f50f Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 25 Nov 2024 04:58:23 -0500 Subject: [PATCH 15/16] avoid revert in withdrawal edge case --- contracts/core/interfaces/IWithdrawalPool.sol | 2 + contracts/core/priorityPool/PriorityPool.sol | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/contracts/core/interfaces/IWithdrawalPool.sol b/contracts/core/interfaces/IWithdrawalPool.sol index 60e8d1d6..e77f3000 100644 --- a/contracts/core/interfaces/IWithdrawalPool.sol +++ b/contracts/core/interfaces/IWithdrawalPool.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.15; interface IWithdrawalPool { function getTotalQueuedWithdrawals() external view returns (uint256); + function minWithdrawalAmount() external view returns (uint256); + function deposit(uint256 _amount) external; function queueWithdrawal(address _account, uint256 _amount) external; diff --git a/contracts/core/priorityPool/PriorityPool.sol b/contracts/core/priorityPool/PriorityPool.sol index b36e6241..4d08bf4f 100644 --- a/contracts/core/priorityPool/PriorityPool.sol +++ b/contracts/core/priorityPool/PriorityPool.sol @@ -54,7 +54,7 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl uint256 public totalQueued; // total number of tokens deposited into the staking pool or swapped for LSTs since the last distribution uint256 public depositsSinceLastUpdate; - // total number of shares received for depositSinceLastUpdate + // total number of shares received for depositsSinceLastUpdate uint256 private sharesSinceLastUpdate; // list of all accounts that have ever queued tokens @@ -102,6 +102,7 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl error InvalidAmount(); error StatusAlreadySet(); error InsufficientLiquidity(); + error WithdrawFailed(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -238,8 +239,8 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl if (msg.sender == address(token)) { _deposit(_sender, _value, shouldQueue, data); } else if (msg.sender == address(stakingPool)) { - uint256 amountQueued = _withdraw(_sender, _value, shouldQueue); - token.safeTransfer(_sender, _value - amountQueued); + uint256 amountWithdrawn = _withdraw(_sender, _value, shouldQueue, true); + token.safeTransfer(_sender, amountWithdrawn); } else { revert UnauthorizedToken(); } @@ -314,7 +315,12 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl address(this), toWithdraw ); - toWithdraw = _withdraw(account, toWithdraw, _shouldQueueWithdrawal); + toWithdraw -= _withdraw( + account, + toWithdraw, + _shouldQueueWithdrawal, + toWithdraw == _amountToWithdraw + ); } token.safeTransfer(account, _amountToWithdraw - toWithdraw); @@ -654,16 +660,20 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl * @param _account account to withdraw for * @param _amount amount to withdraw * @param _shouldQueueWithdrawal whether a withdrawal should be queued if the the full amount cannot be satisfied - * @return the amount of tokens that were queued for withdrawal + * @param _shouldRevertOnZero whether call should revert if nothing is withdrawn or queued for withdrawal + * @return the amount of tokens that were withdrawn **/ function _withdraw( address _account, uint256 _amount, - bool _shouldQueueWithdrawal + bool _shouldQueueWithdrawal, + bool _shouldRevertOnZero ) internal returns (uint256) { if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled(); uint256 toWithdraw = _amount; + uint256 withdrawn; + uint256 queued; if (totalQueued != 0) { uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued; @@ -672,15 +682,24 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl depositsSinceLastUpdate += toWithdrawFromQueue; sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue); toWithdraw -= toWithdrawFromQueue; + withdrawn = toWithdrawFromQueue; } if (toWithdraw != 0) { if (!_shouldQueueWithdrawal) revert InsufficientLiquidity(); - withdrawalPool.queueWithdrawal(_account, toWithdraw); + + if (toWithdraw >= withdrawalPool.minWithdrawalAmount()) { + withdrawalPool.queueWithdrawal(_account, toWithdraw); + queued = toWithdraw; + } else { + IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toWithdraw); + } } - emit Withdraw(_account, _amount - toWithdraw); - return toWithdraw; + if (_shouldRevertOnZero && withdrawn + queued == 0) revert WithdrawFailed(); + if (_amount != toWithdraw) emit Withdraw(_account, _amount - toWithdraw); + + return withdrawn; } /** From b0792736490f6628b3cb185fac2b00c2452d4129 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 25 Nov 2024 05:56:28 -0500 Subject: [PATCH 16/16] added test for forceWithdraw --- .../core/priorityPool/withdrawal-pool.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/core/priorityPool/withdrawal-pool.test.ts b/test/core/priorityPool/withdrawal-pool.test.ts index 4c9b56ea..71fbf96f 100644 --- a/test/core/priorityPool/withdrawal-pool.test.ts +++ b/test/core/priorityPool/withdrawal-pool.test.ts @@ -225,6 +225,61 @@ describe('WithdrawalPool', () => { ) }) + it('forceWithdraw should work correctly', async () => { + const { signers, accounts, withdrawalPool, token } = await loadFixture(deployFixture) + + await withdrawalPool.queueWithdrawal(accounts[0], toEther(1000)) + await withdrawalPool.queueWithdrawal(accounts[1], toEther(250)) + await withdrawalPool.queueWithdrawal(accounts[0], toEther(500)) + await withdrawalPool.deposit(toEther(1200)) + + await expect(withdrawalPool.forceWithdraw([1, 3], [1, 1])).to.be.revertedWithCustomError( + withdrawalPool, + 'InvalidWithdrawalId()' + ) + + await withdrawalPool.deposit(toEther(550)) + + await expect(withdrawalPool.forceWithdraw([1], [2])).to.be.revertedWithCustomError( + withdrawalPool, + 'InvalidWithdrawalId()' + ) + + let startingBalance1 = await token.balanceOf(accounts[1]) + let startingBalance0 = await token.balanceOf(accounts[0]) + + await withdrawalPool.forceWithdraw([2, 1, 3], [2, 1, 2]) + assert.equal(fromEther((await token.balanceOf(accounts[1])) - startingBalance1), 250) + assert.deepEqual( + (await withdrawalPool.getWithdrawalIdsByOwner(accounts[1])).map((id) => Number(id)), + [] + ) + assert.deepEqual( + (await withdrawalPool.getWithdrawals([2])).map((d: any) => [ + fromEther(d[0]), + fromEther(d[1]), + ]), + [[0, 0]] + ) + + assert.equal(fromEther((await token.balanceOf(accounts[0])) - startingBalance0), 1500) + assert.deepEqual( + (await withdrawalPool.getWithdrawalIdsByOwner(accounts[1])).map((id) => Number(id)), + [] + ) + assert.deepEqual( + (await withdrawalPool.getWithdrawals([1, 2, 3])).map((d: any) => [ + fromEther(d[0]), + fromEther(d[1]), + ]), + [ + [0, 0], + [0, 0], + [0, 0], + ] + ) + }) + it('getWithdrawalIdsByOwner should work correctly', async () => { const { signers, accounts, withdrawalPool } = await loadFixture(deployFixture)