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

`PoolCreated` event omits initialized pool owner

Author Revealed upon completion

Description

ConfidencePoolFactory.createPool() deploys a pool clone and initializes it with the current
caller as the pool owner. This means the created pool has a concrete admin address from the moment
initialization completes.

The factory's PoolCreated event does not emit that owner value. Off-chain consumers that index
pool creation from factory logs alone cannot fully reconstruct the initialized admin state, and may
mis-attribute ownership when pool creation is proxied through another contract rather than called
directly by an EOA.

function createPool(
address agreement,
address stakeToken,
uint256 expiry,
uint256 minStake,
address recoveryAddress,
address[] calldata accounts
) external whenNotPaused returns (address pool) {
// ...
IConfidencePool(pool)
.initialize(
agreement,
stakeToken,
address(safeHarborRegistry),
defaultOutcomeModerator,
expiry,
minStake,
recoveryAddress,
msg.sender, // @> pool owner is initialized here
accounts
);
emit PoolCreated(
agreement,
pool,
stakeToken,
expiry,
minStake,
recoveryAddress,
defaultOutcomeModerator,
address(safeHarborRegistry) // @> owner is omitted from the creation event
);
}

Risk

Likelihood:

  • Pool creation through routers, factories, safes, or other intermediate contracts is a common integration pattern.

  • Indexers and monitoring systems often rely on creation events as their primary source of deployment metadata.

Impact:

  • Off-chain systems cannot reconstruct pool ownership from PoolCreated alone.

  • Admin attribution, alerting, and integration state may be incorrect until an additional owner() read is performed against each created pool.

Proof of Concept

function test_createPool_eventOmitsInitializedOwnerWhenCallerIsAContract() external {
uint256 expiry = block.timestamp + 31 days;
FactoryCaller caller = new FactoryCaller();
MockAgreement contractOwnedAgreement = new MockAgreement(address(caller));
contractOwnedAgreement.setContractInScope(SCOPE_ACCOUNT, true);
registry.setAgreementValid(address(contractOwnedAgreement), true);
address created =
caller.createPool(factory, address(contractOwnedAgreement), address(token), expiry, ONE, recovery, _scope());
assertEq(ConfidencePool(created).owner(), address(caller));
Vm.Log[] memory logs = vm.getRecordedLogs();
// PoolCreated is emitted, but it contains no owner field to recover address(caller).
}

Recommended Mitigation

- event PoolCreated(
- address indexed agreement,
- address indexed pool,
- address stakeToken,
- uint256 expiry,
- uint256 minStake,
- address recoveryAddress,
- address outcomeModerator,
- address safeHarborRegistry
- );
+ event PoolCreated(
+ address indexed agreement,
+ address indexed pool,
+ address stakeToken,
+ uint256 expiry,
+ uint256 minStake,
+ address recoveryAddress,
+ address outcomeModerator,
+ address safeHarborRegistry,
+ address owner
+ );
emit PoolCreated(
agreement,
pool,
stakeToken,
expiry,
minStake,
recoveryAddress,
defaultOutcomeModerator,
- address(safeHarborRegistry)
+ address(safeHarborRegistry),
+ msg.sender
);

Support

FAQs

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

Give us feedback!