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

Factory `createPool` Does Not Pre-Validate `expiry` Upper Bound

Author Revealed upon completion

Root + Impact

Description

The factory's createPool validates expiry < block.timestamp + _MIN_EXPIRY_LEAD (lower bound, 30 days) but does NOT check expiry <= type(uint32).max (upper bound). This check exists in ConfidencePool.initialize() (line 197) and setExpiry() (line 625), but by the time initialize catches it, the clone has already been deployed at line 87. The caller wastes gas on the clone deployment without a factory-level early check.

// ConfidencePoolFactory.sol — createPool() lines 78-87
@> if (expiry < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();
// Missing: if (expiry > type(uint32).max) revert ExpiryTooFar();
// ...
@> pool = Clones.cloneDeterministic(poolImplementation, salt); // deployed before init check

Risk

Likelihood: Low — requires caller to pass expiry > uint32 max, which is an input error.

Impact: Low — gas wasted on clone deployment. No funds lost. The factory should provide consistent validation with the pool.

Proof of Concept

File: L7-Factory-Expiry-Prevalidate.poc.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
/// @notice PoC for L-7: Factory createPool doesn't pre-validate expiry uint32 upper bound
contract L7_Factory_Expiry_Prevalidate_POC is BaseConfidencePoolTest {
function testPOC_L7_expiry_checkedInPoolInit_notInFactory() external {
// The pool's initialize checks expiry > type(uint32).max and reverts ExpiryTooFar.
// The factory's createPool does NOT — the clone is deployed before the revert.
ConfidencePool implementation = new ConfidencePool();
ConfidencePool freshPool = ConfidencePool(Clones.clone(address(implementation)));
// Expiry above uint32 max passes factory validation but fails pool init
uint256 tooFarExpiry = type(uint256).max;
vm.warp(tooFarExpiry - 31 days); // satisfy the 30-day lead check
vm.expectRevert(IConfidencePool.ExpiryTooFar.selector);
freshPool.initialize(
agreement, address(token), address(safeHarborRegistry),
moderator, tooFarExpiry, ONE, recovery, address(this), _defaultScope()
);
// Clone is already deployed — gas wasted. Factory has no pre-flight check.
}
}

Run: forge test --match-path 'L7-Factory-Expiry-Prevalidate.poc.t.sol' -vv

Recommended Mitigation

Add the upper-bound check in the factory before clone deployment:

+ if (expiry > type(uint32).max) revert ExpiryTooFar();
if (expiry < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();

Support

FAQs

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

Give us feedback!