pokeRiskWindow() satisfies the auto-CORRUPTED gate, letting a griefer sweep the principal of stakers whose agreement survived the entire pool termLow — Impact: High (100% staker principal loss), Likelihood: Low (requires a permanently-absent moderator across the 180-day grace window plus a post-expiry breach).
claimExpired()'s permissionless auto-CORRUPTED backstop uses riskWindowStart != 0 as its proxy for "the in-scope contracts were exposed to risk during the pool term." That proxy is unsound: riskWindowStart can be sealed by a post-expiry active-risk observation through the permissionless pokeRiskWindow(), capped to expiry. A griefer can thereby force the scope-blind auto-CORRUPTED resolution on an agreement that survived the full term the stakers underwrote but was corrupted after expiry — sweeping stakers' principal to recoveryAddress instead of returning it via EXPIRED. Without the poke, the identical post-expiry breach resolves to EXPIRED and returns principal, so the poke is the load-bearing act that manufactures the loss.
The auto-CORRUPTED branch gates on the registry reading CORRUPTED and riskWindowStart != 0:
riskWindowStart is meant to evidence in-term risk, but _markRiskWindowStart() caps a late observation to expiry rather than rejecting it, and pokeRiskWindow() is permissionless and callable at any time before the pool is resolved (it only no-ops on outcome != UNRESOLVED, not on block.timestamp >= expiry):
Consequently riskWindowStart == expiry is ambiguous — it means either a genuine in-term observation at the expiry second, or a spurious post-expiry observation that got capped. The auto-CORRUPTED gate cannot distinguish them, so post-term risk qualifies the backstop exactly as in-term risk would.
AttackRegistry)An agreement stays in a non-active-risk pre-attack state (ATTACK_REQUESTED, deadline beyond expiry, never approved) for the entire pool term. Stakers deposit; riskWindowStart stays 0.
block.timestamp passes expiry. The agreement survived the term → the correct resolution is EXPIRED (principal + bonus returned).
Post-expiry, the agreement becomes attackable (UNDER_ATTACK). A griefer calls pokeRiskWindow(), sealing riskWindowStart = expiry.
The post-expiry attack culminates in markCorrupted → registry reads CORRUPTED (entirely post-term).
The moderator (DAO) is absent for the full MODERATOR_CORRUPTED_GRACE (180 days).
Anyone calls claimExpired(): state == CORRUPTED && riskWindowStart != 0 → auto-CORRUPTED; claimCorrupted() sweeps the full principal to recoveryAddress.
Stakers whose in-scope contracts survived every second of the term they insured lose 100% of principal to recoveryAddress, on account of a breach that occurred entirely after expiry. The precondition is a moderator absent for the whole 180-day grace window (a documented trust-model degradation), but the trigger within that window is permissionless and cheap, and — unlike the out-of-scope case in DESIGN.md §6 — the loss does not exist without the griefer's poke.
Confirmed against the real reachable registry state (ATTACK_REQUESTED; NEW_DEPLOYMENT is unreachable per AttackRegistry._getAgreementState). Drop into test/, run forge test --match-contract PostExpiryPokePoC -vvv. Both tests pass.
Manual review; Foundry (forge) PoC; multi-agent differential review grounded in the real lib/battlechain-safe-harbor-contracts registry source.
Gate the auto-CORRUPTED backstop on a risk window observed strictly before expiry, so a capped post-expiry observation cannot qualify it:
The only excluded genuine case is an active-risk state first observed in the exact second of expiry, which then falls through to the staker-favorable EXPIRED (principal returned) — a negligible edge. Equivalently, record a dedicated inTermRiskObserved flag set only when _isActiveRiskState(state) && block.timestamp < expiry and gate on that.
This shares the absent-moderator precondition of the accepted trust assumption in DESIGN.md §6, which is why it is filed as Low. However, §6 dismisses the poke lever ("a poke does not 'manufacture' it") specifically for the in-term, out-of-scope breach, where the loss exists regardless of the poke. The post-expiry variant demonstrated here is materially different: the control test proves the loss does not occur without the griefer's poke. The one-line riskWindowStart < expiry gate removes the lever without affecting any documented behavior.
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.