Bonus is split across stakers with a k=2 time weighting: each deposit's share is proportional to stake * (T - entryTime)^2, rebuilt at claim time from running sums.
_bonusShare builds that score in expanded form and adds the two positive terms before the negative cross-term cancels them. When the observed window is short the positive sum is roughly 2 * T * T * stake, so it can overflow uint256 and revert with Panic(0x11) before the trailing Math.mulDiv ever runs. The global term uses snapshotTotalStaked, so one oversized stake poisons the denominator every claimant shares. The same pattern sits in _markRiskWindowStart (totalEligibleStake * t * t), which reverts every registry observation once the window opens.
To head off the obvious objection: this isn't the balance-freezing token the allowlist is documented to exclude (DESIGN.md §12). The token behaves normally and the revert is in the pool's own scoring math, not the token's transfers. And §7's note that the k=2 multiplies "widen to uint256 first" is about the uint32 timestamps, not this uint256 product — which the existing testBonusShareDoesNotOverflowAtHighMagnitudes only exercises up to 1e34, far below where this bites.
Likelihood: Triggers once a pool's total staked amount reaches about uint256.max / (2 * T^2) base units (~1.8e58 for a normal 1-2 year pool). That threshold is only reachable when the factory owner has allowlisted a stake token whose base-unit magnitudes get that high, meaning pathological decimals or supply that no standard 18-decimal token comes within ~30 orders of magnitude of. That single precondition holds the likelihood to Low. The trigger is otherwise a plain permissionless stake() before the pool resolves: no privileged role, no registry manipulation, no special timing.
Impact: Every SURVIVED/EXPIRED claim in the pool reverts on the shared-denominator overflow, so all stakers, including an honest one who deposited a single token, permanently lose principal and bonus. The clones are non-upgradeable and T is frozen at resolution, so there is no recovery path; sweepUnclaimedBonus keeps the liabilities reserved but cannot touch the math. The _markRiskWindowStart site also reverts every registry observation, claimExpired included, blocking exits for all stakers while the agreement stays in an active-risk state.
Save as test/unit/OverflowPoC.t.sol and run forge test --match-contract OverflowPoCTest -vvv. It passes against the unmodified contracts on the existing fixtures. The attacker stakes an extreme amount of a high-magnitude allowlisted token, an honest staker puts in one token, the pool resolves SURVIVED normally, and the honest staker can never claim.
Bound the aggregate at deposit so every later T^2 * totalEligibleStake product stays in range. In stake(), right after the received checks:
Or drop the expanded form and accumulate stake * (T - entryTime)^2 directly (or run the terms through 512-bit math) so the intermediate can't overflow before it cancels. The trailing Math.mulDiv can't help either way, since it's downstream of the overflow.
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.