Liquid Staking

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

Incorrect stLINK issuance in PriorityPool::deposit when fulfilling queued withdrawals, leading to users receiving more stLINK than entitled

Summary

The stake.link protocol incorrectly calculates the amount of stLINK a user should receive when depositing LINK into the WithdrawalPool. Specifically, the issue lies in the deposit function of the WithdrawalPool contract, where the protocol uses a fixed 1:1 ratio instead of using the correct conversion mechanism based on _getSharesByStake. This can lead to improper conversion between LINK and stLINK, resulting in financial losses for users when the pool's value increases or decreases.

Vulnerability Details

Code Path Leading to the Issue

When a user attempts to deposit LINK into the PriorityPool, the protocol checks for available space in the Chainlink staking contracts. If there is insufficient space, the excess tokens are either queued or allocated to satisfy queued withdrawals, triggering a call to the deposit function in the WithdrawalPool:

  1. The user initiates a deposit via the deposit function in PriorityPool:

function deposit(uint256 _amount, bool _shouldQueue, bytes[] calldata _data) external {
if (_amount == 0) revert InvalidAmount();
token.safeTransferFrom(msg.sender, address(this), _amount);
_deposit(msg.sender, _amount, _shouldQueue, _data);
}
  1. The _deposit function tries to fulfill queued withdrawals from the WithdrawalPool. If tokens are required to finalize withdrawals, the protocol transfers LINK to the WithdrawalPool and calls its deposit function:

function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
...
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
@> withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
...
}
  1. The issue arises in the WithdrawalPool::deposit function where a fixed 1:1 ratio is used to transfer stLINK tokens in exchange for deposited LINK:

function deposit(uint256 _amount) external onlyPriorityPool {
token.safeTransferFrom(msg.sender, address(this), _amount);
@> lst.safeTransfer(msg.sender, _amount); // @audit wrong _amount sent to the user
_finalizeWithdrawals(_amount);
}

The protocol transfers stLINK to the PriorityPool and afterwards to the user using a fixed 1:1 ratio, which is incorrect. Instead of calculating the correct number of stLINK tokens based on the pool’s current ratio of LINK to stLINK (which fluctuates due to rewards and slashing events), the function simply transfers an equal amount of stLINK to LINK deposited. The correct approach would involve using the _getSharesByStake function to calculate how many stLINK shares should be transferred based on the current pool ratio.

Proof of Concept

Consider a scenario where the pool has earned rewards and its value has increased:

  1. Initially, the pool contains 1000 LINK and 1000 stLINK, so 1 stLINK = 1 LINK.

  2. After rewards are distributed, the pool contains 1100 LINK but still has only 1000 stLINK, meaning 1 stLINK now equals 1.1 LINK.

  3. A user deposits 100 LINK to the pool.

Incorrect Calculation:

  • The protocol transfers 100 stLINK to the user based on the fixed 1:1 ratio.

  • However, due to the rewards, 100 LINK should entitle the user to only ~90.91 stLINK (since 1 stLINK = 1.1 LINK).

  • The user receives more stLINK than they should, leading to over-allocation and diluting other stakers' shares in the pool.

Alternatively, in a negative scenario where slashing occurs and the pool’s value decreases, the user may receive less stLINK than they should, leading to financial loss.

Impact

  • Positive Scenario (Pool Value Increases): Users receive more stLINK than they should, leading to dilution of other stakers’ shares.

  • Negative Scenario (Pool Value Decreases): Users receive less stLINK than they should, resulting in financial losses for the depositor.

Tools Used

Manual code review

Recommendations

To prevent improper conversion between LINK and stLINK, replace the fixed 1:1 ratio in the WithdrawalPool::deposit function with the _getSharesByStake function. This will ensure that the correct amount of stLINK is transferred to the user based on the current pool ratio, maintaining the integrity of the staking pool and preventing financial losses.

function deposit(uint256 _amount) external onlyPriorityPool {
token.safeTransferFrom(msg.sender, address(this), _amount);
- lst.safeTransfer(msg.sender, _amount); // @audit wrong _amount sent to the user
+ lst.safeTransfer(msg.sender, _getSharesByStake(_amount)); // Corrected calculation
_finalizeWithdrawals(_amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.