DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: high
Invalid

Vested-staked amount will be locked forever due to misconfiguration of checks in unstaking functions.

Summary

If any user staked any vested amount at epoch zero it will be locked forever due to unstake-vested misconfigured checks

Vulnerability Details

stakeVested() currently allows users to stake vested tokens at epoch 0 :

function stakeVested(uint256 _streamID) external checkEpochRollover redeemPendingRewards {
//CHECK
...
DepositReceipt storage dr = deposits[msg.sender][currentEpoch];
if (dr.epoch == 0) { //---------------------------ROOT----------------------//
dr.vestedStaked = _amount;
dr.epoch = currentEpoch;
_activeDeposits[msg.sender].add(currentEpoch);
} else {
dr.vestedStaked += _amount;
}
_streamIDs[msg.sender][_streamID] = NFTData({ epoch: currentEpoch, amount: _amount });
_streamIDOwners[_streamID] = msg.sender;
newStaked += _amount;
newVestedStaked += _amount;
//INTERACT
sablier.transferFrom({ from: msg.sender, to: address(this), tokenId: _streamID });
points.onStaked(msg.sender, _amount);
emit VestedStaked(msg.sender, currentEpoch, _streamID, _amount);
}

The problem lies in unstaking this vested amount using unstakeVested as this function has implemented a check that reverts on every DepositReceipt call with Epoch==0

function unstakeVested(uint256 _streamID) external checkEpochRollover redeemPendingRewards {
//CHECK
NFTData memory data = _streamIDs[msg.sender][_streamID];
DepositReceipt memory dr = deposits[msg.sender][data.epoch];
if (data.epoch == 0 || data.amount == 0 || dr.vestedStaked == 0 || dr.epoch == 0) {//--------Issue---//
revert DepositNotFound();
}
// If epoch is same as current epoch then user can unstake immediately
if (currentEpoch != data.epoch) {
// If epoch less than current epoch then user can unstake after at complete lockCycle
if (currentEpoch - data.epoch <= lockCycle) revert UnstakeEarly();
}
_unstakeVested(msg.sender, _streamID, data.amount);
}

Even in unstakeAll() there is a continue statement that simply skips the receipts with zero epoch values.

function unstakeAll()
external
checkEpochRollover
redeemPendingRewards
returns (uint256 totalStakedAmount)
{
uint256[] memory activeDeposits = getActiveDeposits(msg.sender);
if (activeDeposits.length == 0) revert NoActiveDeposit();
for (uint16 i = 0; i < activeDeposits.length; i++) {
uint16 epoch = uint16(activeDeposits[i]);
DepositReceipt storage dr = deposits[msg.sender][epoch];
if (dr.epoch == 0 || currentEpoch - epoch <= lockCycle) continue;//------------ISSUE-------//
totalStakedAmount += dr.staked;
...
}

Impact

The staked amount will be locked forever

Tools Used

Manual Review

Recommendations

Configure the checks properly

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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