Normally, every SURVIVED or EXPIRED staker receives principal plus their individually rounded k=2 bonus share. sweepUnclaimedBonus() is documented as a repeat-callable cleanup path that transfers any balance above the remaining stakers' actual entitlement reserve, including k=2 rounding dust, to recoveryAddress.
The reserve does not represent those actual future entitlements. Each claim rounds the user's bonus down and adds only the paid value to claimedBonus, while sweepUnclaimedBonus() reserves the entire global remainder snapshotTotalBonus - claimedBonus whenever even one staker remains. A final staker whose future bonus is zero can therefore keep all accumulated rounding remainder reserved indefinitely by not claiming.
For an equal-score cohort of N stakers sharing bonus B, every user receives floor(B / N). After N - 1 claims, the last user's actual bonus is still floor(B / N), but the contract reserves B - (N - 1) * floor(B / N). When B < N, every bonus share is zero and one final non-claimer pins the full bonus even though they can never receive any of it.
Likelihood: Medium
This occurs when the factory allowlists a standard low-decimal ERC20 and per-user bonus shares round down materially. The official compatibility section excludes fee-on-transfer and rebasing tokens but defines no minimum decimals.
A staker can trigger the indefinite phase by remaining as the final non-claimer. Survivor and expired bonus claims have no deadline. With an existing claimant cohort, the attacker needs one minimum stake and no privileged role, callback, timing race, or post-resolution transaction.
Likelihood is not High because no deployed allowlisted low-decimal token was identified, and manufacturing the claimant cohort requires additional positions, temporary principal, and transaction costs.
Impact: Medium
Sponsor/recovery-side bonus that no remaining staker can claim stays locked for an unbounded period, while the documented dust cleanup path reverts with NothingToSweep. The canonical PoC pins all 9 bonus units behind a 1-unit final position; the 100-staker variant pins 99 units.
Impact is not High because the attacker receives no direct profit and no staker principal is stolen or frozen. Materiality depends on token granularity and claimant count because aggregate floor-rounding residue is less than the number of claimants in raw token units.
Add the following test as test/poc/CodexGpt5_20260710_CanonicalFactoryLowDecimalReservePin.t.sol and run:
Runtime output:
The same root cause also reproduces through the permissionless EXPIRED resolution path. The full local suite after adding the canonical PoC completed with 284 passed, 0 failed, and 3 RPC-dependent fork skips.
Track a safe upper bound on the bonus weight still attributable to unclaimed users instead of treating every unpaid raw bonus unit as claimable. For the normal score path, the sum of remaining individual floor-rounded payouts is no greater than floor(snapshotTotalBonus * remainingScore / snapshotGlobalScore). Use the equivalent remaining-stake bound for the amount-weighted fallback.
Alternatively, introduce a bounded bonus-claim deadline after which unclaimed positions retain principal but forfeit bonus, and make both claim and sweep logic enforce the same deadline. Enforcing and documenting a minimum token-decimals policy reduces materiality but does not correct the accounting mismatch by itself.
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.