stake.link

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

If a user queues too many lock updates, he will have his lock blocked forever

Summary

There is no limit for queueing lock updates in SDLPoolSecondary. As a result, a user can get his lock blocked forever if he queues too many updates in the same indexBatch

Vulnerability Details

In SDLPoolSecondary we can see that there is a limit for queuing new locks per user.

function _queueNewLock(
address _owner,
uint256 _amount,
uint64 _lockingDuration
) internal {
if (newLocksByOwner[_owner].length >= queuedNewLockLimit) revert TooManyQueuedLocks(); <-
Lock memory lock = _createLock(_amount, _lockingDuration);
queuedNewLocks[updateBatchIndex].push(lock);
newLocksByOwner[_owner].push(NewLockPointer(updateBatchIndex, uint128(queuedNewLocks[updateBatchIndex].length - 1)));
queuedRESDLSupplyChange += int256(lock.amount + lock.boostAmount);
if (updateNeeded == 0) updateNeeded = 1;
emit QueueCreateLock(_owner, _amount, lock.boostAmount, _lockingDuration);
}

However, there is no limit for queueing lock updates. That means that a user unconsiously can queue too many lock updates in the same index batch and get his lock blocked.

When queueing lock updates we can see that there is no check for a limit

function _queueLockUpdate(
address _owner,
uint256 _lockId,
uint256 _amount,
uint64 _lockingDuration
) internal onlyLockOwner(_lockId, _owner) {
Lock memory lock = _getQueuedLockState(_lockId);
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);
}

Once a user has queued too many updates, when the keeper send it to the primary chain and he is allowed to execute the updates, he will run out of gas because the function will loop through the whole array

function _executeQueuedLockUpdates(address _owner, uint256[] memory _lockIds) internal updateRewards(_owner) {
uint256 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); // batchIndex that has been sent to the L1
for (uint256 i = 0; i < _lockIds.length; ++i) {
uint256 lockId = _lockIds[i];
_onlyLockOwner(lockId, _owner);
uint256 numUpdates = queuedLockUpdates[lockId].length;
Lock memory curLockState = locks[lockId];
uint256 j = 0;
while (j < numUpdates) {
...
// Here are all the computations that are performed with each update
...
++j;
}
for (uint256 k = 0; k < numUpdates; ++k) {
if (j == numUpdates) {
queuedLockUpdates[lockId].pop();
} else {
queuedLockUpdates[lockId][k] = queuedLockUpdates[lockId][j];
++j;
}
}
}
}

If this scenario is reached, the user will not be able ever again to change any state of his lock. He will receive the rewards that his lock generates but will not be able to withdraw his funds.

Impact

Medium

Tools Used

Manual review

Recommendations

Add a limit for lock updates in the same index batch. Just as queueing new locks works.

+ uint256 public queuedUpdatesLimit;
function initialize(
string memory _name,
string memory _symbol,
address _sdlToken,
address _boostController,
uint256 _queuedNewLockLimit,
+ uint256 _queuedUpdatesLimit
) public initializer {
__SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController);
updateBatchIndex = 1;
currentMintLockIdByBatch.push(0);
queuedNewLocks.push();
queuedNewLocks.push();
queuedNewLockLimit = _queuedNewLockLimit;
+ queuedUpdatesLimit = _queuedUpdatesLimit;
}
function _queueLockUpdate(
address _owner,
uint256 _lockId,
uint256 _amount,
uint64 _lockingDuration
) internal onlyLockOwner(_lockId, _owner) {
+ uint256 updates;
+ for(uint256 i; i < queuedLockUpdates[_lockId].length; ++i){
+ if (queuedLockUpdates[lockId][i].updateBatchIndex > finalizedBatchIndex) break;
+ ++updates;
+ }
+ if (updates >= queuedUpdatesLimit) revert TooManyQueuedUpdates();
Lock memory lock = _getQueuedLockState(_lockId);
LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration)); // returns lock with _amount increased and lockingDuration as new duration
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 over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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