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

Missing Upper-Bound Expiry Validation In Factory Enables Permanent Clone Orphaning

Author Revealed upon completion

Description

ConfidencePoolFactory.createPool only validates the lower bound on expiry (must be at least 30 days from now) but does not validate the upper bound (expiry <= type(uint32).max). The pool's initialize function DOES check both bounds. This validation asymmetry creates a reachable input that passes the factory but fails initialize, directly triggering the orphaned-clone DoS described in Finding 1.

// ConfidencePoolFactory.sol L78
if (expiry < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();
// @> No check for expiry > type(uint32).max — values like 2^32 + 1 pass
// ConfidencePool.sol L196-197 (inside initialize)
if (expiry_ < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();
if (expiry_ > type(uint32).max) revert ExpiryTooFar(); // @> THIS check causes the init revert

Risk

Likelihood:

  • Any agreement owner can pass expiry = type(uint32).max + 1 (or any value above 4294967295). The factory's only check (expiry >= now + 30 days) trivially passes for such large values.

  • This is a single-parameter input validation gap that directly compounds into Finding 1's permanent DoS.

Impact:

  • Enables the permanent bricking of pool creation for an agreement (see Finding 1).

  • Since expiry is a uint256 parameter, there is a large input space (any value from type(uint32).max + 1 to type(uint256).max) that triggers this path.

Proof of Concept

contract Finding3_MissingExpiryUpperBound is Test {
// ... (same setup as Finding1_OrphanedCloneDoS)
function test_finding3_factoryAllowsOversizedExpiry() external {
uint256 overflowExpiry = uint256(type(uint32).max) + 1;
// Factory lower-bound check passes (value is enormous)
assertGt(overflowExpiry, block.timestamp + 30 days);
// Pool initialize() fails with ExpiryTooFar — clone orphaned
vm.prank(sponsor);
vm.expectRevert(IConfidencePool.ExpiryTooFar.selector);
factory.createPool(
address(agr), address(token), overflowExpiry, ONE, recovery, _scope()
);
}
}

Run: forge test --match-contract Finding4_MissingExpiryUpperBound -vvv

Terminal Output:

Ran 1 test for test/audit/AuditPoC.t.sol:Finding4_MissingExpiryUpperBound
[PASS] test_finding4_factoryAllowsOversizedExpiry() (gas: 28005)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.37ms (152.33µs CPU time)

Recommended Mitigation

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

To address the missing upper-bound expiry validation, we introduce an explicit type(uint32).max check within ConfidencePoolFactory.createPool. This ensures the factory enforces the same upper limit as the pool's initialize function, preventing oversized inputs from passing the factory logic only to revert and trigger clone orphaning.

Support

FAQs

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

Give us feedback!