I was working on a new project recently and wanted to do something new. It seems that all the rage these days is omni-chain deployments, contracts deployed to the same address on different chains. Given that people constantly annoy me to update addresses on our documentation for all our different chains, it sounded like a good idea to me. There’s lots of ways to do these kinds of deployments, but in my research I was particularly intrigued by one method: Deployment Factories. These are contracts that use incredibly intricate knowledge of the EVM to deploy contracts deterministically. This led me down a rabbit hole of inline-assembly that help make me a better developer, and have some fun along the way.

Constructors

Everyone who has ever deployed a smart contract knows you need a constructor. It’s the piece of code that runs only once when you deploy your smart contract. It runs only once, but has a special property. In it, you can set an immutable variable. These variables are declared alongside your other storage variables with the “immutable” keyword. Once they’re set, they are constant and can’t be changed, just like variables with “constant”. The differences and similarities however are fascinating. Constant variables are set at compile-time. The solidity compiler goes in and replaces all references to the variable with the literal variable. Immutable variables however, are set at creation time. There’s two different types of EVM-code: Creation Code and Runtime Code.

Code Types

Creation Code is the EVM bytecode that runs when your contract is deployed. It can do everything, but its purpose is actually to create the runtime time. At the end of a constructor the returned data isn’t a value, but bytecode itself. The creation code’s job is to construct and return the runtime code, that runs every time the contract is called. When you define a value for an immutable variable in your constructor, the creation code is actively going through all the references to it in the runtime-code, and replacing it with a literal value. This is why it’s immutable. Since the value is hard-coded into the runtime bytecode, changing its value means changing the contract byte-code, which can’t be done once deployed. Remember this, that the value returned from creation code is ONLY the runtime code of that newly deployed contract.

Another interesting quirk of the constructor, is that its parameters are pushed on in strange orderings. When you call a function in solidity, the first 4-bytes of the calldata are the function-selector, followed by the abi-encoding of the parameters. A contract deployment call does the opposite, and appends the parameters to the end of the creation-code, not the beginning.

An example deployment of a contract

You can see this in an example deployment transaction I did recently. The address 0xC4C319E2D4d66CcA4464C0c2B32c9Bd23ebe784e is for the alETH-ETH curve pool, and I passed it as a parameter to the contract on creation. This will matter later.

Example Constructor

Create and Create2

When i was researching these deployment factories I was curious how to use them, and how they worked. Like many, my experience with creation factories is with the OpenZeppelin Clones Library. This library let you clone an existing contract into a new address. Using the create2 opcode, you can even clone a contract to a deterministic location. However, this comes with caveats. When you clone a contract, you can’t run a constructor on it. This is because of quirks in the ERC-1167 standard used by the library, so you need to use an initializer function if you want to change something.

cloneDeterministic Function in OpenZeppelin Clone Libraru

When I was looking up contracts that were deployed using this factory, I noticed that none of them actually used an initializer function, they all had constructors and set immutable variables. I wondered how this was possible since the factories used create2 to allow the user to deterministically decide where to deploy the contract to. I started to dive into the factory code itself. I ended up learning a lot about how the EVM and contract creation works as a result.

Solmate Create3.sol Factory Deployer

Above is the bread and butter of the Solmate Create3.sol deployment factory. In less than 15 lines of code, you get a master class in the EVM deployment process. Let’s go through it.

The function

The function takes in 3 values: a salt, creation code, and value. Value is simply how much eth you would like to deploy your new contract with, in Wei. Salt is an arbitrary value decided by the user when calling. Pick any 32-byte value in the world. I choose to use the keccak256 of a string related to the purpose of the contract. The contract seems daunting but it’s really not so let’s dive in to what’s going on

bytes memory proxyChildBytecode = PROXY_BYTECODE;
address proxy;

Let’s skip over this for right now, but know that we’re simply taking some constant bytecode and moving it into memory so it is easier and cheaper to access later. We’re also going to allocate in memory a slot for an address;

proxy := create2(0, add(proxyChildBytecode, 32), mload(proxyChildBytecode), salt)

There’s a lot going on here so let’s go step by step:

Create2 Explanation

The create2 opcode takes 4 values:

The amount of ether to send to the new contractAn location in memory to start reading from.The size of our creation code, in bytes.A salt to act as a seed in the address generation.

The opcode takes in all of these, determines the address of a new contract based on it, and deploys a contract to that address using the creationCode of size size beginning at offset. Not that hard so far. What you might have noticed however, is that we’re not deploying the creation code supplied by the user, but some creation code defined as constant, PROXY_BYTECODE. That’s right. Instead, we’re gonna deploy a very small and optimized contract instead. Stay with me here.

When you store a bytes object in memory in solidity, the compiler also needs to store the length of that object. The first memory slot, before the object itself stores this length. This is why we did

bytes memory proxyChildBytecode = PROXY_BYTECODE;add(proxyChildBytecode, 32)

We stored our bytecode in memory, but we don’t want to deploy the length of the bytecode also, so we’re going to simply jump over it. We take the slot the creation code was placed into, and move one more slot, 32-bytes to the right where the actual bytecode begins.

PROXY_BYTECODE is the creation code of our new proxy contract being created by that create2 call. It’s only 14-bytes long, but incredibly powerful. Let’s go through it

0x67 — PUSH8 bytecode

We’re going to push 8-bytes onto the stack. The 8-bytes in the top section of that table. This is going to be our actual contract bytecode that we want created when the create2 is over.

0x3d & 0x52 — RETURNDATASIZE & MSTORE

We’re gonna push a zero onto the stack and then call mstore. This means we’re going to store the 8 bytes we pushed on first starting at memory offset 0. As I write this the PUSH0 opcode was added recently into the EVM, and as a result this factory may be redeployed.

0x6008 — PUSH1 08
0x6018 — PUSH1 18

We push the values 8 and 24 (18 in hex = 24 decimal) onto the stack

0xf3 — RETURN

The return opcode takes in 2 values: an offset, and a size. So we are saying to the EVM, return 8-bytes starting at offset-24. So you’re probably wondering now, why offset 24? This is a quirk of the EVM, when writing to memory MSTORE only lets you write 32-bytes at a time. If you try to store a value less than 32-bytes in length, it will left-pad that value with 0’s out to 32. Since our bytecode that we want returned is only 8-bytes in length it will be padded when stored in memory as “00000000….bytecode”, 24-bytes of zeros followed by our 8-byte bytecode. But we don’t want to return all those zeros, just our bytecode, so we say “return me only the 8 bytes of data beginning at offset-24” which is our intended bytecode.

So our creation code has run, and it’s set some random 8-bytes of bytecode. But what do we do with this and what is that bytecode? Let’s keep going through the solidity

require(proxy != address(0), “DEPLOYMENT_FAILED”);

This is a simple check to make sure that our create2 of our proxy works. proxy in this case is the address of our new proxy contract that we just set the runtimeCode to. But what why did we do this?

(bool success, ) = proxy.call{value: value}(creationCode);

Now we get to use our parameters from earlier. We’re going to make a call to this new contract we made. We’re sending in value, and using creationCode as the only parameter. This creationCode is the code of the actual contract we want deployed. So how does the contract execute?

0x36 — CALLDATASIZE

First we push the size of our creation code onto the stack

0x3d 0x3d — RETURNDATASIZE

Followed by two zero’s. You’ll see why in a second

0x37 — CALLDATACOPY

CALLDATACOPY takes 3 inputs:

A destination offset in memory to start writing atthe offset in the calldata to start reading fromThe number of bytes to read

So with this opcode we’re telling it we want to copy the entirety of the calldata into memory, starting at offset zero. Remember that at this point the calldata is the creation code of our intended new contract.

0x36 — CALLDATASIZE
0x3d — RETURNDATASIZE
0x34 — CALLVALUE

Push the size of our creation code again followed by another zero and then finally the amount of wei to send to the new contract.

0xf0 — CREATE

Unlike CREATE2, CREATE works slightly differently. It takes

An amount of wei to sendan offset in memory to start reading fromThe amount of data to read as creation code.

So now our stack reads {value, 0, size(creationCode)}, and we’ve stored our creation code in memory at offset 0. So we’re telling the EVM to create a new contract using that creation code. The creation code that we passed in originally as a parameter. Now, for CREATE, the address of the new contract is based on the address of the deployer and its nonce. Since we just deployed this deployer proxy-contract, the nonce is zero, and our address is

address = keccak256(rlp([sender_address,sender_nonce]))[12:]

So why does this matter? Think about this from an omni-chain deployment standpoint. We used a Create Factory to deploy a proxy contract deterministically. It always deploys the same creation code. If our factory is deployed to address(0x123) on every chain, then as long as I use the same salt on every chain, the proxy contract it creates will have the same address. All of these contracts in the same transaction call create. However, since all of them have nonce 0, since we just created the contract, the address of the contract it creates will be the same as well. So you’re probably wondering “why do they need the proxy contract? Why not just use create2 every time and deploy based on the salt and creation code?”. that’s a good question. There actually is no reason you couldn’t do that. However, it makes deployments tougher. The address returned from create2 is based on the creationCode itself.

initialisation_code = memory[offset:offset+size]
address = keccak256(0xff + sender_address + salt + keccak256(initialisation_code))[12:]

As long as the salt and the initialization code is the same then you could deploy easily. However, by using a proxy setup like we have here, we make it even easier. Now the address of your deployment is based ONLY on what the provided salt is. You only need to keep track of one thing. In fact, we can take this further too. Let’s say you wanted to upgrade this contract. How would you do it? You could use proxy patterns like UUPS, but that’s cumbersome and involves additional risk. What if you instead used selfdestruct first to delete the contract? Since the address of our new contract is based on the salt only, you can deploy an upgraded version of a contract, with different bytecode, to the same address later. If you only used Create2 and changed the creation code, the address would be different. Upgradability without proxy interface contracts.

So how do you use this?

So you probably saw that code earlier and said “well how do I get my contract’s creation code to pass to the deployer?” I was also interested in that. Luckily the solidity maintainers have you covered.

Example Deployment using a factory

Everyone knows you can use address.code to get the bytecode of a contract, but this actually misses some of the nuance. When you use that, the compiler converts it into the opcode

EXTCODECOPY and EXTCODESIZE

However, this only works for deployed contracts. If we want to get the raw bytecode of a non-deployed contract we can use type(C).creationCode or type(C).runtimeCode. This fields actually compile down to the literal bytecode of those contracts, and inserts it directly into the bytecode of your parent contract at compile-time. This is certainly cool, but what about constructor parameters? This is the question that sent me down this whole rabbit hole. How do I put together creation code that includes parameters to pass to the factory? Recall earlier how I pointed out that constructor parameters are actually stored at the end of the creation code? This is where it matters. By simply appending our parameters to our creation-code, set at compile-time, we have now put together all the code we need to deploy.

In the above example I tried to deploy an ERC-20 token using the parameters for name and symbol. I did this because they’re set in the constructor and immutable

OpenZeppelin ERC20.sol constructor

I then passed that creation code, alongside a random salt I chose, and deployed it. As you can see, I then checked the contract I had deployed to see if the parameters I included had been set, and they had. The immutable variable I had wanted it to set had been done correctly.

If I wanted to deploy this same contract with the same inputs to another chain I would simply use a foundry-cheatcode to switch the chain and run the same code again, the output would be the same. Remember this works because as long as the salt is the same, the newly deployed contract will be as well.

The Lesson

It’s very easy to see a bunch of inline assembly and get scared off. But when you really start to pick it apart, it’s not that difficult. There’s so many incredible hacks people have managed to come up with for the EVM and its amazing to constantly see innovation in the space. This certainly gave me a kick in the ass to keep working on my assembly skills, and I hope this does the same for y’all. This article was inspired by the ZeframLou CREATE3 Factory Contract and the Solmate Create3.sol library. I highly recommend you check it out, as I used the Zefram contract in my tests while writing this article.

GitHub – ZeframLou/create3-factory

https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol

It won’t byte. Omni-Chain deployment factories and the Create3 Library was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

By

Leave a Reply

Your email address will not be published. Required fields are marked *