ConfidencePoolFactory.createPool() deploys a new pool clone, initializes it, then records it in _poolsByAgreement. The mapping tracks all pools per agreement and is queried via getPoolsByAgreement() and poolCountByAgreement().
The state write _poolsByAgreement[agreement].push(pool) (line 103) happens after the external call IConfidencePool(pool).initialize(...) (line 90–101). Inside initialize(), _replaceScope() calls IAgreement(agreement).isContractInScope(account) for every scope account, transferring execution control to the caller-supplied agreement contract. During this entire window, _poolsByAgreement has not yet been updated, violating Checks-Effects-Interactions.
Likelihood:
A malicious or compromised agreement contract must override isContractInScope() to trigger a reentrant call. A reenter with the same agreement is self-neutralizing (same salt → same address → CREATE2 fails), limiting direct exploitation.
Exploitation becomes realistic only if a future factory method reads poolCountByAgreement and makes security decisions based on it during the same transaction.
Impact:
During the initialize() execution window, poolCountByAgreement(agreement) returns a stale value (one fewer than reality). Any on-chain logic or integration that reads this during the window operates on incorrect state.
Future factory upgrades that add logic relying on _poolsByAgreement correctness within the same call could introduce exploitable reentrancy paths, as the invariant is silently broken today.
The attack requires a malicious agreement contract whose isContractInScope() function re-enters the factory. When createPool() calls pool.initialize(), control passes to _replaceScope(), which calls agreement.isContractInScope(account) for each scope account. At this exact point, _poolsByAgreement[agreement] has not yet been updated, so its length is stale. A reentrant call to createPool() with a different agreement address will succeed because the salt for the new pool uses a different key. The original agreement's pool count remains wrong until the outermost call completes.
Capture the current pool index before deploying, push to the mapping immediately after deployment (before any external call), then initialize:
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.