stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: medium
Valid

Secondary pool could lock the funds of users forever and return an incorrect value of `balanceOf()`

Summary

The secondary pool allows users to add more SDL tokens to an already removed lock. This could result in funds being indefinitely locked in the pool and an incorrect return value for balanceOf().

Vulnerability Details

In the SDLPoolSecondary contract, all actions must be queued before execution. When a user fully withdraws from a lock, this does not immediately update the owner information, lock details, or reSDL balance. Instead, these updates are queued for later execution.

Simultaneously, if a user deposits more SDL tokens into an existing lock, checks are performed for the current lock owner. However, because the owner and lock information updates are only queued and not carried out as described before, these checks still pass. This allows the user to add more SDL tokens into a lock that has, in fact, been fully withdrawn and will be deleted.

In the end, when both actions are executed in the function _executeQueuedLockUpdates(), the user’s lock is deleted, but totalEffectiveBalance and effectiveBalances[owner] are still increased.

Consequently, the user cannot withdraw or claim rewards from this lock because lockOwners[lockId] has been deleted. Rewards continue being allocated to this lock, but they cannot be claimed.

The PoC is modified from the test should be able to withdraw and burn lock NFT in sdl-pool-secondary.test.ts.

it.only('should be able to withdraw and burn lock NFT', async () => {
let ts1 = await mintLock()
await time.increase(200 * DAY)
await sdlPool.initiateUnlock(1)
let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp
await time.increase(200 * DAY)
let startingBalance = await sdlToken.balanceOf(accounts[0])
await sdlPool.withdraw(1, toEther(20))
assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), -120)
await updateLocks()
assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0)
assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startingBalance)), 20)
assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 80)
assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 80)
assert.equal(fromEther(await sdlPool.totalStaked()), 80)
assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 80)
assert.equal(fromEther(await sdlPool.staked(accounts[0])), 80)
assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [
{
amount: 80,
boostAmount: 0,
startTime: ts1,
duration: 365 * DAY,
expiry: ts2 + (365 / 2) * DAY,
},
])
startingBalance = await sdlToken.balanceOf(accounts[0])
await sdlPool.withdraw(1, toEther(80))
///////////////////////////////
// MODIFIED FROM HERE
///////////////////////////////
await sdlToken.transferAndCall(
sdlPool.address,
toEther(100),
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0])
)
await updateLocks()
// @audit Even though `totalEffectiveBalance` and `effectiveBalanceOf(user)` are 100
// The `balanceOf(user)` return 0
assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 100)
assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), toEther(100))
assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 0)
await expect(sdlPool.ownerOf(1)).to.be.revertedWith('InvalidLockId()')
})

Impact

This issue has three main impacts:

  1. SDL tokens are locked, but the lock owner is removed, meaning no one can access or withdraw them. This results in the tokens being locked in the pool forever.

  2. Although no one can access the lock, the totalEffectiveBalance still accounts for the amount of locked SDL tokens. This means rewards continue to be distributed to this lock, but no one can claim them, leading to a loss in rewards.

  3. The balanceOf() function will return incorrect results when queried.

Recommendations

Actions on a fully withdrawn lock in the secondary pool should be disallowed.

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

add-to-old-lock

User trying to update a fully withdrawn lock in same batch id on secondary pool

Support

FAQs

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