The pool treats a registry reading of CORRUPTED as exogenous evidence of a breach. §1 states it outright: "Only the terminal CORRUPTED state is evidence of an actual breach." §5, §6 and §11 all reason from that premise.
The premise is false: the party who writes that flag is the party who receives the swept funds.
pool.owner() also controls setRecoveryAddress:611, which has no lifecycle gate. One key holds the verdict, the payout destination, and the pool. The verdict is free: markCorrupted accepts no proof and calls _markBondClaimable (AttackRegistry.sol:333 -> BondManager.sol:219), which refunds the bond rather than slashing it.
What the call actually does: it deletes the pool's permissionless resolution and arms a 180-day fuse. With riskWindowStart sealed (any stake or poke during the normal UNDER_ATTACK phase), claimExpired's CORRUPTED branch returns before the EXPIRED branch is ever reached, so the staker-favourable outcome becomes structurally unreachable:
So one free call converts "stakers recover principal + bonus at expiry" into "stakers recover nothing unless the DAO flags SURVIVED within 180 days of expiry". If the DAO does not, claimCorrupted:413 sweeps stakeToken.balanceOf(address(this)), every staker's principal and the whole bonus to the sponsor. withdraw is already latched shut throughout by riskWindowStart != 0.
§6 asks exactly this question and checks the wrong input. Its pokeRiskWindow subsection argues the loss cannot be manufactured: "The out-of-scope + absent-moderator loss above is the accepted consequence regardless of who seals the window. A poke does not 'manufacture' it." That audits one of the backstop's two inputs, the riskWindowStart seal and correctly finds it outcome-neutral. It never asks whether the other input, the CORRUPTED flag, can be manufactured. It can, by the beneficiary, for free.
§6 accepted consequence presupposes a real breach: "if the moderator is absent for the full grace window AND the breach was out-of-scope, stakers lose principal + bonus." That accepts an unlucky coincidence. No breach need occur, and the trigger is sponsor-held and profitable, a risk modelled as an accident is not an accepted risk when it is a cost-free action for the party who gains.
§10's "Sponsor trust surface" lists exactly three levers, recoveryAddress, expiry, scope and tells stakers to "verify pool parameters before depositing"; the CORRUPTED verdict is not among them and lives in the registry, where no pool parameter reveals it. §11's carve-out covers only a registry that is "bricked or repointed" and promises "No funds can be stolen... principal returned to stakers rather than swept", here the registry is honest and unmodified, and principal is swept. Across all three documents markCorrupted appears once (§1 line 23), noting which states it is reachable from, never who may call it.
Severity: Medium - High Impact x Low Likelihood.
Likelihood: Low. Deleting the EXPIRED path and arming the fuse is unilateral, free and certain. Completing the sweep additionally needs the DAO to not flag SURVIVED for the whole expiry + 180d window; a live moderator defeats it, and §6 asserts the backstop "should never fire in practice." That is a genuine barrier and the reason this is not filed higher. It is amplified by the moderator being unrotatable per-pool, outcomeModerator is written once at ConfidencePool.sol:207 with no setter, while the factory does expose setDefaultOutcomeModerator - so ordinary DAO key rotation turns "moderator absent" from a tail risk into routine key hygiene.
Impact: High. The sweep transfers every staker's principal plus the entire bonus to an address the sponsor controls, with no breach and no cost. Short of that, staker funds are unreachable for up to 180 days past expiry with no permissionless recovery, and the pool's fate is moved from a mechanical outcome to DAO discretion.
The identity collapse is checkable against BattleChain testnet (chainId 627) at block 16000 - no test needed:
The agreement's owner, whom createPool:82,99 forces to be the pool's owner and recoveryAddress controller, is the party the registry lets declare that agreement corrupted
The pool-side consequence reproduces with no RPC against the existing BaseConfidencePoolTest harness. Both PASS, the first shows the fuse burning, the second the freeze before it:
A fork run against the live testnet confirms the chain end-to-end: creating a real pool against the live SafeHarborRegistry as the real agreement owner, calling the real markCorrupted (state flips to CORRUPTED, bond refunded, no breach), then sweeping the pool to that same address after the grace window
The party holding the trigger must not be the party receiving the funds. Refuse the scope-blind backstop when the CORRUPTED attestation was authored by the sweep's beneficiary, and fall through to EXPIRED. IAttackRegistry.getAttackModerator already exposes the query.
This keeps the backstop's purpose, a permanently-absent moderator on a third-party-attested breach, while removing the sponsor's ability to pay themselves. The same predicate applied to the AgreementCorruptedAwaitingModerator deferral stops a self-authored flag stalling resolution. §6's "A poke does not 'manufacture' it" analysis should also be re-run against the CORRUPTED input, and §10 should disclose that the sponsor is the agreement's attack moderator
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.