Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Valid

Malicious User Can Cause a DoS in `WithdrawalPool::updateWithdrawalBatchIdCutoff` by Delaying Token Withdrawals

Summary

The WithdrawalPool::updateWithdrawalBatchIdCutoff function is designed to update the withdrawalBatchIdCutoff, which helps efficiently return data in getBatchIds by skipping old withdrawal batches. However, a malicious user can exploit this function by delaying their token withdrawal indefinitely, preventing the cutoff from advancing. This manipulation can lead to a DoS situation when the function tries to iterate over a large number of withdrawals, risking an out-of-gas error.

Vulnerability Details

The updateWithdrawalBatchIdCutoff function iterates over the queuedWithdrawals array to find the first unprocessed or partially filled withdrawal and updates the withdrawalIdCutoff accordingly. Since numWithdrawals is an ever-growing array, the function stores newWithdrawalIdCutoff to avoid recalculating from the beginning each time.

Here's the relevant code section:

function updateWithdrawalBatchIdCutoff() external {
uint256 numWithdrawals = queuedWithdrawals.length;
uint256 newWithdrawalIdCutoff = withdrawalIdCutoff;
// find the first withdrawal that has funds remaining
for (uint256 i = newWithdrawalIdCutoff; i < numWithdrawals; ++i) {
newWithdrawalIdCutoff = i;
Withdrawal memory withdrawal = queuedWithdrawals[i];
if (withdrawal.sharesRemaining != 0 || withdrawal.partiallyWithdrawableAmount != 0) {
break;
}
}
uint256 numBatches = withdrawalBatches.length;
uint256 newWithdrawalBatchIdCutoff = withdrawalBatchIdCutoff;
// find the last batch where all withdrawals have no funds remaining
for (uint256 i = newWithdrawalBatchIdCutoff; i < numBatches; ++i) {
if (withdrawalBatches[i].indexOfLastWithdrawal >= newWithdrawalIdCutoff) {
break;
}
newWithdrawalBatchIdCutoff = i;
}
withdrawalIdCutoff = uint128(newWithdrawalIdCutoff);
withdrawalBatchIdCutoff = uint128(newWithdrawalBatchIdCutoff);
}

A malicious user whose withdrawal request is at the withdrawalIdCutoff can intentionally delay withdrawing their tokens. This causes the list of queued withdrawals to grow without updating the cutoff. Eventually, the loop will iterate over too many unprocessed withdrawals, risking an out-of-gas DoS. The attack is low-cost, as the attacker can simply withdraw their tokens whenever they choose.

Illustration explaining withdrawalIdCutoff:

  1. [0, 0, 0, 1, 0, 0, 0, 1, 1] : withdrawalIdCutoff = 2

  2. [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1] : withdrawalIdCutoff = 6

  3. [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1] : withdrawalIdCutoff = 9

If the user at the cutoff delays their withdrawal indefinitely, the difference between the current withdrawalIdCutoff and the next valid cutoff grows, making future updates to the cutoff computationally expensive.

Impact

If getBatchIds becomes unusable, users will struggle to retrieve accurate parameters for withdrawing their tokens. This makes the withdrawal process inefficient and can disrupt normal protocol operations, causing financial loss and frustration for users.

Tools Used

Manual

Recommendations

Implement a timeout or penalty system for users who do not withdraw their tokens within a reasonable timeframe. Alternatively, limit the number of iterations per transaction to prevent an out-of-gas DoS when the queue grows too large.


Let me know if you'd like any further changes!

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

M-1 Cyfrin not properly fixed - if someone forgets to withdraw the withdrawalBatches array is still ever increasing

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.