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

Early claim penalty unfairly applied to long-term stakers' rewards

Summary

The early claim mechanism in the FjordStaking::claimReward applies a penalty to all unclaimed rewards, regardless of when they were earned.

Vulnerability Details

The early claim mechanism in the FjordStaking::claimReward applies a penalty to all unclaimed rewards, regardless of when they were earned. This unfairly penalizes long-term stakers who have accumulated rewards over many epochs when they attempt to claim early.

FjordStaking.sol#L644

function claimReward(bool _isClaimEarly)
external
checkEpochRollover
redeemPendingRewards
returns (uint256 rewardAmount, uint256 penaltyAmount)
{
//CHECK
UserData storage ud = userData[msg.sender]; // <--- all of user's deposits
// existing code...
@> rewardAmount = ud.unclaimedRewards;
penaltyAmount = rewardAmount / 2;
rewardAmount -= penaltyAmount;
// existing code...
}

Impact

Users who have staked for many epochs and accumulated substantial rewards will have all their rewards penalized if they claim early, even for rewards that have long passed the intended vesting period.

This implementation may discourage users from maintaining long-term stakes, as they risk losing a significant portion of their older rewards if they need to claim early.

Proof of Concept

A scenario demonstrating the issue:

  1. A user stakes for 50 epochs and accumulates significant rewards.

  2. They make a new deposit in epoch 51.

  3. Two epochs later (epoch 53), they need to claim their rewards early.

  4. All their rewards, including those from the first 50 epochs, will be subject to a 50% penalty.

This penalizes the user for rewards that should have already vested and been claimable without penalty.

Tools Used

Manual code review

Recommended Mitigation Steps

Implement a per-epoch reward tracking system and modify the claimReward function to apply the early claim penalty only to rewards from recent epochs.

Here's an example of how it might look like:

diff --git a/src/FjordStaking.sol b/src/FjordStaking.sol
index a1b2c3d..e4f5g6h 100644
--- a/src/FjordStaking.sol
+++ b/src/FjordStaking.sol
@@ -15,7 +15,7 @@ contract FjordStaking {
struct UserData {
uint256 totalStaked;
uint256 unclaimedRewards;
- uint16 unredeemedEpoch;
+ mapping(uint16 => uint256) rewardsByEpoch;
uint16 lastClaimedEpoch;
}
@@ -655,6 +655,7 @@ contract FjordStaking {
function claimReward(bool _isClaimEarly)
external
checkEpochRollover
+ redeemPendingRewards
returns (uint256 rewardAmount, uint256 penaltyAmount)
{
UserData storage ud = userData[msg.sender];
@@ -668,25 +669,30 @@ contract FjordStaking {
if (!_isClaimEarly) {
claimReceipts[msg.sender] =
ClaimReceipt({ requestEpoch: currentEpoch, amount: ud.unclaimedRewards });
-
emit ClaimReceiptCreated(msg.sender, currentEpoch);
return (0, 0);
}
- rewardAmount = ud.unclaimedRewards;
- penaltyAmount = rewardAmount / 2;
- rewardAmount -= penaltyAmount;
+ uint16 penaltyThresholdEpoch = currentEpoch > 3 ? currentEpoch - 3 : 0;
+
+ for (uint16 epoch = ud.lastClaimedEpoch + 1; epoch < currentEpoch; epoch++) {
+ uint256 epochReward = ud.rewardsByEpoch[epoch];
+ if (epoch > penaltyThresholdEpoch) {
+ uint256 epochPenalty = epochReward / 2;
+ penaltyAmount += epochPenalty;
+ rewardAmount += epochReward - epochPenalty;
+ } else {
+ rewardAmount += epochReward;
+ }
+ delete ud.rewardsByEpoch[epoch];
+ }
if (rewardAmount == 0) return (0, 0);
totalRewards -= (rewardAmount + penaltyAmount);
- userData[msg.sender].unclaimedRewards -= (rewardAmount + penaltyAmount);
+ ud.lastClaimedEpoch = currentEpoch - 1;
fjordToken.safeTransfer(msg.sender, rewardAmount);
-
emit EarlyRewardClaimed(msg.sender, rewardAmount, penaltyAmount);
}
-
- // ... rest of the contract
}

This change ensures that only rewards from recent epochs (less than 3 epochs old) are subject to the early claim penalty, while older rewards can be claimed in full.

Note: The _redeem function and other related functions would need to be updated to work with this new reward tracking system.

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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