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

Wrong accounting in `_checkEpochRollover` will inflate the rewards

Impact

  1. Incorrect internal accounting led to insolvency

  2. Early claimants earn more rewards, while late claimants may receive none due to insufficient contract funds

Summary

_checkEpochRollover calculates the new epoch rewards before updating totalStaked, causing that value to be overinflated. Early users can claim more rewards, leaving some users unable to claim any as the contract will be emptied.

Vulnerability Details

_checkEpochRollover is called during an epoch to calculate the new rewards for each epoch. The pendingRewardsPerToken value is multiplied by the user's balance to calculate their rewards for that epoch. pendingRewardsPerToken calculation boils down to pendingRewards / totalStaked.

uint256 currentBalance = fjordToken.balanceOf(address(this));
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked) - totalStaked - newStaked - totalRewards;
// pendingRewards * 1e18 / totalStaked
uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;
totalRewards += pendingRewards;
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded] + pendingRewardsPerToken;
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}

Later, _checkEpochRollover updates totalStaked:

totalStaked += newStaked;
totalVestedStaked += newVestedStaked;
newStaked = 0;
newVestedStaked = 0;
lastEpochRewarded = currentEpoch - 1;

The vulnerability here is that totalStaked is used to calculate user rewards, but it's updated after the rewards are calculated. This means that an epoch's rewards are calculated without accounting for the new stakes from the last epoch. Essentially, if users stake in epoch X, epoch X+1 rewards are calculated based on the totalStake in X-1 (not counting withdrawals). However, that user would receive rewards for X+1 since they staked before the epoch began.

Example:

prerequisites values
Rewards per epoch 1000e18
totalStake 100e18
Current epoch 2
  1. User1 was the only staker for epoch 2, so they claim all of the rewards.

  2. User2 stakes 100e18 tokens mid-epoch, so they would be able to earn rewards from epoch 3 and onward.

  3. Epoch 3 starts, and _checkEpochRollover is triggered, calculating the new pendingRewardsPerToken to be:

pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked
=> 1000e18 * 1e18 / 100e18
=> 10e18
  1. totalStaked gets increased to 200.

  2. Epoch 3 starts with no new rewards added.

Currently, both users have stakes of 100 tokens and are able to claim 1000 reward tokens each, but there are only 1000 tokens inside the contract.

Root Cause

totalStaked is increased after the rewards are calculated.

Tools Used

Manual review.

Recommendations

Compute the new totalStaked before calculating rewards.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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