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

Incorrect reward calculation will lead to unfair reward distribution among stakers

Summary

Vulnerability Details

The FjordStaking contract allows users to stake tokens and earn rewards over time. The _redeem() function is responsible for calculating and updating user rewards based on their staked amounts. However, the current implementation contains a critical flaw that leads to incorrect reward calculations and unfair distribution of rewards among stakers.

The _redeem() function calculates rewards using the totalStaked amount for all epochs since the last claim. This approach fails to account for changes in a user's staked amount over time, leading to inaccurate reward calculations. Specifically, the function does not properly handle intermediate deposits or unstakes, and it updates totalStaked at the end, which means subsequent calls to _redeem() will use an incorrect staked amount for previous epochs.

Here's the problematic part of the _redeem() function:

File: FjordStaking.sol
729: function _redeem(address sender) internal {
730: //1. Get user data
731: UserData storage ud = userData[sender];
732:
733: ud.unclaimedRewards +=
734: calculateReward(ud.totalStaked, ud.lastClaimedEpoch, currentEpoch - 1);
735: ud.lastClaimedEpoch = currentEpoch - 1;
736:
737: if (ud.unredeemedEpoch > 0 && ud.unredeemedEpoch < currentEpoch) {
738: // 2. Calculate rewards for all deposits since last redeemed, there will be only 1 pending unredeemed epoch
739: DepositReceipt memory deposit = deposits[sender][ud.unredeemedEpoch];
740:
741: // 3. Update last redeemed and pending rewards
742: ud.unclaimedRewards += calculateReward(
743: deposit.staked + deposit.vestedStaked, ud.unredeemedEpoch, currentEpoch - 1
744: );
745:
746: ud.unredeemedEpoch = 0;
747: ud.totalStaked += (deposit.staked + deposit.vestedStaked);
748: }
749: }

The main issues are:

  1. It uses the current totalStaked amount to calculate rewards for all epochs since the last claim, which is incorrect if the staked amount has changed.

  2. It only considers the most recent unredeemed deposit, ignoring any intermediate deposits or unstakes.

  3. It updates totalStaked at the end, leading to incorrect calculations in subsequent calls.

These issues can result in significant discrepancies in reward distribution, where users who increase their stake over time receive fewer rewards than they should, while users who decrease their stake receive more rewards than they should.

Impact

The incorrect reward calculation leads to an unfair distribution of rewards among stakers. Users who frequently adjust their staked amounts (either increasing or decreasing) will receive disproportionate rewards compared to their actual staking behavior.

Stakers who increase their stake frequently may be disincentivized from doing so, as they will receive fewer rewards than they should. Conversely, stakers who decrease their stake may be incentivized to game the system by unstaking and restaking to receive more rewards than they should.

Proof of Concept

Consider the following scenario:

  1. Alice stakes 100 tokens in epoch 1.

  2. In epoch 5, Alice stakes an additional 100 tokens (total 200).

  3. In epoch 10, Alice unstakes 50 tokens (total 150).

  4. Alice calls _redeem() in epoch 15.

Current behavior:

  • The function calculates rewards based on 150 tokens (current totalStaked) for all epochs from 1 to 14.

  • Alice receives fewer rewards than she should for epochs 1-9 and more rewards than she should for epochs 10-14.

Correct behavior:

  • Alice should receive rewards based on 100 tokens for epochs 1-4, 200 tokens for epochs 5-9, and 150 tokens for epochs 10-14.

Tools Used

Manual review

Recommendation

To fix this issue, the contract needs to track staked amounts per epoch and calculate rewards based on the actual staked amount for each epoch. This requires a significant restructuring of the reward calculation and storage mechanism. Here's a suggested approach:

  1. Maintain a mapping of staked amounts per epoch for each user.

  2. Calculate rewards based on the actual staked amount for each epoch, rather than using a single totalStaked value.

  3. Update the _redeem() function to iterate through each epoch and calculate rewards based on the staked amount for that specific epoch.

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.