Jordan Hall
Jordan Hall | oighty.eth

Jordan Hall | oighty.eth

Deterministic Smart Contract Deployments in Foundry with Vanity Addresses

Deterministic Smart Contract Deployments in Foundry with Vanity Addresses

Jordan Hall's photo
Jordan Hall
·Sep 14, 2022·

8 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • Overview of CREATE2
  • CREATE2 Deployments in Forge scripts
  • Mining a salt for a vanity address
  • Deploying to the target address
  • Bonus: Setting up a Gnosis Safe Multi-sig contract with a Vanity Address

A number of smart contract protocols and developers use deterministic deployments to get the same contract addresses on various chains that they deploy a set of contracts on. There are two main ways to do this:

  • Creating a fresh wallet with no transactions on any chain, deploying your contracts from this wallet each time
  • Using a deployer contract with the same address on each chain (likely from method 1) and using the CREATE2 opcode to deploy contracts from this contract. There are several permissionless deployers that other people have deployed across several chains that any developer can use, lowering the barrier to entry.

In addition to having the same addresses, a developer may want to have vanity addresses (e.g. 0x1337...) that are easily identifiable as their contracts. Vanity addresses require finding specific conditions which will result in an address with the characteristics you want. Additionally, having an address with a large number of zero bytes reduces gas costs for storing that address (which applies to token transfers). 0age wrote a detailed article about this and more recently the OpenSea Seaport Protocol was deployed to an address with a 5 leading zero bytes.

For deployments that want to use vanity addresses, using the fresh wallet method fails because you would need to deploy each contract with a special wallet that has been mined to have the first transaction produce an address with the target sequence. This can be tedious and adds some complications to a deployment workflow. The preferred method in this case is to use a CREATE2 deployer.

There are various articles that discuss how to deploy contracts with CREATE2, but it hasn't been covered for the current nuances in the Foundry development environment yet (except in some GitHub issues which require a bit of digging to find). Therefore, I'm going to show you how to mine salts and deploy contracts to target addresses using CLI tools and Foundry.

Overview of CREATE2

Before we dive into the "how", here's a brief overview of CREATE2 so you have context for the task at hand.

CREATE2 is an alternative style of contract deployments from the basic CREATE method. While a CREATE transaction can be sent by an EOA or contract address, CREATE2 can only be called by a contract to deploy another contract. The main difference in the two is that a 32 byte salt value replaces the sequential nonce value for a deployer wallet or contract that is used to calculate the address of the deployed contract. Therefore, the sender can control the resulting address.

Contracts that are deployed with CREATE2 have their addresses calculated as keccak256(0xff + address(this) + salt + keccak256(bytecode))[12:] where address(this) refers to the deployer contract address, salt is a bytes32 value provided by the sender, and keccak256(bytecode) is the hash of the contract creation code combined with abi-encoded construction parameters (also called the init_code). The first 12 bytes of the resulting 32 byte value are ignored and the address is the last 20 bytes.

Originally, you had to use an inline assembly block to do a CREATE2 deployment, but the new interface was updated to accept a salt value in Solidity 0.6.2. Therefore, deploying a contract with CREATE2 from a contract is done by simply adding this value to your existing statement, e.g.:

MyContract deployed = new MyContract{salt: mySalt}(param1, param2, param3);

CREATE2 Deployments in Forge scripts

Forge scripts are a relatively new and easy-to-use method for deploying contracts. I covered them at a high-level in a previous Dev's Journal. The gist is that you write a list of commands in Solidity to execute (e.g. contract deployments, transactions, etc.), add the vm.broadcast flag to the ones you want to send to the network, and run the script from the command line. Foundry creates a list of transactions and executes them on the network sequentially from the address that you configured in the CLI command.

Since Forge is sending all these transactions from an EOA, you would assume that you can't use CREATE2 directly from a script since it has to be called from a contract. However, Forge has a special exception for CREATE2 deployments. Various developers have created simple CREATE2 deployers and deployed them across a number of EVM blockchains. One in particular is 0x4e59b44847b379578588920cA78FbF26c0B4956C. Forge currently has a hard-coded solution where any CREATE2 deployments that are specified in a script use this deployer (the EOA you're using sends a transaction to this deployer with the salt and bytecode of the contract). Therefore, we can use this address as the deployer when calculating the salts we need to submit for our vanity addresses.

There have been some requests from the community to make this more flexible (e.g. allow users to config which deployer) and that is currently in the works.

Mining a salt for a vanity address

In order to find a salt that will yield a target address when deploying a specific contract, you need a program that will iterate through a bunch of salts until it finds one that will work. In other words, you have to brute force it. There are various solutions for this, but the one that has worked best for me in generating vanity CREATE2 salts is vanity-eth-create2. It's a javascript-based CLI tool that is pretty straightforward to use. For CREATE2 salts, the pattern is:

vanityeth -i <prefix, e.g. 0x1234> --create2 <deployer contract> -b <contract bytecode to deploy>

The bytecode needs to include the constructor params that will be provided to the contract on deployment. An easy way to get this is by passing in your arguments and writing the bytecode out from your Forge script via the console or to a file:

bytes memory data = abi.encodePacked(
  type(MyContract).creationCode,
  abi.encode(param1, param2, param3)
);

console2.logBytes(data);
// OR
vm.writeFile("./bytecode/MyContract.bin", vm.toString(data));

Warning: Mining salts consumes a lot of CPU and memory resources. It's best to do with a powerful machine (you could use a cloud VM if you don't have a machine with a lot of memory) and not be doing anything else on the machine while it's running. Additionally, computing addresses is exponentially harder based on the target sequence length (this is why no one can brute force the zero address). I've had success with 6 character, checksummed sequences in a reasonable amount of time. More powerful machines will generally find a match faster, but the distribution is pretty broad given the random nature of the problem. If the program doesn't find an address before your system runs out of memory, then you may need to restart it a few times. I found that if memory gets full and you get a Approximate time estimate: Infinity message, then it is better to kill the process and restart it.

If successful, the CLI will output the address it found and the corresponding salt.

vanity-eth-create2 example

Another option for creating gas-efficient (containing one or more zero bytes) addresses is create2crunch by 0age, which is written in Rust and likely faster than the above. Some people have forked this to find addresses with other properties, and a general use version for a given prefix or suffix might be an improvement over the javascript tool.

Deploying to the target address

Once you have the salts mined, you can provide it to your script and deploy the contract(s). I like to use environment variables for salts as a way to make it more configurable if they need to change and as a way to keep salts secret so someone doesn't deploy your contracts on another chain before you if it's a public repository (e.g. with a different admin or bad params).

MyContract deployed = new MyContract{salt: vm.envBytes32("MYCONTRACT_SALT")}(param1, param2, param3);

A good way to test the deployment to the target address is to run your Forge script against the target network without broadcasting.

Forge has contract verification implemented for CREATE2 deployed contracts as well. Adding a --verify flag and api key to your script call will verify your contracts the same as any other deployment.

Bonus: Setting up a Gnosis Safe Multi-sig contract with a Vanity Address

The contracts I was working with had several construction parameters that need to be determined before the salts are calculated. One of those is an address to perform admin functions on the contracts. While this could be a placeholder EOA and transferred to another address, it seemed cleaner to just identify the Multi-sig admin address ahead of time and be able to deploy it to the same address across chains (if cross-chain support is desired).

Luckily, there is a purpose built tool for generating salts to deploy a Gnosis Safe v1.3 via a Proxy Factory which is deployed to the same address on all the main EVM chains called deadbeef. You simply clone and build the tool and then call it from the releases folder per the instructions in the README. It is very similar to vanity-eth-create2, except it generates calldata that can sent to the proxy factory to deploy the safe. Here is how to generate calldata for a target address prefix:

#!/bin/sh

# Input parameters
PREFIX=0xbeef
OWNER_1=0x1000000000000000000000000000000000000001
OWNER_2=0x2000000000000000000000000000000000000002
OWNER_3=0x3000000000000000000000000000000000000003
THRESHOLD=2

# Find a salt for the address and print out the calldata to create it
./target/release/deadbeef \
    --owner $OWNER_1 \
    --owner $OWNER_2 \
    --owner $OWNER_3 \
    --threshold $THRESHOLD \
    --prefix $PREFIX

The output will look something like: deadbeef example

Once you have the calldata, you can simply send a transaction with cast:

# Load environment variables
source .env

# Deploy the safe using calculated calldata
cast send --private-key=$DEPLOYER_PRIVATE_KEY --rpc-url=$RPC_URL --from=$DEPLOYER_ADDRESS --chain=$CHAIN \
   $SAFE_FACTORY_ADDRESS $SAFE_CALLDATA
 
Share this