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

Users will receive incorrect rewards due to linear reward calculation in `FjordStaking::calculateReward`

Summary

The calculateReward() function in the FjordStaking contract assumes a linear increase in rewards between epochs, leading to inaccurate reward distributions when reward rates or staked amounts change significantly between epochs.

Vulnerability Details

The FjordStaking contract is designed to distribute rewards to users who stake tokens. The reward calculation mechanism is primarily handled by the calculateReward() function, which is called within the _redeem() function to update users' unclaimed rewards.

The calculateReward() function calculates rewards based on the difference in rewardPerToken between two epochs:

File: FjordStaking.sol
775: function calculateReward(uint256 _amount, uint16 _fromEpoch, uint16 _toEpoch)
776: internal
777: view
778: returns (uint256 rewardAmount)
779: {
780: rewardAmount =
781: (_amount * (rewardPerToken[_toEpoch] - rewardPerToken[_fromEpoch])) / PRECISION_18;
782: }
783:

This implementation assumes a linear increase in rewards between _fromEpoch and _toEpoch. However, this assumption can lead to significant inaccuracies if the reward rates or staked amounts change non-linearly between these epochs.

The _redeem() function uses calculateReward() to update unclaimedRewards for a user:

function _redeem(address sender) internal {
UserData storage ud = userData[sender];
ud.unclaimedRewards +=
calculateReward(ud.totalStaked, ud.lastClaimedEpoch, currentEpoch - 1);
ud.lastClaimedEpoch = currentEpoch - 1;
if (ud.unredeemedEpoch > 0 && ud.unredeemedEpoch < currentEpoch) {
DepositReceipt memory deposit = deposits[sender][ud.unredeemedEpoch];
ud.unclaimedRewards += calculateReward(
deposit.staked + deposit.vestedStaked, ud.unredeemedEpoch, currentEpoch - 1
);
ud.unredeemedEpoch = 0;
ud.totalStaked += (deposit.staked + deposit.vestedStaked);
}
}

The issue arises when a user does not claim rewards for multiple epochs, and there are significant changes in reward rates or staked amounts during this period. The linear calculation will not accurately reflect the actual rewards earned during each individual epoch.

Impact

The incorrect reward calculation can lead to an uneven distribution of rewards among stakers. Users who are aware of this issue could gain an advantage over those who are not, leading to potential manipulation of the reward system. This undermines the intended incentive structure and can result in users receiving fewer rewards than they should.

Proof of Concept

Consider the following scenario:

  1. Alice stakes 100 tokens in epoch 1.

  2. The reward rates for epochs 1, 2, and 3 are 10, 20, and 30 tokens respectively.

  3. Alice doesn't claim her rewards until the end of epoch 3.

  4. The calculateReward() function is called in _redeem():

rewardAmount = (100 * (rewardPerToken[3] - rewardPerToken[1])) / PRECISION_18;
  1. Assuming rewardPerToken[1] = 10, rewardPerToken[2] = 30, and rewardPerToken[3] = 60:

rewardAmount = (100 * (60 - 10)) / PRECISION_18 = 5000 / PRECISION_18
  1. However, the correct calculation should be:

epoch1Reward = (100 * 10) / PRECISION_18
epoch2Reward = (100 * 20) / PRECISION_18
epoch3Reward = (100 * 30) / PRECISION_18
totalReward = epoch1Reward + epoch2Reward + epoch3Reward = 6000 / PRECISION_18

In this scenario, Alice receives 5000 / PRECISION_18 tokens instead of the 6000 / PRECISION_18 tokens she should have received, resulting in a loss of 1000 / PRECISION_18 tokens.

Tools Used

Manual review

Recommendation

To address this issue, the calculateReward() function should be modified to calculate rewards on a per-epoch basis. Here's a suggested fix:

function calculateReward(address user, uint16 _fromEpoch, uint16 _toEpoch)
internal
view
returns (uint256 rewardAmount)
{
rewardAmount = 0;
for (uint16 epoch = _fromEpoch; epoch < _toEpoch; epoch++) {
uint256 stakedInEpoch = getStakedAmountForEpoch(user, epoch);
uint256 rewardRateForEpoch = getRewardRateForEpoch(epoch);
rewardAmount += (stakedInEpoch * rewardRateForEpoch) / PRECISION_18;
}
}

This modification requires implementing two new functions:

  1. getStakedAmountForEpoch(address user, uint16 epoch) to retrieve the user's staked amount for a specific epoch.

  2. getRewardRateForEpoch(uint16 epoch) to get the reward rate for a specific epoch.

Additionally, the contract should be updated to maintain historical data of staked amounts and reward rates for each epoch. This will ensure accurate reward calculations regardless of when users claim their 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.