Skip to content
On this page

L2-7: CREATE2 (Deterministic Deployment)

1. Problem

When deploying a contract on Ethereum, the contract address is determined jointly by the deployer's address and their nonce (CREATE opcode). This means the same contract code gets different addresses on different chains, and the address cannot be known before deployment. For cross-chain bridges, L2 scaling solutions, ERC-4337 smart wallets, and other scenarios requiring "same address across multiple chains," this is a deal-breaker.

The CREATE2 opcode solves this problem -- it makes the contract address depend solely on: the deployer's address, a 32-byte salt, and the hash of the contract's initialization code. As long as these three remain unchanged, the resulting address is the same regardless of which chain you deploy on or how many times you deploy. Even more powerful: you can compute the address before deployment, enabling "counterfactual instantiation" -- sending funds to and interacting with a contract before it actually exists.

This challenge requires implementing a Create2Deployer contract (refer to src/level2/Create2Deployer.sol) that supports CREATE2 deployment with arbitrary salt and initCode, and provides on-chain address prediction.

2. Why

CREATE2 is one of Ethereum's most critical opcodes. L2 bridge contracts (such as Optimism and Arbitrum's L1 bridges) rely on CREATE2 to deploy mirror contracts at the same address on L1 and L2. The EntryPoint contract in ERC-4337 account abstraction uses the same CREATE2 address on all EVM chains, ensuring wallet compatibility. Uniswap V4 Hook contracts also leverage CREATE2 for deterministic deployment.

Understanding CREATE2's address formula is an important step toward deeply grasping Ethereum's underlying EIP mechanisms. The address formula is more than just a keccak256 computation -- it involves concatenating four elements: the 0xff prefix, the deployer address, the salt, and the initCodeHash. The 0xff prefix is specifically designed to distinguish CREATE2 addresses from CREATE addresses, preventing collision attacks. Mastering this formula means you can independently compute the same address both on-chain and off-chain without relying on any library, which is critical for security auditing and protocol design.

Furthermore, CREATE2 has an important characteristic: even if a contract at an address has been destroyed via SELFDESTRUCT, the same salt + initCode combination can deploy a new contract at the same address again. This "address reuse" feature is both a powerful tool and a potential security trap -- you must explicitly check address.code.length > 0 before deployment to prevent overwriting already-deployed contracts.

3. Solution

Core Architecture

The Create2Deployer contract provides three layers of interface:

  1. Deployment layer (deploy function): executes the CREATE2 opcode using inline assembly, checking whether the address is already occupied before deploying
  2. Address computation layer (computeAddress function): implements the standard CREATE2 address formula, with two overloads accepting (salt, initCodeHash) and (salt, initCode) respectively
  3. Utility layer (computeInitCodeHash, isDeployed): provides initCode hash computation and deployment status querying

Address Formula

address = last 20 bytes of keccak256(0xff + sender + salt + keccak256(initCode))

Where:

  • 0xff: a single-byte constant prefix to prevent collisions with CREATE addresses
  • sender: the deployer contract address (20 bytes)
  • salt: a user-specified 32-byte arbitrary value
  • keccak256(initCode): the keccak256 hash of the initialization code (32 bytes)

Key Implementation Details (refer to src/level2/Create2Deployer.sol)

solidity
// Deployment function -- compute first, then deploy
function deploy(bytes32 _salt, bytes calldata initCode) external returns (address deployedAddress) {
    if (initCode.length == 0) revert EmptyInitCode();

    // Precompute address and check if already deployed
    deployedAddress = computeAddress(_salt, computeInitCodeHash(initCode));
    if (deployedAddress.code.length > 0) revert AlreadyDeployed();

    // Copy initCode from calldata to memory
    bytes memory _initCode = initCode;
    assembly {
        // create2(value, offset, length, salt)
        deployedAddress := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
    }

    if (deployedAddress == address(0)) revert DeploymentFailed();
    emit Deployed(deployedAddress, _salt, msg.sender);
}

Key points:

  • add(_initCode, 0x20): skips the 32-byte length prefix of Solidity's dynamic bytes array, pointing directly to the actual byte data
  • mload(_initCode): reads the length prefix to obtain the byte count of initCode
  • create2 returns address(0) to indicate deployment failure (e.g., insufficient gas or initCode execution revert)
solidity
// Address computation -- pure function, callable both on-chain and off-chain
function computeAddress(bytes32 _salt, bytes32 initCodeHash) public view returns (address addr) {
    bytes32 hash = keccak256(
        abi.encodePacked(bytes1(0xff), address(this), _salt, initCodeHash)
    );
    addr = address(uint160(uint256(hash)));
}

Cross-Chain Same-Address Deployment

To obtain the same address on different chains, the deployer contract (Create2Deployer) itself must have the same address on each chain. This can be achieved in two ways:

  1. Use the same deployer address on all target chains (requires a deterministic deployer account, such as Nick's Method using the 0x4e59b44847b379578588920cA78FbF26c0B4956C singleton factory)
  2. Control the determinism of the first factory address through the nonce of the originating transaction

4. Pitfalls Encountered

  • Insufficient deployed-address checking: checking only deployedAddress.code.length > 0 is not enough; after SELFDESTRUCT, code.length becomes 0, causing the contract to be accidentally overwritten
  • initCode memory layout errors: when passing the initCode pointer directly in assembly, forgetting that the first 32 bytes of Solidity bytes is the length prefix causes CREATE2 to read incorrect data
  • initCodeHash confusion: confusing keccak256(initCode) with the final bytecode containing constructor arguments, causing the computed address not to match the actual deployment address
  • Cross-chain gas limit differences: different chains have different gas limits; larger initCode may succeed on one chain but fail on another, making the "same address" goal unachievable
  • Metamorphic contract risk: using CREATE2 + SELFDESTRUCT allows deploying different code at the same address, potentially enabling malicious contract "upgrade" attacks

5. Why the Pitfalls Happen

Deployed-address checking relies on code.length, which is a runtime dynamic property. After a contract is SELFDESTRUCTed, the code is removed from state but the address still exists -- the next CREATE2 with the same salt + initCode will generate a new contract at the same address. This "address resurrection" feature is useful in some scenarios (such as Metamorphic Contract Factory), but if not guarded against, it can accidentally overwrite user funds or logic.

The initCode memory layout issue stems from Solidity's ABI encoding: in memory, the bytes type stores the length in the first 32 bytes, with actual byte data starting at offset 32. In calldata, the ABI encoding itself has a 32-byte offset and length field. When using assembly's create2, you need to control pointer position and length yourself -- add(_initCode, 0x20) skips the length prefix, mload(_initCode) reads the length value.

The target of initCodeHash computation must be the contract's "creation bytecode," not the "runtime bytecode." Creation bytecode includes constructor logic and arguments, and only after execution does it produce the runtime bytecode. Many people copy runtime bytecode from Remix or Etherscan and use it to compute addresses, which leads to completely wrong results.

6. How to Resolve the Pitfalls

For insufficient deployed-address checking: in addition to checking code.length > 0, add extra anti-overwrite mechanisms. For example, record used salts in a mapping, or check whether a contract is "active" (e.g., via a specific storage slot value):

solidity
mapping(bytes32 => bool) public saltUsed;

function deploy(bytes32 _salt, bytes calldata initCode) external returns (address) {
    // Double check: code length + salt usage record
    address predicted = computeAddress(_salt, computeInitCodeHash(initCode));
    if (deployed.code.length > 0 || saltUsed[_salt]) revert AlreadyDeployed();
    saltUsed[_salt] = true;
    // ... deployment logic
}

For initCode memory layout errors: always copy calldata bytes to a memory variable before passing into the assembly block; use add(ptr, 0x20) to skip the length prefix; use mload(ptr) to get the length. Avoid directly manipulating calldata bytes variables in assembly (Solidity 0.8 calldata variables have different representations in assembly):

solidity
bytes memory _initCode = initCode;  // copy to memory
assembly {
    addr := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
}

For initCodeHash confusion: ensure you use the keccak256 hash of contract creation bytecode. In Foundry, use vm.getCode("ContractName") to get creation bytecode; in ethers.js, use ethers.keccak256(contractFactory.bytecode). Always use the computeInitCodeHash() helper function as a unified hash computation entry point.

For cross-chain gas limits: verify gas consumption on testnets of target chains before deployment; design initCode to avoid overly large constructor logic; use immutable variables instead of storage writes in constructors to reduce deployment gas.

7. Technical Highlights

Key PointDescription
CREATE2 vs CREATECREATE2 address does not depend on nonce, only on (0xff, deployer, salt, initCodeHash)
Address formulakeccak256(0xff + deployer + salt + keccak256(initCode))[:20]
0xff prefixPrevents collisions with CREATE addresses, clearly distinguishing the two deployment methods in address space
Counterfactual instantiationCompute and interact with an address (send ETH, approve, etc.) before the contract is deployed
Metamorphic contractsCREATE2 + SELFDESTRUCT = same address can deploy different code; use with caution
Assembly opcodecreate2(value, offset, length, salt) -- note that offset must skip the bytes length prefix
Cross-chain deploymentThe deployer address itself must be the same on each chain (via deterministic deployer or nonce control)
Address reuseAddresses after SELFDESTRUCT can be reused by CREATE2; code.length check is needed

Built with AiAda