createPool clones whatever address poolImplementation currently points to. setPoolImplementation is a plain onlyOwner setter with no timelock, no pending-value/commit-reveal pattern, and no way for the calling sponsor to pin or verify the value they expect.
Because EIP-1167 clones bake their delegatecall target into their own bytecode at deploy time, whichever implementation is live in the exact block createPool executes is permanently bound to that one clone — completely independent of what it was before or what it becomes immediately after. A malicious implementation isn't limited to feeding the clone false external state; it can carry arbitrary extra logic (e.g. an owner-only drain function) with the real ConfidencePool's full storage layout underneath it, bypassing every accounting/entitlement check the real contract enforces.
createPool also reads safeHarborRegistry live the same way, with the same lack of commitment — but that half of the surface is narrower: docs/DESIGN.md §11 already treats a registry reporting false state as an accepted, out-of-model trust assumption ("no funds can be stolen" that way, only resolution outcome/liveness). The poolImplementation swap below is the vector that actually defeats that claim, so it's the one this submission centers on.
A malicious (or transiently-compromised) factory owner can watch the mempool for a sponsor's createPool transaction, front-run it with setPoolImplementation(evilImpl), let the sponsor's original transaction land unchanged and clone the evil implementation, then immediately back-run with setPoolImplementation(realImpl). Every other pool — past and future — is completely untouched; only the one targeted sponsor's clone runs malicious code.
Likelihood:
Requires the factory-owner role, but no special preconditions beyond that — no waiting on an external event, no cooperation from anyone else. A privileged actor with mempool visibility can execute the full attack unilaterally, atomically, against any single targeted createPool call, at a time entirely of their choosing.
Doesn't strictly require malice to be triggered: an ordinary, honest setPoolImplementation upgrade landing in the same block as an unrelated sponsor's createPool produces the same order-dependent binding by accident, with neither party able to detect or prevent it from the calldata alone.
Impact:
The victim's clone is indistinguishable from a legitimate pool through any normal interaction — initialize, stake, and every event fire exactly as expected (confirmed in the PoC below) — while silently running attacker-authored bytecode with the real ConfidencePool's full storage layout plus arbitrary extra logic (e.g. a backdoored drain function).
Directly contradicts the README's stated Factory Owner LIMITATION: "cannot move funds held by pool clones." This attack doesn't call a fund-moving function on the factory directly, but deterministically achieves the same outcome by controlling which code a specific future clone executes — a full, silent, single-pool fund drain.
EvilConfidencePool inherits the real ConfidencePool wholesale (so initialize/stake/etc. behave identically) and adds one extra function only its deployer knows about:
Run: forge test --match-path "test/FactoryImplementationFrontrun.t.sol" -vvvv → PASS (gas: 704667)
Let the sponsor pin the exact implementation they expect at call time, so a mid-flight swap causes the call to revert instead of silently binding to something the sponsor never inspected.
This closes both the deliberate front-run and the accidental race: the sponsor's transaction now reverts cleanly instead of silently binding to whatever happened to be live, and they can safely retry once they've re-checked the new value. Pairing it with a short timelock on setPoolImplementation (so a change is visible for at least one block before it can affect a createPool call) further shrinks the window without requiring every integrator to remember to pass the new parameter. The same expected* pinning pattern extends to safeHarborRegistry too if the weaker, DESIGN.md-accepted registry-swap variant is worth closing at the same time.
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.