Deploying a deterministic smart contract on Ethereum

Introduction

Deployment of a new contract on an EVM-based protocol usually produces a contract address that cannot be known in advance. Fortunately, a way of precomputing a contract address was introduced in EIP-1014.

In this post we will explore: 

  • How a contract address is usually generated.
  • How you can know a contract address before deploying a new contract instance.
  • What the advantages and use cases of deterministic deployment are.

This article works for most EVM-based protocols—Ethereum, Polygon, BNB Smart Chain, Avalanche C-Chain, Fantom, Harmony, and others.

Deploying a smart contract—the classic way

Whenever a new contract is deployed to an EVM-based network classically, a couple of variables are used to generate the contract address, resulting in different addresses for the same deployer and the same contract. Even though every contract address is deployed deterministically, the main difference between the classic way and the approach we will see later is the use of different creation functions.

Classically, the address of a smart contract is computed using the deployer address (sender) and the number of transactions sent by this account (nonce). The deployer and the nonce are RLP-encoded and hashed with Keccak-256.  

An example function from pyethereum

def mk_contract_address(sender, nonce): 
    return sha3(rlp.encode([normalize_address(sender), nonce]))[12:] 

This way, even when having the same account and the same smart contract code, the address of this contract will change if we choose to re-deploy it. But there is a way to precompute a contract address and use this address to perform transactions—like sending ETH to it—even when this address has nothing in it yet.

Deploying a smart contract—the deterministic way

There are a number of approaches to generate a deterministic address for a smart contract—for example, one that seeks to lower gas costs, and older ways by using assembly code. However, newer approaches are available just by using smart contract functions and a factory contract approach. 

First, let us write a simple smart contract that returns its balance and uses the deployer address as the constructor parameter. The contract can also store funds and allow the contract owner to withdraw the funds. This will be useful in the future. 

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract SimpleWallet {
    address public owner;
    // Only owners can call transactions marked with this modifier
    modifier onlyOwner() {
        require(owner == msg.sender, "Caller is not the owner");
        _;
    }
    constructor(address _owner) payable {
        owner = _owner;
    }
    // Allows the owner to transfer ownership of the contract
    function transferOwnership(address _newOwner) external onlyOwner {
        owner = _newOwner;
    }
    // Returns ETH balance from this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }  
    // Allows contract owner to withdraw all funds from the contract
    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
    // Destroys this contract instance
    function destroy(address payable recipient) public onlyOwner {
        selfdestruct(recipient);
    }
}

What if we deploy this contract just like this? Let’s use Remix for simplicity and deploy the contract on Goerli.

First, select Injected Web3 and be sure to have a wallet extension such as MetaMask. You can also check this plugin to deploy contracts on Remix from your local environment.

Once you’ve done that, add your Ethereum Goerli endpoint to MetaMask. You can get a free public node from Chainstack.

Now, let’s deploy the contract, be sure that your network selected on MetaMask is correct:

Also, you will need to fund your account with some ETH on Goerli from a faucet.

You can now deploy the contract.

Click on your account address from MetaMask to copy it to clipboard and then pass it as argument to the Remix deployment option.

For example, Deploy: 0x06908fDbe4a6af2BE010fE3709893fB2715d61a6

Once the transaction gets mined, you can check the output from Remix. Also, you can check your transactions on MetaMask, select the last one. It should say Contract Deployment. You can always refer to the Chainstack article on how MetaMask transactions work.

Once you click on it, it will display a few details of the transaction. Let us click on View on block explorer. This will open Etherscan, so we can see check our contract creation in depth.

We can see that a new instance of our SimpleWallet contract was created on address 0x4388C588f2a28343dB614FFd3817eE5459f85760. Notice that we didn’t know in advance what address would be generated, and only when the contract was created and the transaction mined it was provided.

What if we can precompute a contract address prior to its deployment and perform operations like sending funds to it, and then allow someone to get back those funds only when the contract gets deployed? We can achieve this by using the CREATE2 function.

Let’s create a factory contract that also has the SimpleWallet contract and that will use the CREATE2 opcode as stated in Solidity docs: Salted contract creations / create2.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleWallet {
    address public owner;
    // Only owners can call transactions marked with this modifier
    modifier onlyOwner() {
        require(owner == msg.sender, "Caller is not the owner");
        _;
    }
    constructor(address _owner) payable {
        owner = _owner;
    }
    // Allows the owner to transfer ownership of the contract
    function transferOwnership(address _newOwner) external onlyOwner {
        owner = _newOwner;
    }
    // Returns ETH balance from this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
    // Allows contract owner to withdraw all funds from the contract
    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
    // Destroys this contract instance
    function destroy(address payable recipient) public onlyOwner {
        selfdestruct(recipient);
    }
}
contract Factory {
    // Returns the address of the newly deployed contract
    function deploy(
        uint _salt
    ) public payable returns (address) {
        // This syntax is a newer way to invoke create2 without assembly, you just need to pass salt
        // https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2
        return address(new SimpleWallet{salt: bytes32(_salt)}(msg.sender));
    }
    // 1. Get bytecode of contract to be deployed
    function getBytecode()
        public
        view
        returns (bytes memory)
    {
        bytes memory bytecode = type(SimpleWallet).creationCode;
        return abi.encodePacked(bytecode, abi.encode(msg.sender));
    }
    /** 2. Compute the address of the contract to be deployed
        params:
            _salt: random unsigned number used to precompute an address
    */ 
    function getAddress(uint256 _salt)
        public
        view
        returns (address)
    {
        // Get a hash concatenating args passed to encodePacked
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 0
                address(this), // address of factory contract
                _salt, // a random salt
                keccak256(getBytecode()) // the wallet contract bytecode
            )
        );
        // Cast last 20 bytes of hash to address
        return address(uint160(uint256(hash)));
    }
}

Let’s deploy this factory contract on Goerli using Remix again. We are deploying the contract the classic way so that we can later use it to precompute the deployment address.

Again, select the deployment options in Remix and switch the contract to be deployed to Factory. Once the right contract is selected, deploy it.

Once the deployment gets confirmed, select the deployed contract to expand it and check its available functions. Example of a deployed factory contract.

Our motivation is now to deploy a new instance of our SimpleWallet contract, but knowing its contract address in advance. Fortunately, our Factory contract allows us to precompute this address.

The getAddress function returns a precomputed address of a new SimpleWallet instance. It requires a salt parameter in order to return this address. For simplicity we will use 123 as salt, but it cant be any uint256 value. It’s important to note that, if you’re using your own deployed Factory contract the addresses wont be the same, as the getAddress function utilizes the Factory contract instance address to compute new SimpleWallet instance addresses. However, you can still use the one that has been deployed in this tutorial and (if using the same salt) get the same address as result.

Let’s pass 123 as argument to the getAddress function and execute it from Remix.

In this particular example the precomputed address is 0xf49521d876d1add2D041DC220F13C7D63c10E736.

Now that we know in advance at which contract address our SimpleWallet will be deployed through our Factory contract, let’s send some funds to it. Don’t worry if nothing exists yet in there, we will recover our funds later.

Go to MetaMask, input the contract address that was returned by the getAddress function and send some ETH.

Now, let’s actually deploy our SimpleWallet instance and check if it’s correctly deployed to our precomputed address. In Remix, search for the Deploy function in our Factory contract instance and pass 123 as salt. Wait for the transaction and head to Etherscan to confirm that it deployed correctly.

In the transaction details section (on Etherscan) select the Internal Txns tab.

Once there, we see that the CREATE2 function was called from our Factory contract, and a new instance of our SimpleWallet contract was created. Click on the address of the created contract and check that the address is the same that was precomputed previously.

Note that because Etherscan seems to be running OpenEthereum nodes on Goerli, CREATE2 is rendered as CREATE in the Etherscan interface. See OpenEthereum Issue 10922.

If everything was correct, we should be able to withdraw the amount of ETH that we sent to our SimpleWallet contract. But first, we need a way to interact with it.

For simplicity, you can also verify the contract on Etherscan, connect to it using MetaMask and withdraw the funds.

Contract in our example.

Interacting with our smart contract

To be able to retrieve our funds, we need a way to interact with our SimpleWallet contract.

First, let’s start a new Node.js project and install some required packages.

mkdir deterministic-deployment-factory && cd deterministic-deployment-factory
npm init --y
npm i ethers

Create a new file called SimpleWalletAbi.js and paste the following content. This file will serve as interface to our contract so we can interact with it using the ethers.js library.

const abi =  [
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_owner",
                "type": "address"
            }
        ],
        "stateMutability": "payable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "internalType": "address payable",
                "name": "recipient",
                "type": "address"
            }
        ],
        "name": "destroy",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "getBalance",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "owner",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_newOwner",
                "type": "address"
            }
        ],
        "name": "transferOwnership",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "withdraw",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
];
module.exports = {
    abi
}

Now, let’s create our index.js file and add the initial configuration required to interact with the contract

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY =
  "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider(
    "<YOUR_GOERLI_PROVIDER>"
  );
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // Inits a new SimpleWallet instance
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
}
main();

Good time to remind that you can get a free public node from Chainstack for your Goerli endpoint.
Once our initial configuration is done, let’s check that everything is in order and we get no errors by running the script.

node index.js

Now, let’s add a simple query to fetch the contract balance. In this example we sent 0.2 GoerliETH to the contract, so it should show correctly the amount sent.

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider("<YOUR_GOERLI_PROVIDER>");
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // Inits a new SimpleWallet instance
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
  // Query the balance of the contract by calling
  // the getBalance function
  provider.getBalance(simpleWalletAddress).then((balance) => {
 // convert a currency unit from wei to ether
 const balanceInEth = ethers.utils.formatEther(balance)
 console.log(`Current balance in SimpleWallet: ${balanceInEth} ETH`)
})
};
main();

Running the script again should now output the current balance in our SimpleWallet.

Now, let’s actually recover the funds stored in the contract. Add the following code to the script and run it again. We should expect to receive the ETH stored in the contract and the contracts balance updated later.

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider("<YOUR_GOERLI_PROVIDER>");
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // Inits a new SimpleWallet instance
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
  // Withdraw funds from the contract
  try {
    console.log("Attempting to withdraw funds...");
    const receipt = await simpleWallet.withdraw();
    console.log("Funds withdrawn! :)");
    console.log(receipt);
  } catch (error) {
    console.log("Funds can't be withdrawn");
    console.error(error);
  }
  // Query the balance of the contract by calling
  // the getBalance function
  provider.getBalance(simpleWalletAddress).then((balance) => {
 // convert a currency unit from wei to ether
 const balanceInEth = ethers.utils.formatEther(balance)
 console.log(`Current balance in SimpleWallet: ${balanceInEth} ETH`)
})
};
main();

Run the script one last time. Once the transaction succeeds, the funds will be withdrawn back to your address and the contract balance will show 0.

In case you need it, the complete code can be found in the blog’s repository.

Wrap up 

Precomputing the address of a contract may increase the security and reliability of decentralized applications since the code in the smart contract is (generally) the same and will not change. It also allows to deploy a recreated version of a contract to the same address after it has been destroyed in case things get messed up. More important it allows to implement use cases for counterfactual interactions with code that has been created by a particular piece of init code that can be even generated off-chain.

In this post we have reviewed a great topic about the ability to set a deterministic address for our smart contracts. We have included some great points on top of it: 

  • How a contract address is usually generated.
  • How can we know a contract address before deploying a new contract instance.
  • What are the advantages and use cases of deterministic deployment.

Power-boost your project on Chainstack

Have you already explored what you can achieve with Chainstack? Get started for free today.

Chainstack uses cookies to provide you with a secure and
personalized experience on its website. Learn more.