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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.