Liquid Staking

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

An attacker could detect when the withdrawal queue is nearly full and immediately deposit just enough tokens to fill it.

Summary

The deposit function in the Priority Pool contract is vulnerable to front-running attacks. An attacker can monitor the state of the withdrawal queue and, by depositing just before legitimate users, fulfill the withdrawal queue with their tokens, causing legitimate deposits to be delayed or locked. This front-running attack can create a denial-of-service (DoS) condition where legitimate users are unable to receive their Liquid Staking Tokens (LST) or have their deposits queued indefinitely.

Vulnerability Details

The vulnerability lies in how the deposit function prioritizes filling queued withdrawals before depositing into the staking pool. Specifically, the following portion of the function is vulnerable:

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;
}
}
}

An attacker can exploit the interaction between the deposit and withdrawal queue as follows:

  • There are 80 LINK currently in the withdrawal queue.

  • A legitimate user, User A, wants to deposit 100 LINK into the Priority Pool.

  • According to the logic of the deposit function, if there are queued withdrawals, the first 80 LINK of User A’s deposit would go to fulfilling these queued withdrawals, and the remaining 20 LINK would be sent to the staking pool or queued for a future deposit.

  1. Monitor the Queue: The attacker monitors when the withdrawal queue is nearly full or just about to be filled. Let’s assume there are 80 LINK in the withdrawal queue.

  2. Legitimate User Transaction: A legitimate user (User A) initiates a deposit transaction for 100 LINK, intending to deposit it into the staking pool.

  3. Attacker Front-Runs: The attacker detects this transaction and immediately submits their own transaction to deposit 80 LINK, but with higher gas fees to ensure that their transaction is processed first.

  4. Attack Execution:

    • The attacker's 80 LINK deposit fulfills the withdrawal queue, and they receive the LST tokens immediately.

    • User A's transaction is processed next, but since the withdrawal queue is now empty, their deposit bypasses the withdrawal pool and is queued or deposited into the staking pool, potentially causing a delay or forcing User A’s deposit to be locked in a queue.

function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
// Check if there are queued withdrawals
if (totalQueued == 0) {
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) { // Attacker detects that there are withdrawals
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
withdrawalPool.deposit(toDepositIntoQueue); // Attacker's deposit goes here
toDeposit -= toDepositIntoQueue;
// Transfers LST to fulfill queued withdrawal
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue); // The attacker gets their LST tokens
}
if (toDeposit != 0) {
uint256 canDeposit = stakingPool.canDeposit();
if (canDeposit != 0) {
uint256 toDepositIntoPool = toDeposit <= canDeposit ? toDeposit : canDeposit;
stakingPool.deposit(_account, toDepositIntoPool, _data);
toDeposit -= toDepositIntoPool;
}
}
}
// If the user’s deposit couldn’t be queued, it will return the amount to the user
if (toDeposit != 0) {
if (_shouldQueue) {
_requireNotPaused();
if (accountIndexes[_account] == 0) {
accounts.push(_account);
accountIndexes[_account] = accounts.length - 1;
}
accountQueuedTokens[_account] += toDeposit;
totalQueued += toDeposit;
} else {
token.safeTransfer(_account, toDeposit);
}
}
}

Impact on Legitimate User

  • When User A's transaction is processed next, the withdrawal queue is now empty.

  • User A’s deposit of 100 LINK now goes directly into the staking pool, but they don’t receive LST tokens (because the pool has no queued withdrawals to fill), or they are placed in a queue if the staking pool is full.

  • User A experiences a delay in receiving their LST tokens, or in some cases, their deposit might be stuck in a queue.

Impact

Legitimate users' deposits may be significantly delayed or locked if the attacker continuously front-runs their transactions.

The attacker can repeatedly execute this strategy to keep front-running deposits and prevent legitimate users from receiving their LST tokens promptly, effectively denying users the ability to withdraw their assets.

Tools Used

Manual review

Recommendations

Implement rules to prevent deposits from being used to fulfill an excessive portion of the queue (e.g., limit each deposit's contribution to withdrawals to a percentage of the total queue).

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.