Liquid Staking

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

Rewards transfer to `lst` address lead to a failure in the rewards distribution mechanism in `LSTRewardsSplitter.sol`

Summary

The LSTRewardsSplitter contract has a vulnerability related to how rewards are transferred to the lst address (which represents the Liquid Staking Token). In the _splitRewards function, when the reward distribution includes a transfer to the lst address, the contract attempts to burn these tokens. However, since the contract relies on IERC677, which does not natively support a burn function, this can result in unexpected behavior or even a failure in the rewards distribution mechanism.

Vulnerability Details

The issue arises in the _splitRewards function, where the contract checks if the fee.receiver is the same as the lst address, and if so, it attempts to call the burn function on the lst contract. The problem here is that IERC677 (the interface for the lst token) does not include the burn function. Depending on the implementation of the lst token, this call may fail, revert, or even be ignored.

If the contract is deployed using an lst token that does not implement a burn function, this could lead to failed transactions or incomplete rewards distribution, making the system vulnerable to disruptions.

The problematic part of the _splitRewards function is as follows:

if (fee.receiver == address(lst)) {
IStakingPool(address(lst)).burn(amount);
} else {
lst.safeTransfer(fee.receiver, amount);
}

This logic assumes that the lst token has a burnable feature, which is not guaranteed by the IERC677 interface. Attempting to call burn on a token that doesn’t support it can cause a failure, stopping the reward distribution process and leaving the contract in an inconsistent state.

Here a test to prove vulnerability:

describe("Rewards Transfer to LST Address Vulnerability", function () {
it("Should revert if the LST contract does not support burn", async function () {
const [owner, receiver] = await ethers.getSigners();
// Mock LST token without burn function
const lst = await ethers.getContractFactory("ERC677TokenMock");
const lstInstance = await lst.deploy("Mock LST", "MLST", 1000000);
// Deploy the LSTRewardsSplitter
const lstRewardsSplitter = await ethers.getContractFactory("LSTRewardsSplitter");
const splitterInstance = await lstRewardsSplitter.deploy(lstInstance.address, [], owner.address);
// Add fee to lst address (which doesn’t support burn)
await splitterInstance.addFee(lstInstance.address, 1000); // 10% fee to LST (should trigger burn)
// Deposit tokens into the splitter contract
await lstInstance.transfer(owner.address, 1000);
await lstInstance.approve(splitterInstance.address, 1000);
await splitterInstance.deposit(1000);
// Try to split rewards
await expect(splitterInstance.splitRewards())
.to.be.revertedWith("burn function is not supported"); // Expect failure due to no burn function
});
});

Impact

  • If a portion of the rewards is supposed to go to the lst address (with the intention of being burned), the entire rewards distribution can fail, leaving all recipients without their rewards.

  • Because the rewards distribution process is central to the contract's function, failing to distribute rewards disrupts the staking process and undermines user trust in the system.

  • If the contract fails mid-operation, there may be situations where rewards are not properly distributed, leading to a financial imbalance and potential losses.

Tools Used

Manual review.

Recommendations

  • The contract should check whether the lst token supports a burn function before attempting to call it. This can be achieved by using try-catch or implementing an interface check.

if (fee.receiver == address(lst)) {
try IStakingPool(address(lst)).burn(amount) {
// Successfully burned tokens
} catch {
revert("burn function is not supported");
}
}
  • Instead of burning the tokens directly, the contract could transfer the tokens to a designated burn address, which acts as a null account (e.g., the zero address or another address designated for burning tokens).

if (fee.receiver == address(lst)) {
lst.safeTransfer(0x000000000000000000000000000000000000dEaD, amount); // Burn to the dead address
} else {
lst.safeTransfer(fee.receiver, amount);
}
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.