Normal behavior: outcomeModerator is set once at initialize() (with a zero-address guard) and serves as the sole authority for flagOutcome(). Similarly, ConfidencePoolFactory's owner (via OpenZeppelin Ownable) is the sole authority for _authorizeUpgrade, setSafeHarborRegistry, setPoolImplementation, setDefaultOutcomeModerator, setStakeTokenAllowed, pause, and unpause.
The specific issue: there is no rotation or transfer function for outcomeModerator within an already-deployed ConfidencePool. The factory's setDefaultOutcomeModerator only affects pools created after the call, not existing pools. If a moderator's key is lost or compromised, an existing pool has no on-chain path to designate a new moderator, and flagOutcome becomes permanently uncallable by any legitimate party for that pool, with resolution falling back only to the permissionless claimExpired/sweep paths after their respective timeouts.
Likelihood:
Occurs only if a moderator's private key is lost or compromised, or the moderator becomes unresponsive or malicious — an operational/key-management event rather than a smart-contract exploit.
Impact:
For the affected pool, flagOutcome becomes permanently uncallable by any legitimate party.
Resolution is limited to whatever the registry's live state naturally produces via claimExpired's auto-resolution logic. The moderator-driven CORRUPTED path (with proper goodFaith/attacker attribution) becomes unavailable, potentially forcing an EXPIRED-only resolution regardless of actual breach circumstances.
This is a centralization-risk observation consistent with standard competitive-audit practice, not a code defect. The design appears to intentionally treat the moderator as a fixed, non-rotatable trust anchor for the life of the pool, consistent with the project's broader philosophy (seen elsewhere in docs/DESIGN.md) that deadlines and outcomes "must never be extendable" or overridable once set.
Not applicable in the traditional sense, since this is a structural/centralization observation rather than an exploitable code path. The absence can be confirmed directly by inspecting ConfidencePool.sol for any function that writes to outcomeModerator outside of initialize():
Consider, for future versions, an optional moderator-rotation mechanism gated by a timelock or multisig, restricted to a pre-risk-window state only (e.g., only callable while riskWindowStart == 0), to avoid reintroducing the mid-dispute manipulation surface the current design already guards against elsewhere. If the current design is intentional, no change is strictly required, but explicit documentation of this tradeoff in docs/DESIGN.md — alongside the existing sections on deadline and outcome immutability — would help future auditors and integrators understand the operational risk up front.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.