s — they are plain uint256 multiplies. Solidity 0.8 reverts on uint256 overflow, so a sufficiently large stake or stake-token balance can push one of these intermediates over 2^256, and the revert cascades into every codepath that observes registry active-risk state — permanently halting pool resolution and trapping every staker's principal.
The five offending sites, all in ConfidencePool.sol:
The upstream expiry guard at ConfidencePool.sol:197 caps expiry <= type(uint32).max; _markRiskWindowStart (line 807) and _markRiskWindowEnd (line 824) further cap their t at expiry. So every time operand is at most uint32.max ≈ 4.29×10^9, and T² is at most ~1.84×10^19. Overflow becomes possible when the amount operand reaches ~2^256 / T² ≈ ~6.3×10^57 minimum-units. Math.mulDiv is applied ONLY at the final share computation (lines 715, 719) — these five upstream sites are bare unchecked multiplies.
Activation requires a stake token whose aggregate totalEligibleStake (or a single received deposition, or userEligible per-claim) in one ConfidencePool clone approaches ~6.3×10^57 minimum-units (= 2^256 / max(uint32)²). The factory owner's allowedStakeToken allowlist (ConfidencePoolFactory.sol:31) has no decimals() cap and no supply cap — the inline natspec at ConfidencePoolFactory.sol:27-30 even acknowledges non-standard / exotic tokens are an accepted operator-error tail. Standard 18-decimal ERC20s realistically cap at ~10^27 base units (total supply ~10^9 tokens), leaving the trigger ~30 orders of magnitude out of reach — but synthetic high-decimal tokens (EIP-20 permits up to 78 decimals) or rebasing/inflator tokens that evolve over many years can cross the threshold. A single maliciously-allowlisted synthetic token (or a legitimate one over a long-enough timespan) makes the trigger reachable.
Once totalEligibleStake * t * t overflows at _markRiskWindowStart line 815, every pool-resolution and staker-fund-codepath reverts unconditionally: stake, contributeBonus, withdraw, flagOutcome, claimExpired, and pokeRiskWindow all call _observePoolState() BEFORE branching (ConfidencePool.sol:227, 271, 290, 328, 523, 638/doWork), and _observePoolState calls _markRiskWindowStart any time the registry is in active-risk and riskWindowStart == 0 (ConfidencePool.sol:793-795). There is no admin or moderator release valve — _markRiskWindowStart has no bounds check, outcome cannot be set to a terminal value through any other path (UNRESOLVED forever), and expiry + MODERATOR_CORRUPTED_GRACE eventually passes with claimExpired ALSO reverting because it calls _observePoolState at line 523 before its terminal-state branch.
Once triggered, the lock is permanent and unrecoverable without a contract upgrade. outcome is forever UNRESOLVED; no moderator flag, no permissionless claimExpired, no pokeRiskWindow, and no stake/withdraw/contributeBonus can proceed past the overflow. Every staker's principal is trapped forever.
The critical-vs-Low distinction: the trigger is operator-gated (factory-owner setStakeTokenAllowed), so an outside attacker cannot force it without DAO misbehavior. But within the documented trust surface (ConfidencePoolFactory.sol:27-30 natspec explicitly accepts "fee-on-transfer or rebasing token" as a known operator-error tail), the consequence is a total, permanent freeze of all pool funds — the most severe outcome short of direct theft. The bar is matched by the existing low-severity cutoff: trigger conditions are bounded and unlikely to occur in production, but the consequence is severe and the code author's own Math.mulDiv usage at lines 715/719 shows they understood the overflow risk at the final-step only — the upstream intermediate multiplies were an oversight.
Run: forge test --match-test test_PoC_k2OverflowLocksPool -vvv
Use Math.mulDiv consistently for any amount × t × t (or amount × t followed by anything multiplicative) computation, mirroring what the author already did for the final share distribution at lines 715 and 719. Apply the same 512-bit intermediate treatment to the five upstream sites:
Alternatively, insert a single upper-bound sanity check on the stake amount at the entry to stake (and/or a per-clone decimals() cap verified cheaply at init via IERC20Metadata(stakeToken_).decimals()):
Either approach closes the overflow window without changing the contract's behavior for realistic 18-decimal ERC20s (which top out at ~10^27 minimum-units — ~30 orders of magnitude below the threshold).
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.