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

Factory CREATE2 salt ignores `poolImplementation`, so an implementation change invalidates predicted pool addresses

Author Revealed upon completion

Factory CREATE2 salt ignores poolImplementation, so an implementation change invalidates predicted pool addresses

Description

Off-chain integrators and users may compute the future pool address with Clones.predictDeterministicAddress(implementation, salt, factory) and optionally pre-fund or display that address before createPool executes.

Factory salt is only keccak256(abi.encode(agreement, index)). ERC-1167 initcode embeds the implementation, so a factory-owner setPoolImplementation() changes the CREATE2 address for the same salt. Tokens or approvals prepared against the old predicted address no longer match the pool that is actually deployed.

bytes32 salt = keccak256(abi.encode(agreement, _poolsByAgreement[agreement].length));
// @> Salt does not include poolImplementation.
pool = Clones.cloneDeterministic(poolImplementation, salt);
function setPoolImplementation(address newPoolImplementation) external onlyOwner {
// @> Future predicted addresses diverge for the same agreement/index salt.
poolImplementation = newPoolImplementation;
}

Risk

Likelihood:

  • Occurs when addresses are predicted under one implementation and the factory owner updates poolImplementation before create.

  • Requires an off-chain or front-end workflow that stores or funds the predicted address across an implementation change.

Impact:

  • Prefunded tokens or approvals can be sent to an unused counterfactual address.

  • There is no theft from an existing pool; the loss is stranded capital or a broken integration against a stale prediction.

Proof of Concept

Setup: append the function below to test/unit/ConfidencePoolFactory.t.sol (same ConfidencePoolFactoryTest harness already used by the factory suite), then run:

forge test --match-test testImplementationChangeInvalidatesPredictedPoolAddress -vv
  1. Predict the next pool address for (agreement, index = 0) under the current poolImplementation.

  2. The factory owner calls setPoolImplementation to a new implementation.

  3. The same salt now predicts a different address.

  4. Prefunding the old predicted address strands tokens there.

  5. createPool deploys at the new prediction, so the stranded balance never reaches the live pool.

// PoC: CREATE2 salt is only (agreement, index), so changing poolImplementation invalidates
// any previously predicted pool address for the same agreement/index.
function testImplementationChangeInvalidatesPredictedPoolAddress() external {
bytes32 salt = keccak256(abi.encode(address(agreement), uint256(0)));
address predictedBefore =
Clones.predictDeterministicAddress(address(poolImplementation), salt, address(factory));
ConfidencePool newImpl = new ConfidencePool();
factory.setPoolImplementation(address(newImpl));
address predictedAfter = Clones.predictDeterministicAddress(address(newImpl), salt, address(factory));
assertTrue(predictedBefore != predictedAfter, "same salt yields a different address after impl change");
// Prefunding the stale prediction strands tokens away from the pool that is actually created.
token.mint(predictedBefore, 5 * ONE);
vm.prank(agreementOwner);
address created =
factory.createPool(address(agreement), address(token), block.timestamp + 31 days, ONE, recovery, _scope());
assertEq(created, predictedAfter);
assertTrue(created != predictedBefore);
assertEq(token.balanceOf(predictedBefore), 5 * ONE, "prefund stranded at stale counterfactual");
assertEq(token.balanceOf(created), 0, "live pool did not receive the stranded prefund");
}

Recommended Mitigation

Document that predicted addresses are invalid after setPoolImplementation, or include the implementation in the salt or prediction helper that front-ends must use.

- bytes32 salt = keccak256(abi.encode(agreement, _poolsByAgreement[agreement].length));
+ bytes32 salt = keccak256(
+ abi.encode(agreement, _poolsByAgreement[agreement].length, poolImplementation)
+ );
pool = Clones.cloneDeterministic(poolImplementation, salt);

Support

FAQs

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

Give us feedback!