Developing Factory Contracts with CREATE2 (Deterministic Deployment)
CREATE2 is an EVM opcode (EIP-1014) introduced in the Constantinople hard fork. The address of a deployed contract is calculated beforehand: it's deterministic from deployer address, salt, and keccak256(initcode). Change any parameter and you get a different address. This radically changes protocol architecture approach.
Where CREATE2 Solves Real Problems
Counterfactual deployment. A user can obtain the address of their Smart Account before deployment. They can pass this address to receive funds — while the contract only deploys on the first transaction. EIP-4337 account abstraction is entirely built on this pattern: initCode in UserOperation contains a factory call that deploys the Account contract at a predictable address through CREATE2.
Uniswap V2/V3 pair addresses. Any Uniswap V2 pool address is calculated off-chain using the CREATE2 formula: keccak256(abi.encodePacked(hex'ff', factory, keccak256(abi.encodePacked(token0, token1)), INIT_CODE_PAIR_HASH)). The Router doesn't store a mapping of pairs — it calculates the address on the fly. This saves thousands of SLOAD operations.
Cross-chain consistency. A protocol deploys contracts on 8 chains at the same address. Users and integrators know the address in advance, whitelists are configured once. This is achieved through Arachnid's Deterministic Deployment Proxy (0x4e59b44847b379578588920cA78FbF26c0B4956C) — also known as Nick's factory, deployed on hundreds of chains with identical address.
Implementing Factory with CREATE2
contract ContractFactory {
event Deployed(address indexed contractAddress, bytes32 indexed salt);
function deploy(bytes memory bytecode, bytes32 salt)
external returns (address contractAddress) {
assembly {
contractAddress := create2(
0, // value (ETH)
add(bytecode, 0x20), // bytecode start (skip length prefix)
mload(bytecode), // bytecode length
salt // salt
)
}
require(contractAddress != address(0), "Deploy failed");
emit Deployed(contractAddress, salt);
}
function computeAddress(bytes memory bytecode, bytes32 salt)
external view returns (address) {
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
));
return address(uint160(uint256(hash)));
}
}
Initialization via Constructor vs Initializer
With CREATE2 deployment, a contract with constructor parameters includes them in the initcode — same parameters give the same address, different ones give different addresses. This is normal.
If the contract uses a proxy pattern (deployed as minimal proxy via CREATE2, with implementation separate), the constructor doesn't run. Initialization through an initialize() function is mandatory, and it must be protected from double-invocation (OpenZeppelin's initializer modifier or a manual flag).
Subtlety: CREATE2 with a single salt can only be executed once — if a contract already exists at that address, deployment returns address(0). If the contract was selfdestruct-ed (before Cancun EIP-6780), the address is freed and CREATE2 with the same salt can be repeated. After Cancun EIP-6780, selfdestruct only removes ETH, the code remains — the address is not reused.
Salt Design
Salt is bytes32. A careless salt opens frontrunning: someone sees your deployment transaction in the mempool, takes the same bytecode and salt, deploys first to the desired address. Your transaction fails.
Protection: include msg.sender in the salt:
bytes32 salt = keccak256(abi.encodePacked(msg.sender, userProvidedSalt));
Now an adversary with a different msg.sender gets a different address — your address is inaccessible to them.
For protocols where the address must be identical on all chains regardless of deployer, use a fixed salt without msg.sender — but then deployment goes through a trusted deployer (multisig or deployment script with DEPLOY_KEY).
Minimal Proxy (EIP-1167) + CREATE2
This combination is often used in protocols with thousands of instances (lending positions, yield vaults, game characters). Minimal proxy is a 45-byte contract that delegates all calls to the implementation. Deployment costs ~40K gas instead of 200K-500K for a full contract.
function deployProxy(address implementation, bytes32 salt)
external returns (address proxy) {
bytes memory bytecode = abi.encodePacked(
hex"3d602d80600a3d3981f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3"
);
assembly {
proxy := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
require(proxy != address(0), "Deploy failed");
IInitializable(proxy).initialize(/* params */);
}
OpenZeppelin provides Clones.cloneDeterministic(implementation, salt) — a ready-made wrapper over this pattern.
Testing
Foundry simplifies CREATE2 testing: vm.computeCreate2Address(salt, keccak256(bytecode), deployer) gives the predictable address in tests. Check:
- Deployed address matches the calculated
computeAddress() - Re-deploying with the same salt returns address(0)
- Initialization via
initialize()isn't called twice - Frontrunning salt is protected (if needed)
Timeline
Basic factory with CREATE2 and address computation: 2-3 days. Factory with minimal proxy pattern and initialization: 3-4 days. Multi-chain deployment infrastructure with Arachnid proxy: 4-5 days including deployment scripts and verification.
Cost is calculated after clarifying deployment infrastructure requirements and target chain count.







