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

Native EraVM `CREATE2` prevents all ConfidencePool clone deployments

Author Revealed upon completion

Description

ConfidencePoolFactory.createPool() is intended to deploy and initialize a new EIP-1167
ConfidencePool clone for each valid agreement. It derives a per-agreement salt, deploys the
clone, initializes it, and registers the pool.

BattleChain uses native zkSync EraVM semantics. The factory relies on OpenZeppelin's
Clones.cloneDeterministic, which builds minimal-proxy creation bytecode in memory and deploys it
through raw assembly CREATE2. Native EraVM does not support deploying arbitrary dynamically
assembled bytecode through this EVM-style deployment path; zkSync requires compiler-known bytecode
hashes and its ContractDeployer mechanism.

As a result, every createPool() call reverts at clone deployment with
Errors.FailedDeployment(). No pool can be created, initialized, or registered.

function createPool(
address agreement,
address stakeToken,
uint256 expiry,
uint256 minStake,
address recoveryAddress,
address[] calldata accounts
) external whenNotPaused returns (address pool) {
// ... validation omitted ...
bytes32 salt = keccak256(
abi.encode(agreement, _poolsByAgreement[agreement].length)
);
@> pool = Clones.cloneDeterministic(poolImplementation, salt);
IConfidencePool(pool).initialize(
agreement,
stakeToken,
address(safeHarborRegistry),
defaultOutcomeModerator,
expiry,
minStake,
recoveryAddress,
msg.sender,
accounts
);
}

The affected OpenZeppelin implementation executes:

assembly ("memory-safe") {
mstore(0x00, ...)
mstore(0x20, ...)
@> instance := create2(value, 0x09, 0x37, salt)
}

The mstore calls are not the issue: they correctly construct and initialize the required
55-byte byte range. The failure is native EraVM's inability to deploy that dynamically assembled
minimal-proxy bytecode through raw assembly CREATE2.

Risk

Likelihood:

  • Every valid pool-creation request reaches Clones.cloneDeterministic after passing factory
    validation.

  • zkSync Foundry compiling the actual factory emits an EraVM warning on OpenZeppelin's raw
    create2 instruction.

  • A native zkSync execution reproduces the revert deterministically.

Impact:

  • Agreement owners cannot create new pools.

  • No clone can be initialized or recorded in _poolsByAgreement.

  • The protocol's primary pool-creation lifecycle is unavailable on its intended chain.

Proof of Concept

The deterministic PoC is test/fresh/ZkSyncCreate2PoC.t.sol. It deploys the real
ConfidencePool implementation, ConfidencePoolFactory behind its real ERC1967Proxy, and the
required token, registry, and agreement mocks. It then invokes a valid createPool() call and
asserts the native EraVM failure:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Errors} from "@openzeppelin/contracts/utils/Errors.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
/// @dev Run only with foundry-zksync: this intentionally asserts native EraVM behavior.
contract ZkSyncCreate2PoC is Test {
address internal constant SCOPE = address(0xC0FFEE);
address internal agreementOwner = makeAddr("agreement-owner");
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
function test_zkSync_nativeAssemblyCreate2PreventsPoolCreation() external {
MockERC20 token = new MockERC20();
MockSafeHarborRegistry registry = new MockSafeHarborRegistry();
MockAgreement agreement = new MockAgreement(agreementOwner);
agreement.setContractInScope(SCOPE, true);
registry.setAgreementValid(address(agreement), true);
ConfidencePool poolImplementation = new ConfidencePool();
ConfidencePoolFactory factoryImplementation = new ConfidencePoolFactory();
ConfidencePoolFactory factory = ConfidencePoolFactory(
address(
new ERC1967Proxy(
address(factoryImplementation),
abi.encodeCall(
ConfidencePoolFactory.initialize,
(address(registry), address(poolImplementation), moderator)
)
)
)
);
factory.setStakeTokenAllowed(address(token), true);
address[] memory scope = new address[](1);
scope[0] = SCOPE;
vm.prank(agreementOwner);
vm.expectRevert(Errors.FailedDeployment.selector);
factory.createPool(
address(agreement), address(token), block.timestamp + 31 days, 1e18, recovery, scope
);
}
}

Run with native zkSync Foundry:

~/.foundry/versions/foundry_zksync_v0.0.26/forge test \
--zksync --threads 1 \
--match-path 'test/fresh/ZkSyncCreate2PoC.t.sol' -vv

Observed result:

[PASS] test_zkSync_nativeAssemblyCreate2PreventsPoolCreation()

For comparison, the ordinary EVM factory test succeeds:

forge test --match-path 'test/unit/ConfidencePoolFactory.t.sol' \
--match-test 'test_createPool_success' -vv

Recommended Mitigation

Replace EIP-1167 raw assembly clone deployment with a zkSync-native-compatible deployment flow.

  1. Deploy a concrete pool using Solidity's native deployment syntax:

    new ConfidencePool{salt: salt}(...)

    Use constructor-based immutable configuration or a zkSync-compatible initialization design.

  2. Use zkSync's ContractDeployer system contract or supported deployment library with
    compiler-known factoryDeps.

  3. Alternatively, explicitly deploy the protocol as EVM bytecode through zkSync's EVM Bytecode
    Interpreter, then add deployment tests proving createPool() succeeds, the clone has code,
    initialization succeeds, and delegated pool calls execute correctly.

Support

FAQs

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

Give us feedback!