Skip to content

Latest commit

 

History

History
134 lines (100 loc) · 8.67 KB

058.md

File metadata and controls

134 lines (100 loc) · 8.67 KB

Brilliant Menthol Jaguar

High

"Critical Reward Precision Loss: Vulnerability in Calculating Tiny Rewards for Large Stakes

Summary

This report outlines a critical Reward Precision Loss vulnerability in the smart contract’s reward distribution mechanism. When extremely small rewards are computed for very large staked amounts, integer arithmetic in Solidity causes a non-trivial loss of precision. The root issue is that multiplying a tiny reward by a large stake, then dividing by the total stake, can truncate fractional components. Although the deviation may appear insignificant in a single payout, these inaccuracies can accumulate over time, potentially leading to substantial discrepancies in rewards and undermining user confidence.

Vulnerability Detail

  1. Mechanics of the Vulnerability

    • Integer Arithmetic Truncation: In Solidity, operations on uint256 discard any fractional components. Therefore, if a small reward (e.g., 1 GWEI) is multiplied by a large stake (e.g., 10,000,000 ETH) and then divided by the total stake, the result can be significantly lower than the intended reward.
    • No Native Floating-Point: Solidity lacks floating-point math, so fractional parts are lost during multiplication/division unless the developer implements a fixed-point or scaling approach.
  2. Where It Occurs in the Contract

    a. _scaledUnclaimedReward()

    function _scaledUnclaimedReward(Deposit storage deposit) internal view virtual returns (uint256) {
        return deposit.scaledUnclaimedRewardCheckpoint
          + (deposit.earningPower * (rewardPerTokenAccumulated() - deposit.rewardPerTokenCheckpoint));
    }
    • Here, deposit.earningPower (a uint96) is multiplied by the difference between two large integers (rewardPerTokenAccumulated() and deposit.rewardPerTokenCheckpoint). Because this is integer arithmetic, any fractional remainder is discarded.
    • If earningPower is very large or if the difference in reward accumulators is relatively small (e.g., due to a tiny reward), the truncated fractions can accumulate to a noticeable shortfall over multiple distributions.

    b. rewardPerTokenAccumulated()

    function rewardPerTokenAccumulated() public view virtual returns (uint256) {
        if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint;
    
        return rewardPerTokenAccumulatedCheckpoint
          + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime))
            / totalEarningPower;
    }
    • This function calculates the global reward per token.
    • The expression (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalEarningPower uses integer division. If scaledRewardRate is small (due to a tiny reward) and totalEarningPower is extremely large, the division may truncate much of the fractional component, leading to understated rewards.

    c. notifyRewardAmount()

    if (block.timestamp >= rewardEndTime) {
        scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
    } else {
        uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
        scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
    }
    • Although a SCALE_FACTOR (1e36) is applied to improve precision, a very small _amount can make scaledRewardRate still too low compared to a huge totalEarningPower. Later divisions by totalEarningPower can further reduce the reward portion.
    • The contract attempts to maintain precision by scaling, but integer arithmetic can still lose fractions once you divide by large numbers.
  3. Example of Failure

    • Expected Reward: 1 GWEI
    • Actual Reward: 0.9999999 GWEI (or even less, depending on scaling and integer truncation)

    Repeated daily or across many participants, these rounding gaps can add up to a substantial overall discrepancy.

Impact

  1. Incremental Financial Losses

    • Underpayment of Stakers: With each reward cycle, users may receive slightly less than expected. Over a long period or with frequent rewards, these underpayments accumulate into significant losses.
    • Potential Exploit: A sophisticated actor could theoretically create transactions timed to maximize truncation, gradually siphoning reward shortfalls. Although it’s not a straightforward exploit, the risk remains non-negligible in edge cases.
  2. Reputational Risks

    • Trust Erosion: Users noticing persistent shortfalls in rewards will lose confidence in the protocol’s fairness and accuracy. Repeated precision errors can also raise questions about the contract’s overall integrity.
  3. System Instability

    • Verification Failures: Testing frameworks (e.g., Forge) rely on assertApproxEqAbs or similar checks. As soon as the result falls below the acceptance threshold, tests break. This signals the system may not meet its design specifications for precise reward distribution.
    • Inter-Contract Dependencies: Other contracts or libraries expecting exact reward amounts might behave unpredictably if those expectations are not fulfilled, causing broader ecosystem issues.

Code Snippet

Below is a distilled version of a test scenario demonstrating the discrepancy. Here, the assertion fails because the calculated reward is smaller than tinyReward:

uint256 tinyReward = 1e9; // 1 GWEI
uint256 largeStake = 10_000_000e18; // 10M ETH
uint256 totalStake = largeStake;

uint256 reward = (tinyReward * largeStake) / totalStake;

// Test assertion fails due to precision loss
assertApproxEqAbs(reward, tinyReward, 1, "Precision issue for small reward");

A comparable logic path is seen in the actual contract code (_scaledUnclaimedReward() and rewardPerTokenAccumulated()), where integer operations similarly truncate fractions.

Tool Used

Manual Review

  • The testing was performed by minting tokens, simulating reward calculations with large stakes, and then comparing expected vs. actual results.
  • A thorough review of the math in _scaledUnclaimedReward(), rewardPerTokenAccumulated(), and notifyRewardAmount() confirmed that integer division discards fractional components, producing under-rewards in edge scenarios.

Recommendation

  1. Utilize Fixed-Point or High-Precision Libraries

  2. Adopt a Scaling Strategy

    • Scale Up, Then Scale Down: Multiply all relevant values (e.g., tinyReward, stakerStake, etc.) by a large constant (e.g., 1e18) before dividing, then scale down at the end.
    • Example Implementation:
      function calculateReward(uint256 tinyReward, uint256 stakerStake, uint256 totalStake)
          public
          pure
          returns (uint256)
      {
          require(totalStake > 0, "Total stake cannot be zero");
          uint256 scaledReward = (tinyReward * 1e18 * stakerStake) / totalStake; // Scale up
          return scaledReward / 1e18; // Scale down
      }
    • This ensures you capture and preserve the fractional value until the final step.
  3. Minimum Reward Threshold

    • Threshold Setting: If the calculated reward is below a certain cutoff (e.g., 1e5 Wei), accumulate it until it reaches a more meaningful amount. This avoids repeated micro-truncation for very tiny distributions.
    • Benefits: Dramatically reduces the frequency of round-off events for negligible rewards.
  4. Comprehensive Testing and Fuzzing

    • Edge Cases:
      • Extremely small reward amounts (1 GWEI or less).
      • Extremely large stake totals (10M ETH or more).
      • Large participant pools that amplify tiny rounding errors.
    • Fuzz Testing: Randomly vary stakes and reward sizes to detect unanticipated rounding errors under diverse conditions.
  5. Clear Documentation

    • Transparency: Document known limitations around integer-based calculations and highlight potential rounding errors.
    • Versioning & Audits: Track code changes impacting reward calculation logic. Periodic external audits help verify that any modifications do not reintroduce precision loss issues.