stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Invalid

SDLPoolPrimary::After unlocking lockId is not removed which enables a user to lock SDL for lifetime and get reward for lifetime

Summary

A user can lock their SDL for lifetime and she can get rewards for lifetime.

Vulnerability Details

Here is the problematic function:
https://github.com/Cyfrin/2023-12-stake-link/blob/549b2b8c4a5b841686fceb9c311dca9ac58225df/contracts/core/sdlPool/SDLPoolPrimary.sol#L107
After initiating unlock the lockId is not removed from the locks mapping, only just expiry and boostAmount is updated. As a result the lock position stays forever in the protocol even after maxLockingDuration & the effectiveBalance remains same.

POC

Run this test:

it('locks struct is not updated for that id after unlocking', async () => { //@audit-issue submitted
await sdlToken.transferAndCall(
sdlPool.address,
toEther(10000),
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY])
)
let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp
await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x')
await time.increase(200 * DAY)
console.log('withdrawable rewards before unlocking:', fromEther(await rewardsPool.withdrawableRewards(accounts[0])))
await sdlPool.initiateUnlock(1) //@audit we are initiating unlocking
console.log('withdrawable rewards after unlocking:', fromEther(await rewardsPool.withdrawableRewards(accounts[0])))
let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp
await time.increase(1000 * DAY) //@audit after 1000 days the locked position still present
//@audit check if lock id is present in system or not
assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [
{
amount: 10000,
boostAmount: 0,
startTime: ts1,
duration: 365 * DAY,
expiry: ts2 + (365 / 2) * DAY,
},
])
await time.increase(100000 * DAY) // @audit after 100000 days the locked position still present
expect(parseLocks(await sdlPool.getLocks([1]))[0].duration).to.be.greaterThan(0) //@audit if locking duration is not 0 then will it get reward??
assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 10000) //@audit still having effective balance
assert.equal(fromEther(await rewardsPool.withdrawableRewards(accounts[0])), 1000) //@audit you can see the withdrawable reward
console.log("effective balance is: ", fromEther(await sdlPool.effectiveBalanceOf(accounts[0])))
console.log('withdrawable rewards :', fromEther(await rewardsPool.withdrawableRewards(accounts[0])))
})

Impact

The position will remain locked for lifetime as it is not removed while unlocking & as the effectiveBalance is not changed the user will keep getting rewards until the protocol is in production.

Tools Used

Manual analysis.

Recommendations

Remove the lockId from locks mapping by delete locks[lockId] while initiating unlock phase.

Updates

Lead Judging Commences

0kage Lead Judge over 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.