Root cause: claimExpired's auto-resolver refuses to auto-CORRUPT while riskWindowStart == 0 and falls through to EXPIRED (refunding principal) and latches claimsStarted — even when the registry is terminal CORRUPTED (a real in-scope breach).
Impact: an unprivileged staker front-runs the moderator's flagOutcome(CORRUPTED) with claimExpired, permanently escaping an in-scope breach with their full principal and foreclosing the moderator's correct CORRUPTED resolution; recoveryAddress (the protocol's breach compensation) receives nothing.
Normal behavior: on a real in-scope breach the registry reaches terminal CORRUPTED; the moderator flags CORRUPTED and the pool sweeps to recoveryAddress (bad-faith) or a named whitehat (good-faith). claimExpired is a permissionless post-expiry backstop.
The issue: claimExpired's CORRUPTED branch fires only when state == CORRUPTED && riskWindowStart != 0. When riskWindowStart == 0 (no pool interaction ever observed the active-risk window — a documented reachable state), a CORRUPTED registry is not auto-resolved; execution falls through the else to EXPIRED, refunds the caller's principal, and sets claimsStarted = true, closing the moderator's re-flag window. An in-scope-breached staker thus self-refunds and forecloses the correct CORRUPTED outcome.
Why this is not covered by the DESIGN §5 "known issue". DESIGN §5 documents this path but defends it as accepted "because the moderator is expected to flag a known in-scope breach promptly — they have the whole pool term plus the post-expiry window first." That defense is incomplete: the moderator can only flag CORRUPTED once the registry is terminal CORRUPTED. When the breach materialises at/after expiry (the registry marks CORRUPTED near the deadline) with riskWindowStart == 0, the moderator's window to flag before an unprivileged claimExpired front-run is ~zero — a staker calls claimExpired the instant block.timestamp >= expiry. The "flag promptly" assumption fails precisely when the breach and expiry coincide, which is exactly when the escape is valuable to the staker. This is the same finality-foreclosure class as the permissionless-claimCorrupted finding, via a different function and state.
Likelihood:
The registry reaches terminal CORRUPTED (a real in-scope breach) while no pool interaction ever sealed riskWindowStart — a documented reachable state (§5).
The breach is confirmed by the registry at or shortly before expiry, so the moderator has not yet flagged CORRUPTED; a staker monitoring the chain calls claimExpired the moment block.timestamp >= expiry, front-running the moderator's flagOutcome.
Impact:
The in-scope-breached staker escapes with their full principal instead of losing it to recoveryAddress under the correct CORRUPTED outcome.
The moderator's documented CORRUPTED authority is permanently foreclosed (claimsStarted latched); the protocol's breach compensation (recoveryAddress) receives nothing.
Full test in test/unit/ConfidencePool.expiredEscapesCorrupted.t.sol (2 tests pass, incl. a control showing the escape is impossible when risk WAS observed). Core:
When the registry reads CORRUPTED, claimExpired should defer to the moderator regardless of riskWindowStart — i.e., a terminal-CORRUPTED registry must never auto-resolve EXPIRED. Extend the grace-deferral to the riskWindowStart == 0 case:
Trade-off (acknowledged): this reintroduces the §6 scope-blind concern for out-of-scope breaches — an out-of-scope breach with riskWindowStart == 0 would now defer/auto-CORRUPT rather than refund. The team may prefer instead to require the moderator to resolve any CORRUPTED registry explicitly and only fall back to EXPIRED once the grace elapses with the registry NOT CORRUPTED.
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.