Permissionless infrastructure deployments on 900+ EVM chains

Permissionless infrastructure deployments on 900+ EVM chains

At thirdweb, we work hard to make our tools EVM chain agnostic — including our smart contract infrastructure.

Today, anyone can deploy thirdweb contracts for a fraction of the gas cost of a regular deployment, on 900+ EVM chains.

The process uses a keyless signer, and deterministic contract addresses which results in a completely permissionless system to deploy any infrastructure contract, without requiring our intervention or us to hold funds on any chain.

Permissionless deployments with keyless signer and create2 factory
Permissionless deployments with keyless signer and create2 factory

In this article, we wanted to share how we achieved this permissionless deploy system, and what problems we faced along the way.

Anatomy of a thirdweb contract

Prebuilt contracts is one of the core offerings of the thirdweb tool suite. These are ready-to-use contracts that can be deployed from the thirdweb dashboard or SDKs, and provide building blocks for web3 applications such as Accounts, Tokens, NFTs, Editions, Marketplaces, Staking etc.

A user deploying a prebuilt contract is essentially deploying a “clone” (ERC1167 Minimal Proxies) of a full implementation, which is deployed through a factory as depicted below. Such deployments are cheap, while maintaining full functionality and immutability of the original implementation with full ownership to the deployer.

Cloning prebuilt contracts via a factory contract for cheaper deployments
Cloning prebuilt contracts via a factory contract for cheaper deployments

However, facilitating this way of deploying contracts, requires having pre-existing infrastructure deployed on any supported chain. We need to have pre-deployed clone factories and implementation contracts deployed and up to date on every chain. This means having complex deploy scripts and most importantly funds for every chain.

How can we scale this process to 900+ EVM chains? Let’s dive in.

Old flow - how we used to handle deployments

The architecture depicted below was active until a few months ago. It involved:

  • thirdweb deployer wallet, which is an EOA
  • Infrastructure contracts
  • Prebuilt implementation contracts
  • A way to save the addresses offchain
BEFORE: deploying infrastructure contracts via the thirdweb deployer EOA
BEFORE: deploying infrastructure contracts via the thirdweb deployer EOA

Both the infrastructure as well as implementation contracts were manually deployed through a dedicated EOA under our control. All the addresses of the implementation contracts were then registered on a clone factory factory contract, and associated with a type and version. This process had to be repeated on every supported chain.

Every time a user wants to deploy a contract, the clone factory will fetch the saved address of the target implementation contract for that chain and deploy a proxy pointing to it.

This approach was limiting in many ways, as discussed below.

Shortcomings of the old flow

1. Not scalable

Our focus at thirdweb is to support any EVM chain. However, manually deploying contracts to hundreds of chains each time there’s a new contract, or a contract needs an update / bug-fix was not feasible.

2. Hard to maintain, error prone

All deployed addresses had to be saved somewhere for later use. This approach was error prone since it was easy to map an address to a wrong network. Additionally, keeping track of old addresses for was cumbersome.

3. Restricted to a single wallet

All deployments depended on a single wallet controlled by the thirdweb team. This dedicated deployer wallet presented a bottleneck in terms of how quickly / reliably the contracts can be rolled out on all chains, as opposed to any wallet being able to bootstrap thirdweb stack on a chain.

To overcome these limitations and truly support any EVM network, we devised a new approach which is discussed in next sections.

New flow - permissionless infrastructure deployments

In the new method, we still have infrastructure and prebuilt contracts similar to the previous approach. However, there are two key additions:

  • Keyless Signer (a re-usable EOA which no-one knows the private key of)
  • A Create2 Factory
  • A collection of published contracts (infra and implementations)
AFTER: Permissionless deployments with a keyless signer
AFTER: Permissionless deployments with a keyless signer

The keyless deployment method is what’s known as Nick’s Method. It derives from the idea that you don’t need to know the private key to create a signed transaction. A transaction can be crafted with an arbitrary signature that results in a predictable public signer address.

We use this method to deploy a stateless Create2 factory at a deterministic address on each chain. The keyless transaction is pre-constructed including arbitrary signature values. Since there can only be one signer recovered from the signature, this pre-constructed transaction will always have the same signer address, referred to as the keyless signer.

This is how the deploy transaction gets crafted:

// Transaction to send
transaction = {
  gasPrice: 100 * 10 ** 9,
  gasLimit: 100000,
  nonce: 0,
  data: CREATE2_FACTORY_BYTECODE,
  ...
}

// Arbitrary signature, e.g.
signature = {
  v: 27,
  r: "0x2222222222222222222222222222222222222222222222222222222222222222",
  s: "0x2222222222222222222222222222222222222222222222222222222222222222",
};

// Serialize the transaction to send
serialized = serializeTransaction(transaction, signature)

// Recover the Keyless signer from serialized transaction (EC recover)
signer = recoverAddress(transactionDigest, signature)

This transaction can be directly sent to the network, which will interpret it as sent by the keyless signer EOA. We just need to fund the keyless signer based on gas values selected above and broadcast the transaction to execute it.

This way, we can deterministically deploy the Create2 factory on each chain, which only needs to be deployed once when going to a new chain. Once the factory is deployed, we can deploy all other contracts through this factory and get a deterministic address for each. This includes both infrastructure as well as implementation contracts (as described in previous sections).

 The Create2 factory in turn gives us predictable address for all infra contracts
The Create2 factory in turn gives us predictable address for all infra contracts

When an end user deploys a contract, the deployment logic on SDK (code example below) will compute the address of the target implementation based on its bytecode instead of reading it from a hardcoded set of addresses.

Advantages of the new flow

This method overcomes the limitations of the old method as discussed above.

1. Predictable

We get deterministic addresses for each contract. This means all thirdweb contracts on any chain will have predictable addresses that can be computed. No more hardcoding addresses anywhere.

2. Permissionless

A new chain compatible with EVM can now bootstrap all thirdweb contracts without needing thirdweb team to do the deployments. The flow is stateless and can be initiated by any wallet. Final addresses only depend on the Keyless Signer and Create2 factory addresses rather than the user’s wallet. This means we don’t need to hold funds for any chains, any user can bootstrap a chain permissionlessly.

3. Scalable

The first user (usually the chain owners) that deploys a contract on a given chain, pays for the deployment of the infrastructure contracts for all other users of that chain. The second user deploying to the same chain will only have to deploy a very cheap minimal clone proxy.

Scaling contract versioning and management

With this new flow, all it takes is one click of a button on our dashboard to bootstrap all of our infrastructure to a new chain. But what about updates? Our contract publishing tool is key to this system.

Every time we create or update a prebuilt contract, we use our publish CLI to publish a new version with version tracking and change logs. Publishing does not mean deploying, instead it means making a contract available for deployment through the thirdweb dashboard. The contract metadata is published onchain on a Polygon contract, which anyone can read.

This is how we can dynamically fetch the latest bytecode for any given contract, which then is used to compute the predicted address where it should be deployed on any chain.

Pushing an update to a contract simply means publishing a new version, which in turn will trigger the new implementation deployment the first time someone tries to deploy it, and all following deployments will just require a cheap proxy deployment.

A simple API for deterministic deployments

Once a contract is published, it unlocks an easy-to-use API to predict its address on any chain and deploy it deterministically.

// Deploy a published contract deterministically
// Can also provide optional inputs such as salt, publisher address
const deployedContractAddress =
  await sdk.deployer.deployPublishedContractDeterministic(
    contractName,
    constructorArgs
  );

// Predict address of a published contract
const predictedAddress =
  await sdk.deployer.predictAddressDeterministic(
    contractName,
    constructorArgs
  );

Anyone can use these for their own published contracts. And for non-published contracts, you can pass bytecode and ABI directly. See the full documentation here.

We’ve also integrated this feature directly on the dashboard, to get predictable addresses on any chain for any published contract.

Challenges of the new flow

We learned a lot building this system, and had to overcome some interesting challenges along the way.

This is the biggest challenge when it comes to sending the same keyless transaction on any network. Replay protection (EIP-155) guidelines implemented by EVM chains now require a chain-id component to be included in a transaction. While some chains still allow pre-EIP155 transactions, most chains have now moved to enforcing replay protection.

This means we can no longer deploy the Create2 at the same address. The pre-EIP155 deployments of this factory can be found on different chains at this address — 0x4e59b44847b379578588920cA78FbF26c0B4956C (example). However, for most chains now, EIP-155 enforcement results in different addresses of this factory on each chain determined by the chain-id property.

Another related problem is that there is no standardized way of knowing which chain enforces EIP-155 and which doesn’t. Error messaging too is inconsistent across different EVM implementations.

As a workaround, we maintain a list of all (most) errors related to EIP-155. To determine whether a chain enforces EIP-155 or not, a dummy transaction is sent to the chain which would throw an error if EIP-155 is enforced. We then find a full / partial match against the errors in the list, and construct the transaction accordingly, based on EIP-155 or pre-EIP155.

Another way could have been to always send the EIP-155 transaction. However, we wanted to maintain same address for the Create2 factory wherever possible.

It's also worth noting that most popular chains choose to have the Create2 factory deployed at the pre-EIP155 address at genesis (example), avoiding the problem altogether.

Chain-specific issues

Each EVM implementation can have differences in terms of attributes like base-fee, error codes, permissioned deployments etc. While not all issues are / can be handled, we do handle the base-fee differences.

Whenever a chain presents with non-standard or extremely high / low values of base fee, which differ from the standard base fee used in keyless transaction above (100 gwei), we need to add an exception for that chain and save the custom base fee for that chain. This is important not only to successfully deploy the Create2 factory, but also compute the same address for it each time any contract is deployed on that chain.

For chains with permissioned deployments, we’re exploring ways to allow custom create2 factories that can check if the user sending the deployment transactions is authorized to do so.

Summary

To sum up, we built a permissionless, scalable way of deploying thirdweb contracts on any EVM chain by relying on Create2 and keyless signer methods. This allows us to streamline deployments, get deterministic addresses for all our infrastructure, and make the process work across any EVM without requiring our team to handle deployments.

We have found workarounds to various issues as discussed above, and are constantly updating / optimizing the method to handle other unique challenges across chains.

To experience the deployment method, you can check out the prebuilt contract offerings available on thirdweb dashboard and deploy it on any chain of your choice (including local nodes!). Or, you can try publishing your own contract with thirdweb CLI.