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

Unbounded Loop in ```FjordStaking::unstakeAll``` leading to gas exhaustion

Summary

The FjordStaking::unstakeAll iterates over the entire activeDeposits array without an upper bound. This leads to gas exhaustion if the array is very large, causing the transaction to fail.

Link: https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordStaking.sol#L570C5-L607C6

Vulnerability Details

The for loop in the unstakeAll function does not have an upper bound, which means it will attempt to process all active deposits in a single transaction. If a user has a large number of active deposits, the gas required to execute the function may exceed the block gas limit, resulting in a failed transaction.

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;
totalStakedAmount += dr.staked;
// no vested staked and stake is 0 then delete the deposit
if (dr.vestedStaked == 0) {
delete deposits[msg.sender][epoch];
_activeDeposits[msg.sender].remove(epoch);
} else {
// still have vested staked, then only delete the staked
dr.staked = 0;
}
}
totalStaked -= totalStakedAmount;
userData[msg.sender].totalStaked -= totalStakedAmount;
fjordToken.transfer(msg.sender, totalStakedAmount);
points.onUnstaked(msg.sender, totalStakedAmount);
// emit event
emit UnstakedAll(
msg.sender, totalStakedAmount, activeDeposits, getActiveDeposits(msg.sender)
);
}

Impact

Add this test in the unstakeAll.t.sol and

run: forge test --match-test test_Unstake_All_GasExaustion -vv

function test_Unstake_All_GasExaustion() public {
uint256 initialGas = gasleft();
//Deposit 195 times
for (uint256 i = 0; i < 195; i++) {
_addRewardAndEpochRollover(2 ether, 1);
assertEq(fjordStaking.currentEpoch(), i + 2);
vm.prank(alice);
fjordStaking.stake(1 ether);
}
_addRewardAndEpochRollover(2 ether, 7);
// unstake all epoch position deposits
vm.prank(alice);
fjordStaking.unstakeAll();
uint256 finalGas = gasleft();
uint256 gasUsed = initialGas - finalGas;
console2.log("gasUsed", gasUsed);
uint256 GAS_LIMIT = 30_000_000;
assert(gasUsed > GAS_LIMIT);
}
Ran 1 test for test/unit/unstakeAll.t.sol:UnstakeAll_Unit_Test
[PASS] test_Unstake_All_GasExaustion() (gas: 24340800)
Logs:
gasUsed 30427659
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 629.64ms (42.46ms CPU time)

The test shows that for an active deposit array equal to 195 the block gas limit is passed.

Users with a large number of active deposits are unable to unstake their tokens due to gas exhaustion (Dos).

Tools Used

Manual review

Recommendations

Implement an upper limit to handle unstaking for the large deposits.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

kiteweb3 Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
kiteweb3 Submitter
about 1 year ago
inallhonesty Lead Judge about 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.