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).
@> if (!allowedStakeToken[stakeToken]) revert StakeTokenNotAllowed();
@> function initialize(...) external initializer {
@> stakeToken = IERC20(stakeToken_);
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");
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.