Normally, a pool that reaches expiry while its agreement was never attackable (riskWindowStart == 0) resolves EXPIRED: stakers recover their principal and the unearned bonus sweeps to recoveryAddress. docs/DESIGN.md §5 states the auto-CORRUPTED backstop "by definition cannot apply when no risk window was observed," and §6 states "a poke does not 'manufacture' it."
riskWindowStart != 0 is the contract's proxy for "risk materialized during the coverage term," gating both the auto-CORRUPTED principal sweep and the bonus payout. But _markRiskWindowStart caps the seal timestamp at expiry, so the permissionless pokeRiskWindow() seals a nonzero riskWindowStart = expiry even when the agreement is first observed active-risk after expiry. Stakers in such a pool bore no risk and could have withdraw()n freely the whole term, yet one post-expiry poke flips resolution against them.
Likelihood:
Reached when a pool is created early and its agreement stays in a pre-attack state (NEW_DEPLOYMENT / ATTACK_REQUESTED) for the full >=30-day term, then becomes attackable only after expiry, and any address (even an innocent or automated one) calls the permissionless pokeRiskWindow() before a participant resolves the pool at expiry.
The principal-loss endpoint is additionally reached when the agreement then reaches CORRUPTED post-term and the moderator does not intervene through the 180-day grace; the resolution-freeze and bonus-misdirection endpoints are reached with no moderator absence at all.
Impact:
All staker principal (plus bonus) is swept to recoveryAddress via the auto-CORRUPTED backstop, for stakers who survived their entire coverage term risk-free and would otherwise recover principal via EXPIRED.
Until the grace elapses (or the moderator flags), all pool funds are frozen: withdraw is disabled and claimExpired reverts AgreementCorruptedAwaitingModerator, a multi-month DoS that does not exist without the poke.
Under SURVIVED/EXPIRED, the entire bonus pool is redirected from recoveryAddress (the sponsor) to stakers who bore no risk.
Drop into test/audit/ and run forge test --match-contract PostExpiryPokePoC -vv. Both tests pass against the unmodified contract; the only difference between the safe baseline and the exploit is a single post-expiry pokeRiskWindow().
A risk window first observed after the coverage term is out of scope for this pool. Do not open an in-term risk window from a purely post-expiry observation:
Alternatively, record a dedicated observedDuringTerm boolean at seal time (set only when block.timestamp <= expiry) and gate both _bonusShare and claimExpired's auto-CORRUPTED branch on that flag instead of on riskWindowStart != 0.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.