How to Deploy a Contract to the Same Address on Multiple Networks
This two-part tutorial will explore two ways you can deterministically deploy a contract across multiple networks.
How to deploy a contract to the same address on multiple networks (Part 1)
Create2: An alternative to deriving contract addresses (Part 2)
When you deploy a smart contract to the Ethereum network, the address is derived from your wallet address, the wallet's transaction count (i.e., the nonce), and your contract's bytecode. This ensures that each contract deployed to Ethereum has a unique address.
However, perhaps you want to deploy your contract across multiple networks with the same address. For example, you might want to deploy on Ethereum, Polygon, and Optimism with the same address. This can be useful for testing purposes and helpful to users interacting with your addresses across various networks. To do this, you must ensure your wallet's nonce is equivalent on each network, as illustrated below:
In the image above, transactions A and B are identical and so will deploy the contract to the same address on both networks. In transaction C, however, the nonce is different and thus changes the deployed contract address. Now that we know what conditions need to be satisfied to deploy deterministically, let's deploy some contracts!
Overview
Part 2: (Create2: An Alternative to deriving contract addresses ->)
Prerequisites
Before you begin this tutorial, please ensure you have the following:
-
An Alchemy account (Create a free Alchemy account).
-
An Ethereum address or MetaMask wallet (Create a MetaMask wallet).
-
Node.JS (>14) and npm installed (Install NodeJs and NPM)
Connect to Alchemy
In this tutorial, we will deploy deterministically to the following test networks:
-
Ethereum Goerli
-
Ethereum Sepolia
-
Polygon Mumbai
-
Arbitrum Goerli
-
Optimism Goerli
Tip:
Whenever possible, use the Sepolia testnet as all other current POW testnets will be depreciated post-merge.
Choosing a testnet
While you can use the Goerli testnet, we caution against it as the Ethereum Foundation has announced that Goerli will soon be deprecated.
We therefore recommend using Sepolia as Alchemy has full Sepolia support and a free Sepolia faucet also.
For each network, we will create a unique Alchemy API key to connect to the five chains above:
Repeat the following steps for each network:
-
From the Alchemy Dashboard, hover over Apps, then click +Create App.
-
Name your app: <Network Name> testing.
-
Select the correct chain and network of your choice.
-
Click Create app.
If successful, your Alchemy Dashboard should look like this:
Configure your MetaMask wallet
Because we need our wallet's nonce to be the same across all networks, we advise configuring a new Metamask wallet to ensure no transactions have occurred. There are two ways to go about this:
-
Option 1: Create an Account
-
Option 2: Import an Account
When you create an account in MetaMask there is no way to delete it from your wallet. Therefore, if you want to later remove the account use the import account
Option 1: Create an Account
To create a new wallet address, open MetaMask, click your profile icon > Create Account, set a name and create a new account:
Warning: Once you add an account there is no way to remove it
If you want to remove the account later, you can use the method in Option 2 and Import Account instead.
Option 2: Import an Account
You can generate a random private key to import into MetaMask using Node.js. In your terminal, start Node.js with the following command:
node
Then paste the following into your terminal to generate the key:
crypto.randomBytes(32, (_, bytes) => console.log(bytes.toString("hex")))
In your terminal, you should see an output of a randomly generated private key, similar to the following:
a07b2417424c253d5e33fa746a5e98049986e13cdd3e8fd178e6adae2f58616e
Warning:
The random secret key above is for demonstration purposes only and will never be used. Remember to never share your private key, especially if you intend to use it!
To import a new wallet address, open MetaMask, click your profile icon > Import Account, and enter your key:
Add testnets
For each testnet, we need to add the correct network settings to MetaMask so we can verify that the faucets will deliver the Ether we will request in the next step.
Head to your Alchemy Dashboard. For each app using an L2 network (Polygon,Optimism, and Arbitrum) click Add to Wallet:
If you enable Show test networks in MetaMask > Settings > Advanced, you should already have the Goerli testnet added by default. If the above was successful, you should have the following networks enabled:
-
Goerli Test Network
-
Polygon Mumbai
-
Optimism Goerli
-
Arbitrum Goerli
See the image below of the manually added testnets.
Request testnet ETH from faucets
To deploy your contract on each testnet, you will need fake test Ether to pay the required network gas fees. Below, you can find some preferred faucets for each network:
Note:
Depending on the network traffic, you may have to sign in with your Alchemy account to receive ETH.
First, request Ether from one of the three faucets below. Per the upcoming Goerli deprecation mentioned above, we recommend you use the Sepolia testnet:
After making a request to each faucet, check your MetaMask and ensure you have available balance.
Bridges
Because Arbitrum and Optimism do not have dedicated testnet faucets, you will need to send test Ether to your address on those networks via their respective bridges.
Head to the Arbitrum bridge and set your MetaMask network to Arbitrum Goerli. Then, send some Goerli Ether across to Arbitrum like so:
Note:
The deposit may take 10 minutes to appear on Arbitrum
Next, send Goerli Ether to Optimism. Because the Optimism Goerli testnet is new, the Goerli bridge UI has not yet been built. However, you can still send Ether across the bridge via the L1 Standard Bridge smart contract.
In MetaMask, change your Network to Goerli Test Network and click Send. Then paste the L1 Standard Bridge contract address: 0x636Af16bf2f682dD3109e60102b8E1A089FedAa8 and send some Ether.
The deposit may take up to 10 minutes to appear in your Optimism Goerli balance.
Because you are sending Ether across the Optimism bridge, this will add to the nonce of your wallet on the Goerli network. Later in the guide, you will send an additional transaction on the other networks to account for this offset.
If all deposits were successful, you should see some Ether in your MetaMask wallet on each of the testnets!
Setup project environment
Tip:
We will use a Hardhat project to deploy our contracts. For reference, here is the completed version of what we will build in the remainder of this guide.
Open VS Code (or your preferred IDE) and enter the following in terminal:
mkdir Deterministic-deploy-factory
cd Deterministic-deploy-factory
Once inside our project directory, create the following two directories:
mkdir contracts
mkdir scripts
The contracts
folder will store our project smart contracts and the scripts
folder will contain our deployment and interaction scripts.
Next, initialize npm (node package manager) with the following command:
npm init
Press enter and answer the project prompt as follows:
package name: (Deterministic-deploy-factory)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
Press enter again to complete the prompt. If successful, a package.json
file will have been created in your directory.
Install Hardhat
Hardhat is a development environment that allows us to interact, test, and deploy our contracts within one environment.
To install Hardhat, type the following in terminal:
npm install --save-dev hardhat
Once Hardhat is installed create a new project by typing the following command:
npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
Welcome to Hardhat v2.10.1
? What do you want to do? ...
Create a JavaScript project
Create a TypeScript project
> Create an empty hardhat.config.js
Quit
Select Create an empty hardhat.config.js
. Nice work! With Hardhat installed, we are nearly ready to start coding!
Install environment tools
The tools you will need to complete this tutorial are:
-
hardhat-ethers (A Hardhat plugin that allows us to use the Ethers.js library)
-
dotenv (so that you can store your private key and API key safely)
To install the above tools, ensure you are still inside your root folder and type the following commands in terminal:
Hardhat Ethers:
npm install npm install --save-dev @nomiclabs/hardhat-ethers 'ethers@^5.0.0'
Alchemy's Web3:
npm install @alch/alchemy-web3
Dotenv:
npm install dotenv --save
Above, we have imported the libraries that we installed and all of the necessary variables to interact with .env
Create a Dotenv File
Create an .env
file in your root folder. The file must be named .env
or it will not be recognized.
In the .env
file, we will store all of our sensitive information (i.e., our Alchemy API key and MetaMask private key).
Copy the following into your .env
API_URL_GOERLI = "{YOUR_ALCHEMY_GOERLI_API_KEY}"
API_URL_MUMBAI = "{YOUR_ALCHEMY_MUMBAI_API_KEY}"
API_URL_ARBITRUM = "{YOUR_ALCHEMY_ARB-GOERLI-API_KEY}"
API_URL_OPTIMISM = "{YOUR_ALCHEMY_OPT-GOERLI_API_KEY}"
PRIVATE_KEY = "{YOUR_PRIVATE_KEY}"
-
Replace
{YOUR_ALCHEMY_<NETWORK>_API_KEY}
with the respective Alchemy API keys found on Alchemy's dashboard, under VIEW KEY: -
Replace
{YOUR_PRIVATE_KEY}
with your MetaMask private key.
To retrieve your MetaMask private key:
Open the extension, click on the three dots menu, and choose Account Details. Then click Export Private Key:
Edit hardhat.config.js
hardhat.config.js
Add the following statements to the top of your hardhat.config.js
file to process your .env
file:
require("@nomiclabs/hardhat-ethers");
require("dotenv").config();
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const { task } = require("hardhat/config");
const {
API_URL_GOERLI,
API_URL_MUMBAI,
API_URL_ARBITRUM,
API_URL_OPTIMISM,
PRIVATE_KEY,
} = process.env;
Lastly, let's update the hardhat.config.js
file so that we can interact with our API keys and private keys. Add the following networks inside the modules.exports
object:
module.exports = {
solidity: "0.8.9",
networks: {
hardhat: {},
goerli: {
url: API_URL_GOERLI,
accounts: [`0x${PRIVATE_KEY}`],
},
mumbai: {
url: API_URL_MUMBAI,
accounts: [`0x${PRIVATE_KEY}`],
},
arbitrum: {
url: API_URL_ARBITRUM,
accounts: [`0x${PRIVATE_KEY}`],
},
optimism: {
url: API_URL_OPTIMISM,
accounts: [`0x${PRIVATE_KEY}`],
},
},
};
Now that we are finished setting up our Hardhat environment, we can create and deploy some contracts deterministically!
Deterministic deployment setup
In this section of the guide, we will first set up a Hardhat task to query our wallet's nonce so we can be certain our contract will deploy to the same address on each network. Next, we will create a smart contract and deploy it using a deploy script.
Create a Hardhat task to query the nonce
Hardhat tasks allow us to automate certain processes within the project environment. Because we need to be sure the nonce is identical across all networks, we will create a task that will return the nonce of each network. Additionally, let's include our wallet balance to ensure we have enough funds to deploy.
Inside your hardhat.config.js
file, add the following task above module.exports
:
const web3Goerli = createAlchemyWeb3(API_URL_GOERLI);
const web3Mumbai = createAlchemyWeb3(API_URL_MUMBAI);
const web3Arb = createAlchemyWeb3(API_URL_ARBITRUM);
const web3Opt = createAlchemyWeb3(API_URL_OPTIMISM);
Also within the async function, add a network ID array, provider array, and empty result array:
const networkIDArr = ["Ethereum Goerli:", "Polygon Mumbai:", "Arbitrum Rinkby:", "Optimism Goerli:"]
const providerArr = [web3Goerli, web3Mumbai, web3Arb, web3Opt];
const resultArr = [];
Lastly, create a for loop to push our requested nonce and balance into the empty result array and print the results to console:
for (let i = 0; i < providerArr.length; i++) {
const nonce = await providerArr[i].eth.getTransactionCount(address.address, "latest");
const balance = await providerArr[i].eth.getBalance(address.address)
resultArr.push([networkIDArr[i], nonce, parseFloat(providerArr[i].utils.fromWei(balance, "ether")).toFixed(2) + "ETH"]);
}
resultArr.unshift([" |NETWORK| |NONCE| |BALANCE| "])
console.log(resultArr);
Your completed task should look like this:
task("account", "returns nonce and balance for specified address on multiple networks")
.addParam("address")
.setAction(async address => {
const web3Goerli = createAlchemyWeb3(API_URL_GOERLI);
const web3Mumbai = createAlchemyWeb3(API_URL_MUMBAI);
const web3Arb = createAlchemyWeb3(API_URL_ARBITRUM);
const web3Opt = createAlchemyWeb3(API_URL_OPTIMISM);
const networkIDArr = ["Ethereum Goerli:", "Polygon Mumbai:", "Arbitrum Rinkby:", "Optimism Goerli:"]
const providerArr = [web3Goerli, web3Mumbai, web3Arb, web3Opt];
const resultArr = [];
for (let i = 0; i < providerArr.length; i++) {
const nonce = await providerArr[i].eth.getTransactionCount(address.address, "latest");
const balance = await providerArr[i].eth.getBalance(address.address)
resultArr.push([networkIDArr[i], nonce, parseFloat(providerArr[i].utils.fromWei(balance, "ether")).toFixed(2) + "ETH"]);
}
resultArr.unshift([" |NETWORK| |NONCE| |BALANCE| "])
console.log(resultArr);
});
Now let's run our task with the following:
npx hardhat account --address {YOUR_WALLET_ADDRESS}
If successful, you should see this result:
[
[ ' |NETWORK| |NONCE| |BALANCE| ' ],
[ 'Ethereum Goerli:', 1, '1.47ETH' ],
[ 'Polygon Mumbai:', 0, '9.99ETH' ],
[ 'Arbitrum Rinkby:', 0, '1.49ETH' ],
[ 'Optimism Goerli:', 0, '2.26ETH' ]
]
Besides Goerli, all of our nonces are 0. This is because of the transaction we sent to the Optimism bridge.
To make all of our nonces equal, let's send transactions on the other networks and make each nonce equivalent to one. We can send a transaction to our own MetaMask wallet address like so:
Repeat the same for the other two networks until your nonces are equivalent:
[
[ ' |NETWORK| |NONCE| |BALANCE| ' ],
[ 'Ethereum Goerli:', 1, '1.47ETH' ],
[ 'Polygon Mumbai:', 1, '9.99ETH' ],
[ 'Arbitrum Rinkby:', 1, '1.49ETH' ],
[ 'Optimism Goerli:', 1, '2.26ETH' ]
]
With the necessary setup complete, let's create and deploy a contract.
Create vault contract
For this tutorial, we will create a vault where funds can be deposited and withdrawn by the owner only after a specified period of time. However, feel free to deploy any contract you would like!
In your contracts
folder, create a new solidity file named Vault.sol
and add the following lines of code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Vault {
uint public unlockTime;
address payable public owner;
constructor(uint _unlockTime) {
}
function deposit() payable public {
}
function withdraw() public {
}
}
Above, we have created a contract with three functions:
-
The
constructor
, which runs when our contract is deployed and takes an unlock time. -
The
deposit
function, which we will call to deposit funds to our contract. -
The
withdraw
function, which will allow us to withdraw funds after the unlock time has expired.
First, let's add some logic to our constructor
function:
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);
unlockTime = _unlockTime;
owner = payable(msg.sender);
Next, let's create an event we can emit every time the deposit
function is called. Add an event above the constructor
, and emit it from within the deposit
function. The entire contract should look like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Vault {
uint public unlockTime;
address payable public owner;
event Deposit(uint amount, uint when);
constructor(uint _unlockTime) {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);
unlockTime = _unlockTime;
owner = payable(msg.sender);
}
function deposit() payable public {
emit Deposit(msg.value, block.timestamp);
}
function withdraw() public {
}
}
Lastly, let's add some logic to the withdraw
function. We need to check if the unlock time has passed, then require that the account withdrawing is the owner. Then, we emit an event with the withdrawn balance.
Add a Withdraw event below the Deposit event:
event Withdrawal(uint amount, uint when);
Within the withdraw
function body, add the following logic:
require(block.timestamp >= unlockTime, "You can't withdraw yet");
require(msg.sender == owner, "you aren't the owner");
emit Withdrawal(address(this).balance, block.timestamp);
owner.transfer(address(this).balance);
Your Vault.sol
file should look like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Vault {
uint public unlockTime;
address payable public owner;
event Deposit(uint amount, uint when);
event Withdrawal(uint amount, uint when);
constructor(uint _unlockTime) {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);
unlockTime = _unlockTime;
owner = payable(msg.sender);
}
function deposit() payable public {
emit Deposit(msg.value, block.timestamp);
}
function withdraw() public {
require(block.timestamp >= unlockTime, "You can't withdraw yet");
require(msg.sender == owner, "you aren't the owner");
emit Withdrawal(address(this).balance, block.timestamp);
owner.transfer(address(this).balance);
}
}
Awesome! Our Vault contract is complete. Before we move on to writing the deployment script, let's compile the contract by typing the following in the terminal:
npx hardhat compile
If successful, Hardhat will return:
Compiled 1 Solidity file successfully
Vault deploy script
In your scripts
folder, create a file named vaultDeploy.js
:
const main = async () => {
const unlockTime = "2605659962"; // unlock time must be > deployment time.
const Vault = await ethers.getContractFactory("Vault");
const vault = await Vault.deploy(unlockTime);
await vault.deployed()
console.log("Vault deployed to:", vault.address);
};
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Above, the unlock time is set using a Unix timestamp.
Before we continue, let's compile our contract with the following Hardhat command:
npx hardhat compile
If successful, you should see:
Compiled 1 Solidity file successfully
Before deploying, ensure you replace unlockTime with your Unix timestamp and run the deploy script for each network with this Hardhat command:
npx hardhat run scripts/vaultDeploy.js --network goerli
npx hardhat run scripts/vaultDeploy.js --network mumbai
npx hardhat run scripts/vaultDeploy.js --network arbitrum
npx hardhat run scripts/vaultDeploy.js --network optimism
If successful, your vault contract will deploy on each testnet to the same address:
Deployed to: 0xd216001476CC8F8a277F45d9bFE3996c3f38da5a
Deployed to: 0xd216001476CC8F8a277F45d9bFE3996c3f38da5a
Deployed to: 0xd216001476CC8F8a277F45d9bFE3996c3f38da5a
Deployed to: 0xd216001476CC8F8a277F45d9bFE3996c3f38da5a
Tip:
To check the status of your deployment, search the following blockchain explorers for your contract address:
At this point in the tutorial, we've deployed a contract on each test network to the same address. Having set the nonces, we can guarantee this result. However, there are some downsides to this method:
-
Maintaining the nonces can be a hassle. For instance, if you need to use the address to sign a transaction on one network, you must ensure you update the nonces on all other networks if you want to deploy another contract deterministically.
-
Although the contract deploys to the same address, there is no way to predetermine what that address will be in the counterfactual.
Note:
If you plan to deploy only one contract and know that no future transactions will need to be signed, setting the nonces may be a simple option for deterministic deployment.
In Part 2 of this tutorial, we will address the above downsides by adding a more robust alternative to deployment that does not rely on setting the nonces.
Appendix (Optional interaction scripts)
Below are optional interaction scripts to deposit and withdraw for your Vault contract. These are not necessary to complete the tutorial but provide a way to interact with the contract.
Vault deposit script
Before we deploy, we need to write a script to interact with our deposit function. In your scripts
folder, create a file named vaultDeposit.js
. Add the following async function and catch statement:
const deposit = async () => {
};
deposit()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Inside the deposit
function, add the following statements:
const depositAmount = ethers.utils.parseEther("0.001");
console.log("Depositing", depositAmount / 10 ** 18 + "ETH...");
const Vault = await ethers.getContractFactory("Vault");
const vaultAddress = "0xd216001476CC8F8a277F45d9bFE3996c3f38da5a" //Replace this with your deployed address
const vaultContract = await Vault.attach(vaultAddress);
Warning:
Replace the vault address variable with your deployed address.
Here, we have set the deposit amount to 0.001
Ether and added the necessary variables to connect to the contract once it is deployed.
Now that we are "connected" to the contract, let's call the deposit function and send our deposit amount. Then, let's wait for the transaction receipt and print the deposit event to console:
const sendEther = await vaultContract.deposit({ value: depositAmount });
const depositTxReciept = await sendEther.wait();
console.log(depositTxReciept.events[0].args[0]._hex.toString() / 10 ** 18 + "ETH deposited!");
Your vaultDeposit.js
file should look like this:
const deposit = async () => {
const depositAmount = ethers.utils.parseEther("0.001");
console.log("Depositing", depositAmount / 10 ** 18 + "ETH...");
const Vault = await ethers.getContractFactory("Vault");
const vaultAddress = "0xd216001476CC8F8a277F45d9bFE3996c3f38da5a" //Replace this with your deployed address
const vaultContract = await Vault.attach(vaultAddress);
const sendEther = await vaultContract.deposit({ value: depositAmount });
const depositTxReciept = await sendEther.wait();
console.log(depositTxReciept.events[0].args[0]._hex.toString() / 10 ** 18 + "ETH deposited!");
};
deposit()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Vault withdraw script
Since we want to withdraw our test Ether after the timer is up, let's create a withdraw script.
In your scripts
folder, create a file named vaultWithdraw.js
and add the following async function and catch statement:
const withdraw = async () => {
};
withdraw()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Inside the function body, repeat the ethers connection statements from the deploy script:
const Vault = await ethers.getContractFactory("Vault");
const vaultAddress = "0xd216001476CC8F8a277F45d9bFE3996c3f38da5a" //Replace this with your deployed address
const vault = await Vault.attach(vaultAddress);
Next, let's call the withdraw function on the Vault contract, wait for the transaction receipt, and print the withdraw event to console:
const withdraw = await vault.withdraw();
const withdrawRes = await withdraw.wait();
console.log(withdrawRes.events[0].args[0]._hex.toString() / 10 ** 18 + "ETH Withdrawn!");
Your vaultWithdraw.js
file should look like this:
const withdraw = async () => {
const Vault = await ethers.getContractFactory("Vault");
const vaultAddress = "0xREPLACE_ADDRESS"; //Replace this with your deployed Vault address
const vault = await Vault.attach(vaultAddress);
const withdraw = await vault.withdraw();
const withdrawRes = await withdraw.wait();
console.log(withdrawRes.events[0].args[0]._hex.toString() / 10 ** 18 + "ETH Withdrawn!");
};
withdraw()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Now, let's use our deposit script to send some Ether to the Vault. Run the following commands:
npx hardhat run scripts/vaultDeposit.js --network goerli
npx hardhat run scripts/vaultDeposit.js --network mumbai
npx hardhat run scripts/vaultDeposit.js --network arbitrum
npx hardhat run scripts/vaultDeposit.js --network optimism
If successful, the script will return the deposited amount of Ether:
Depositing 0.001ETH...
0.001ETH deposited!
Finally, let's withdraw the Ether we just sent to our vault. Keep in mind, If your unlock timer has not expired, the transaction will fail. If you know the time has expired, run the vaultWithdraw.js
script with the following Hardhat command:
npx hardhat run scripts/vaultWithdraw.js --network goerli
npx hardhat run scripts/vaultWithdraw.js --network mumbai
npx hardhat run scripts/vaultWithdraw.js --network arbitrum
npx hardhat run scripts/vaultWithdraw.js --network optimism
If successful, you will see the withdraw event in console:
0.001ETH Withdrawn!
Updated over 1 year ago