How To Deploy To The Same Contract Address Across Multiple Chains Paying Only Single Source Blockchain Gas & Fees Using The multichain-deploy Hardhat Plugin

Timmy Ho
6 min readMar 14, 2024

--

the unofficial multichain-deploy plugin mascot: the “hardhan” cat

For the Foundry implementation of multichain-deploy, please see this post.

Deploying the same smart contract under the same contract address across multiple blockchains can be a boon for UX, especially for protocols/projects that need consistent addressing. However, to do so typically requires a minor but cumbersome step that materially impacts devEx — you must acquire native tokens in order to pay for the deployment cost of each chain.

To get around this problem, you can perform this same action but pay gas/fees from only one “source” chain using a Hardhat plugin developed by ChainSafe called multichain-deploy. It utilizes Sygma’s cross-chain interoperability protocol on the backend to fire the instructions for cross-chain deployment.

Setup is quite simple. Let’s dive in with a simple example where we will use Sepolia as the source chain and make cross-chain deployments to Mumbai and Holesky using Hardhat’s tutorial Lock.sol contract:

Create an example project folder:

$ mkdir exampleProject-hardhat-multichain-deploy
$ cd exampleProject-hardhat-multichain-deploy

Initialize dependency managers inside your project folder (we will use npm for the rest of the example):

$ npm init

or

$ yarn init 

Add hardhat as a development dependency to your project:

$ npm install --save-dev hardhat

Initialize the hardhat development environment:

$ npx hardhat init

Choose a JavaScript project during hardhat initialization + select all the default prompts.

Install the other dependencies we will require for the example project:

$ npm install --save-dev @nomicfoundation/hardhat-toolbox @chainsafe/hardhat-ts-artifact-plugin @nomicfoundation/hardhat-web3-v4 @chainsafe/hardhat-plugin-multichain-deploy @buildwithsygma/sygma-sdk-core dotenv

Jump into VSCode and look at the exampleProject-hardhat-multichain-deploy folder:

$ code .

Let’s modify the existing Lock.sol contract slightly. We will add a public readable stringname” to the contract:

contract Lock {
uint public unlockTime;
address payable public owner;
string public name;

And modify the constructor to include some additional parameters, such as deployer / owner of the contract:

  constructor(address _owner, uint _unlockTime) payable {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);

unlockTime = _unlockTime;
owner = payable(_owner);
}

And add a new setName function. We will primarily use this to demonstrate post-deployment cross-chain function calls:

    function setName(string memory _name) external {
if (bytes(name).length > 0) return;
name = _name;
}
}

Next, configure the Hardhat development environment by adding requirements in hardhat.config.js:

require(“@nomicfoundation/hardhat-toolbox”);
require("@chainsafe/hardhat-ts-artifact-plugin");
require("@nomicfoundation/hardhat-web3-v4");
require("@chainsafe/hardhat-plugin-multichain-deploy");
const { Environment } = require("@buildwithsygma/sygma-sdk-core");
require('dotenv').config();

We will configure the networks we want to deploy from in hardhat.config.js. This allows us to choose which “source” network we have available to deploy from with the — —network flag when we eventually run our script:

const config = {
solidity: "0.8.19",
networks: {
sepolia: {
chainId: 11155111,
url: "https://ethereum-sepolia.publicnode.com",
// Use process.env to access environment variables
accounts: process.env.PK ? [process.env.PK] : [],
},
mumbai: {
chainId: 80001,
url: "https://polygon-mumbai-pokt.nodies.app",
// Use process.env to access environment variables
accounts: process.env.PK ? [process.env.PK] : [],
},
holesky: {
chainId: 17000,
url: "https://ethereum-holesky-rpc.publicnode.com",
accounts: process.env.PK ? [process.env.PK] : [],
},
},

Note: you will need to configure the Solidity compiler versions to match.

Note #2: if you receive RPC errors during deployment, you may need to choose a different RPC URL from ChainList.

Add the multichain object from the multichain-deploy plugin and set the environment to testnet. This tells the plugin to interact with the Sygma testnet:

multichain: {
environment: Environment.TESTNET,
}

module.exports = config;

We will modify the default scripts/deploy.jsscript that comes with hardhat init. Make a new file called deployMultichain.js (in a scripts folder) and let’s adjust the script for cross-chain. First, we must import the multichainand web3 objects from our Hardhat plugins.

We are not adjusting much with the main() function, but take note of the fact that we must adjust the unlockTime by an order of magnitude (+60 in the original deploy.js script to + 600 in our new script). Why? The Sygma bridging protocol requires time to pass along the events across chains (currently ~5 minutes). Hence, we increase the timestamp in seconds to 600. Else, the logic will fail and the execution cross-chain will revert!

Finally, setting the deployer tells Hardhat that we will deploy from the same address (as derived from our private key) across chains:

const { multichain, web3 } = require(“hardhat”);
async function main() {
const currentTimestampInSeconds = Math.round(Date.now() / 1000);
const unlockTime = BigInt(currentTimestampInSeconds + 600);
const deployerAccounts = await web3.eth.getAccounts();
const deployer = deployerAccounts[0];

We can then set the networkArguments object, which interacts with the deployMultichain function later on. We also specify which networks we will be deploying to here (sepolia, mumbai, and holesky). For each network we are deploying to, we pass Solidity constructor arguments to args, which initializes the contract with an owner and theunlockTime. initData allows us to make post-deployment contract calls to the setName Solidity function. In this example, we set the names of each chain we are deploying to using initMethodArgs:

const networkArguments = {
sepolia: {
args: [deployer, unlockTime],
initData: {
initMethodName: "setName",
initMethodArgs: ["sepolia"],
},
},
mumbai: {
args: [deployer, unlockTime],
initData: {
initMethodName: "setName",
initMethodArgs: ["mumbai"],
},
},
holesky: {
args: [deployer, unlockTime],
initData: {
initMethodName: "setName",
initMethodArgs: ["holesky"],
},
},
};

We can then call the multichain object’s deployMultichain function and pass on: 1) which contract we are deploying in quotations and 2) our previously defined networkArguments. The code then waits for the deployment to complete and then retrieves and processes the deployment information. If the deployment is successful, it moves forward to obtain further details about the deployment using the transaction hash and Sygma-specific domain IDs:

const deploymentResult = await multichain.deployMultichain(“Lock”, networkArguments);

if (deploymentResult) {
const { transactionHash, domainIDs } = deploymentResult;
await multichain.getDeploymentInfo(transactionHash, domainIDs);
}
}

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

To run this code, you will need an exported private key from a wallet like MetaMask. Obtain that, then create a .env file in the root folder of the exampleProject-hardhat-multichain-deploy. Paste in your exported private key into the .env as such:

PK=”<your_exported_private_key_here>”

Warning: We make use of the dotenv module to manage exported private keys using an environment variable file. Please note that accidentally committing a .env file containing private keys to a wallet with real funds onto GitHub could result in the complete loss of your funds. Never expose your private keys.

Finally, you’re ready to run this script. We will test deployment from Sepolia as the “source” chain, meaning we only pay deployment gas from Sepolia. We can configure the “source” chain by adjusting the network flag:

npx hardhat run scripts/deployMultichain.js --network sepolia

Success!

For a video walkthrough of the hardhat-multichain-deploy example, take a look at this video:

You can find the source code in the hardhat-multichain-deploy repo. All code that was executed in this tutorial can be found in my repo here.

This Hardhat plugin currently supports the Sepolia, Mumbai, and Holesky testnets, with mainnet support coming soon.

Cross-chain contract deployment (via Sygma’s generic message passing capabilities) currently costs a fixed amount of 0.001 ETH on Sygma’s testnet environment. You can take a look at fee schemes in Sygma’s Testnet environment page. So, if you make two cross-chain deployments to Mumbai and Holesky, it will cost 0.002 ETH. This fee scheme will be updated soon to be more dynamic in nature.

You can trace and track your cross-chain transactions using Sygma’s block explorer. Here’s the testnet explorer.

To summarize, thanks to Sygma’s cross-chain interoperability protocol and the handy Harplugin that ChainSafe has built, you can now seamlessly deploy the same smart contract across multiple blockchains under the same contract address while paying only source chain gas/fees.

Much thanks to the engineers at Sygma and ChainSafe for guiding me through this!

--

--