Categories

  • crypto
  • solidity
  • smart_contracts

TLDR

  • CREATE2’s deployment address depends on constructor args
  • CREATE3 avoids this. It uses:
    • user-provided salt
    • address of factory
    • msg sender’s address*
  • It does this by using CREATE2 to deploy a single-use factory.
  • This single-use factory uses CREATE to deploy our smart contract.
    • This factory has a deterministic address, and always deploys with nonce = 0.

* see A Possible 3rd Parameter

Background - CREATE and CREATE2

CREATE and CREATE2 are opcodes in the Ethereum Virtual Machine. Each one deploys a new smart contract, but they differ in how the new smart contract’s address is determined.

CREATE

CREATE determines the address using:

  • address of the deployer
  • nonce of the deployer

Specifically, it uses the formula: new_contract_address = keccak256(rlp(deployer ++ nonce))[12:] where:

  • ++ denotes concatenation
  • deployer is the (EOA or smart contract) address which is deploying
  • nonce is the nonce of the deployer
  • [12:] denotes taking the last 20 bytes from the 32 byte keccak result
  • rlp denotes an RLP encoding function.
CREATE2

CREATE2 determines the address using:

  • address of the deployer
  • arbitrary salt data
  • the smart contract’s initcode

Specifically, it uses the formula: new_contract_address = keccak256(0xff ++ deployer ++ salt ++ init_code_hash)[12:] where:

  • ++ denotes concatenation
  • 0xff is to avoid collision with CREATE’s RLP encoding (this prefix requires a lot of encoded data)
  • deployer is the (EOA or smart contract) address which is deploying
  • salt is an arbitrary 32 byte piece of data
  • init_code_hash = keccak256(init_code)
  • [12:] denotes taking the last 20 bytes from the 32 byte keccak result

Limitation of CREATE2

CREATE2 suits many use cases. However, addresses are influenced by the constructor args. This is a limitation. When wanting to deploy across multiple chains, where constructor arguments are different in each environment, you would need to choose one of three solutions.

The first solution is to accept that your addresses will be different.

Secondly, you can replace the constructor with an initializer.

The third solution is to use CREATE3.

CREATE3

We are going to be looking at Axelar’s CREATE3 implementation. For other examples, see the Appendix. They should all function the same. I chose Axelar’s because it doesn’t rely as heavily on direct bytecode.

Inheritance Diagram

Here is a diagram of Axelar’s smart contract inheritance, for reference.

Deployer Contract

First, let’s look at the main entry point, the Deployer contract.

(omitting comments)

abstract contract Deployer is IDeployer {
    using SafeNativeTransfer for address;

    function deploy(bytes memory bytecode, bytes32 salt) external payable returns (address deployedAddress_) {
        bytes32 deploySalt = keccak256(abi.encode(msg.sender, salt));
        deployedAddress_ = _deploy(bytecode, deploySalt);

        emit Deployed(deployedAddress_, msg.sender, salt, keccak256(bytecode));
    }

    function deployAndInit(
        bytes memory bytecode,
        bytes32 salt,
        bytes calldata init
    ) external payable returns (address deployedAddress_) {
        bytes32 deploySalt = keccak256(abi.encode(msg.sender, salt));
        deployedAddress_ = _deploy(bytecode, deploySalt);

        emit Deployed(deployedAddress_, msg.sender, salt, keccak256(bytecode));

        (bool success, ) = deployedAddress_.call(init);
        if (!success) revert DeployInitFailed();
    }

    function deployedAddress(
        bytes memory bytecode,
        address sender,
        bytes32 salt
    ) public view returns (address) {
        bytes32 deploySalt = keccak256(abi.encode(sender, salt));
        return _deployedAddress(bytecode, deploySalt);
    }

    function _deploy(bytes memory bytecode, bytes32 deploySalt) internal virtual returns (address);

    function _deployedAddress(bytes memory bytecode, bytes32 deploySalt) internal view virtual returns (address);
}

It is an abstract contract, so where do the implementations for _deploy and _deployedAddress reside? We can find them in Create3Deployer.

Create3Deployer

contract Create3Deployer is Create3, Deployer {
    function _deploy(bytes memory bytecode, bytes32 deploySalt) internal override returns (address) {
        return _create3(bytecode, deploySalt);
    }

    function _deployedAddress(
        bytes memory, /* bytecode */
        bytes32 deploySalt
    ) internal view override returns (address) {
        return _create3Address(deploySalt);
    }
}

OK, so we have a generic Deployer which can get inherited from either this Create3Deployer, or presumably a Create2Deployer too (wen create4? 👀👀). Something like a template method pattern in OOP talk.

Create3 Contract

Now let’s look at that other parent contract, Create3.


contract Create3 {
    function _create3(bytes memory bytecode, bytes32 deploySalt) internal returns (address deployed) {
        deployed = _create3Address(deploySalt);

        if (bytecode.length == 0) revert EmptyBytecode();
        if (deployed.isContract()) revert AlreadyDeployed();

        if (msg.value > 0) {
            deployed.safeNativeTransfer(msg.value);
        }

        // Deploy using create2
        CreateDeploy create = new CreateDeploy{ salt: deploySalt }();

        if (address(create) == address(0)) revert DeployFailed();

        // Deploy using create
        create.deploy(bytecode);
    }

    function _create3Address(bytes32 deploySalt) internal view returns (address deployed) {
        ...
    }
}

Now we are getting somewhere. The first step seems to be fetching the deterministic CREATE3 deployed address. Then it deploys a CreateDeploy contract. I wonder what this is for… And then finally it calls CreateDeploy::deploy, passing it the initcode of our smart contract.

Address calculation

Let’s look at _create3Address.

contract Create3 {
    function _create3(bytes memory bytecode, bytes32 deploySalt) internal returns (address deployed) {
        ...
    }

    function _create3Address(bytes32 deploySalt) internal view returns (address deployed) {
        address deployer = address(
            uint160(uint256(keccak256(abi.encodePacked(hex'ff', address(this), deploySalt, DEPLOYER_BYTECODE_HASH))))
        );

        deployed = address(uint160(uint256(keccak256(abi.encodePacked(hex'd6_94', deployer, hex'01')))));
    }
}

Right. So it calculates two separate addresses. The first is keccak256(0xff ++ address(this) ++ deploySalt ++ DEPLOYER_BYTECODE_HASH). Look familiar? That looks like a CREATE2 address, using address(this) (the deployer factory), the user-provided salt, and a hardcoded bytecode hash. This indicates that the smart contract is going to deploy the CreateDeploy smart contract using CREATE2.

The next line is keccak256(0xd694 ++ deployer ++ 0x01). This looks like a CREATE deterministic address. It has the predicted CreateDeploy address, followed by 1 (the first nonce). What is that 0xd694? That’s the RLP encoding. Let’s step through it.

First byte = 0xd6. This is the prefix to encode 0x94 ++ deployer ++ 0x01. In other words, we are encoding a list, where the combined length of everything inside the list is 1 + 20 + 1 = 22 == 0x16. Since 22 < 55, this prefix should be 0xc0 + 0x16 = 0xd6.

Second byte = 0x94. This is the prefix to encode deployer. deployer is of length 20. Since 20 < 55, this prefix should be 0x80 + 0x14 = 0x94.

And then the final 0x01 is already encoded as per the “single byte 0 < x < 0x7f” rule.

This is the same RLP encoding as you will see in any Solidity code that predicts a CREATE address.

OK so this address estimation is saying that we are apparently going to, later in this transaction, deploy some CreateDeploy contract using CREATE2. Using that, we will then deploy our contract with CREATE. Let’s look at how this actually happens.

Ephemeral CREATE Factory

The next part of Create3::_create3 is

// Deploy using create2
CreateDeploy create = new CreateDeploy{ salt: deploySalt }();

Just deploying this CreateDeploy contract with CREATE2. That’s where the salt comes in! So we don’t actually even use the salt when deploying our contract. We use it when deploying this ephemeral CreateDeploy factory contract.

Let’s have a look at this contract.

contract CreateDeploy {
    function deploy(bytes memory bytecode) external payable {
        assembly {
            if iszero(create(0, add(bytecode, 32), mload(bytecode))) {
                revert(0, 0)
            }
        }
    }
}

Simple. It has a deploy function which uses create to deploy the bytecode provided.

Final Step

Finally, we actually call the CreateDeploy::deploy function.

create.deploy(bytecode);

Summary

CREATE2 has a problem: initcode is part of what determines the address, meaning different constructor args lead to different deployment addresses. CREATE3 first deploys a single-use factory called CreateDeploy, using CREATE2(hash(0xFF|sender|salt|bytecode), with the user-provided salt.

CreateDeploy’s bytecode never changes. Therefore, assuming the CREATE3 factory’s address is constant, the user-provided salt is the only factor that influences this contract’s address.

CreateDeploy deploys using CREATE, and it’s nonce is always zero (it is ephemeral). Therefore, it always deploys to the same address.

Therefore, the only thing that influences the deployed address is the user-provided salt, unless:

  • CREATE3 factory address is not constant across networks
  • The CREATE3 factory does some operations on the salt, such as appending msg.sender (see A Possible 3rd Parameter)

A Possible 3rd Parameter

Since collisions could easily happen with equivalent salts, many implementations combine the transacation’s sender address with their salt like bytes32 deploySalt = keccak256(abi.encode(msg.sender, salt));.

This isn’t actually a requirement, but maybe should be.

Appendix