A Confidence Pool settles CORRUPTED only when the outcomeModerator says so. As a liveness backstop, claimExpired() lets anyone mechanically finalize a bad-faith CORRUPTED once the agreement's registry state is CORRUPTED and the moderator has stayed silent for expiry + MODERATOR_CORRUPTED_GRACE (180 days), sweeping the entire pool to the sponsor-set recoveryAddress so funds are not trapped when the DAO becomes permanently unavailable. DESIGN.md §6 accepts the resulting principal loss, and §11 places a compromised registry out of model.
The backstop treats registry-CORRUPTED as ground truth, but the sponsor is their own agreement's attackModerator and can therefore declare that state with no breach whatsoever. AttackRegistry._registerAgreement assigns attackModerator = agreementOwner, and markCorrupted is gated on that role alone, taking no breach evidence. The same address is also the pool creator (ConfidencePoolFactory.createPool requires IAgreement(agreement).owner() == msg.sender) and sets recoveryAddress with no constraint separating it from the agreement owner. One untrusted party thus holds the trigger, the destination, and the profit motive. §6 documents this loss as following a genuine breach that happened to be out-of-scope — an external accident; §11 covers only a registry compromised or repointed to report false state. Neither describes the actual vector: the agreement owner legitimately exercising their own registry authority to declare a fabricated corruption, with the registry behaving exactly as designed and no trusted entity compromised. The moderator-liveness assumption is therefore load-bearing against deliberate, single-party-controlled theft, not merely the passive misfortune the docs describe.
Likelihood:
The role concentration holds for every pool created through ConfidencePoolFactory, by construction: createPool requires IAgreement(agreement).owner() == msg.sender, and AttackRegistry._registerAgreement assigns attackModerator = agreementOwner. The sponsor is always their own agreement's attack moderator — this is the default state of every deployment, not a misconfiguration.
markCorrupted succeeds on role plus registry state alone and takes no breach evidence, so a sponsor who has reached UNDER_ATTACK declares corruption at will. Reaching UNDER_ATTACK runs through the DAO's approveAttack, which is the normal pool lifecycle — that approval is how the risk window opens in the first place, and it vouches for the attack request, not against a later fabrication.
riskWindowStart gets sealed by any ordinary pool interaction or the permissionless pokeRiskWindow() during the UNDER_ATTACK interval — routine, and stakers are economically incentivised to seal it for their own bonus.
Realised loss additionally requires the outcomeModerator to remain absent for the entire expiry + 180 days. This is the sole remaining guardrail and it is a liveness assumption rather than a code control — which is precisely why this is filed as a documentation / threat-model gap at Low, not as a code defect. A present moderator defeats the vector completely (proven below).
Impact:
Stakers who price a pool from DESIGN.md §6 materially underestimate the risk they are underwriting: the docs present the CORRUPTED precondition as a genuine-but-out-of-scope breach, so moderator liveness reads as a backstop against misfortune. It is in fact the only control standing against a sponsor-controlled, on-demand theft lever whose trigger, destination and beneficiary are one untrusted address. This changes the character of the assumption, and greatly raises its likelihood, without being documented as such.
When the outcomeModerator stays absent for the full grace window, the fabricated path routes 100% of staker principal plus bonus to the sponsor's recoveryAddress, the sponsor having risked only the (typically far smaller) bonus as bait — a profitable, deliberate theft rather than the accident §6 describes.
No code control distinguishes an owner-self-declared corruption from a DAO-adjudicated one, and nothing prevents recoveryAddress == IAgreement(agreement).owner() — the configuration that makes the lever profitable is the default one, and stakers are given no documented signal to treat it as a red flag.
Runnable suite: test/unit/ZZZSponsorFabricatedCorrupted.poc.t.sol — 3 tests, all passing under forge test (solc 0.8.26, via-IR, optimizer 200). Registry states are staged on MockAttackRegistry to represent the outcome of the real chain the sponsor drives (UNDER_ATTACK == DAO approveAttack; CORRUPTED == sponsor markCorrupted); the real-stack reachability of markCorrupted with no breach is covered by the dependency's own passing AttackRegistryTest.testMarkCorrupted().
The other two legs bound the claim honestly rather than inflate it:
test_refuted_presentModerator_flagsSurvived_refundsStakers — mid-grace, an honest moderator's flagOutcome(SURVIVED) (which accepts a CORRUPTED registry) refunds alice the full P + B, recoveryAddress receives 0, and the later claimExpired() reverts InvalidOutcome. The sponsor can neither pre-empt, block, nor hijack this valve (outcomeModerator is init-only, no setter).
test_noShortcut_autoCorruptedBlockedForEntireGrace — the backstop cannot fire one second before expiry + grace.
So the vector is real and reachable, and it is fully defeated by a present moderator. The finding is the undocumented character of that dependency, not a claim that the guardrail is breakable.
The scope-blind backstop is deliberate — re-defaulting it to EXPIRED would let genuine in-scope corruption escape. The fix should therefore target the role concentration, not the backstop's default. Breaking the trigger↔destination link is a two-line change that makes the lever unprofitable while leaving the backstop's liveness purpose intact:
(IAgreement is already imported at ConfidencePool.sol:17 and agreement is the public state var at :61; the pool already calls IAgreement(agreement).isContractInScope() at :768, so this compiles as-is.)
Stronger alternative — route the permissionless auto-CORRUPTED sweep to a neutral or timelocked escrow rather than directly to the sponsor-set address, leaving the moderator-flagged CORRUPTED path unchanged.
At minimum, document it:
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.