Liquid Staking

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

`WithdrawalPool::withdraw` does not update `totalQueuedShareWithdrawals` or call `_finalizeWithdrawals`, leading all new tokens to be deposited in the WithdrawalPool.

Description

WithdrawalPool::withdraw allows the execution of a group of fully and/or partially finalized withdrawals owned by the sender. However, this function never calls WithdrawalPool::_finalizeWithdrawals, failing to update totalQueuedShareWithdrawals.

For every withdrawal with that function, totalQueuedShareWithdrawals won't be updated and will increase indefinitely, but in reality, tokens will be transferred.

function withdraw(uint256[] calldata _withdrawalIds, uint256[] calldata _batchIds) external {
address owner = msg.sender;
uint256 amountToWithdraw;
for (uint256 i = 0; i < _withdrawalIds.length; ++i) {
uint256 withdrawalId = _withdrawalIds[i];
Withdrawal memory withdrawal = queuedWithdrawals[_withdrawalIds[i]];
uint256 batchId = _batchIds[i];
WithdrawalBatch memory batch = withdrawalBatches[batchId];
if (withdrawalOwners[withdrawalId] != owner) revert SenderNotAuthorized();
if (
batchId != 0 && withdrawalId <= withdrawalBatches[batchId - 1].indexOfLastWithdrawal
) revert InvalidWithdrawalId();
if (
batchId != 0 &&
withdrawalId > batch.indexOfLastWithdrawal &&
withdrawal.partiallyWithdrawableAmount == 0
) revert InvalidWithdrawalId();
if (withdrawalId <= batch.indexOfLastWithdrawal) {
amountToWithdraw +=
withdrawal.partiallyWithdrawableAmount +
(uint256(batch.stakePerShares) * uint256(withdrawal.sharesRemaining)) /
1e18;
delete queuedWithdrawals[withdrawalId];
delete withdrawalOwners[withdrawalId];
} else {
amountToWithdraw += withdrawal.partiallyWithdrawableAmount;
queuedWithdrawals[withdrawalId].partiallyWithdrawableAmount = 0;
}
}
token.safeTransfer(owner, amountToWithdraw);
emit Withdraw(owner, amountToWithdraw);
}

This will have two real impact:

  1. performUpkeep will always set toWithdraw to canWithdraw, even when the the contract balance is not enough to proceed.

  2. getTotalQueuedWithdrawals will always be positive and, any deposit in the PriorityPool will lead to sending the token to WithdrawalPool, even if there is no queued withdrawal, instead of staking them in the Chainlink contract.

  • performUpkeep:

function performUpkeep(bytes calldata _performData) external {
uint256 canWithdraw = priorityPool.canWithdraw(address(this), 0);
uint256 totalQueued = _getStakeByShares(totalQueuedShareWithdrawals);
if (
totalQueued == 0 ||
canWithdraw == 0 ||
block.timestamp <= timeOfLastWithdrawal + minTimeBetweenWithdrawals
) revert NoUpkeepNeeded();
timeOfLastWithdrawal = uint64(block.timestamp);
uint256 toWithdraw = totalQueued > canWithdraw ? canWithdraw : totalQueued;
bytes[] memory data = abi.decode(_performData, (bytes[]));
priorityPool.executeQueuedWithdrawals(toWithdraw, data);
_finalizeWithdrawals(toWithdraw);
}
  • _deposit

function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
if (totalQueued == 0) {
@> uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
@> if (queuedWithdrawals != 0) {
@> uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
@> ? toDeposit
@> : queuedWithdrawals;
@> withdrawalPool.deposit(toDepositIntoQueue);
@> toDeposit -= toDepositIntoQueue;
@> IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
if (toDeposit != 0) {
uint256 canDeposit = stakingPool.canDeposit();
if (canDeposit != 0) {
uint256 toDepositIntoPool = toDeposit <= canDeposit ? toDeposit : canDeposit;
stakingPool.deposit(_account, toDepositIntoPool, _data);
toDeposit -= toDepositIntoPool;
}
}
)
...
}
...
}

Risk

Likelyhood: High.

  • When any user directly withdraws from the WithdrawalPool.

Impact: High

  • Every new deposit (when the priority queue is empty) will be sent to the WithdrawalPool instead of staking them.

Recommended Mitigation

Update totalQueuedShareWithdrawals in withdraw or use _finalizeWithdrawals.

Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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