Liquid Staking

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

Missing The `_totalFeesBasisPoints() > 10000` Check in `LSTRewardsSplitter::constructor()` May Lead to Principal Deposits Loss

Summary

The LSTRewardsSplitter:constructor() allows to set fees array, but it does not validate that the total sum of fees in basis points is below 10000.

Vulnerability Details

If the Owner calls LSTRewardsSplitterController:addSplitter() and mistakenly misconfigures the _fees input array (e.g., setting the total sum of basis points to more than 10000), then theLSTRewardsSplitter:_splitRewards() will begin withdrawing not only from the rewards, as expected, but also from the principal deposits to satisfy excess fees.

Impact

This can lead to unintended loss of principal deposits, which affects both the functionality and trustworthiness of the system.

PoC

Add following code to lst-rewards-splitter.test.ts:

async function newDeployFixture() {
const { accounts, signers } = await getAccounts()
const token = (await deploy('LSTMock', ['Token', 'TKN', 100000000])) as LSTMock
await setupToken(token, accounts)
const controller = (await deploy('LSTRewardsSplitterController', [
token.target,
toEther(100),
])) as LSTRewardsSplitterController
await controller.addSplitter(accounts[0], [
{ receiver: accounts[5], basisPoints: 10000 }, // <--- wrong basis points
{ receiver: accounts[6], basisPoints: 2000 },
])
await controller.addSplitter(accounts[1], [
{ receiver: accounts[7], basisPoints: 2000 },
{ receiver: accounts[8], basisPoints: 4000 },
])
const splitter0 = await ethers.getContractAt(
'LSTRewardsSplitter',
await controller.splitters(accounts[0])
)
const splitter1 = await ethers.getContractAt(
'LSTRewardsSplitter',
await controller.splitters(accounts[1])
)
return { signers, accounts, token, controller, splitter0, splitter1 }
}
it('LSTRewardsSplitter work incorrectly', async () => {
const { accounts, controller, token, splitter0 } = await loadFixture(newDeployFixture)
await token.transferAndCall(controller.target, toEther(100), '0x')
await token.transfer(splitter0.target, toEther(100))
// principal deposits before split rewards
const principalDeposits0 = await splitter0.principalDeposits()
await splitter0.splitRewards()
// principal deposits after split rewards
const principalDeposits1 = await splitter0.principalDeposits()
expect(principalDeposits1).to.be.lt(principalDeposits0)
assert.equal(fromEther(principalDeposits1), 80)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 80)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 100)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 20)
})

Tools Used

Manual review.

Recommendations

Add validation in the LSTRewardsSplitter:constructor():

constructor(address _lst, Fee[] memory _fees, address _owner) {
controller = ILSTRewardsSplitterController(msg.sender);
lst = IERC677(_lst);
for (uint256 i = 0; i < _fees.length; ++i) {
fees.push(_fees[i]);
}
+ if (_totalFeesBasisPoints() > 10000) revert FeesExceedLimit();
_transferOwnership(_owner);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.