OpenZeppelin Blog

EVM Deterministic Deployments Made Easy with OpenZeppelin Defender

Written by OpenZeppelin | September 28, 2023

With the growth of users on layer two networks, many projects seek to deploy their smart contracts onto multiple chains so that users can transact with lower gas fees. However, contract deployment involves the creation of a unique address derived from the deployer's wallet details, and the contract bytecode. To ensure a smooth and secure user experience, projects can configure their setup so that each contract deployment retains the same address as the original.

 

Beneath the Hood of Deployment

How Contract Addresses Are Generated

Smart contracts on EVM chains are commonly deployed using the CREATE opcode, which instantiates a contract at an address determined by the deployer address and the deployer account's nonce (i.e., the count of transactions sent from that address). The address is derived from the following formula:

keccak256(deployingAddress ++ nonce)[12:]

Under the hood contract deployments are just regular transactions, with no address specified in the recipient field. This kind of deployment results in a new contract instance deployed on-chain with a distinct, but not replicable address for every deployment transaction.

While CREATE provides the foundation for most contract deployments, its inability to get a contract address independent of the nonce ahead of time presents challenges for complex decentralized applications that want to deploy across multiple EVM chains. To address this, the Ethereum community introduced the CREATE2 opcode in 2019. This new opcode allows for what we call deterministic or counterfactual deployments – the ability to pre-determine contract addresses before deployment.

How CREATE2 Enables Deterministic Deployment

The CREATE2 opcode is similar to CREATE, but it introduces the concept of a 'salt', a nonce-independent parameter, that along with the deployer’s address and the bytecode of the contract are used to derive the address. CREATE2 allows the creation of a contract at an address that can be computed off-chain before the transaction is sent, and more importantly it allows projects to deploy the same contract at the same address across different chains without worrying about how many transactions the deployer has done before. The address of the deployed contract is derived using this formula:

keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]

This mechanism enables contracts to be interacted with, or even referenced, before they actually exist on-chain. This can help more complex projects because they can know the addresses where their contracts will get deployed, making documentation and integration easier (there’s only one address per contract across all supported chains).

The way to use deterministic deployments is to interact with what is called a contract factory, a parent contract that is deployed at the same address across all chains where we want to do deterministic deployments(in order to keep the deployer address the same) beforehand and whose sole purpose is to create new contracts using CREATE2.

A minimalistic example of such factory is shown below:

contract CreateCall {
    /// @notice Emitted when a new contract is created
    event ContractCreation(address indexed newContract);

    /**
    * @notice Deploys a new contract using the create2 opcode.
    * @param value The value in wei to be sent with the contract creation.
    * @param deploymentData The initialisation code of the contract to be created.
    * @param salt The salt value to use for the contract creation.
    * @return newContract The address of the newly created contract.
    */
    function performCreate2(uint256 value, bytes memory deploymentData, bytes32 salt) public returns (address newContract) {

        assembly {

            newContract := create2(value, add(0x20, deploymentData), mload(deploymentData), salt)
        }

        require(newContract != address(0), "Could not deploy contract");
        emit ContractCreation(newContract);
    }
}

 

Risks to keep in mind

Great powers don’t usually come without great risks, and that is the case with CREATE2. It's important to consider a few key aspects when working with contracts that employ this deployment method: 

  • Metamorphic contracts could be exploited by malicious actors leading to a rug pull.
  • Variations in the EVM across different chains may result in developers unintentionally deploying to unexpected addresses despite consistent input parameters.
  • Role assignment to the contract deployer, leading to privileged roles being stuck in the factory

We'll delve into these points shortly.

Metamorphic Contract Risks

Metamorphic Contracts is a relatively complex topic,but the TL;DR is that it enables a contract to “metamorphose” into something else by using a few clever tricks.

This can be accomplished if the metamorphic contract is deployed using CREATE2 by a factory and uses selfdestruct, which is the only way the EVM (for now) allows a contract and its associated state to be deleted and a new bytecode re-deployed at the same address. 

How can this be possible since determining an identical address relies on the bytecode being the same? Even if the contract destructs itself, only the same bytecode should be allowed to be deployed to an identical address. However, if an attacker loads the code dynamically from the implementation contract, they retain the init code of the metamorphic contract, generating the same deterministic address, yet be able to change the runtime code as desired.

Metamorphic contracts have legitimate use cases, such as an alternative to the popular proxy pattern for upgrades, but in general, they are dangerous, since unsuspecting users might trust them, believing they hold the promise of immutable code, when in reality they have a back door hidden, ready for an attacker to change the bytecode under the user’s feet. Read more here.

Cross-Chain Compatibility Risks

Networks that are EVM compatible but have different implementation details can result in different bytecode and different address derivation mechanisms than Ethereum. This the case with zkSync Era, and can result in the same contract being deployed to different addresses on different chains despite having identical address derivation details.

Privilege and Role assignment

While not ideal, it is still a common practice to assign privileged roles to the deployer of the contract(using msg.sender). This works well when using regular deployments, since msg.sender is the EOA that initiated the transaction. But in the case of deterministic deployments, since they are conducted via a factory contract, if you assign permissions to the sender of the message it ends up being the factory contract, which is most likely not intended.

Since factory contracts are usually immutable (and they should be), those roles end up stuck forever, and if you don’t have a way to revoke and re-assign(e.g. You didn't assign the admin role to an account under your control) you end up with a contract where roles can no longer be managed.

We suggest being always explicit when assigning roles, for example passing the address(es) to the contract’s constructor. This helps avoid situations where roles are stuck forever.

How OpenZeppelin Defender makes it easy

Now that we've comprehensively covered deterministic deployments, including their mechanics, the role of a factory, the opcode involved, and the associated risks and challenges, you're likely keen on applying this knowledge to your project. As you begin exploring deterministic deployments with tools like hardhat/foundry/any other framework you prefer, you may find that the process isn't always straightforward or user-friendly. Often, it demands extra plugins or crafting transactions manually and deploying them on-chain.

However, OpenZeppelin Defender can significantly simplify and enhance the safety of your deterministic deployments and overall deployment process. Specifically, for projects utilizing hardhat(and Foundry coming soon), we've integrated convenient methods into our widely-used hardhat-upgrades plugin, enabling effortless CREATE2 deployments. Moreover, this integration includes the added benefit of contract verification on Etherscan, making your deployment journey smoother and more secure.

If you don’t have a Defender account, you can join the waitlist here

1. After you login follow this tutorial to guide you through getting your API key and secret and configure your Deploy environment. 

2. Once you have your API key, it’s time to add some code to your hardhat.config file and your deployments scripts. There’s just a few things you need to add:

// hardhat.config.ts
export default {
  solidity: "0.8.19",
  ...,
  defender: {
    apiKey:<YOUR_DEFENDER_API_KEY>,
    apiSecret: <YOUR_DEFENDER_API_SECRET>,
  }

};

 

// deploy script
import { ethers, defender } from "hardhat";

async function main() {
  const ContractFactory = await ethers.getContractFactory("Box");
  const contract = await defender.deployContract(ContractFactory, {
    salt: “Use the same salt value to deploy to the same address across                      
chains”,

});

  console.log(`Contract deployed to ${contract.address}`);
}



main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

 

3. We are all set. Now it’s time to deploy to any of Defender’s supported chains.

To deploy,  run:

$ yarn hardhat run scripts/deploy.ts – network <yourNetwork>

As you can from the example below, our simple Box contract has been deployed to both Goerli and Sepolia to the same address, with the source code verified! 

Here’s the result on Etherscan:

To further verify, you can check the Deploy page inside your configured environment in Defender for a history of the deployments and its status. We can see that our Box contract has been successfully deployed and verified.Deterministic deployment to multiple chains continues to be an important feature for both large projects and their users.   Until now, however, deterministic deployment  has been beset with both operational and security challenges.  With OpenZeppelin Defender  these challenges are easily addressed.  Please contact us for more information about how our Professional Services team helps clients through challenges encountered throughout the development lifecycle, including deterministic deployments.  Or, if you feel confident in your team’s ability to work independently, you can request direct access to Defender’s beta here.