CREATE3 explained: how deterministic deployment works beyond CREATE2

In the first article of our series on CREATE3 we’ll discuss what a CREATE3 address actually depends on and when two "CREATE3 deployments" land on the same address.
CREATE3 is a deployment technique that lets developers keep the same smart contract address even if they change the contract before deploying it. We’re digging deep into it CREATE3 to explore options it opens up for developers.
1. Why 1inch needs deterministic deployment
1inch deploys related smart contracts across multiple Ethereum Virtual Machine (EVM) chains. For these deployments, a contract address is part of the integration interface: other contracts, deployment scripts, configuration files and off-chain services may need it. This creates two practical requirements:
- Use the same address on every chain. A shared address reduces chain-specific configuration and lets integrations refer to one contract address across supported networks.
- Use a pre-mined vanity address on every chain. Some contracts need an address with a recognizable hexadecimal prefix or another chosen pattern. The address must be mined before deployment and reproduced on each target chain.
The EVM does not provide one deployment mechanism that satisfies every version of these requirements. Plain CREATE derives an address from the deployer and its nonce. CREATE2 replaces the nonce with a salt and the target's init-code hash, which supports vanity mining but binds the mined address to the exact creation code. CREATE3 is not an opcode: it combines CREATE2 and CREATE so the final address can depend on the factory address and salt rather than the target's creation code. This distinction matters when constructor arguments differ between chains or the bytecode changes after a vanity salt has been mined.
This article first examines the native CREATE and CREATE2 address derivations, then shows how the CREATE3 pattern composes them and which conditions must hold to reproduce a target address across chains.
2. How CREATE determines a contract address
The Ethereum Virtual Machine (EVM) has exactly two contract-creating instructions: CREATE (opcode 0xf0) and CREATE2 (0xf5). Everything else, including CREATE3, is built from these two.
For CREATE, the new address depends on the deployer and the deployer's account nonce:
address = keccak256(rlp([deployer, nonce]))[12:]
Simplified:
address = f(deployer, nonce)
rlp is Recursive Length Prefix encoding, Ethereum's canonical serialization; [12:] means the last 20 bytes of the 32-byte hash. The executable Ethereum specification implements exactly this (compute_contract_address), using the deployer's nonce as it was before the creation increments it.
Two properties follow:
- The created contract's code is not an input: what lands there is whatever init code you run.
- The address is predictable only if the nonce is. For an externally owned account that means predicting its transaction count at deployment time; for a contract, tracking every creation it performs with CREATE or CREATE2 (even some failed creations consume a nonce).
That second property is why CREATE alone does not solve cross-chain deployment: it means keeping an account's nonce synchronized across half a dozen networks, forever. One emergency transaction on one chain, and that chain's future addresses diverge until you pad the nonce everywhere else.
3. How CREATE2 improves deterministic deployment
EIP-1014 added the second creation opcode, whose formula replaces the nonce with two values the deployer chooses:
address = keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode))[12:]
Simplified:
address = f(deployer, salt, initCode)
where ++ is byte concatenation. Each input has a distinct job:
- deployer is the contract executing CREATE2 (20 bytes); addresses are namespaced per deployer, so two factories can use the same salt without colliding.
- salt is an arbitrary 32-byte value: the "which one of my deployments is this" selector.
- keccak256(initCode) commits the address to the exact init code: the bytecode that executes at creation, running the constructor and returning the runtime code.
- 0xff is a one-byte domain separator. EIP-1014's rationale: an RLP encoding of [deployer, nonce] can never start with 0xff (that prefix would imply an absurdly long payload). So a CREATE2 address can never collide with a CREATE address, and the hash preimage has a fixed size of 85 bytes.
This buys a lot. The address no longer depends on account history: a deployment can happen at any time, from any transaction, and still land where predicted. Salts can be brute-forced for vanity addresses. And with no chain-specific input in the formula, the same (deployer, salt, init code) triple produces the same address on every chain that implements Ethereum's creation semantics.
4. The limitation of CREATE2: the address commits to the init code
different initCode → different address
To see how sensitive this is, look at what "init code" contains for a Solidity contract. Сonstructor arguments are ABI-encoded and appended after the compiled creation bytecode, and by default the compiler embeds in the bytecode a metadata hash covering the compiler version, the settings and the source files.
So the CREATE2 address changes when any of the following changes:
- Constructor arguments. A different wrapped native currency address per chain means a different init code per chain, and "same address everywhere" is gone.
- The compiler version. With solc's default metadata mode, upgrading the compiler changes the creation bytecode even if the source is untouched. (Teams doing deterministic deployments sometimes strip the metadata hash for this reason.)
- Optimizer settings. Measured concretely (solc 0.8.30, the small contract from section 10): optimizer at 200 runs, at 1,000,000 runs, and off produce three different creation-bytecode hashes.
- The source itself. Adding one require-check changes the bytecode, and with default metadata settings the Solidity documentation warns that even renaming a source file can.
- Timing against a mined salt. A vanity salt brute-forced for one init-code hash is valid only for that exact bytecode. Find the salt first, touch the code later, and the vanity address no longer belongs to your contract.
None of this is a flaw in CREATE2. Committing the address to the code is a security feature: whoever verifies the address knows exactly which init code must have created any contract that appears there. But for the multichain scenario from section 1, the formula couples two things the team wants to decouple: the address's identity and the exact bytes of the release candidate.
5. How CREATE3 works
5.1. CREATE3 is a pattern, not an opcode
The name suggests a third EVM instruction. There is none. The current instruction set contains CREATE (0xf0) and CREATE2 (0xf5) only.
"CREATE3" is a community name for a pattern that composes the two real opcodes:
Factory
│
│ CREATE2(salt, fixed proxy init code)
▼
Single-use proxy
│
│ CREATE(target init code)
▼
Target contract
The factory deploys a minimal proxy with CREATE2; the proxy's only ability is to deploy whatever it is sent, using plain CREATE. Chain the two formulas and the target's init code drops out of the derivation.
Because this is a pattern rather than a standard, every implementation makes its own choices. Solady ships it as a Solidity library (Vectorized/solady, src/utils/CREATE3.sol), CreateX as a shared public factory contract (pcaversaccio/createx), 1inch as an owner-gated factory plus a vanity-salt miner (1inch/create3-contract).
5.2. Step one: deploying the proxy with CREATE2
The factory deploys the proxy using the ordinary CREATE2 formula:
proxy = keccak256(0xff ++ factory ++ salt ++ keccak256(proxyInitCode))[12:]
The decisive property: proxyInitCode is a constant, the same bytes for every deployment the implementation ever performs. Solady, CreateX and the current 1inch Create3 code all use the same minimal proxy bytecode:
proxyInitCode = 0x67363d3d37363d34f03d5260086018f3
keccak256(of it) = 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f
Since the third hash input is a constant, the proxy address depends on the factory address and the salt, and on nothing else; the factory does not yet need to know what will be deployed.
Three different "codes" are in play here:
- Proxy init code: the one-time setup code that creates the proxy and installs its minimal behavior.
- Proxy runtime code: the small program that remains at the proxy address and deploys the target contract.
- Target init code: the deployment code for the actual contract. It is used only in step two, so it does not affect the proxy address.
The proxy is a general-purpose deployer and explained in detail in Part 2.
5.3. Step two: deploying the target with CREATE
The factory now calls the proxy, passing the target's init code as raw calldata. The proxy copies it and executes CREATE, so the plain CREATE formula from section 2 applies, with the proxy as deployer:
target = keccak256(rlp([proxy, proxyNonce]))[12:]
Which nonce? EIP-161 requires that a newly created contract's nonce start at 1, not 0. The proxy, fresh from step one, has never deployed anything, so its first CREATE uses nonce exactly 1.
Now compose the two steps. The proxy address came from (factory, salt, proxy init code); the target address comes from (proxy, constant 1). Writing effective salt for the value that actually reaches CREATE2 (some factories transform user input first — Part 2), the composition gives:
target = f(factory address, effective salt, proxy init code)
When the factory and its proxy init code are fixed — the usual case inside one deployment system — that simplifies further to:
address = f(factory, salt)
and the target address does not depend on: target init code, constructor arguments, target runtime bytecode.
5.4. What CREATE3 actually guarantees
The precise guarantee:
A CREATE3-style factory lets you compute the target's address in advance, independent of the target's init code, provided the factory address, the effective salt and the proxy init code are all fixed.
Beware the popular shorthand "the address depends only on the salt". Inside a single factory that reading is harmless (the other inputs are that factory's constants); across systems it is wrong. The full identity of a CREATE3 deployment is:
CREATE3 identity = factory address
+ effective salt
+ proxy init code
Fix all three and the address is yours regardless of what the target code becomes; change any one and the address moves. The word effective is doing real work there: the next section turns the identity into a compatibility condition.
6. When two CREATE3 deployments share an address
Two systems can truthfully say "we use CREATE3" and still derive different addresses from the same user-provided salt. They produce the same target address if and only if:
same target address
= same factory address
∧ same effective salt (after each factory's transformation)
∧ same proxy init code (byte-identical)
∧ proxy nonce = 1 (target is the proxy's first CREATE)
Break any one of those and the addresses diverge:
- Different factory address. The factory is hashed into the proxy address, so two factories at different addresses produce disjoint address spaces even with identical code and salts.
- Different proxy init code. One byte change in the proxy constant moves every target address. The hash is over the bytes: "equivalent" is not enough.
- Different salt transformation. The user salt and the effective salt that reaches CREATE2 need not be the same. Compare effective salts, not user salts; how each implementation derives that value is Part 2.
- Proxy nonce not 1. Any extra creation by the proxy before the target changes the nonce and the address.
7. CREATE, CREATE2 and CREATE3 compared

Each method fits a different deployment workflow:
- Use CREATE for simple, one-off deployments. It is the most direct option when the contract address does not need to be chosen in advance or reproduced across chains.
- Use CREATE2 when the address must be known before deployment and the code is already fixed. The address is tied to that exact deployment code, which gives integrators an additional integrity check.
- Use CREATE3 when the address must remain stable while the code or constructor arguments may change. This is useful for multichain deployments and vanity addresses mined before the final contract is ready.
No method is a universal upgrade. The choice is between simplicity, an address tied to fixed code and an address that stays stable as the deployment changes. CREATE3 provides the last property, but the deployment process must verify the code separately, as the next section explains.
8. Security implications
8.1. Same address does not mean same code
Under CREATE2, the address itself certifies the init code. Under CREATE3, by construction, it certifies nothing about the code: any init code deployed through the same factory with the same salt would have received the same address.
CREATE3 stabilizes the address, and in exchange it moves code-integrity control from the address formula into the deployment process.
8.2. Salt squatting and front-running
A public, permissionless factory will deploy for anyone. If your planned salt is observable (in a pending transaction, a public repository, or a deployment on another chain) and the factory does not bind salts to senders, someone else can submit it first and occupy your address, on the chain you were about to use or on one you have not reached yet.
The three implementations answer the threat differently in cost and in what they protect; the comparison belongs to Part 2. What matters here: a salt not bound to a sender on a shared factory is claimable by anyone, on every chain the factory exists on.
8.3. Initialization
CREATE3 changes where an address comes from - not the state of the contract that appears there. The classic hazard is unchanged: a proxy-based or otherwise initializable contract deployed and left uninitialized is an open invitation. A stable, beautiful, pre-announced address that briefly hosts an uninitialized contract is a stable, beautiful, pre-announced attack target.
9. Conclusion
CREATE2 makes the address depend on init code; CREATE3 removes that dependency by chaining the two real opcodes: a factory deploys a fixed proxy with CREATE2, and the proxy deploys the target with CREATE. The target address is then fixed before the target's code exists:
address = f(factory, salt)
when the factory and its proxy init code are fixed. Across systems the full identity is factory address, effective salt and proxy init code - break any one and the addresses diverge. The trade-off is that the address no longer certifies the code; that check moves into the deployment process, along with salt-squatting and initialization risks.
Part 2 of this series opens the shared proxy bytecode and compares Solady, CreateX and the 1inch deployer. Part 3 will walk through 1inch’s CREATE3 deployment end-to-end.
Stay tuned for more 1inch tech content.
