FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: medium
Likelihood: medium

`globalScore` precision amplification leading to reward manipulation

Author Revealed upon completion

Root + Impact

Under short risk durations, the value of globalScore approaches zero. When calculating bonus rewards, dividing by an extremely small globalScore amplifies precision math anomalies, allowing early claimers or specific user scores to extract a disproportionately high amount of the bonus pool.

Description

The distribution of the bonus pool utilizes a time-weighted algorithm. A staker's score is scaled by the overall globalScore of the pool.

When the risk window duration is extremely short (e.g. 1 second or 1 block), both the global sum of squared times and individual user squared times collapse to minimal values, pushing globalScore close to zero. During reward claims under the SURVIVED path, the expression Math.mulDiv(userScore, snapshotTotalBonus, globalScore) will scale the output up significantly due to the minute denominator. If multiple users have slight timestamp discrepancies, this precision amplification creates an unbalance where earlier claimers can completely deplete the bonus allocation.

function _bonusShare(address u, uint256 userEligible) internal view returns (uint256) {
// ...
@> return Math.mulDiv(userScore, snapshotTotalBonus, globalScore);
}

Risk

Likelihood:

  • Occurs in pools where the active-risk window (the time between UNDER_ATTACK and PRODUCTION/CORRUPTED states) is observed and closed within a very brief timeframe.

  • Can be easily engineered on-chain by actors calling pokeRiskWindow() in consecutive blocks.

Impact:

  • Unfair bonus distribution where early or specific stakers get the entire bonus pool, leaving late or honest stakers with zero reward.

  • Mathematical inflation of individual shares past the correct allocation bounds.

Proof of Concept

function test_PoC_GlobalScoreManipulation() public {
_stake(alice, ONE); _contributeBonus(bob, 100 * ONE);
_passThroughUnderAttack();
vm.warp(block.timestamp + 1); // 1-second active risk duration
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
pool.pokeRiskWindow();
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.prank(alice);
pool.claimSurvived();
assertEq(token.balanceOf(alice) - ONE, 100 * ONE);
}

Recommended Mitigation

Enforce a minimum threshold/floor for the active-risk window duration or add a guard to fall back to a simple amount-weighted split if the calculated globalScore is below a safe precision threshold.

Support

FAQs

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

Give us feedback!