A deployment transaction can contain several layers of bytes. Some initialize the new contract; others tell a factory what to deploy. Separating those layers makes address calculation much easier to reason about.
Historical implementation notes. The deployment snippets below are extracts from the original example and have not been executed for this revision. The explanation distinguishes EVM initialization code from factory-specific calldata; the snippets assume their surrounding client, types, and transaction parameters already exist.
Creation code and deployed code
Creation bytecode runs during deployment and produces the runtime bytecode stored at the contract address. Constructor arguments are ABI-encoded and appended to the compiled creation bytecode when the constructor needs them.
A factory may require an additional calldata format. For example, a deployment proxy can accept a 32-byte salt followed by the creation bytes. That salt prefix is a convention of that factory’s interface; it is not another universal piece of EVM initialization code.
The original note used “init code” for both initialization instructions and a factory’s leading salt. Here, init_code means the initialization code used by the EVM, following EIP-1014.
CREATE uses the creator and its nonce
For a normal creation transaction, the creator is the sending account. The resulting address depends on that creator and the nonce used for the creation. In the original snippet, nonce had already been incremented while preparing the transaction, so the address calculation subtracts one:
address = getContractAddress({
from: walletClient.account.address,
nonce: BigInt(nonce - 1),
});
That subtraction belongs to the surrounding example’s bookkeeping. It is not a rule to apply to every nonce value.
A factory contract can also execute CREATE. In that case, the creator is the factory contract. “Called through a factory” does not, by itself, mean CREATE2 was used.
CREATE2 adds a salt and a code hash
CREATE2 calculates the address from the creating contract, a 32-byte salt, and the hash of its initialization code:
keccak256(0xff ++ deployer_address ++ salt ++ keccak256(init_code))[12:]
The creator is the contract executing CREATE2, not the EOA that submitted the outer transaction. Changing the initialization code, including constructor arguments, changes its hash and can change the resulting address. The precise formula and initialization-code definition are specified in EIP-1014.
For the salt-prefixed deployment interface used in the original example, the calldata is split as follows:
const inputData = data.substring(2);
const salt: Hex = `0x${inputData.substring(0, 64)}`; // 32 bytes = 64 hex characters
const creationByteCode: Hex = `0x${inputData.substring(64)}`;
address = getCreate2Address({
from: deterministicDeployer,
salt: salt,
bytecodeHash: keccak256(creationByteCode),
});
This parsing is specific to that interface. A factory with an ABI-encoded function call needs its own decoding rather than this substring split.
Inspect the bytes at each boundary
These examples from the original note show why an explorer’s creation code and a transaction’s input data are not interchangeable:
| Example | What to inspect |
|---|---|
| Deterministic deployment proxy | Its deployment transaction includes initialization instructions around the proxy’s runtime code |
| EntryPoint 0.8 | The factory input has a salt prefix before the contract creation bytes |
| SimpleAccountFactory 0.8 | The creation payload includes the constructor’s EntryPoint address |
An explorer capture from the original note. The highlighted bytes illustrate that the transaction input includes more than the final runtime code.
The original public reference addresses were:
- Deterministic deployer:
0x4e59b44847b379578588920cA78FbF26c0B4956C - EntryPoint 0.8:
0x4337084d9e255ff0702461cf8895ce9e3b5ff108 - SimpleAccountFactory 0.8:
0x13E9ed32155810FDbd067D4522C492D6f68E5944
An address alone is not a deployment recipe. Check the intended chain, the contract at that address, and the actual factory interface before relying on it.
Check the deployed result
The original example also inspected the code returned for each calculated address:
const verifyDeployed = async (addresses: Address[]) => {
for (const address of addresses) {
const bytecode = await client.getCode({
address,
});
if (!bytecode || bytecode === '0x') {
console.log(`CONTRACT ${address} NOT DEPLOYED!!!`);
// process.exit(1);
}
}
};
A nonempty response is a useful first check that code exists at an address. It does not establish that the code is the expected implementation or that its initialization is correct.