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.
* *
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 userScore = userPlus > userMinus ? userPlus - userMinus : 0;
uint256 plus = T \* T \* snapshotTotalStaked + snapshotSumStakeTimeSq;
uint256 minus = 2 \* T \* snapshotSumStakeTime;
uint256 globalScore = plus > minus ? plus - minus : 0;
}
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:
Proof of Concept
The overflow occurs when T² × totalStaked > 2^256. Demonstrating with concrete values:
uint256 T = type(uint32).max;
uint256 tSquared = T * T;
uint256 threshold = type(uint256).max / tSquared;
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.