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.
When a user attempts to withdraw LINK from the PriorityPool, they initiate a transaction by calling the withdraw function:
The user sends a withdrawal request to the PriorityPool.
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:
After the stLINK transfer, the _withdraw function is called:
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:
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.
Let's walk through a positive scenario where the pool has increased in value due to rewards:
The pool initially contains 1000 LINK and 1000 stLINK (1 stLINK = 1 LINK).
Due to rewards, the pool's value increases to 1100 LINK, but the stLINK amount remains at 1000 (1 stLINK = 1.1 LINK).
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.
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.
Manual code review
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.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.