Summary
The WithdrawalPool contract sets the minWithdrawalAmount variable during initialization, this variable is supposed to prevent spamming small amount in the withdrawalPool, but it can actually cause legitimate users funds which are less than the minWithdrawalAmount to be stuck.
Vulnerability Details
The check for the minWithdrawalAmount is done in the queueWithdrawal
function in the WithdrawalPool contract. This function can only be called by the PriorityPool::withdraw
function and only calls the queueWithdrawal
function when there are no queued tokens in the contract, and the user passes the _shouldQueueWithdrawal = true. if the minWithdrawalAmount is set to 10 ether and the remaining tokens that the user wants to withdraw is less than or equal to 10 ether, the transaction will revert due to AmountTooSmall().
https://github.com/Cyfrin/2024-09-stakelink/blob/main/contracts/core/priorityPool/PriorityPool.sol#L659
https://github.com/Cyfrin/2024-09-stakelink/blob/main/contracts/core/priorityPool/WithdrawalPool.sol#L302
function _withdraw(address _account, uint256 _amount, bool _shouldQueueWithdrawal) internal returns (uint256) {
if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled();
uint256 toWithdraw = _amount;
if (totalQueued != 0) {
uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued;
totalQueued -= toWithdrawFromQueue;
depositsSinceLastUpdate += toWithdrawFromQueue;
sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue);
toWithdraw -= toWithdrawFromQueue;
}
if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
@> withdrawalPool.queueWithdrawal(_account, toWithdraw);
}
emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
}
function queueWithdrawal(address _account, uint256 _amount) external onlyPriorityPool {
@> if (_amount < minWithdrawalAmount) revert AmountTooSmall();
lst.safeTransferFrom(msg.sender, address(this), _amount);
uint256 sharesAmount = _getSharesByStake(_amount);
queuedWithdrawals.push(Withdrawal(uint128(sharesAmount), 0));
totalQueuedShareWithdrawals += sharesAmount;
uint256 withdrawalId = queuedWithdrawals.length - 1;
queuedWithdrawalsByAccount[_account].push(withdrawalId);
withdrawalOwners[withdrawalId] = _account;
emit QueueWithdrawal(_account, _amount);
}
Impact
Denial of service for users whose tokens left in the PriorityPool is less than the minWithdrawalAmount.
Denial of service for users whose leftover tokens during the withdrawal process is less than the minWithdrawalAmount.
Tools Used
Manual Review
Recommendations
Check if the leftover toWithdraw is greater than the minWithdrawalAmount before calling withdrawalPool.queueWithdrawal().
function _withdraw(address _account, uint256 _amount, bool _shouldQueueWithdrawal) internal returns (uint256) {
if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled();
uint256 toWithdraw = _amount;
if (totalQueued != 0) {
uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued;
totalQueued -= toWithdrawFromQueue;
depositsSinceLastUpdate += toWithdrawFromQueue;
sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue);
toWithdraw -= toWithdrawFromQueue;
}
if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
+ if (toWithdraw > withdrawalPool.minWithdrawalAmount()){
+ withdrawalPool.queueWithdrawal(_account, toWithdraw);
+ }
- withdrawalPool.queueWithdrawal(_account, toWithdraw);
}
emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
}