Liquid Staking

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

Missing Token Swap in Withdrawal Function Leads to Incorrect Token Distribution

Summary

The _withdraw function in the PriorityPool contract lacks a crucial step: swapping liquid staking tokens (LSTs) for underlying asset tokens during withdrawals. This omission, likely due to an incomplete implementation of the withdrawal logic, allows users to withdraw LSTs instead of the expected underlying tokens. The bug leads to severe consequences including accounting discrepancies, potential loss of funds, and a fundamentally broken withdrawal mechanism.

Vulnerability Details

The vulnerability is located in the _withdraw function:

/**
* @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) {
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;
}

Root cause: The function lacks a step to swap LSTs for underlying tokens before transferring them to the user or queueing the withdrawal.

Exploit path:

  1. A user initiates a withdrawal through the withdraw function, which calls _withdraw internally.

  2. The function processes queued tokens if available.

  3. For any remaining withdrawal amount, instead of swapping LSTs for underlying tokens, it either:
    a) Queues the withdrawal without converting tokens, or
    b) Implicitly allows the transfer of LSTs instead of underlying tokens.

  4. The function updates internal accounting as if a proper withdrawal of underlying tokens occurred.

Impact

  1. Users receive LSTs instead of underlying tokens, violating the expected behavior of the withdrawal process.

  2. Significant accounting discrepancies arise between the contract state and actual token balances.

  3. Potential financial losses for users who are unaware they're receiving LSTs instead of underlying tokens.

  4. The withdrawal mechanism becomes effectively non-functional for its intended purpose.

  5. Inaccurate liquidity reporting, potentially causing unnecessary transaction reverts.

  6. Withdrawals may be unnecessarily queued in the withdrawal pool.

How it changes the state of the protocol

  1. totalQueued is reduced incorrectly, as LSTs are not actually swapped for underlying tokens.

  2. depositsSinceLastUpdate and sharesSinceLastUpdate are increased based on LST amounts rather than underlying token movements, leading to inflated figures.

  3. The withdrawal pool may accumulate unnecessary or incorrectly denominated withdrawal requests.

  4. Users' balances in the contract remain unchanged in terms of underlying tokens, while they receive LSTs, creating a mismatch between expected and actual token holdings.

Tools Used

  • Manual code review

Recommendations

  1. Implement token swapping mechanism:
    Add a function to swap LSTs for underlying tokens within the _withdraw function.

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) {
uint256 underlyingTokens = _swapLSTForUnderlying(toWithdraw);
if (underlyingTokens < toWithdraw) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
withdrawalPool.queueWithdrawal(_account, toWithdraw - underlyingTokens);
}
token.safeTransfer(_account, underlyingTokens);
}
emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
}
function _swapLSTForUnderlying(uint256 _amount) internal returns (uint256) {
// Implement the actual swapping logic here
// This could involve interacting with a DEX or a dedicated swapping contract
// Return the amount of underlying tokens received
}
  1. Add proper checks and balances:
    Implement checks to ensure that the correct amount of underlying tokens is received after swapping.

  2. Update accounting accurately:
    Ensure that depositsSinceLastUpdate and sharesSinceLastUpdate are updated based on the actual underlying tokens withdrawn, not the LST amount.

  3. Implement slippage protection:
    Add a parameter for minimum expected underlying tokens to protect users from excessive slippage during swaps.

  4. Add events for transparency:
    Emit events that clearly show the amount of LSTs swapped and underlying tokens received.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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