FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

`initialize()` not factory-gated — rogue clones bypass stake-token allowlist (X-2)

Author Revealed upon completion

Root + Impact

Users of rogue pools with non-standard tokens lose funds to fee-on-transfer or rebasing mechanics that the pool accounting cannot handle. This is self-harm only — canonical factory pools are completely unaffected.

Description

  • Pools are created through ConfidencePoolFactory.createPool(), which checks allowedStakeToken[stakeToken] before cloning and initializing. This ensures only owner-approved, standard ERC20 tokens back pools.

  • ConfidencePool.initialize() is external and gated only by the one-time initializer modifier — there is no msg.sender == factory check. Anyone can deploy their own ConfidencePool clone and call initialize() directly with any arbitrary stake token, bypassing the factory's allowlist entirely. This is the documented X-2 cross-contract invariant (On-chain: No).

// ConfidencePoolFactory.sol:createPool — line 77
@> if (!allowedStakeToken[stakeToken]) revert StakeTokenNotAllowed(); // factory check
// ConfidencePool.sol:initialize — line 179
@> function initialize(...) external initializer { // initializer only, no factory gate
@> stakeToken = IERC20(stakeToken_); // accepts ANY token, no allowlist re-check

Risk

Likelihood:

Reason 1 — Anyone can deploy a ConfidencePool clone. The `Clones` library is permissionless. The `initializer` modifier only prevents double-initialization, not unauthorized initialization.
Reason 2 — A rogue pool can be made to look identical to a canonical pool — same implementation address, same registry, same agreement — differing only in the deployer address and stake token.

Impact:

Impact 1 — Users who deposit into a rogue pool with a fee-on-transfer, rebasing, or malicious stake token can lose funds. The pool's accounting assumes standard ERC20 behavior.
Impact 2 — Self-harm only for users of the rogue instance. Canonical factory pools are completely unaffected — the factory's `allowedStakeToken` check still holds for the canonical deployment path.

Proof of Concept

function test_L3_InitializeNoFactoryGate_RoguePool() public {
ConfidencePool implementation = new ConfidencePool();
ConfidencePool roguePool = ConfidencePool(Clones.clone(address(implementation)));
address rogueModerator = makeAddr("rogueModerator");
address rogueOwner = makeAddr("rogueOwner");
// initialize() succeeds — no factory-gate, no allowlist check
roguePool.initialize(
agreement, address(token), address(safeHarborRegistry),
rogueModerator, block.timestamp + 31 days, ONE,
makeAddr("rogueRecovery"), rogueOwner, _defaultScope()
);
assertEq(roguePool.owner(), rogueOwner);
assertEq(roguePool.outcomeModerator(), rogueModerator);
}

Forge output:

[PASS] test_L3_InitializeNoFactoryGate_RoguePool()
Rogue pool deployed outside factory
Self-harm only -- canonical factory pools unaffected

Recommended Mitigation

No code change required. The X-2 invariant is documented and accepted: rogue clones are self-harm only and do not affect canonical factory pools. Users should verify a pool was created through the canonical factory before depositing.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!