Liquid Staking

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

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

Summary

The stake.link protocol miscalculates the amount of LINK a user can withdraw when redeeming their stLINK tokens due to incorrect handling of the stLINK to LINK conversion in the executeQueuedWithdrawals function of the PriorityPool contract. Specifically, the protocol fails to account for the rebasing nature of stLINK, leading to inaccurate withdrawals when the pool value has fluctuated. The core of the issue lies in the use of _amount as if stLINK were equivalent to LINK, resulting in incorrect amounts being transferred, as well as incorrect burning of tokens. This issue can lead to financial losses for users in positive scenarios (where the pool’s value has increased) and financial loss for the protocol in negative scenarios (where the pool’s value has decreased).

Vulnerability Details

Code Path Leading to the Issue

The issue occurs when the protocol processes queued withdrawals via the performUpkeep function in the WithdrawalPool contract. When a user’s withdrawal is queued, the protocol will later execute these withdrawals using the executeQueuedWithdrawals function in PriorityPool. However, the conversion between stLINK and LINK is incorrectly handled during this process. Here's the detailed flow of the issue:

  1. Upkeep Execution in WithdrawalPool:

    • When upkeep is performed, the performUpkeep function in the WithdrawalPool contract is triggered. It calculates how much LINK can be withdrawn and attempts to withdraw that amount from the PriorityPool:

function performUpkeep(bytes calldata _performData) external {
uint256 canWithdraw = priorityPool.canWithdraw(address(this), 0);
uint256 totalQueued = _getStakeByShares(totalQueuedShareWithdrawals);
...
priorityPool.executeQueuedWithdrawals(toWithdraw, data);
_finalizeWithdrawals(toWithdraw);
}
  1. Incorrect Withdrawal Execution in PriorityPool:

    • The executeQueuedWithdrawals function in the PriorityPool contract is responsible for executing the queued withdrawals. This function calls stakingPool.withdraw() to process the withdrawals:

function executeQueuedWithdrawals(uint256 _amount, bytes[] calldata _data) external onlyWithdrawalPool {
IERC20Upgradeable(address(stakingPool)).safeTransferFrom(msg.sender, address(this), _amount);
@> stakingPool.withdraw(address(this), address(this), _amount, _data);
token.safeTransfer(msg.sender, _amount);
}
  • The issue is that _amount represents an amount of stLINK, but the protocol treats it as if it were LINK. This results in incorrect transfers and conversions, especially when the ratio of stLINK to LINK has changed due to rebasing (rewards or penalties).

  1. Incorrect Handling of stLINK in StakingPool's withdraw:

    • Inside the stakingPool.withdraw() function, the amount of stLINK provided is processed without correctly converting it to the corresponding amount of LINK:

function withdraw(address _account, address _receiver, uint256 _amount, bytes[] calldata _data)
external
onlyPriorityPool
{
uint256 balance = token.balanceOf(address(this));
if (_amount > balance) {
@> _withdrawLiquidity(_amount - balance, _data); // Withdraws liquidity from strategies
}
require(token.balanceOf(address(this)) >= _amount, "Not enough liquidity available to withdraw");
@> _burn(_account, _amount); // Incorrectly burns stLINK as if it were LINK
token.safeTransfer(_receiver, _amount); // Incorrect transfer based on miscalculated amounts
}
  • The _burn() function is then called with the incorrect _amount, treating the stLINK value as if it were LINK. This leads to an incorrect burning of shares:

function _burn(address _account, uint256 _amount) internal override {
@> uint256 sharesToBurn = getSharesByStake(_amount); // Incorrect conversion of stLINK to LINK
...
totalShares -= sharesToBurn;
shares[_account] -= sharesToBurn;
}
  • The conversion function getSharesByStake is incorrectly used here, resulting in burning the wrong amount of stLINK. The correct function, getStakeByShares, must be used to calculate the correct amount of LINK that corresponds to the stLINK provided.

  1. Impact of Incorrect Conversion:

    • Because of the improper conversion and handling of stLINK as if it were LINK, users receive incorrect amounts during withdrawal. In positive scenarios (when the pool value has increased), users withdraw less than they should, resulting in financial loss for users. In negative scenarios (when the pool value has decreased), users withdraw more than they should, causing financial loss for the protocol.

Proof of Concept

Let’s walk through an example of a positive scenario where the pool’s value has increased 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 protocol treats the 100 stLINK as if it were 100 LINK and transfers only 90 LINK to the user, instead of the correct 110 LINK. This results in a financial loss for the user.

Impact

  • Positive Scenario: When the pool value increases, users lose LINK due to incorrect conversion, as they receive fewer LINK tokens than they should.

  • Negative Scenario: When the pool value decreases, users withdraw more LINK than they should, resulting in financial loss for the protocol.

Tools Used

Manual code review

Recommendations

Correct Function Usage: Replace the use of getSharesByStake with getStakeByShares in both the withdraw function and the _burn function to ensure that the proper conversion between stLINK and LINK is maintained.

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.