Liquid Staking

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

Insufficient Liquidity Validation in StakingPool Withdraw Function

Summary

The withdraw function in the StakingPool contract does not properly validate that the total amount of tokens withdrawn from the pool and strategies matches the requested withdrawal amount. This can lead to users receiving less than the expected amount when withdrawing from the pool if there is insufficient liquidity.

Vulnerability Details

Because the withdraw function first checks the balance of the pool, and if the requested withdrawal amount exceeds the balance, it calls the _withdrawLiquidity function to withdraw the remaining amount from the strategies. However, there is no subsequent check to ensure that the total amount withdrawn from the strategies plus the balance in the pool is sufficient to cover the requested withdrawal amount.

As a result, the function proceeds to burn the tokens from the user's staked balance and transfer the potentially insufficient amount to the receiver, even if the strategies do not have enough funds to cover the remaining withdrawal amount. StakingPool.sol# https://github.com/Cyfrin/2024-09-stakelink/blob/f5824f9ad67058b24a2c08494e51ddd7efdbb90b/contracts/core/StakingPool.sol#L157-L165

function withdraw(
address _account,
address _receiver,
uint256 _amount,
bytes[] calldata _data
) external onlyPriorityPool {
uint256 toWithdraw = _amount;
if (_amount == type(uint256).max) {
toWithdraw = balanceOf(_account);
}
uint256 balance = token.balanceOf(address(this));
if (toWithdraw > balance) {
_withdrawLiquidity(toWithdraw - balance, _data);
}
// Vuln: Missing check to ensure total withdrawn amount matches requested amount
require(
token.balanceOf(address(this)) >= toWithdraw,
"Not enough liquidity available to withdraw"
);
_burn(_account, toWithdraw);
totalStaked -= toWithdraw;
token.safeTransfer(_receiver, toWithdraw);
// @Vuln ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}
  1. Assume the pool has a balance of 100 tokens and the user has a staked balance of 200 tokens.

  2. The user attempts to withdraw 150 tokens.

  3. The withdraw function first checks the pool balance and finds that it is insufficient to cover the withdrawal (100 < 150).

  4. The function then calls _withdrawLiquidity to withdraw the remaining 50 tokens from the strategies.

  5. However, if the strategies only have 30 tokens available, the total amount withdrawn will be 130 tokens (100 from the pool + 30 from the strategies).

  6. The function proceeds to burn 150 tokens from the user's staked balance and transfers the insufficient amount of 130 tokens to the receiver.

  7. As a result, the user receives 130 tokens instead of the expected 150 tokens, experiencing a loss of 20 tokens.

Impact

If the pool frequently operates with low liquidity and the strategies do not have sufficient funds to cover withdrawal requests, the bug is more likely to manifest. However, if the pool maintains adequate liquidity and the strategies are well-funded, the chances of encountering this bug may be lower.

Tools Used

Vs Code

Recommendations

function withdraw(
address _account,
address _receiver,
uint256 _amount,
bytes[] calldata _data
) external onlyPriorityPool {
uint256 toWithdraw = _amount;
if (_amount == type(uint256).max) {
toWithdraw = balanceOf(_account);
}
uint256 balance = token.balanceOf(address(this));
if (toWithdraw > balance) {
_withdrawLiquidity(toWithdraw - balance, _data);
}
+ uint256 totalWithdrawn = token.balanceOf(address(this));
+ require(
+ totalWithdrawn >= toWithdraw,
+ "Insufficient liquidity to fulfill withdrawal request"
+ );
_burn(_account, toWithdraw);
totalStaked -= toWithdraw;
token.safeTransfer(_receiver, toWithdraw);
}
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.