Getting the most out of CREATE2

Originally published at: https://blog.zeppelinos.org/getting-the-most-out-of-create2/

In this post, we’ll go in depth into the CREATE2 opcode and its uses in counterfactual instantiation and user onboarding. We’ll see how we can use it in combination with different techniques, such as initializers, proxies, and meta transactions. These open the door to new flows in creating user identities, allowing us to rapidly iterate and fix any bugs even before an identity is created.

Simpler times

When we create a contract from an externally owned account (EOA), or from a contract using the vanilla CREATE operation, the address where the contract is created is easily determined. Every account has an associated nonce: for EOAs, this nonce is increased on every transaction sent; for contract accounts, it is increased on every contract created. The address of a new contract is calculated as a function of the account’s address and its nonce:

address = hash(sender, nonce)

While this makes it easy for calculating the deployment address of a contract in advance, attempting to park an address is difficult. You have to make sure that no other contracts are created, so that when you actually want to deploy your contract, you can use the expected nonce. But why is parking an address interesting?

Note: by “parking”, we mean reserving an address for a contract, in a similar sense to domain parking, except that we cannot choose what the address will be. We want to know where a contract will be deployed in the future, no matter what transactions we send in the meantime.

Counterfactuality

Counterfactual instantiation is a concept that gained popularity in the context of generalized state channels. It refers to the creation of a contract that could happen, but has not; yet the fact that it could is enough. As defined in the Counterfactual white paper:

We use counterfactual X to talk about the case where:

  1. X could happen on-chain, but doesn’t
  2. The enforcement mechanism allows any participant to unilaterally make X happen
  3. Participants can act as though X has happened

This implies that you need to be able to refer to contracts that have not been deployed, even though they could be created at any time. Having the ability to park an address for a contract becomes helpful, as we can refer to it before it even exists on the blockchain. As Vitalik describes in the CREATE2 proposal:

Allows interactions to (actually or counterfactually in channels) be made with addresses that do not exist yet on-chain but can be relied on to only possibly eventually contain code that has been created by a particular piece of init code. Important for state-channel use cases that involve counterfactual interactions with contracts.

In a state channel, this means that if all parties are honest, we do not even need to spend the gas fees to deploy the contract at all. On the other hand, if one of the parties misbehaves, the other can go on-chain and deploy the contract to the address where it was supposed to be.

Though counterfactual instantiation in state channels was the original motivation for CREATE2, there is another, more earthly, use case for this new opcode.

User onboarding

Parking an address has also become a key component in new user onboarding flows. User identity contracts, also called smart accounts, are a very interesting technique by which the user’s account is defined in a single contract that can be managed with multiple EOAs, each of them representing one of the user’s devices. This allows for 2FA or recovery flows that are traditionally impossible for EOAs without relying on a trusted, centralized third party. They also enable support for meta transactions, where they are also referred to as bouncer proxies.

However, bootstrapping this identity contract can be difficult. User onboarding is already complex in Ethereum, where we are asking the user to sign up on an exchange, buy ETH using fiat or other crypto, install a browser extension or a specialized browser, create an account, write down 12 words, come up with a passphrase, extract funds from the exchange to the account, etc. Adding on top of that deploying an identity contract and moving all funds to it makes it even worse.

It turns out that “parking” identity contracts solves many of these problems.

Instead of having the user go through the MetaMask setup process, we can create a throwaway EOA directly in the browser, with no friction to the user. We then park an address for the identity contract and ask the user to directly fund that address from an exchange, even before the identity is created. A gas station relayer can then deploy the contract on behalf of the user, registering the ephemeral EOA to manage it. The reward for the deployment can be paid directly from the transferred funds, or even by the application itself, as part of the customer acquisition cost. Later, the user can gradually add more robust keys to their identity as they engage with the application.

All in all, what we are doing here is counterfactually instantiating the user’s identity, and only creating it once funded.

This is similar to the approach taken by projects with frictionless onboarding flows, such as the Burner Wallet or Universal Logins. In the words of James Young and Chris Whinfrey, who are working on an implementation of this flow as an EVM package:

CREATE2 has the potential to open the design space for user on-boarding and wallet management. According this 2019 Dapp survey, over 3 quarters of developers mentioned user on-boarding was a major obstacle to adoption. Recent projects like the Burner Wallet have demonstrated alternatives along Zooko’s Triangle that ease the burden of key generation with known trade-offs in the design (it is called “burner” for a reason). CREATE2 based wallets have similar user experience goals when it comes to on-boarding. However, since funds are assumed to be held in contracts instead of keys, not only do users now have programmable access to funds, keys can now be considered disposable. Contract address generation is now as easy and as reliable as key generation. The paradox of contract deployment costs can now be addressed by only deploying a contract that holds user funds once funds have been transferred to the user’s contract address. This technique and the rationale is further explored here.

Instead of forcing full decentralization on a new user, the hypothesis is that new users will more likely gravitate toward systems that are flexible and can meet a user where they are at in their journey – referred to as “incremental decentralization“. These systems prioritize user empathy and support a diversity of technical expertise, motivation and use cases. They grow with the user and expose options that are aligned with user incentives.

Now that we have made our case for parking addresses, let’s see how we can implement it.

Enter CREATE2

CREATE2 is a new opcode introduced in the Constantinople hard fork to provide an alternative to the original CREATE. The main difference lies in how the contract’s address is calculated. Instead of depending on the account’s nonce, the new address is calculated as a hash of:

None of these depend on the state of the creator. This means that you can create as many other contracts as you want, without worrying about the nonce, and still be able to deploy to the parked address whenever you need to.

An important detail is that the last ingredient in the calculation of the contract’s address is not its code, but its creation code. This is the code that is used to set up the contract, which then has to return its runtime bytecode. In most cases, this is the contract constructor, with its arguments, along with the contract code itself.

This means that once you have settled on which contract you want to deploy and its constructor arguments, you can pick a salt value and know that it will always be deployed at the same address. This enables some interesting scenarios.

CREATE2-powered factory

Having a public factory contract that uses CREATE2, any user can share a contract (such as an identity contract), its constructor arguments, and salt, and have anyone deploy that exact contract on a predefined address.

contract Factory {
  function deploy(bytes memory code, bytes32 salt) public returns (address addr) {
    assembly {
      addr := create2(0, add(code, 0x20), mload(code), salt)
      if iszero(extcodesize(addr)) { revert(0, 0) }
    }
  }
}

No kind of access control is needed from the user to the deployer, since the deployer is restricted to running the exact creation code provided by the user if he or she wants the deployment address to match. Also, note that the user or deployer addresses do not matter in the calculation of the contract deployment address, since the sender address used is that of the factory contract, not the user who initiated the transaction.

As a side note, by this point you may have noticed that having a reproducible deployment address allows you to deploy a new contract where an old one was self-destructed. You can deploy a contract, self-destruct it, and then use the same nonce to deploy again to the same address. This opens the door for alternative upgradeabilty patterns, which are explored in depth here.

While this flow is well-suited to many use cases, having to settle on a contract and its constructor arguments can be too restrictive for others. Let’s see if we can find a way around this.

Constructors vs. initializers

As we mentioned before, constructors in Solidity (along with their arguments) become part of the contract creation code. This means that they will directly affect the CREATE2 deployment address.

Initializers, on the other hand, are standard Solidity functions that fulfil the same role as a constructor. They are intended to initialize the contract state, and have a manual guard to ensure that they are not called more than once.

contract Multisig {
  address[] owners;
  uint256 required;

function initialize(address memory _owners, uint256 _required) public {
require(required == 0, “Contract has already been initialized”);
require(_required > 0, “At least one owner is required”);
owners = _owners;
required = _required;
}
}

Since initializers are regular functions, they can be called at any time after the contract’s creation. However, they should be called immediately after, within the same transaction, to ensure no one front-runs the initialization and changes the initial values of our instance.

And since they are regular functions, they are not executed within the contract creation code. This means that we can remove the constructor arguments (now initialization arguments) from the calculation of the deployment address. In other words, we can wait until deployment to choose which arguments we use to initialize our contract.

This technique requires you to write contracts with initializers instead of constructors. Nevertheless, if you are using ZeppelinOS, the good news is that you are already doing so.

contract Factory {
  function deploy(bytes memory code, bytes32 salt, bytes memory initdata) public returns (address addr) {
    assembly {
      addr := create2(0, add(code, 0x20), mload(code), salt)
      if iszero(extcodesize(addr)) { revert(0, 0) }
    }

(bool success,) = addr.call(initdata);
require(success);

}
}

Now that we have deferred the choice of initialization arguments, let’s see if we can do better. Let’s try to defer the choice of the contract itself.

Same old proxies

If you’re a reader of this blog, there’s a good chance you’re already familiar with proxies. Proxies are small contracts that users interact with and which hold all the application state, but delegate every call to a logic contract that holds the business logic to execute.

They are used at the core of ZeppelinOS to power seamless upgrades: a proxy holds a reference to its logic contract, but it can be changed at any time to swap the code being run by a different implementation.

An interesting consequence of using proxies is that the contract code for a proxy is always the same, regardless of the logic contract that backs it. In a proxy-based system, deploying an ERC20 token, a multisig, or a Fomo3D game is exactly the same: you only need to deploy a proxy that points to the corresponding implementation.

This means that if we always use the same contract code, regardless of the contract we intend to create, we will get the same deployment address from CREATE2 no matter what we deploy. We have deferred the choice of what contract we want to create at a specific address until the very time of deployment, while retaining the same initial address. Not only that, but we have also granted our users the option to upgrade their identity contracts at any time. And we have also reduced deployment costs by deploying a thin proxy instead of a full identity contract for every user.

Our factory then becomes slightly more complex. It needs to first use CREATE2 to set up a new proxy, then initialize the proxy with the logic contract address, and then actually call the logic contract initializer via the proxy. However, this change grants us as much flexibility as we want.

contract Factory {
  function deploy(address logic, bytes32 salt, bytes memory initdata) public returns (address addr) {
    bytes memory code = type(Proxy).creationCode;
    assembly {
      addr := create2(0, add(code, 0x20), mload(code), salt)
      if iszero(extcodesize(addr)) { revert(0, 0) }
    }
    Proxy(addr).initialize(logic);
    (bool success,) = addr.call(initdata);
    require(success);
  }
}

While adding this kind of flexibility is amusing, we have inadvertently introduced an attack vector to our factory. An attacker that learns a user’s intended salt for CREATE2 can now deploy any other contract at the destination address by calling first into deploy, providing a different logic contract or different initialization data. Let’s patch this by adding some extra ingredients to our salt.

Baking in the sender address

As mentioned before, CREATE2 uses the sender, which in this case is the factory address, to calculate the contract creation address. However, we can leverage the sender’s address to also play a role in this calculation by baking it into the salt we pass in to CREATE2.

Instead of supplying the salt parameter directly to the CREATE2 operation, we can first hash it together with the caller of the deploy function. This means that only the original user will be able to call into this function and deploy into the address they had parked. Any different msg.sender would yield a different salt, and thus a different deployment address.

contract Factory {
  function deploy(
    address logic, bytes32 salt, bytes memory initdata
  ) public returns (address addr) {
    bytes32 newsalt = keccak256(abi.encodePacked(salt, msg.sender)); 
    bytes memory code = type(Proxy).creationCode;
    assembly {
      addr := create2(0, add(code, 0x20), mload(code), newsalt)
      if iszero(extcodesize(addr)) { revert(0, 0) }
    }
    Proxy(addr).initialize(logic);
    (bool success,) = addr.call(initdata);
    require(success);
  }
}

This is one of the implementations you will find in the new ZeppelinOS ProxyFactory released in version 2.3.1 You can also easily use this feature via the command line using the zos create2 command from the CLI.

$ zos create2 --salt 42 --query
> Instance using salt 42 will be deployed at 0x123456
...
$ zos create2 MyContract --salt 42 --init initialize
Instance of MyContract deployed at 0x123456

However, by adding the restriction that the deployment address is calculated based on the sender, we have closed the door to meta transactions, which were one of the use cases we intended to cover. In the context of meta transactions, a user broadcasts the transaction they want to be executed, and a relay picks it up and puts it on-chain. This means that the sender address is different from the user’s, so it will not generate the expected deployment address. Luckily, we can also accommodate that.

Signer, not sender

Our only reason for tying the deployment address to the transaction sender address is to validate that the contract creation is done following the specs of the original user – and that no one has front-run them and inserted a creation transaction with the same salt but different parameters. However, we do not need to force the user to be the one who sends the transaction to validate that. We only need them to sign that they are in agreement with the deployment.

With that in mind, we can request a signature along with the deployment parameters. A user who requests the contract creation only needs to sign the parameters once he or she is willing to deploy. The factory is then tuned to use the signer address, instead of the sender, to calculate the deployment address.

contract Factory {
  function deploy(
    address logic, bytes32 salt, bytes memory initdata, bytes memory signature
  ) public returns (address addr) {
    address signer = keccak256(
      abi.encodePacked(logic, salt, initdata, address(this))
    ).toEthSignedMessageHash().recover(signature);
    bytes32 newsalt = keccak256(abi.encodePacked(salt, signer)); 
    bytes memory code = type(Proxy).creationCode;
    assembly {
      addr := create2(0, add(code, 0x20), mload(code), newsalt)
      if iszero(extcodesize(addr)) { revert(0, 0) }
    }
    Proxy(addr).initialize(logic);
    (bool success,) = addr.call(initdata);
    require(success);
  }
}

This flow is also coded into the ZeppelinOS ProxyFactory contract. As with the previous one, the zos create2 command can be used to call into this method by adding a signature option, as you can see in our sample project.

$ zos create2 --salt 43 --from 0x44
> Instance using salt 43 will be deployed at 0x654321
...
$ zos create2 MyContract --salt 43 --signature 0xabcdef --init initialize --from 0x88
> Instance of MyContract deployed at 0x654321

All in all, this means that any user may choose a random salt and have a uniquely determined address at their disposal to deploy whatever they want, whenever they want. Not only that, the deployment can be executed directly from their address or via any relay address by just signing the deployment parameters.

Putting it all together

Thanks to CREATE2, we can create a frictionless onboarding process for our users. It allows us to counterfactually instantiate our users’ identity contracts, and only deploy them once necessary.

Adding proxies to the mix allows us to make cheaper deployments and to defer the choice of the logic contract used for the identity until it is actually needed. This grants us the flexibility of rapidly iterating identity implementations and ensuring that our users are onboarded directly to the latest one, regardless of when they parked their identity address.

Should we find a bug in our identity implementations, we can use the techniques seen in this post to ensure that any counterfactually created identity is realized on-chain with a fixed version. Since we are no longer bound to a specific implementation when parking an address, we can switch to a different one as needed.

We will be sharing an in-depth post on how to build this solution in another post soon. In the meantime, go ahead and start using CREATE2 today on your own experiments. And make sure to share what you’re building with this new opcode with the rest of the community on the forum!

Happy coding!

 

Be part of the community

1 Like