Normal behavior: ConfidencePoolFactory.createPool() is the intended, sole entry point for producing a legitimate pool — it enforces the stake-token allowlist, an agreement-owner authorization gate, and injects the DAO's defaultOutcomeModerator, before atomically deploying an EIP-1167 clone of poolImplementation and calling initialize() on it in the same transaction.
Specific issue: poolImplementation is a single, public, already-deployed address, and EIP-1167 minimal proxies can be cloned by any deployer via Clones.clone(poolImplementation). ConfidencePool.initialize() itself carries no binding back to the factory — it only checks for zero addresses and that the supplied agreement is valid — so any third party can clone the implementation directly and call initialize() with a real, valid agreement they do not own, an arbitrary (non-allowlisted) stake token, and an attacker-controlled moderator/recovery/owner. The result is a contract that is byte-for-byte ABI-identical to an official pool for that agreement, invisible to factory.getPoolsByAgreement(), with no on-chain way for a caller to distinguish it from the genuine article short of checking the registry.
For contrast, this is the ONLY guard protecting the shared implementation contract itself (constructor-time, EIP-1167 clones bypass it entirely):
Anyone with no special privilege can trigger this — the attacker only needs to find one already-deployed, valid BattleChain Safe Harbor agreement with at least one in-scope account (a public, on-chain fact, not a secret), and the public poolImplementation address (readable directly off the factory).
Realizing this into actual victim fund loss additionally depends on a phished depositor skipping the one on-chain authenticity check available (factory.getPoolsByAgreement) — an off-chain, social precondition the attacker cannot force, which is what keeps the realized-harm likelihood at the lower end even though the deployment step itself is trivial and always available.
Impact:
A victim who deposits into the rogue clone can lose up to 100% of their staked principal: the attacker (as the self-installed moderator, using their own fake registry) can flip the fake registry to CORRUPTED, flag the outcome, and sweep the full deposited balance to their own attacker-controlled recoveryAddress.
No legitimate, factory-created pool or the factory itself is ever put at risk (their funds and configuration remain fully protected) — the impact is bounded to whichever off-registry, unofficial clones a phished user is tricked into using, which is a real, protocol-adjacent property (the factory-level allowlist and DAO-moderator provenance guarantees are supposed to hold system-wide, but do not extend to the pool's own init path).
This is the exact, unmodified content of test/poc/F-002-PoC.t.sol. It is fully self-contained — it only imports forge-std/Test, OpenZeppelin's Clones, the real ConfidencePool contract, and the project's existing mock contracts under test/mocks/; it does not depend on any custom base-test class. Save it to that path in the project and run:
Real captured output:
Deployment ordering requirement: ConfidencePoolFactory is deployed behind an ERC1967Proxy (script/Deploy.s.sol), so its proxy address is fixed at proxy-deployment time, before the factory's own initialize() runs. Deploy the factory's ERC1967Proxy first (its address is now fixed and known, even though the factory isn't initialized yet), then deploy the ConfidencePool implementation with that proxy address passed to its constructor as FACTORY, then point the factory's poolImplementation at it. Every clone made via Clones.clone(poolImplementation) shares the same implementation bytecode and therefore the same embedded FACTORY value — so initialize() reverts for anyone except the real, already-fixed factory address, closing the vector directly rather than adding a second oracle for users to optionally check.
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.