outcomeModerator is immutable; a lost or compromised moderator key cannot be rotated on in-flight poolsEach clone copies the factory default into its own outcomeModerator at initialize (src/ConfidencePool.sol:207) and exposes no setter for it — the only owner setters are setRecoveryAddress, setExpiry, setPoolScope, and pause (src/ConfidencePool.sol:611-668). setDefaultOutcomeModerator mutates only the factory default, read solely at clone-creation time (src/ConfidencePoolFactory.sol:91,146-150), so it affects future pools only.
Impact (funds-safe, availability): if the moderator key compromised, every pre-rotation in-flight pool is stuck with it and cannot be resolved by a moderator decision. Funds are permanently trapped: the permissionless claimExpired backstop mechanically resolves the pool after expiry + MODERATOR_CORRUPTED_GRACE (180 days) (src/ConfidencePool.sol:512-555, :53). But that path is scope-blind and can never name an attacker for the good-faith whitehat bounty, so resolution is delayed up to ~6 months and degraded.
Recommendation: document the per-clone moderator immutability and the 180-day backstop explicitly; optionally add a factory-owner path to rotate a live pool's moderator for operational recovery.
The salt is keccak256(abi.encode(agreement, _poolsByAgreement[agreement].length)) (src/ConfidencePoolFactory.sol:86), and the count increments only via the push at :103, which runs after a successful deploy+initialize. A reverted createPool therefore leaves the index unconsumed, and a retry lands on the same predicted address (:87) regardless of a different stakeToken/config. The pool has no generic token rescue — every transfer path moves only the configured stakeToken (src/ConfidencePool.sol:317,403,423,468,506).
Impact: tokens pre-sent to the predicted address in the originally-announced stake token become unreachable if the pool is redeployed with a different token. Realizing loss requires a user to fund a not-yet-deployed CREATE2 address before PoolCreated, the first deploy to revert, and the retry to switch tokens; createPool is gated to the agreement owner (:82), so no third party can force the retry.
Recommendation: commit the pool config to the salt so a retry with different parameters yields a different address:
Alternatively (or additionally), document that a pool address is not committed until PoolCreated is emitted and must not be funded before then.
uint32 upper expiry bound that initialize enforcescreatePool checks only the lower bound expiry >= block.timestamp + 30 days (src/ConfidencePoolFactory.sol:78); ConfidencePool.initialize rejects expiry > type(uint32).max with ExpiryTooFar (src/ConfidencePool.sol:197). Because initialize runs in the same transaction as the clone deploy (:87,90-101), an out-of-range expiry reverts the entire call atomically — there is no dead clone and no persisted wasted gas (correcting a common misstatement of this issue). The only effect is a redundant bound checked deeper/later than necessary, on an absurd (~year-2106+) input that harms only the caller.
Recommendation: add if (expiry > type(uint32).max) revert ExpiryTooFar(); beside the lower-bound check for a clearer, cheaper early revert.
_bonusShare underflow-guard rationale is misstatedThe plus > minus ? plus - minus : 0 guards in _bonusShare (src/ConfidencePool.sol:704-710) are correct and provably never trip, but the common justification ("clamping guarantees each deposit's entry t ≤ T") is wrong: riskWindowStart > riskWindowEnd = T is reachable if a terminal state is observed before an active-risk one (the two latches in _observePoolState at :793-798 are independent). The real reason the score stays non-negative is that each per-deposit term is a square, a·(T − t)², non-negative regardless of the sign of (T − t), combined with the eager global reset keeping per-user and global scores on an identical basis. (Empirically, a staker in the start > T regime still received the full bonus.)
Recommendation: if the guards are documented, state the correct rationale (sum of squares), not the t ≤ T claim.
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.