Liquid Staking

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

[M-1] Significant amount of LSTRewardsSplitter fees causes DoS and prevents from calling any function that uses the fees array - including splitting rewards

Summary

LSTRewardsSplitter contract allows to split the staking rewards between multiple accounts based on a list of customisable fees. The LSTRewardsSplitter::_splitRewards function invoked by an external LSTRewardsSplitter::splitRewards or LSTRewardsSplitter::performUpkeep function loops through the list of fees and their receivers splitting and sending the rewards accordingly. These loops can cause a Denial of Service.

Vulnerability Details

One of the loops in LSTRewardsSplitter:

function _splitRewards(uint256 _rewardsAmount) private {
>> for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
...
}
...
}

Due to this loop, every fee added increases the gas/time cost of any function that loops through the fees array at some point effectively preventing normal usage due to a huge price of the transaction.

Due to the a constraint of maximum 10000 BIPs per splitter, this array can have that many elements in the worst case scenario.

Impact

If the LSTRewardsSplitter owner adds a significant amount of fees (with either bad intentions or not - depending on the aggreement with other account holders), each additional fee increases the processing time and gas cost of splitting rewards transaction which results in diminishing returns from the staking itself, subsequently causing loses for the stakers and in the end preventing normal usage of the splitter.

The affected external functions are:

  • LSTRewardsSplitter::performUpkeep

  • LSTRewardsSplitter::splitRewards

  • LSTRewardsSplitter::addFee

  • LSTRewardsSplitter::updateFee

Proof of Concept

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

async function auditFixture() {
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: 1000 },
{ receiver: accounts[6], basisPoints: 2000 }
])
await controller.addSplitter(accounts[1], [
{ receiver: accounts[7], basisPoints: 1 },
{ receiver: accounts[8], basisPoints: 1 },
])
const splitter = await ethers.getContractAt(
'LSTRewardsSplitter',
await controller.splitters(accounts[0])
)
const splitterMax = await ethers.getContractAt(
'LSTRewardsSplitter',
await controller.splitters(accounts[1])
)
return { signers, accounts, token, controller, splitter, splitterMax }
}
it('significant amount of fees should work correctly', async () => {
const { signers, accounts, token, controller, splitter, splitterMax } = await loadFixture(
auditFixture
)
await token.transferAndCall(controller.target, toEther(100), '0x')
await token.transfer(splitter.target, toEther(100))
await token.transfer(splitterMax.target, toEther(100))
for (let i = 0; i < 1998; i++) {
await splitterMax.addFee(accounts[7], 1)
}
let startTime2Fees = performance.now()
const tx = await splitter.splitRewards()
let endTime2Fees = performance.now()
let startTime2000Fees = performance.now()
const txMax = await splitterMax.splitRewards()
let endTime2000Fees = performance.now()
console.log('2 fees gas used: ', (await tx.wait())?.cumulativeGasUsed)
console.log('2000 fees gas used: ', (await txMax.wait())?.cumulativeGasUsed)
console.log(`Call to splitRewards with 2 fees took ${endTime2Fees - startTime2Fees} milliseconds`)
console.log(`Call to splitRewards with 2000 fees took ${endTime2000Fees - startTime2000Fees} milliseconds`)
}).timeout(1000000)

The above test outputs:

2 fees gas used: 107472n
2000 fees gas used: 20296789n
Call to splitRewards with 2 fees took 3.9930940000049304 milliseconds
Call to splitRewards with 2000 fees took 509.8611949999904 milliseconds

which shows the significantly increased cost and processing time of splitting rewards transaction.

Tools Used

Manual investigation
Hardhat tests

Recommendations

Consider adding a constraint on maximum number of fees or minimum number of BIPs per fee or a combination of both of these per splitter so the time/gas cost remains at reasonable levels even in worst case scenario.

Updates

Lead Judging Commences

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