diff --git a/contracts/core/interfaces/IPriorityPool.sol b/contracts/core/interfaces/IPriorityPool.sol index 953bd8c1..5b9523b5 100644 --- a/contracts/core/interfaces/IPriorityPool.sol +++ b/contracts/core/interfaces/IPriorityPool.sol @@ -14,6 +14,8 @@ interface IPriorityPool { function poolStatus() external view returns (PoolStatus); + function canWithdraw(address _account, uint256 _distributionAmount) external view returns (uint256); + function pauseForUpdate() external; function setPoolStatus(PoolStatus _status) external; @@ -24,4 +26,6 @@ interface IPriorityPool { uint256 _amountDistributed, uint256 _sharesAmountDistributed ) external; + + function executeQueuedWithdrawals(uint256 _amount, bytes[] calldata _data) external; } diff --git a/contracts/core/priorityPool/PriorityPool.sol b/contracts/core/priorityPool/PriorityPool.sol index f2867ad7..9780e078 100644 --- a/contracts/core/priorityPool/PriorityPool.sol +++ b/contracts/core/priorityPool/PriorityPool.sol @@ -128,6 +128,14 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl _; } + /** + * @notice reverts if sender is not withdrawal pool + **/ + modifier onlyWithdrawalPool() { + if (msg.sender != address(withdrawalPool)) revert SenderNotAuthorized(); + _; + } + /** * @notice returns a list of all accounts in the order that they appear in the merkle tree * @return list of accounts @@ -462,6 +470,12 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl _pause(); } + function executeQueuedWithdrawals(uint256 _amount, bytes[] calldata _data) external onlyWithdrawalPool { + IERC20Upgradeable(address(stakingPool)).safeTransferFrom(msg.sender, address(this), _amount); + stakingPool.withdraw(address(this), address(this), _amount, _data); + token.safeTransfer(msg.sender, _amount); + } + /** * @notice sets the pool's status * @param _status pool status @@ -515,6 +529,14 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl * @param _withdrawalPool address of withdrawal pool */ function setWithdrawalPool(address _withdrawalPool) external onlyOwner { + if (address(withdrawalPool) != address(0)) { + IERC20Upgradeable(address(stakingPool)).safeApprove(address(withdrawalPool), 0); + token.safeApprove(address(withdrawalPool), 0); + } + + IERC20Upgradeable(address(stakingPool)).safeApprove(_withdrawalPool, type(uint256).max); + token.safeApprove(_withdrawalPool, type(uint256).max); + withdrawalPool = IWithdrawalPool(_withdrawalPool); } diff --git a/contracts/core/priorityPool/WithdrawalPool.sol b/contracts/core/priorityPool/WithdrawalPool.sol index 97e3c108..5a00fa30 100644 --- a/contracts/core/priorityPool/WithdrawalPool.sol +++ b/contracts/core/priorityPool/WithdrawalPool.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "../interfaces/IStakingPool.sol"; +import "../interfaces/IPriorityPool.sol"; /** * @title Withdrawal Pool @@ -27,7 +28,7 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { IERC20Upgradeable public token; IERC20Upgradeable public lst; - address public priorityPool; + IPriorityPool public priorityPool; Withdrawal[] internal queuedWithdrawals; mapping(address => uint256[]) internal queuedWithdrawalsByAccount; @@ -42,12 +43,13 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { event QueueWithdrawal(address indexed account, uint256 amount); event Withdraw(address indexed account, uint256 amount); - event Deposit(uint256 amount); + event WithdrawalsFinalized(uint256 amount); event SetMinWithdrawalAmount(uint256 minWithdrawalAmount); error SenderNotAuthorized(); error InvalidWithdrawalId(); error AmountTooSmall(); + error NoUpkeepNeeded(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -71,14 +73,15 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { __Ownable_init(); token = IERC20Upgradeable(_token); lst = IERC20Upgradeable(_lst); - priorityPool = _priorityPool; + lst.safeApprove(_priorityPool, type(uint256).max); + priorityPool = IPriorityPool(_priorityPool); minWithdrawalAmount = _minWithdrawalAmount; withdrawalBatches.push(WithdrawalBatch(0, 0)); queuedWithdrawals.push(Withdrawal(0, 0)); } modifier onlyPriorityPool() { - if (msg.sender != priorityPool) revert SenderNotAuthorized(); + if (msg.sender != address(priorityPool)) revert SenderNotAuthorized(); _; } @@ -274,7 +277,50 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { function deposit(uint256 _amount) external onlyPriorityPool { token.safeTransferFrom(msg.sender, address(this), _amount); lst.safeTransfer(msg.sender, _amount); + _finalizeWithdrawals(_amount); + } + + /** + * @notice Returns whether withdrawals should be executed based on available withdrawal space + * @return true if withdrawal should be executed, false otherwise + */ + function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { + if (_getStakeByShares(totalQueuedShareWithdrawals) != 0 && priorityPool.canWithdraw(address(this), 0) != 0) { + return (true, ""); + } + return (false, ""); + } + + /** + * @notice Executes withdrawals if there is sufficient available withdrawal space + * @param _performData encoded withdrawal data to be passed to strategies + */ + function performUpkeep(bytes calldata _performData) external { + uint256 canWithdraw = priorityPool.canWithdraw(address(this), 0); + uint256 totalQueued = _getStakeByShares(totalQueuedShareWithdrawals); + if (totalQueued == 0 || canWithdraw == 0) revert NoUpkeepNeeded(); + + uint256 toWithdraw = totalQueued > canWithdraw ? canWithdraw : totalQueued; + bytes[] memory data = abi.decode(_performData, (bytes[])); + + priorityPool.executeQueuedWithdrawals(toWithdraw, data); + _finalizeWithdrawals(toWithdraw); + } + + /** + * @notice Sets the minimum amount of lst tokens that can be queued for withdrawal + * @param _minWithdrawalAmount min token amount + */ + function setMinWithdrawalAmount(uint256 _minWithdrawalAmount) external onlyOwner { + minWithdrawalAmount = _minWithdrawalAmount; + emit SetMinWithdrawalAmount(_minWithdrawalAmount); + } + /** + * @notice Finalizes withdrawal accounting after withdrawals have been executed + * @param _amount amount to finalize + */ + function _finalizeWithdrawals(uint256 _amount) internal { uint256 sharesToWithdraw = _getSharesByStake(_amount); uint256 numWithdrawals = queuedWithdrawals.length; @@ -306,16 +352,7 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable { assert(sharesToWithdraw == 0); - emit Deposit(_amount); - } - - /** - * @notice Sets the minimum amount of lst tokens that can be queued for withdrawal - * @param _minWithdrawalAmount min token amount - */ - function setMinWithdrawalAmount(uint256 _minWithdrawalAmount) external onlyOwner { - minWithdrawalAmount = _minWithdrawalAmount; - emit SetMinWithdrawalAmount(_minWithdrawalAmount); + emit WithdrawalsFinalized(_amount); } /** diff --git a/test/core/priorityPool/withdrawal-pool.test.ts b/test/core/priorityPool/withdrawal-pool.test.ts index 1fd0d1c4..1b6122e8 100644 --- a/test/core/priorityPool/withdrawal-pool.test.ts +++ b/test/core/priorityPool/withdrawal-pool.test.ts @@ -7,7 +7,7 @@ import { getAccounts, setupToken, } from '../../utils/helpers' -import { ERC677, StakingPool, StrategyMock } from '../../../typechain-types' +import { ERC677, PriorityPool, StakingPool, StrategyMock } from '../../../typechain-types' import { ethers } from 'hardhat' import { Signer } from 'ethers' import { WithdrawalPool } from '../../../typechain-types/WithdrawalPool' @@ -15,6 +15,7 @@ import { WithdrawalPool } from '../../../typechain-types/WithdrawalPool' describe('WithdrawalPool', () => { let withdrawalPool: WithdrawalPool let stakingPool: StakingPool + let strategy: StrategyMock let token: ERC677 let accounts: string[] let signers: Signer[] @@ -38,11 +39,11 @@ describe('WithdrawalPool', () => { [], ])) as StakingPool - let strategy = (await deployUpgradeable('StrategyMock', [ + strategy = (await deployUpgradeable('StrategyMock', [ token.address, stakingPool.address, toEther(1000000000), - toEther(0), + toEther(5000), ])) as StrategyMock withdrawalPool = (await deployUpgradeable('WithdrawalPool', [ @@ -59,7 +60,7 @@ describe('WithdrawalPool', () => { await token.approve(withdrawalPool.address, ethers.constants.MaxUint256) await stakingPool.approve(withdrawalPool.address, ethers.constants.MaxUint256) - await stakingPool.deposit(accounts[0], toEther(100000)) + await stakingPool.deposit(accounts[0], toEther(100000), ['0x']) await token.transfer(strategy.address, toEther(100000)) await stakingPool.updateStrategyRewards([0], '0x') }) @@ -351,4 +352,45 @@ describe('WithdrawalPool', () => { ) assert.equal(fromEther(data[1]), 250) }) + + it('checkUpkeep and performUpkeep should work correctly', async () => { + let priorityPool = (await deployUpgradeable('PriorityPool', [ + token.address, + stakingPool.address, + accounts[0], + 0, + 0, + 0, + ])) as PriorityPool + withdrawalPool = (await deployUpgradeable('WithdrawalPool', [ + stakingPool.address, + stakingPool.address, + priorityPool.address, + toEther(10), + ])) as WithdrawalPool + await stakingPool.approve(priorityPool.address, ethers.constants.MaxUint256) + await stakingPool.setPriorityPool(priorityPool.address) + await priorityPool.setWithdrawalPool(withdrawalPool.address) + + await priorityPool.withdraw(toEther(199000), 0, 0, [], false, true, ['0x']) + assert.deepEqual(await withdrawalPool.checkUpkeep('0x'), [false, '0x']) + await expect(withdrawalPool.performUpkeep('0x')).to.be.revertedWith('NoUpkeepNeeded()') + + await strategy.setMinDeposits(toEther(4000)) + assert.deepEqual(await withdrawalPool.checkUpkeep('0x'), [true, '0x']) + await withdrawalPool.performUpkeep(ethers.utils.defaultAbiCoder.encode(['bytes[]'], [['0x']])) + assert.equal(fromEther(await token.balanceOf(withdrawalPool.address)), 1000) + assert.equal(fromEther(await stakingPool.balanceOf(withdrawalPool.address)), 3000) + assert.equal(fromEther(await withdrawalPool.getTotalQueuedWithdrawals()), 3000) + + await strategy.setMinDeposits(toEther(0)) + assert.deepEqual(await withdrawalPool.checkUpkeep('0x'), [true, '0x']) + await withdrawalPool.performUpkeep(ethers.utils.defaultAbiCoder.encode(['bytes[]'], [['0x']])) + assert.equal(fromEther(await token.balanceOf(withdrawalPool.address)), 4000) + assert.equal(fromEther(await stakingPool.balanceOf(withdrawalPool.address)), 0) + assert.equal(fromEther(await withdrawalPool.getTotalQueuedWithdrawals()), 0) + + assert.deepEqual(await withdrawalPool.checkUpkeep('0x'), [false, '0x']) + await expect(withdrawalPool.performUpkeep('0x')).to.be.revertedWith('NoUpkeepNeeded()') + }) })