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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.