According to docs/DESIGN.md §5, when a pool expires while the registry reports CORRUPTED but riskWindowStart == 0, claimExpired() intentionally resolves the pool to EXPIRED instead of CORRUPTED. This conservative fallback exists because the automatic expiry path cannot determine whether the corruption falls within the committed scope, leaving that judgment to the moderator.
The design document explicitly frames this race as one initiated by a staker who permissionlessly resolves the pool to EXPIRED in order to recover their own principal before the moderator flags the correct outcome. This is presented as an accepted tradeoff based on the assumption that the caller is an economically interested participant.
However, the implementation allows any address, including one with zero stake and no economic relationship to the pool, to trigger this irreversible transition.
When claimExpired() executes while the outcome is still UNRESOLVED, it performs the automatic resolution and immediately enables claim finality:
if (outcome == PoolStates.Outcome.UNRESOLVED) {
IAttackRegistry.ContractState state = _observePoolState();
}Only after claimsStarted has been permanently enabled does the function verify whether the caller actually has any claimable stake:
As a result, a caller with no stake simply pays gas, receives nothing, and exits successfully while permanently locking the pool into the mechanically chosen outcome.
Once claimsStarted becomes true, flagOutcome() can no longer modify the unresolved outcome:
Therefore, during the documented riskWindowStart == 0 edge case, any external account can permanently foreclose the moderator's opportunity to record the correct CORRUPTED outcome.
This wider permissionless surface is not reflected in the design rationale, which consistently describes the race as originating from participating stakers.
Likelihood:
Fires automatically any time a pool reaches expiry while the registry reads CORRUPTED and riskWindowStart == 0 and the moderator has not yet flagged — a narrow but reachable state (registry skipped straight from NEW_DEPLOYMENT/pre-attack to CORRUPTED with nobody having interacted with or poked the pool during any active-risk state in between).
Requires only that the caller notice the pool's public expiry timestamp and the public registry state — no privileged information, no capital, and (per the soft-success path) no stake in the pool.
Impact:
Forecloses the moderator's ability to flag the in-scope-breach CORRUPTED outcome for this pool, permanently routing any stakers who haven't yet claimed toward EXPIRED (principal returned, bonus swept to recoveryAddress per §5) instead of the correct CORRUPTED sweep — the exact outcome-mismatch DESIGN.md §5 acknowledges as the accepted worst case of this race, just reachable by a wider actor set (any address, not only a self-refunding staker) than the design doc's stated rationale describes.
No direct fund theft or protocol insolvency; the affected value (stakers' principal being returned rather than swept) moves in the staker-favorable direction, consistent with the documented "conservative EXPIRED is the staker-favorable default."
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.