I’ve created a GitHub repository that explores the pros and cons of various ways to achieve the goal of getting the same address on different blockchains for a contract. My conclusion is that the best way to do it is to deploy the contract using a keylessly-deployed CREATE3 factory.
I’m now sharing my findings to the community, so check it out at: GitHub - SKYBITDev3/SKYBIT-Keyless-Deployment: Deploy your smart contract to the same address on many blockchains (with fewer pitfalls)
With the CREATE3 method, contract code doesn’t affect the deployed contract’s address. The address depends on:
- The factory’s contract address
- A user-provided salt
CREATE3 factories usually also factor in the deploying account’s address to prevent contract address clashing / front-running.
The way CREATE3 method works is that first CREATE2
is used to deploy a new CREATE factory (so nonce is 1
) which then deploys your contract.
Keyless deployment eliminates:
- dependency on the owner of the account who deployed the factory contract
- account nonce synchronization on multiple blockchains
There have been a few CREATE3 factories out there for a while (e.g. ZeframLou’s), but I think that my pure Yul one is the most gas-efficient:
object "SKYBITCREATE3FactoryLite" {
code { // Constructor code of the contract
datacopy(0, dataoffset("runtime"), datasize("runtime")) // Deploy the contract
return (0, datasize("runtime"))
}
object "runtime" {
code { // Executable code of the object
mstore(0, caller()) // 32 bytes. The user's address.
mstore(0x20, calldataload(0)) // 32 bytes. User-provided salt.
let callerAndSaltHash := keccak256(0x0c, 0x34) // Hash caller with salt to help ensure unique address, prevent front-running. 12 0s skipped as addresses are only 20 bytes. Store result on stack.
datacopy(0, dataoffset("CREATEFactory"), datasize("CREATEFactory")) // Write CREATEFactory bytecode to memory position 0, overwriting previous data. Data is on left of slot, 0-padded on right.
let createFactoryAddress := create2(0, 0, datasize("CREATEFactory"), callerAndSaltHash) // Deploy the CREATE factory via CREATE2, store its address on the stack.
if iszero(createFactoryAddress) {
mstore8(0, 1) // An error code made up to help identify where it failed
revert(0, 1) // Return the error code so that it appears for user
}
mstore(0, 0) // make first slot 0 to reserve for address from call output
let creationCodeSize := sub(calldatasize(), 32) // Store creation code size on stack. Skipping first 32 bytes of calldata which is salt.
calldatacopy(0x20, 32, creationCodeSize) // Overwrite memory from position 0x20 with incoming contract creation code. We take full control of memory because it won't return to Solidity code.
if iszero(
call( // Use the deployed CREATEFactory to deploy the user's contract. Returns 0 on error (eg. out of gas) and 1 on success.
gas(), // Gas remaining
createFactoryAddress,
0, // Native currency value to send
0x20, // Start of contract creation code
creationCodeSize, // Length of contract creation code
0, // Offset of output. Resulting address of deployed user's contract starts here. If call fails then whatever was here may remain, so we left it empty beforehand.
20 // Length of output (address is 20 bytes)
)
) {
mstore8(0, 2) // An error code made up to help identify where it failed
revert(0, 1)
}
if iszero(mload(0)) { // Call output was 0 or not received
mstore8(0, 3) // An error code made up to help identify where it failed
revert(0, 1)
}
return (0, 20) // Return the call output, which is the address (20 bytes) of the contract that was deployed via CREATEFactory
}
object "CREATEFactory" {
code {
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return (0, datasize("runtime"))
}
object "runtime" {
code {
calldatacopy(0x20, 0, calldatasize())
mstore(0, create(0, 0x20, calldatasize())) // Create returns 0 if error
return (12, 20) // Addresses are only 20 bytes, so skip the first 12 bytes
}
}
}
}
}