Liquid Staking

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

Incorrect calculation of stLINK to LINK conversion in `queueWithdrawal` function leading to financial loss

Summary

The stake.link protocol incorrectly calculates the amount of LINK a user can withdraw when redeeming their stLINK tokens. Specifically, the issue lies in the queueWithdrawal function of the WithdrawalPool contract, where the protocol uses _getSharesByStake instead of the correct function, _getStakeByShares. This leads to an improper conversion between stLINK and LINK, causing financial losses for users when the pool's value increases and for the protocol when the pool's value decreases.

Vulnerability Details

Code Path Leading to the Issue

When a user attempts to withdraw LINK from the PriorityPool, they initiate a transaction by calling the withdraw function:

  1. The user sends a withdrawal request to the PriorityPool.

  2. If there are insufficient tokens in the queue to satisfy the request, the protocol transfers stLINK tokens from the user to the contract with the following line:

if (toWithdraw != 0) {
IERC20Upgradeable(address(stakingPool)).safeTransferFrom(account, address(this), toWithdraw);
toWithdraw = _withdraw(account, toWithdraw, _shouldQueueWithdrawal);
}
  1. After the stLINK transfer, the _withdraw function is called:

function _withdraw(
address _account,
uint256 _amount,
bool _shouldQueueWithdrawal
) internal returns (uint256) {
...
if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
withdrawalPool.queueWithdrawal(_account, toWithdraw);
}
emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
}
  1. If the PriorityPool cannot satisfy the withdrawal, the queueWithdrawal function of the WithdrawalPool contract is called to queue the withdrawal for later processing. The issue arises here due to the incorrect conversion function being used:

function queueWithdrawal(address _account, uint256 _amount) external onlyPriorityPool {
...
uint256 sharesAmount = _getSharesByStake(_amount); // Incorrect function
totalQueuedShareWithdrawals += sharesAmount;
...
}

The queueWithdrawal function uses _getSharesByStake, which converts LINK to stLINK. However, since the _amount parameter represents stLINK, the correct function should be _getStakeByShares to convert stLINK back to the correct amount of LINK.

Proof of Concept (PoC)

Let's walk through a positive scenario where the pool has increased in value due to rewards:

  1. The pool initially contains 1000 LINK and 1000 stLINK (1 stLINK = 1 LINK).

  2. Due to rewards, the pool's value increases to 1100 LINK, but the stLINK amount remains at 1000 (1 stLINK = 1.1 LINK).

  3. A user attempts to withdraw 100 stLINK. Based on the updated ratio, they should receive 110 LINK.

Incorrect Calculation:

  • The user sends 100 stLINK for withdrawal.

  • The protocol calls the queueWithdrawal function and incorrectly applies _getSharesByStake.

  • Because of the fact that we use _getSharesByStake, which is designed to convert a given amount of LINK (the stake) into stLINK (the shares). However, in this scenario, the user is already providing stLINK and the goal is to calculate how much LINK the user can withdraw against their stLINK. Instead of correctly calculating the equivalent amount of LINK the user is entitled to, the protocol incorrectly tries to determine how many stLINK shares it would need to match 100 LINK of stake.

    Due to the rewards, the pool has grown, and now 1 stLINK equals 1.1 LINK, the protocol calculates that 90 stLINK corresponds to 100 LINK and as a result, the user is only allowed to withdraw 90 LINK, even though they should receive 110 LINK based on the correct ratio.

    This incorrect logic stems from using the wrong function, which calculates the shares (stLINK) you get for a given stake (LINK), when it must be calculating the amount of stake (LINK) the user gets for their shares (stLINK) which can be done with _getStakeByShares function. Therefore, instead of receiving the correct amount of 110 LINK, the user only receives 90 LINK, leading to a financial loss.

Impact

  • In a positive scenario (increased pool value), users lose LINK due to incorrect conversion.

  • In a negative scenario (decreased pool value), users withdraw more LINK than they should, causing financial losses for the protocol.

Tools Used

Manual code review

Recommendations

To prevent financial loss, replace the use of _getSharesByStake with _getStakeByShares in the queueWithdrawal function. This ensures the correct conversion from stLINK to LINK during withdrawals, maintaining the proper ratio between shares and the underlying LINK in the pool.

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.