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

Inflated rewards due to double counting of vested stakes

Summary

The reward calculation in the FjordStaking::_checkEpochRollover incorrectly includes vested stakes twice, leading to an overestimation of pending rewards.

Vulnerability Details

The reward calculation in the FjordStaking::_checkEpochRollover incorrectly includes vested stakes twice (once directly through totalVestedStaked and once indirectly through totalStaked), leading to an overestimation of pending rewards.

This results in users receiving substantially more rewards than they should, potentially depleting the contract's funds faster than intended.

FjordStaking.sol#L702-L703

function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
@> uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked)
@> - totalStaked - newStaked - totalRewards;
uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;
// existing code...
}
// existing code...
}
// existing code...

Impact

This bug significantly inflates the calculated pending rewards, leading to overpayment of rewards to users, potentially draining the contract's funds faster than intended.

Proof of Concept

Imagine a scenario where:

  1. On the plus side:

    • +Contract balance: 100,000 FJORD tokens

    • +Total vested staked: 70,000 FJORD

    • +New vested staked this epoch: 3,000 FJORD

  2. On the minus side:

    • -Total staked (including vested): 90,000 FJORD (20,000 regular + 70,000 vested)

    • -New staked this epoch: 5,000 FJORD

    • -Total rewards distributed so far: 2,000 FJORD

pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked) - totalStaked - newStaked - totalRewards
= (100,000 + 70,000 + 3,000) - 90,000 - 5,000 - 2,000
= 173,000 - 97,000
= 76,000 FJORD

This shows that the current implementation calculates 76,000 FJORD in pending rewards, which is almost the entire balance of the contract.

Tools Used

Manual review

Recommendations

Since totalStaked and newStaked represent all staked tokens, we should avoid adding totalVestedStaked and newVestedStaked to the pending rewards calculation:

function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
- uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked)
- - totalStaked - newStaked - totalRewards;
+ uint256 pendingRewards = currentBalance - totalStaked - newStaked - totalRewards;
uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;
// existing code...
}
// existing code...
}
// existing code...

With this, the calculation comes out to:

pendingRewards = currentBalance - totalStaked - newStaked - totalRewards
= 100,000 - 90,000 - 5,000 - 2,000 = 3,000 FJORD

which is a much more realistic reward per a single epoch.

Updates

Lead Judging Commences

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

Support

FAQs

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