stake.link

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

A Specific Order of Queued Actions in `SecondaryPool` can Lead to Lost Ownership and Locked Funds

Summary

  • Due to a flaw in the update execution of the Secondary Pool, users can inadvertently lose ownership of their lock IDs, resulting in their staked SDL tokens being irretrievably locked within the pool. This occurs when queued full withdrawals followed by additional stakes on the same lock ID, are not processed in a manner that preserves ownership continuity.

Vulnerability Details

  • Actions that affect a user's effective reSDL balance in the Secondary Pool, such as staking, locking, withdrawing, or initiating a withdrawal, are queued for later execution rather than being applied immediately. Users may queue multiple updates to a single lock within a batch. When an update from the primary pool arrives, these queued updates are executed in the order they were requested. If updates for a lock are not processed in the correct sequence, it can lead to a state where the owner a lock id is lost and the funds remain locked in the secondary pool .

  • consider a scenario where :

  • bob owns lockId 10 with amount: 1000 sdl .

  • bob decides to withdraw his staked 1000 sdl from this lockId, which will be queued.

  • bob decides to add another 1000 sdl tokens to the same lockId(10) , this resulting in another queued action after the withdraw one.

After the CCIPController relays the update to the primary chain and it's confirmed:

  • bob invokes the executeQueuedOperations function to process her pending updates.

  • The internal function _executeQueuedLockUpdates iterates through the updates of this lockId(10):

    • The first update processes which is bob's withdrawal removes the ownership of his lockId(10) and decreases his balance balance and send him the sdl tokens .

if (baseAmountDiff < 0) {
emit Withdraw(_owner, lockId, uint256(-1 * baseAmountDiff));
if (updateLockState.amount == 0) {
delete locks[lockId];
delete lockOwners[lockId]; <<
balances[_owner] -= 1;
if (tokenApprovals[lockId] != address(0)) delete tokenApprovals[lockId];
emit Transfer(_owner, address(0), lockId);
} else {
locks[lockId].amount = updateLockState.amount;
}
sdlToken.safeTransfer(_owner, uint256(-1 * baseAmountDiff));
}
  • The second update, which adds more tokens, is processed next. Since baseAmountDiff is positive, it will updating the locks[lockId] with the new state and adjusting bob's effective and total balances. However, the ownership of lockId 10 is not restored since it was removed in the first update.

  • As a result, lockId 10 holds valid staking data but lacks an associated owner because lockOwners[10] was cleared during the withdrawal update. Consequently, the staked SDL tokens become permanently locked in the contract, and bob is unable to access his funds due to the loss of ownership. The system does not provide a way to rectify this situation and recover the locked SDL tokens.

Impact

  • the permanent loss of user funds and lock ownership within the Secondary Pool.

Tools Used

vs code
manual review

Recommendations

  • add a check to _queueLockUpdate that revert in case a full withdraw is the last update :

function _queueLockUpdate(address _owner, uint256 _lockId, uint256 _amount, uint64 _lockingDuration)
internal
onlyLockOwner(_lockId, _owner)
{
Lock memory lock = _getQueuedLockState(_lockId);
++ if(lock.amount == 0) revert("empty lock");
LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration));
queuedLockUpdates[_lockId].push(lockUpdate);
queuedRESDLSupplyChange += int256(lockUpdate.lock.amount + lockUpdate.lock.boostAmount) - int256(lock.amount + lock.boostAmount);
if (updateNeeded == 0) updateNeeded = 1;
emit QueueUpdateLock(
_owner, _lockId, lockUpdate.lock.amount, lockUpdate.lock.boostAmount, lockUpdate.lock.duration
);
}
Updates

Lead Judging Commences

0kage Lead Judge almost 2 years 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.