The factory owner allowlists standard ERC20 stake tokens. Stakers deposit principal, any account may contribute bonus, and the pool tracks k=2 time-weighted score sums. Once an unresolved pool reaches expiry, a non-staker may call claimExpired() to finalize EXPIRED or SURVIVED; every staker must then be able to claim at least their principal.
The pool stores ERC20 amounts in raw units and computes score terms with plain uint256 products containing absolute Unix timestamps. A stake can be safely accepted and its risk window safely opened at an earlier timestamp R, while the same amount is too large for the later terminal timestamp T used by _bonusShare().
_bonusShare() skips all score math when snapshotTotalBonus == 0. Otherwise it evaluates T * T * userEligible and T * T * snapshotTotalStaked before reaching the existing 512-bit Math.mulDiv. Because contributeBonus() is permissionless and has no minimum, an attacker can activate these unsafe intermediates by contributing exactly one raw token unit without staking.
At expiry a staker cannot preempt the attack: their claimExpired() reaches the overflow and atomically rolls the attempted resolution back to UNRESOLVED. The attacker then calls claimExpired() as a non-staker. This finalizes the outcome and sets claimsStarted = true, but returns before _bonusShare() because the attacker has no eligible stake. Every actual staker then reaches the overflowing global score and reverts with Panic(0x11). The finalized outcome blocks moderator correction and withdrawal, while sweepUnclaimedBonus() reserves the entire principal plus bonus. The non-upgradeable clone has no remaining recovery route.
The vulnerable raw-stake interval is non-empty whenever R < T:
With the canonical 31-day term and the PoC's standard 59-decimal ERC20, the affected victim principal is about 0.37694 displayed token, while the attacker's one-raw-unit cost is 1e-59 displayed token.
Likelihood: Medium within an affected configuration
The attacker capability is entirely permissionless: contributeBonus(1) followed by a non-staker claimExpired() at expiry. The attacker never stakes and holds no owner, moderator, or Safe Harbor role.
The factory must first allow a standard ERC20 and the pool's aggregate raw stake must exceed MAX_UINT256 / expiry^2. The README excludes fee-on-transfer and rebasing tokens but gives no decimal, raw-supply, or score-capacity limit.
A 59-decimal OpenZeppelin ERC20 satisfies the documented standard-token behavior and makes the boundary less than one displayed token. No affected production allowlist has been identified, so the configuration caveat must remain explicit.
Impact: High
Every staker's principal is permanently frozen after final EXPIRED or SURVIVED resolution. A small staker whose individual score is safe still fails because the shared global score overflows.
The failed claim rolls back without reducing eligibleStake, but no alternate withdrawal path remains. Moderator re-flag and withdraw() revert with OutcomeAlreadySet; sweepUnclaimedBonus() reverts with NothingToSweep because all funds are reserved.
A later terminal registry transition cannot alter the finalized outcome. Existing pools are non-upgradeable clones, and the factory owner explicitly cannot move clone funds.
Full compilable PoC:
test/poc/CodexGpt5_20260710_OneRawBonusPermanentClaimOverflow.t.sol
Run:
The core EXPIRED-path differential is:
Observed output:
The full non-fork suite also passes: 278 passed, 0 failed, 0 skipped.
Enforce a cumulative raw-stake bound before accepting each stake. Use expiry as the maximum possible score timestamp and leave a factor-of-two headroom for the addition/subtraction terms in _bonusShare().
A structural alternative is to store durations relative to riskWindowStart and evaluate every score term with checked 512-bit arithmetic. Checking decimals() alone is insufficient because decimals are display metadata; the invariant must constrain actual raw balances.
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.