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

Precision Loss in Reward Calculation Leading to Zero Reward Distribution

Summary

The _checkEpochRollover function in the FjordStaking contract suffers from a precision loss issue when calculating pendingRewardsPerToken. This occurs due to the division of a small pendingRewards value by a large totalStaked value, resulting in zero reward distribution under certain conditions.

uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;

Vulnerability Details

https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordStaking.sol#L691-L724

  1. Setup:

    • Deploy the FjordStaking contract with a large number of tokens staked (totalStaked).

    • Ensure pendingRewards is set to a very small value.

  2. Execution:

    • Call the addReward function with a minimal reward amount, triggering _checkEpochRollover.

  3. Observation:

    • Calculate pendingRewardsPerToken as (pendingRewards * PRECISION_18) / totalStaked.

    • If pendingRewards is significantly smaller than totalStaked, the result of this division may be zero due to integer division.

  4. Proof:

    • With totalStaked = 1,000,000,000 and pendingRewards = 1, the calculation yields:

      • pendingRewardsPerToken = (1 * 1e18) / 1e9 = 1e9.

    • If pendingRewards is even smaller, the result can be zero, leading to no rewards being distributed to stakers for that epoch.

Impact

  • Users may receive no rewards for an epoch despite there being pending rewards.

  • Malicious actors could manipulate the reward distribution by ensuring pendingRewards remains small relative to totalStaked, effectively nullifying rewards for honest stakers.

Tools Used

  • Manual review

Recommendations

  • Consider scaling pendingRewards before division to maintain precision, then scale down the result.

  • Implement a check to ensure that pendingRewards is above a certain threshold before proceeding with the division. This can prevent the calculation from resulting in zero.

function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
//Time to rollover
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
// no distribute the rewards to the users coming in the current epoch
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked)
- totalStaked - newStaked - totalRewards;
// Define minimum reward threshold
+ uint256 MINIMUM_REWARD = 1e18; // Example threshold, adjust as necessary
// Require that pending rewards exceed the minimum threshold
+ require(pendingRewards > MINIMUM_REWARD, "Insufficient rewards for distribution");
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]);
}
} else {
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded];
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
}
totalStaked += newStaked;
totalVestedStaked += newVestedStaked;
newStaked = 0;
newVestedStaked = 0;
lastEpochRewarded = currentEpoch - 1;
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Low decimal tokens or super small bids can lead to 0 claims

Support

FAQs

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