The k=2 bonus distribution formula uses a terminal timestamp T (riskWindowEnd) as its upper bound. This timestamp is designed to be set on the first observation of a terminal registry state, after which it becomes immutable.
The observation that seals T is permissionless — any staker can trigger it via pokeRiskWindow(). An early staker can front-run the moderator's flagOutcome() call and seal T to the earliest possible block. This amplifies their (T − entry)² score relative to later stakers, shifting a measurable portion of the bonus pool from later entrants to the early entrant. The design doc claims any bias "collapses to ~zero" through counterparty action, but a later staker has no mechanism to delay or overwrite T once sealed — the race is one-sided.
Likelihood: Medium-High
A terminal state transition occurs on every pool that reaches PRODUCTION or CORRUPTED — the normal lifecycle.
Front-running is trivial with MEV infrastructure and requires zero privileges — any early staker can call pokeRiskWindow() in the same or next block.
Impact: Medium
Measurable bonus redistribution from later stakers to the early staker — 0.82% of a 100 ETH bonus pool shifted per 10-day entry gap in testing.
No principal is at risk, and no funds leave the staker pool; it is bounded entirely to the voluntarily contributed bonus pool and the pre-existing entry-time spread.
The following test isolates the front-running effect by forking the same pre-seal state. Both scenarios share identical entry times, stake sizes, and bonus amounts. The only variable is whether the attacker front-runs pokeRiskWindow() or lets T seal naturally via the moderator's flagOutcome() one day later.
A second test confirms that with identical entry times, front-running has zero effect — the 50/50 split is preserved regardless of who seals T, proving the mechanism only amplifies pre-existing entry-time skew.
For the flagOutcome() path (the common, moderator-resolved case), anchor T to the moderator's own confirmation timestamp rather than the permissionlessly-sealed riskWindowEnd. This removes the front-running incentive because the attacker's pokeRiskWindow() no longer influences the bonus split when the moderator resolves the pool. The moderator is already the trusted party for outcome determination, so anchoring T to their observation time is consistent with the existing trust model.
The claimExpired() auto-resolution backstop paths remain unchanged — they continue using riskWindowEnd sealed by the first permissionless observer. This is an accepted residual because the backstop only fires when the moderator is permanently unavailable, and in that scenario there is no trusted party to anchor T to.
This fix was verified against the delta PoC: with the change applied, the front-run delta drops to exactly 0 ETH, and both scenarios produce identical bonus splits regardless of who calls pokeRiskWindow().
Residual limitation (accepted): The claimExpired() auto-resolution backstop still uses riskWindowEnd from the first permissionless observer. This cannot be fixed the same way because there is no trusted moderator to anchor to in the auto-resolution path. This path only fires when the moderator is permanently unavailable (a failure mode the system already accepts as catastrophic) — but the "biases toward the recovery address, not the attacker" mitigation applies only to the auto-CORRUPTED sub-branch, where the pool is swept to recoveryAddress and no staker bonus is paid at all, so front-running has nothing to act on there. The far more common EXPIRED sub-branch (moderator absent, registry never reaches CORRUPTED) still pays stake + bonus to stakers using outcomeFlaggedAt = riskWindowEnd, exactly as before the fix — that path remains fully exposed to the same front-running dynamic described above, unmitigated by this change.
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.