FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

No on-pool recovery path if moderator or factory owner key is lost or compromised

Author Revealed upon completion

Root + Impact

Description

  • 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.

function initialize(..., address outcomeModerator_, ...) external initializer {
if (outcomeModerator_ == address(0)) revert ZeroAddress();
outcomeModerator = outcomeModerator_;
// @> no setter/rotation function exists elsewhere in this contract
}

Risk

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.

Proof of Concept

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():

grep -n "outcomeModerator\s*=" src/ConfidencePool.sol
// Only match: outcomeModerator = outcomeModerator_; inside initialize()
// No other write path exists.

Recommended Mitigation

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.

- (no code change strictly required; optional future enhancement)
+ (optional) add a pre-risk-window-only moderator rotation function, timelocked or multisig-gated

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!