Deploy Contracts to ZKsync
This how-to guide explains how to deploy a smart contract on ZKsync using hardhat-zksync-deploy and hardhat-zksync-ethers.
What you'll learn:
- How to compile smart contracts
- How to deploy contracts with hardhat-zksync-deploy
- How to deploy contracts with hardhat-zksync-ethers
Tools:
- - hardhat-zksync-deploy
- - hardhat-zksync-ethers
This guide provides a step-by-step guide for deploying smart contracts on the ZKsync Sepolia Testnet network,
using capabilities of Hardhat together with the hardhat-zksync-deploy
and hardhat-zksync-ethers
plugins.
Prerequisites
- A Node.js installation running at minimum Node.js version 14.
- Initialized Hardhat TypeScript project
- A wallet with sufficient Sepolia
ETH
on Ethereum and ZKsync Sepolia Testnet to pay for deploying smart contracts. You can get Sepolia ETH from the network faucets.- Get testnet
ETH
for ZKsync Era using bridges to bridge funds to ZKsync.
- Get testnet
- You know how to get your private key from your MetaMask wallet.
Deployment using hardhat-zksync-deploy
One way to deploy smart contracts is by using the hardhat-zksync-deploy plugin, which offers utilities for deploying smart contracts on ZKsync Era with artifacts generated by either the @matterlabs/hardhat-zksync-solc or @matterlabs/hardhat-zksync-vyper plugins.
Check hardhat-zksync-deploy documentation here.
Installation
To install the hardhat-zksync-deploy plugin and additional necessary packages, execute the following command:
npm install -D @matterlabs/hardhat-zksync-deploy hardhat zksync-ethers ethers
Once installed, add the plugin at the top of the hardhat.config.ts file.
import "@matterlabs/hardhat-zksync-deploy";
Create a new smart contract
Here are the steps to create new smart contract:
- Navigate to the root of your project.
- Create a folder named contracts.
- Inside the contracts folder, create a file named SimpleStorage.sol.
Now we should add some code to the new SimpleStorage.sol file:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SimpleStorage {
uint private number;
// Function to set the number
function setNumber(uint _number) public {
number = _number;
}
// Function to get the number
function getNumber() public view returns (uint) {
return number;
}
}
Configuration
Before we continue with the deployment, we must include hardhat-zksync-solc plugin in order to compile our contracts.
npm install -D @matterlabs/hardhat-zksync-solc
Add the plugin at the top of the hardhat.config.ts file:
import "@matterlabs/hardhat-zksync-solc";
Read more about hardhat-zksync-solc plugin here.
To enable deployment across various networks within the hardhat.config.ts file, it's essential to configure the networks section. For this example, we will be deploying on ZKsync Sepolia Testnet, so we need to adjust network accordingly. Furthermore, it's also important to configure the compilers settings.
import "@matterlabs/hardhat-zksync-solc";
import "@matterlabs/hardhat-zksync-deploy";
import { HardhatUserConfig } from "hardhat/config";
const config: HardhatUserConfig = {
zksolc: {
// By not specifying any options, we are using the default settings of zksolc.
},
solidity: {
version: "0.8.17",
},
defaultNetwork: "zkTestnet",
networks: {
zkTestnet: {
url: "https://sepolia.era.zksync.dev", // The RPC URL of ZKsync Era network.
ethNetwork: "https://sepolia.infura.io/v3/<API_KEY>", // The Ethereum Web3 RPC URL.
zksync: true, // Flag that targets ZKsync Era.
},
},
};
export default config;
In the configuration above we have some important fields such as:
zksolc
- enabling adjustment and customization of the zksolc compiler.solidity
- enabling adjustment and customization of the solc compiler.defaultNetwork
- specifying which network is default to use when running hardhat tasks.networks
- define which networks we can use in our development. Only one network is used at the time.url
is a field containing the URL of the ZKsync Era node in case of the ZKsync Era network (with zksync flag set to true), or the URL of the Ethereum node. This field is required for all ZKsync Era and Ethereum networks.ethNetwork
is a field with the URL of the Ethereum node. You can also provide network name (e.g. sepolia) as the value of this field. In this case, the plugin will either use the URL of the appropriate Ethereum network configuration (from the networks section), or the default.zksync
is a flag that indicates if the network is ZKsync Era. This field needs to be set to true for all ZKsync Era networks; it is false by default
Find more details about available configuration options in the official documentation.
Compilation
Execute the following command in your terminal to run the compilation:
npx hardhat compile
After successful compilation, you should see the output similar to this:
Compiling contracts for ZKsync Era with zksolc v1.3.22 and solc v0.8.17
Compiling 1 Solidity file
Successfully compiled 1 Solidity file
Done in 0.69s.
In the root of your project you will see two new folders that represent zksolc compilation result:
artifacts-zk
cache-zk
Deploy script
Here are the steps to create deploy script with hardhat-zksync-deploy plugin.
- Navigate to your project's root directory.
- Create a new folder named deploy.
- Inside the deploy folder, create a file named deploy-simple-storage.ts.
Now we should add some code to the new deploy-simple-storage.ts file:
import { Wallet } from "zksync-ethers";
import * as ethers from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import dotenv from "dotenv";
dotenv.config();
const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
if (!PRIVATE_KEY) {
throw new Error("Wallet private key is not configured in .env file!");
}
// An example of a deploy script
export default async function (hre: HardhatRuntimeEnvironment) {
console.log(`Running deploy script for the SimpleStorage contract`);
// Initialize the wallet.
const wallet = new Wallet(PRIVATE_KEY);
// Create deployer object and load the artifact of the contract you want to deploy.
const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact("SimpleStorage");
// Estimate contract deployment fee
const deploymentFee = await deployer.estimateDeployFee(artifact, []);
const parsedFee = ethers.formatEther(deploymentFee);
console.log(`The deployment is estimated to cost ${parsedFee} ETH`);
// Deploy contract
const simpleStorageContract = await deployer.deploy(artifact, []);
// Show the contract info.
const contractAddress = await simpleStorageContract.getAddress();
console.log(`${artifact.contractName} was deployed to ${contractAddress}`);
}
To deploy contract on ZKsync Sepolia Testnet that we already specified as the default network in our hardhat.config.ts, run command:
npx hardhat deploy-zksync
If you prefer to explicitly select network name within a command, you can run:
npx hardhat deploy-zksync --network zkTestnet
After successful deployment, your console output should look something like this:
Running deploy script for the SimpleStorage contract
The deployment is estimated to cost 0.0002588676 ETH
SimpleStorage was deployed to 0xCE5e67aF41C194aB05fCC3860ef66790A147Adf9
Done in 8.58s.
Check your deployed contract on ZKsync Sepolia Testnet Block Explorer using deployed address of the contract.
Deployment using hardhat-zksync-ethers
With similar steps as above, we will now use hardhat-zksync-ethers to deploy our contracts.
hardhat-zksync-ethers plugin is a wrapper around zksync-ethers SDK that gives additional methods to use for faster development.
Installation
In the root directory of your project, execute this command:
npm install -D @matterlabs/hardhat-zksync-ethers hardhat zksync-ethers ethers
Add the plugin at the top of the hardhat.config.ts file:
import "@matterlabs/hardhat-zksync-ethers";
Configuration
To deploy contracts with hardhat-zksync-ethers, use the similar configuration for our hardhat.config.ts as shown here. The accounts section enables to specify wallet private keys which help us deploy contracts with automatically populated wallet within hardhat-zksync-ethers plugin, but it can still be manually created for other use cases.
.env
file which is explicitly ignored in .gitignore
file.
This way you are protected from accidentally exposing your private key in one of your github repositories.When you are about to use the private key, load it using:
import dotenv from "dotenv";
dotenv.config();
const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
if (!PRIVATE_KEY) {
throw new Error("Private key is not configured in .env file!");
}
{
url: 'https://sepolia.era.zksync.dev', // you should use the URL of the ZKsync network RPC
ethNetwork: 'sepolia',
zksync: true,
accounts:[PRIVATE_KEY] // put your private key here
},
Deployment process
For the following examples, we use same SimpleStorage.sol smart contract as shown here.
Compilation
We also use the same steps as shown above in this section.
Deploy scripts
In this section, we'll create several deployment scripts to demonstrate various methods of deploying a smart contract.
Since we've placed the private key inside the account
section of hardhat.config.ts,
we're able to automatically connect the default wallet to the network.
deploy
folder isn't used for deployments.
Instead, scripts will be stored within the scripts
folder and executed using the hardhat run scripts/SCRIPT_NAME
command.Usage of getWallet helper method
Since we aim to deploy contracts to the ZKsync Sepolia Testnet
,
and considering we've configured an account
section inside the network settings in the hardhat.config.ts,
we can automatically connect the default wallet to the network.
All helper methods for deployments will utilize this approach under the hood,
so users don't need to instantiate a new wallet object.
If we want to obtain a wallet instance for a new private key that is not specified in the account
section, we can do so with:
const wallet = await hre.zksyncEthers.getWallet(PRIVATE_KEY);
If we extend the account
section inside the network
settings
in the hardhat.config.ts with more than one account, we can retrieve the wallets by indexes.
First load your private keys from .env
.
const PRIVATE_KEY_1 = process.env.PRIVATE_KEY_1 || "";
const PRIVATE_KEY_2 = process.env.PRIVATE_KEY_2 || "";
const PRIVATE_KEY_3 = process.env.PRIVATE_KEY_3 || "";
Update accounts
settings with loaded private keys:
....
accounts: [PRIVATE_KEY_1, PRIVATE_KEY_2, PRIVATE_KEY_3]
....
Access wallets by index in the scripts:
const wallet = await hre.zksyncEthers.getWallet(0); //first account
const wallet = await hre.zksyncEthers.getWallet(1); //second account
const wallet = await hre.zksyncEthers.getWallet(2); //third account
Now, we can combine this feature to provide the desired wallet to other helper methods that have a wallet as a method parameter.
To learn more about hardhat-zksync-ethers helper functions check documentation section.
Deploy script with contract factory using contract name
In this example, we demonstrate how to deploy contracts using a contract factory with the contract name.
To achieve this, we need to create a deploy script.
- Navigate to your project's root directory.
- Navigate to your project's scripts directory.
- Inside the scripts folder, create a file named deploy-simple-storage-with-name.ts.
import hre from "hardhat";
async function main() {
console.info(`Running deploy`);
// Returns contract factory which deploys contracts
// Second parameter, Wallet, is optional, you can either provide a wallet or leave it empty, as you've configured the account object inside hardhat.config.ts
const simpleStorageFactory = await hre.zksyncEthers.getContractFactory("SimpleStorage");
// Deploy contract
const simpleStorage = await simpleStorageFactory.deploy();
// Wait for contract to be deployed
await simpleStorage.waitForDeployment();
// Call contract function
const tx = await simpleStorage.setNumber(5);
// Wait for transaction
await tx.wait();
console.info(`Simple storage deployed to: ${await simpleStorage.getAddress()}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
To deploy contract, execute command:
npx hardhat run scripts/deploy-simple-storage-with-name.ts
After successful execution, your console output should look something like this:
Running deploy
Simple storage deployed to: 0xaC040A123496A113e84AaBEf9876Cd5D5c62Effd
Done in 5.73s.
Deploy script with contract factory using abi and bytecode
In this example, we demonstrate how to deploy contracts using a contract factory with the artifact abi and the bytecode
To achieve this, we need to create a deploy script.
- Navigate to your project's root directory.
- Navigate to your project's scripts directory.
- Inside the scripts folder, create a file named deploy-simple-storage-with-abi-and-bytecode.ts.
import hre from "hardhat";
import dotenv from "dotenv";
dotenv.config();
const NEW_PRIVATE_KEY = process.env.NEW_PRIVATE_KEY || "";
if (!NEW_PRIVATE_KEY) {
throw new Error("Wallet private key is not configured in .env file!");
}
async function main() {
console.info(`Running deploy`);
// Returns artifact for contract name
const artifact = await hre.zksyncEthers.loadArtifact("SimpleStorage");
// Use a new wallet for this deployment
const wallet = await hre.zksyncEthers.getWallet(NEW_PRIVATE_KEY);
// Get contract factory using abi and bytecode
const simpleStorageFactory = await hre.zksyncEthers.getContractFactory(artifact.abi, artifact.bytecode, wallet);
// Deploy contract
const simpleStorage = await simpleStorageFactory.deploy();
// Wait for contract to be deployed
await simpleStorage.waitForDeployment();
// Call contract function
const tx = await simpleStorage.setNumber(12);
// Wait for transaction
await tx.wait();
console.info(`Simple storage deployed to: ${await simpleStorage.getAddress()}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
To deploy contract, execute command:
npx hardhat run scripts/deploy-simple-storage-with-abi-and-bytecode.ts
After successful execution, your console output should look something like this:
Running deploy
Simple storage deployed to: 0xaB090A123496A113e84AaBEf9876Cd5D5c62Effd
Done in 5.43s.
Deploy script with deploy contract
In this example, we demonstrate how to deploy contracts using a similar method that is used in hardhat-zksync-deploy plugin.
For deployment, loadArtifact
and deployContract
methods will be used integrated inside the hardhat-zksync-ethers plugin.
To achieve this, we need to create a new deploy script.
- Navigate to your project's root directory.
- Navigate to your project's scripts directory.
- Inside the scripts folder, create a file named deploy-simple-storage.ts.
import hre from "hardhat";
async function main() {
console.info(`Running deploy`);
// Returns artifact for contract name
const artifact = await hre.zksyncEthers.loadArtifact("SimpleStorage");
// Deploy contract
const simpleStorage = await hre.zksyncEthers.deployContract(artifact, []);
// Wait for contract to be deployed
await simpleStorage.waitForDeployment();
// Call contract function
const tx = await simpleStorage.setNumber(15);
// Wait for transaction
await tx.wait();
console.info(`Simple storage deployed to: ${await simpleStorage.getAddress()}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
To deploy contract, execute command:
npx hardhat run scripts/deploy-simple-storage.ts
After successful execution, your console output should look something like this:
Running deploy
Simple storage deployed to: 0xcF030A123496A113e84AaBEf9876Cd5D5c62Effd
Done in 5.63s.