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