Liquid Staking

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

Rewards and PrincipalDeposits Miscalculation Leading to Future Rewards Discrepancies

Summary

In the current implementation of the LSTRewardsSplitter::_splitRewards function, the contract incorrectly updates principalDeposits after rewards are distributed. The remaining undistributed rewards get mixed with the principalDeposits, inflating it. This miscalculation causes future reward distributions to be inaccurately computed, leaving a portion of rewards undistributed over time. The impact of this issue compounds, resulting in significant underpayment to fee receivers.

Vulnerability Detail

The vulnerability arises because the contract incorrectly adds remaining rewards to the principalDeposits after a reward distribution. Instead of properly isolating the undistributed rewards from the principalDeposits, it treats the total balance of the contract as the new principalDeposits. This behavior causes future reward calculations to underestimate the amount of rewards to be distributed, leading to ongoing reward miscalculations.

Example Scenario:

  1. Initial state:

    • Principal deposits: 1000 LST tokens.

    • New rewards: 500 LST tokens.

  2. First reward distribution:

    • Distribute 500 LST tokens:

      • Receiver A (20%) gets 100 LST.

      • Receiver B (10%) gets 50 LST.

    • Remaining contract balance after the distribution: 1350 LST (1000 principal + 350 undistributed rewards).

  3. Principal miscalculation:

    • The contract sets principalDeposits to 1350 LST, incorrectly inflating it with the remaining 350 LST (undistributed rewards).

  4. Next reward calculation:

    • New reward: 400 LST.

    • The contract’s balance becomes 1750 LST (1350 previous + 400 new).

    • Instead of calculating the actual reward to be 750 LST (1750 - 1000), it calculates only 400 LST due to the inflated principal (1750 - 1350).

Over time, this results in an increasing discrepancy between the actual rewards and what is calculated for distribution. Future reward calculations will continue to underestimate the rewards, leading to underpayment to fee receivers.

Impact

The impact of this issue is severe as it compounds over time:

  • Undistributed rewards: The contract systematically leaves rewards undistributed by treating them as part of the principal.

  • Underpayment to receivers: The fee receivers will receive less than what they are entitled to because the rewards are miscalculated.

  • Accumulative discrepancy: Each distribution cycle worsens the problem, resulting in an increasing amount of rewards being stuck in the contract.

This could cause significant financial loss over time as fewer rewards are distributed with each cycle.

Code Snippet

https://github.com/Cyfrin/2024-09-stakelink/blob/f5824f9ad67058b24a2c08494e51ddd7efdbb90b/contracts/core/lstRewardsSplitter/LSTRewardsSplitter.sol#L173

function _splitRewards(uint256 _rewardsAmount) private {
for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
uint256 amount = (_rewardsAmount * fee.basisPoints) / 10000;
if (fee.receiver == address(lst)) {
IStakingPool(address(lst)).burn(amount);
} else {
lst.safeTransfer(fee.receiver, amount);
}
}
@>>> principalDeposits = lst.balanceOf(address(this));
emit RewardsSplit(_rewardsAmount);
}

Tool used

Manual Review

Recommendation

To resolve this issue, the contract should correctly distinguish between the remaining undistributed rewards and the actual principalDeposits. The principalDeposits should remain unchanged, and only the rewards should be tracked and distributed. Here's a potential fix:

Maintain a separate variable to track the undistributed rewards instead of adding them to the principalDeposits.
Update the reward calculation logic to ensure accurate reward splits based on the actual principalDeposits and newly generated rewards.

function _splitRewards(uint256 _rewardsAmount) private {
for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
uint256 amount = (_rewardsAmount * fee.basisPoints) / 10000;
if (fee.receiver == address(lst)) {
IStakingPool(address(lst)).burn(amount);
} else {
lst.safeTransfer(fee.receiver, amount);
}
}
+ uint256 undistributedRewards = lst.balanceOf(address(this)) - principalDeposits;
+ principalDeposits = principalDeposits; // Keep the original principal intact
- principalDeposits = lst.balanceOf(address(this));
emit RewardsSplit(_rewardsAmount);
}

This would prevent future reward miscalculations and ensure that the correct amount of rewards is always distributed.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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