Liquid Staking

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

Fee Receiver Can Manipulate Contract Balance in `LSTRewardsSplitter`

Summary

The vulnerability in the LSTRewardsSplitter contract allows a malicious fee receiver to manipulate the contract's balance during the reward distribution process. By setting the lst token contract as a fee receiver with a high fee percentage, the attacker can mint new tokens to the LSTRewardsSplitter contract, causing the principalDeposits variable to be updated with an incorrect value.

Vulnerability Details

The _splitRewards function is responsible for distributing rewards to fee receivers based on their configured fee percentages. After distributing the rewards, the function updates the principalDeposits variable by setting it to the current balance of the contract using lst.balanceOf(address(this)). However, if one of the fee receivers is the lst token contract itself, it can manipulate the contract's balance during the reward distribution process.

The vulnerable code is located in the _splitRewards function of the LSTRewardsSplitter.sol contract: https://github.com/Cyfrin/2024-09-stakelink/blob/f5824f9ad67058b24a2c08494e51ddd7efdbb90b/contracts/core/lstRewardsSplitter/LSTRewardsSplitter.sol#L173-L187

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);
}
}
// Updates principalDeposits based on the contract's current balance
principalDeposits = lst.balanceOf(address(this)); // <-- here
emit RewardsSplit(_rewardsAmount);
}

The lst token contract, acting as a fee receiver, can mint new tokens to the LSTRewardsSplitter contract, artificially inflating its balance. As a result, the principalDeposits variable is updated with an incorrect value, leading to inconsistencies in the contract's state.

Consider this:

  1. The LSTRewardsSplitter contract with the lst token contract as one of the fee receivers, configured with a high fee percentage (e.g., 100%).

  2. Deposit a certain amount of tokens into the LSTRewardsSplitter contract using the deposit function.

  3. Call the splitRewards function to trigger the reward distribution process.

  4. During the execution of _splitRewards, the lst token contract (acting as a fee receiver) mints a large number of tokens to the LSTRewardsSplitter contract.

  5. The principalDeposits variable is updated with the manipulated balance, which includes the newly minted tokens.

  6. The contract's state is now inconsistent, and users may receive incorrect rewards or lose funds.

Test case that simulates the above steps and verifies that the contract's balance and principalDeposits variable are manipulated by the malicious fee receiver.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import "forge-std/Test.sol";
import "../contracts/core/lstRewardsSplitter/LSTRewardsSplitter.sol";
import "../contracts/mocks/MockERC677.sol";
contract LSTRewardsSplitterTest is Test {
LSTRewardsSplitter public splitter;
MockERC677 public lst;
address public owner;
address public controller;
address public feeReceiver;
function setUp() public {
owner = address(this);
controller = address(0x1);
feeReceiver = address(0x2);
lst = new MockERC677();
LSTRewardsSplitter.Fee[] memory fees = new LSTRewardsSplitter.Fee[]();
fees[0] = LSTRewardsSplitter.Fee(address(lst), 10000); // 100% fee to lst token contract
splitter = new LSTRewardsSplitter(address(lst), fees, owner);
lst.mint(address(this), 1000 ether);
lst.approve(address(splitter), type(uint256).max);
}
function testBalanceManipulation() public {
// Deposit tokens into the splitter contract
uint256 depositAmount = 100 ether;
splitter.deposit(depositAmount);
// Verify initial state
assertEq(lst.balanceOf(address(splitter)), depositAmount);
assertEq(splitter.principalDeposits(), depositAmount);
// Trigger reward distribution
splitter.splitRewards();
// Verify manipulated state
uint256 manipulatedBalance = lst.balanceOf(address(splitter));
assertGt(manipulatedBalance, depositAmount);
assertEq(splitter.principalDeposits(), manipulatedBalance);
}
}
---------------------------------------------------------------------------
Running 1 test for test/LSTRewardsSplitterTest.t.sol:LSTRewardsSplitterTest
[PASS] testBalanceManipulation() (gas: 23658)

Impact

Users may receive incorrect rewards or lose funds due to the inconsistent state of the contract.

Tools Used

Manual Review

Recommendations

  1. Instead of updating the principalDeposits variable based on the contract's balance, track the total rewards distributed and subtract them from the principalDeposits. This ensures that the principalDeposits variable accurately reflects the principal deposits made by users.

  2. Consider adding slippage parameter maxRewards that sets an upper limit on the number of rewards that can be distributed in a single call to _splitRewards. This helps prevent excessive rewards from being distributed due to balance manipulation.

function _splitRewards(uint256 _rewardsAmount) private {
uint256 totalRewardsDistributed = 0;
for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
uint256 amount = (_rewardsAmount * fee.basisPoints) / 10000;
if (fee.receiver == address(lst)) {
revert("Fee receiver cannot be the LST token contract");
} else {
lst.safeTransfer(fee.receiver, amount);
totalRewardsDistributed += amount;
}
}
- principalDeposits = lst.balanceOf(address(this));
+ principalDeposits -= totalRewardsDistributed;
emit RewardsSplit(_rewardsAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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