Summary
An attacker can execute a denial-of-service (DoS) attack on users attempting to queue their withdrawals by exploiting the `unqueueTokens` function. By front-running a legitimate user's withdrawal attempt, an attacker can unqueue their own tokens and requeue them afterward, causing the user’s withdrawal request to fail, particularly making the amount queued less than the minimum withdrawal amount.
Vulnerability Details
The core issue arises when an attacker front-runs a legitimate user's withdrawal attempt by calling `unqueueTokens` to remove their tokens from the queue. This manipulation results in a situation where the amount left in the queue for the legitimate user's withdrawal is less than the `minWithdrawalAmount`, which leads to a transaction revert due to the check in `queueWithdrawal`.
Here’s how the attack occurs:
1. The attacker has tokens queued for Deposit.
2. A legitimate user tries to withdraw their tokens by calling the `_withdraw` function. If there are queued tokens, they will attempt to withdraw from the total queued amount first.
3. The attacker front-runs the legitimate user by calling `unqueueTokens` to remove enough tokens from the queue.
4. The legitimate user’s withdrawal process now attempts to queue their remaining tokens, but since the total queued amount is now insufficient and below the `minWithdrawalAmount`, the transaction reverts.
This process allows the attacker to repeatedly disrupt legitimate withdrawal requests, causing a denial-of-service (DoS) attack on the withdrawal process.
### Relevant Code
1. **Unqueue Tokens**:
* @notice Unqueues queued tokens
* @param _amountToUnqueue amount of tokens to unqueue
* @param _amount amount as recorded in sender's merkle tree entry (stored on IPFS)
* @param _sharesAmount shares amount as recorded in sender's merkle tree entry (stored on IPFS)
* @param _merkleProof merkle proof for sender's merkle tree entry (generated from IPFS data)
*/
function unqueueTokens(
uint256 _amountToUnqueue,
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof
) external whenNotPaused {
if (_amountToUnqueue == 0) revert InvalidAmount();
if (_amountToUnqueue > totalQueued) revert InsufficientQueuedTokens();
address account = msg.sender;
if (accountIndexes[account] < merkleTreeSize) {
bytes32 node = keccak256(
bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount)))
);
if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node))
revert InvalidProof();
}
if (_amountToUnqueue > getQueuedTokens(account, _amount)) revert InsufficientBalance();
accountQueuedTokens[account] -= _amountToUnqueue;
@audit>> 1. >> totalQueued -= _amountToUnqueue;
token.safeTransfer(account, _amountToUnqueue);
emit UnqueueTokens(account, _amountToUnqueue);
}
2. **Withdraw Logic**:
* @notice Withdraws asset tokens
* @dev will swap liquid staking tokens for queued tokens if there are any queued, then
* remaining tokens will be queued for withdrawal in the withdrawal pool if
* `_shouldQueueWithdrawal` is true, otherwise function will revert
* @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
**/
function _withdraw(
address _account,
uint256 _amount,
bool _shouldQueueWithdrawal
) internal returns (uint256) {
if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled();
uint256 toWithdraw = _amount;
if (totalQueued != 0) {
@audit>> 2. reduce towithdraw >> uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued;
totalQueued -= toWithdrawFromQueue;
depositsSinceLastUpdate += toWithdrawFromQueue;
sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue);
toWithdraw -= toWithdrawFromQueue;
}
@audit>> 2.>> if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
@audit>> 2. Queue >> withdrawalPool.queueWithdrawal(_account, toWithdraw);
}
emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
}
3. **Queue Withdrawal**:
* @notice Queues a withdrawal of liquid staking tokens for an account
* @param _account address of account
* @param _amount amount of LST
*/
function queueWithdrawal(address _account, uint256 _amount) external onlyPriorityPool {
@audit>> 3. revert >> if (_amount < minWithdrawalAmount) revert AmountTooSmall();
Impact
This vulnerability enables an attacker to disrupt the entire withdrawal process for other users by strategically removing tokens from the queue and queuing them later. The impact includes:
1. **Denial-of-Service (DoS) Attack**: Legitimate users cannot withdraw their tokens if the amount left in the queue is less than the minimum required withdrawal amount.
2. **Potential Exploit**: The attacker could continue manipulating the queue indefinitely, causing continuous disruptions.
Tools Used
Manual Review
Recommendations
1. **Check Withdrawal and Queue Limits**: Add logic in the `_withdraw` function to ensure that if the remaining `toWithdraw` amount is greater than `totalQueued` but less than the `minWithdrawalAmount`, the contract should queue the minimum required amount first and withdraw the rest from the queue.
function _withdraw(
address _account,
uint256 _amount,
bool _shouldQueueWithdrawal
) internal returns (uint256) {
if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled();
uint256 toWithdraw = _amount;
++ uint256 minamount ;
++ if(toWithdraw>totalQueued && toWithdraw-totalQueued < Withrawalpool.minWithdrawalAmount() && _shouldQueueWithdrawal){
++ minamount = Withrawalpool.minWithdrawalAmount();
++ uint256 toWithdraw -= minamount;
++ }
if (totalQueued != 0) {
uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued;
totalQueued -= toWithdrawFromQueue;
depositsSinceLastUpdate += toWithdrawFromQueue;
sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue);
toWithdraw -= toWithdrawFromQueue;
}
++ uint256 toWithdraw += minamount;
if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
withdrawalPool.queueWithdrawal(_account, toWithdraw);
}