Keyless contract deployment with CREATE3

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
                }
            }
        }
    }
}
1 Like

This is really an awesome idea.

Pretty cool!

I think both create2 and create3 deployments are quite useful if the main requirement is deterministic addresses. When it comes to deploying on a large number of chains, using keyless factories is somehow the only reliable option, giving you a deterministic (if not same) address everywhere.

At thirdweb we use a Create2 factory method (nick’s method, keyless deployment) for deployment of prebuilt contracts.

Have described the architecture as well as challenges faced, in this blog here: Permissionless infrastructure deployments on 900+ EVM chains — Joaquim Verges

Yes, that’s my view after having checked and thought about the various options.

I’ve looked at how other projects have tried to have same addresses across multiple blockchains and most simply try to synchronize account nonce, but eventually one or more goes out of sync and they can no longer get the same address on one or more new blockchains. It’s such an unreliable way to try to maintain same addresses across blockchains.

One problem in our way is the transaction replay ban which I’ve written about in:

Maybe there can be a way to allow transaction replay for the deployment needs of developers.

If you’re using Nick’s Deterministic Deployment Proxy from 4y ago that has been deployed to 0x4e59b44847b379578588920ca78fbf26c0b4956c on many blockchains, then there is front-running vulnerability - if a contract was deployed using the factory then someone else could deploy the same contract to the same address on other EVM-based blockchains (by using the same salt). So I made an update that eliminates that vulnerability:

Great public good. But, is it already deployed somewhere besides Sepolia?

Anyone can deploy any of the offered factories to their chosen blockchains if nobody has done so yet to those blockchains. To deploy a factory just edit (e.g. to choose which factory and EVM version) and run SKYBIT-Keyless-Deployment/scripts/deployKeylessly-Create3Factory.js at main · SKYBITDev3/SKYBIT-Keyless-Deployment · GitHub.

Hello, I work building bridges between various blockchains, and is being quite challenge, because not all EVMs works equally or supports the same set of features, and Keyless Deployment requires the exact same bytecode to work on all blockchains (btw not all of them supports pre-eip155 transactions).

Another issue with most CREATE3 factories is that they actually aren’t an replacement for the CREATE3 opcode because msg.sender is the proxy address, not the actual sender, and the issue is that most of those factories doesn’t provide an way to know who is the actual sender.

Also the CREATE3 is not good an good replacement for Keyless Deployment, if we want anyone to deploy a common good cross-chain contract (such as the EIP-1820), we must allow anyone to deploy it’s bytecode at an deterministic address, without the expenses of the Keyless Deployment.

To accomplish that I implemented this Factory contract, that supports both CREATE3 and CREATE2 methods, with an additional feature that allow you to provide custom arguments to the constructor without influencing the resulting address.