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

Integer Overflow in k=2 Bonus Score Calculation Locks All Staker Funds

Author Revealed upon completion

Root + Impact


Description

  • The _bonusShare() function computes each staker's bonus using T² × eligibleStake where T is outcomeFlaggedAt (a uint32). Solidity 0.8.x uses checked arithmetic — when T² × eligibleStake exceeds 2^256 - 1, the transaction reverts.
    // --->> src/ConfidencePool.sol:704
    uint256 userPlus = T * T * userEligible + userSumStakeTimeSq[u];
    T can be up to 4,294,967,295 (max uint32). T² ≈ 1.84 × 10^19. Any userEligible greater than ≈ 6.27 × 10^57 causes overflow. The same issue exists on line 708 for snapshotTotalStaked.
    Once triggered, claimSurvived(), claimExpired(), and the auto-CORRUPTED path all revert — no claim path works. The claimsStarted flag never sets, so the moderator cannot re-flag. Funds are permanently locked.

* * // Root cause --->> ConfidencePool.sol:704-710
function \_bonusShare(address u, uint256 userEligible) internal view returns (uint256) {
if (snapshotTotalBonus == 0) return 0;
if (riskWindowStart == 0) return 0;
uint256 T = outcomeFlaggedAt;
// --->> OVERFLOW: T \* T \* userEligible exceeds uint256 when userEligible > 2^192
uint256 userPlus = T \* T \* userEligible + userSumStakeTimeSq\[u];
uint256 userMinus = 2 \* T \* userSumStakeTime\[u];
uint256 userScore = userPlus > userMinus ? userPlus - userMinus : 0;
// --->> OVERFLOW: Same pattern in global score calculation
uint256 plus = T \* T \* snapshotTotalStaked + snapshotSumStakeTimeSq;
uint256 minus = 2 \* T \* snapshotSumStakeTime;
uint256 globalScore = plus > minus ? plus - minus : 0;
// ... rest of function
}
The overflow occurs because Solidity 0.8.x performs checked arithmetic by default — the multiplication T \* T \* userEligible reverts when the intermediate result exceeds 2^256 - 1. Since T (outcomeFlaggedAt) is a uint32 with maximum value 4,294,967,295, the product T² is approximately 1.84 × 10^19. Multiplying this by any userEligible value greater than approximately 6.27 × 10^57 (roughly 2^192) triggers the overflow.
Once the pool enters a terminal state (SURVIVED, CORRUPTED, or EXPIRED) and snapshotTotalStaked exceeds the threshold, every subsequent call to claimSurvived(), claimExpired(), or \_bonusShare() will revert. The claimsStarted flag cannot be set because no claim can execute, the moderator cannot re-flag the outcome, and stakers cannot recover their principal.

Risk

Likelihood:

  • Tokens with large total supplies (18+ decimals, supplies in quadrillions) can accumulate stake exceeding the overflow threshold across multiple depositors

  • The pool's stake token is owner-allowlisted — a governance token or high-supply asset could realistically reach this level

Impact:

  • All stakers lose access to principal and bonus with no recovery mechanism

  • The pool becomes insolvent — no value can ever leave the contract

Proof of Concept

The overflow occurs when T² × totalStaked > 2^256. Demonstrating with concrete values:
uint256 T = type(uint32).max; // 4,294,967,295
uint256 tSquared = T * T; // 18,446,744,065,119,617,025 (fits in uint256)
uint256 threshold = type(uint256).max / tSquared; // ~6.27 × 10^57
// Any stakeToken with total supply > threshold triggers the overflow.
// For an 18-decimal token, this is ~6.27 × 10^39 tokens — large but
// achievable for meme tokens, governance tokens, or test tokens with
// inflated supplies.
// After flagOutcome snapshots a totalEligibleStake above this threshold:
// claimSurvived() → reverts (overflow in _bonusShare)
// claimExpired() → reverts (overflow in _bonusShare)
// claimCorrupted() → reverts (sweep depends on claim flow)
//
// No staker can recover funds. The moderator cannot re-flag because
// claimsStarted is never set (the first claim always reverts).
To reproduce on a fork:
1. Deploy a pool with a high-supply ERC20 (e.g., 10^60 total supply)
2. Stake until totalEligibleStake > threshold
3. Trigger terminal registry state
4. Call flagOutcome(SURVIVED, false, address(0))
5. Call claimSurvived() — reverts with panic code 0x11 (arithmetic overflow)

Recommended Mitigation

Use Math.mulDiv (already imported) for the T² multiplication to leverage 512-bit intermediate arithmetic:
function _bonusShare(address u, uint256 userEligible) internal view returns (uint256) {
if (snapshotTotalBonus == 0) return 0;
if (riskWindowStart == 0) return 0;
uint256 T = outcomeFlaggedAt;
- uint256 userPlus = T * T * userEligible + userSumStakeTimeSq[u];
- uint256 userMinus = 2 * T * userSumStakeTime[u];
+ uint256 Tsq = Math.mulDiv(T, T, 1);
+ uint256 userPlus = Math.mulDiv(Tsq, userEligible, 1) + userSumStakeTimeSq[u];
+ uint256 userMinus = Math.mulDiv(2 * T, userSumStakeTime[u], 1);
uint256 userScore = userPlus > userMinus ? userPlus - userMinus : 0;
- uint256 plus = T * T * snapshotTotalStaked + snapshotSumStakeTimeSq;
- uint256 minus = 2 * T * snapshotSumStakeTime;
+ uint256 plus = Math.mulDiv(Tsq, snapshotTotalStaked, 1) + snapshotSumStakeTimeSq;
+ uint256 minus = Math.mulDiv(2 * T, snapshotSumStakeTime, 1);
uint256 globalScore = plus > minus ? plus - minus : 0;
Math.mulDiv computes a × b / c using 512-bit intermediates, preventing overflow. Passing 1 as the denominator performs pure multiplication without division. The same pattern is already used on line 719 for the final share calculation.
Updates

Lead Judging Commences

inallhonesty Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

`_bonusShare` and `stake` mathematical overflow limits for extremely large token supplies

inallhonesty Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

`_bonusShare` and `stake` mathematical overflow limits for extremely large token supplies

Support

FAQs

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

Give us feedback!