ConfidencePoolFactory.createPool deploys each pool as a minimal proxy clone pointing at poolImplementation, then immediately calls initialize() on it through a delegatecall. This is the standard EIP-1167 pattern, cheap to deploy, all the real logic lives at one shared implementation address.
OpenZeppelin's own documentation on Clones.cloneDeterministic states plainly that it does not check if implementation has code, and that a clone pointing at a code-less address cannot actually be initialized, even though the initialization call itself will not revert, it just silently does nothing. Neither initialize() nor setPoolImplementation() on the factory check poolImplementation.code.length > 0, only != address(0). If poolImplementation ever points at an address with no code, whether that happens at the factory's own initial setup or later through setPoolImplementation during an upgrade, every pool created against it looks completely normal on the surface, the clone deploys, createPool does not revert, but the clone was never actually initialized. It sits there, ownerless, waiting for anyone to call initialize() on it directly the moment real code eventually lands at that address.
Likelihood:
The initial deployment itself is safe, the real implementation is deployed inline in the same script run before the factory ever gets initialized with it. The realistic window is a later setPoolImplementation call during an upgrade, a separate manual admin action with no companion tooling anywhere in this repo to catch a mistake, for example a wrong address copied from a different environment, or a multisig batch where the "point the factory at it" transaction lands before the "deploy the new implementation" transaction actually confirms.
Once that single mistake happens, exploiting it needs no attacker sophistication at all. There is no custom bytecode involved, no CREATE2 racing, the attacker just calls initialize() directly on an already-deployed clone the moment real code exists at the implementation address, using nothing but the team's own unmodified contract
Impact:
Because the attacker calls initialize() directly instead of going through createPool, none of the factory's own protections apply. They can name themselves both owner and outcomeModerator on the same pool, something that is structurally impossible through the normal path, where the moderator always comes from the factory's own default, independent of who owns the pool.
They can also wire in a safeHarborRegistry they fully control, so they decide unilaterally when the pool reads as CORRUPTED, no real breach needed anywhere.
The hijacked pool is not merely an address that resembles a real one. It is still literally listed by getPoolsByAgreement as an official pool for that agreement, since that record gets written during the original broken createPool call and is never revoked. There is no function anywhere to delist a pool, so even a sponsor who notices the mistake immediately and creates a correct replacement pool cannot remove the broken one from that list, it stays exposed regardless of how responsible they are afterward.
Pausing the factory, the only emergency lever available, does not help. initialize() has no pause awareness at all, only createPool does, so a team that notices the broken implementation and pauses the factory to contain the damage has not actually stopped this.
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.