Liquid Staking

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

Critical Vulnerability in LSTRewardsSplitter::performUpkeep Function Allows Incorrect Reward Calculation and Potential Exploitation

Summary

A bug was identified in the performUpkeep function IN LSTRewardsSplitter.sol. The function does not correctly update the principalDeposits after a large rewards transfer, leading to incorrect balances and failed assertions in the unit tests.

Vulnerability Details

The performUpkeep function is expected to update the principalDeposits correctly after processing rewards. However, the function fails to do so, resulting in incorrect values for principalDeposits and token balances. This discrepancy was observed during the unit tests, where the expected value of principalDeposits did not match the actual value.

Affected code:
https://github.com/Cyfrin/2024-09-stakelink/blob/f5824f9ad67058b24a2c08494e51ddd7efdbb90b/contracts/core/lstRewardsSplitter/LSTRewardsSplitter.sol#L98C2-L110C6

POC

  • Copy the poc into the existing test test/core/lst-rewards-splitter.test.ts

it('performUpkeep should work correctly2', async () => {
const { signers, accounts, controller, token, splitter0, splitter1 } = await loadFixture(deployFixture)
await token.transferAndCall(controller.target, toEther(100), '0x')
await token.connect(signers[1]).transferAndCall(controller.target, toEther(200), '0x')
await token.transfer(splitter0.target, toEther(100))
await token.transfer(splitter1.target, toEther(80))
// Edge Case 1: No rewards
await expect(
controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[false, false]])
)
).to.be.revertedWithCustomError(controller, 'InvalidPerformData()')
// Edge Case 2: Rewards below threshold
await expect(
controller.performUpkeep(ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]]))
).to.be.revertedWithCustomError(splitter0, 'InsufficientRewards()')
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, false]])
)
assert.equal(fromEther(await splitter0.principalDeposits()), 170)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 170)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 10)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 20)
assert.equal(fromEther(await splitter1.principalDeposits()), 200)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 280)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 0)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 0)
await token.transfer(splitter0.target, toEther(200))
await token.transfer(splitter1.target, toEther(20))
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]])
)
assert.equal(fromEther(await splitter0.principalDeposits()), 310)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 310)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 30)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 60)
assert.equal(fromEther(await splitter1.principalDeposits()), 240)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 240)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 20)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 40)
// @audit Edge Case 3: Rewards exactly at threshold
await token.setMultiplierBasisPoints(1000)
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]])
)
assert.equal(fromEther(await splitter0.principalDeposits()), 31)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 31)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 3)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 6)
assert.equal(fromEther(await splitter1.principalDeposits()), 24)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 24)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 2)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 4)
// @audit Edge Case 4: Large rewards
console.log('Before large rewards transfer:')
console.log('splitter0 principalDeposits:', fromEther(await splitter0.principalDeposits()))
console.log('splitter0 balance:', fromEther(await token.balanceOf(splitter0.target)))
console.log('splitter1 principalDeposits:', fromEther(await splitter1.principalDeposits()))
console.log('splitter1 balance:', fromEther(await token.balanceOf(splitter1.target)))
await token.transfer(splitter0.target, toEther(1000))
await token.transfer(splitter1.target, toEther(1000))
console.log('Before performUpkeep:')
console.log('splitter0 principalDeposits:', fromEther(await splitter0.principalDeposits()))
console.log('splitter0 balance:', fromEther(await token.balanceOf(splitter0.target)))
console.log('splitter1 principalDeposits:', fromEther(await splitter1.principalDeposits()))
console.log('splitter1 balance:', fromEther(await token.balanceOf(splitter1.target)))
console.log('controller balance:', fromEther(await token.balanceOf(controller.target)))
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]])
)
console.log('After performUpkeep:')
console.log('splitter0 principalDeposits:', fromEther(await splitter0.principalDeposits()))
console.log('splitter0 balance:', fromEther(await token.balanceOf(splitter0.target)))
console.log('splitter1 principalDeposits:', fromEther(await splitter1.principalDeposits()))
console.log('splitter1 balance:', fromEther(await token.balanceOf(splitter1.target)))
console.log('controller balance:', fromEther(await token.balanceOf(controller.target)))
assert.equal(fromEther(await splitter0.principalDeposits()), 1031) // Corrected assertion
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 1031)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 130)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 260)
assert.equal(fromEther(await splitter1.principalDeposits()), 1024)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 1024)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 120)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 240)
// @audit Edge Case 5: Rewards below threshold after changing threshold
await token.setMultiplierBasisPoints(1000)
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]])
)
assert.equal(fromEther(await splitter0.principalDeposits()), 131)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 131)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 13)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 26)
assert.equal(fromEther(await splitter1.principalDeposits()), 124)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 124)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 12)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 24)
// @audit Edge Case 6: Negative rewards
await token.transfer(splitter0.target, toEther(100))
await token.transfer(splitter1.target, toEther(100))
await controller.performUpkeep(
ethers.AbiCoder.defaultAbiCoder().encode(['bool[]'], [[true, true]])
)
assert.equal(fromEther(await splitter0.principalDeposits()), 231)
assert.equal(fromEther(await token.balanceOf(splitter0.target)), 231)
assert.equal(fromEther(await token.balanceOf(accounts[5])), 23)
assert.equal(fromEther(await token.balanceOf(accounts[6])), 46)
assert.equal(fromEther(await splitter1.principalDeposits()), 224)
assert.equal(fromEther(await token.balanceOf(splitter1.target)), 224)
assert.equal(fromEther(await token.balanceOf(accounts[7])), 22)
assert.equal(fromEther(await token.balanceOf(accounts[8])), 44)
})
  • Output:

Before large rewards transfer:
splitter0 principalDeposits: 31
splitter0 balance: 31
splitter1 principalDeposits: 24
splitter1 balance: 24
Before performUpkeep:
splitter0 principalDeposits: 31
splitter0 balance: 131
splitter1 principalDeposits: 24
splitter1 balance: 124
controller balance: 0
After performUpkeep:
splitter0 principalDeposits: 128
splitter0 balance: 128
splitter1 principalDeposits: 118
splitter1 balance: 118
controller balance: 0

The expected value for splitter0.principalDeposits() was 1031, but the actual value was 128. This indicates that the performUpkeep function did not correctly update the principalDeposits.

Impact

The incorrect update of principalDeposits can lead to inaccurate accounting of rewards and balances, potentially causing financial discrepancies and affecting the integrity of the staking mechanism. This bug can undermine user trust and lead to financial losses if not addressed promptly.

Tools Used

  • Hardhat

Recommendations

Ensure that the performUpkeep function correctly calculates and updates the principalDeposits based on the new rewards.

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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