Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: high
Invalid

checkUpkeep misses to validate the queue size

Summary

checkUpkeep function misses to validate a limit on the number of withdrawals processed.

And WithdrawalPool contract´s _finalizeWithdrawals function may revert due to missing to validate the max. number of withdrawals.

If many withdrawal requests accumulate, the gas required to process them all can exceed the block gas limit, causing the transaction to revert and halting the withdrawal process for users.

Vulnerability Details

When users request withdrawals that exceed the WithdrawalPool's available balance, their requests are queued in the queuedWithdrawals array.
The performUpkeep function is designed to process these queued withdrawals by calling _finalizeWithdrawals. And this one is triggered by querying checkUpKeep off-chain.
However checkUpkeep does not account for queue size:

Contract: WithdrawalPool.sol
333: function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) {
334: if (
335: _getStakeByShares(totalQueuedShareWithdrawals) != 0 &&
336: priorityPool.canWithdraw(address(this), 0) != 0 &&
337: block.timestamp > timeOfLastWithdrawal + minTimeBetweenWithdrawals
338: ) {
339: return (true, "");
340: }
341: return (false, "");
342: }

So if above condition returns true, the CRON job can call performUpkeep to execute them and calls _finalizeWithdrawals at the end:

Contract: WithdrawalPool.sol
348: function performUpkeep(bytes calldata _performData) external {
349: uint256 canWithdraw = priorityPool.canWithdraw(address(this), 0);
350: uint256 totalQueued = _getStakeByShares(totalQueuedShareWithdrawals);
351: if (
352: totalQueued == 0 ||
353: canWithdraw == 0 ||
354: block.timestamp <= timeOfLastWithdrawal + minTimeBetweenWithdrawals
355: ) revert NoUpkeepNeeded();
356:
357: timeOfLastWithdrawal = uint64(block.timestamp);
358:
359: uint256 toWithdraw = totalQueued > canWithdraw ? canWithdraw : totalQueued;
360: bytes[] memory data = abi.decode(_performData, (bytes[]));
361:
362: priorityPool.executeQueuedWithdrawals(toWithdraw, data);
363: >>> _finalizeWithdrawals(toWithdraw);
364: }

However, _finalizeWithdrawals iterates over the entire queuedWithdrawals array without any limit, which can lead to excessive gas consumption if the array is large.

Contract: WithdrawalPool.sol
422: function _finalizeWithdrawals(uint256 _amount) internal {
423: uint256 sharesToWithdraw = _getSharesByStake(_amount);
424: uint256 numWithdrawals = queuedWithdrawals.length;
425:
426: totalQueuedShareWithdrawals -= sharesToWithdraw;
427:
428: for (uint256 i = indexOfNextWithdrawal; i < numWithdrawals; ++i) {
429: uint256 sharesRemaining = queuedWithdrawals[i].sharesRemaining;
430:
431: if (sharesRemaining < sharesToWithdraw) {
432: // fully finalize withdrawal
433: sharesToWithdraw -= sharesRemaining;
434: continue;
435: }
436:
437: if (sharesRemaining > sharesToWithdraw) {
438: // partially finalize withdrawal
439: queuedWithdrawals[i] = Withdrawal(
440: uint128(sharesRemaining - sharesToWithdraw),
441: uint128(
442: queuedWithdrawals[i].partiallyWithdrawableAmount +
443: _getStakeByShares(sharesToWithdraw)
444: )
445: );
446: indexOfNextWithdrawal = i;
447: withdrawalBatches.push(
448: WithdrawalBatch(uint128(i - 1), uint128(_getStakeByShares(1 ether)))
449: );
450: } else {
451: // fully finalize withdrawal
452: indexOfNextWithdrawal = i + 1;
453: withdrawalBatches.push(
454: WithdrawalBatch(uint128(i), uint128(_getStakeByShares(1 ether)))
455: );
456: }
457:
458: sharesToWithdraw = 0;
459: break;
460: }
461:
462: // entire amount must be accounted for
463: assert(sharesToWithdraw == 0);
464:
465: emit WithdrawalsFinalized(_amount);
466: }

Impact

DOS

Tools Used

Manual Review

Recommendations

Modify the checkUpkeep for checking a threshold qeueu size.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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