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.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.