## Description
`claimExpired` provides a permissionless auto-CORRUPTED backstop for an absent moderator. To protect the moderator's exclusive right to resolve a *good-faith* CORRUPTED (naming a whitehat attacker for the bounty), the backstop defers for a grace period — but that grace is measured from **`expiry`**, not from when the corruption actually became observable (`riskWindowEnd`)
```solidity
if (state == IAttackRegistry.ContractState.CORRUPTED && riskWindowStart != 0) {
@> if (block.timestamp < expiry + MODERATOR_CORRUPTED_GRACE) {
revert AgreementCorruptedAwaitingModerator();
}
outcome = PoolStates.Outcome.CORRUPTED;
...
claimsStarted = true;
return;
}
```
If an agreement becomes terminal `CORRUPTED` **more than `MODERATOR_CORRUPTED_GRACE` (180 days) after `expiry`**, the grace is already fully elapsed at the moment corruption is first observable. The very first `claimExpired` immediately auto-resolves **bad-faith** CORRUPTED and latches `claimsStarted`, so the moderator gets **zero** window to name a good-faith attacker. The whitehat bounty path (`claimAttackerBounty`) is therefore unavailable (`bountyEntitlement == 0`); the pool sweeps to `recoveryAddress` via `claimCorrupted`.
No staker principal is lost or misdirected relative to a correct bad-faith resolution (a CORRUPTED agreement forfeits staker principal to recovery in either case). The only affected party is the **whitehat bounty claimant** — a third party who would have been named had the grace been measured from the corruption event.
**Root cause (GitHub permalinks):**
- Grace anchored to `expiry` in `claimExpired`: https:
- `claimExpired` auto-CORRUPTED latches `claimsStarted`, foreclosing a later good-faith flag: https:
- `flagOutcome` re-flag gate on `claimsStarted`: https:
## Risk
**Likelihood:**
- Requires a corruption that becomes terminal **> 180 days after `expiry`** AND a whitehat the moderator would otherwise have named. Under the DAO-moderator trust model the moderator resolves promptly, so this is an edge of an edge.
**Impact:**
- **No staker principal loss or misdirection**: a truly CORRUPTED agreement forfeits principal to `recoveryAddress` under both good-faith and bad-faith resolution.
- The good-faith **whitehat bounty path is denied** for a late-terminal corruption; the would-be-named attacker (third party) receives nothing.
Proof of Concept
```solidity
function test_HS5B_lateCorruption_zeroModeratorWindow() public {
_stake(alice, 100 * ONE);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
vm.warp(uint256(pool.expiry()) + pool.MODERATOR_CORRUPTED_GRACE() + 1 days);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(alice);
pool.claimExpired();
assertEq(uint256(pool.outcome()), uint256(PoolStates.Outcome.CORRUPTED), "auto bad-faith CORRUPTED");
assertTrue(pool.claimsStarted(), "locked; moderator cannot name a good-faith attacker");
assertEq(pool.bountyEntitlement(), 0, "no good-faith bounty path available");
vm.prank(moderator);
vm.expectRevert(IConfidencePool.OutcomeAlreadySet.selector);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, makeAddr("wh"));
}
```
**Outcome (as run):**
```
[PASS] test_HS5B_lateCorruption_zeroModeratorWindow() (gas: 463743)
```
The test confirms a late-terminal corruption auto-resolves bad-faith CORRUPTED with `bountyEntitlement == 0` and a foreclosed good-faith re-flag.
Recommended Mitigation
Anchor the auto-CORRUPTED grace to the **corruption-observation moment** (`riskWindowEnd`) rather than `expiry`, so the moderator always receives a full window relative to when corruption actually became terminal:
```diff
- if (block.timestamp < expiry + MODERATOR_CORRUPTED_GRACE) {
+ // Grace measured from corruption observation, not the pool's own expiry, so a late-terminal
+ // corruption still grants the moderator a full good-faith window.
+ uint256 graceAnchor = riskWindowEnd != 0 ? riskWindowEnd : expiry;
+ if (block.timestamp < graceAnchor + MODERATOR_CORRUPTED_GRACE) {
revert AgreementCorruptedAwaitingModerator();
}
```