Deploying Smart Contracts


Deploying Smart Contracts

This tutorial 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

  • Node.js 14.x or later
  • Yarn or npm Package Manager
  • Initialized Hardhat TypeScript project
  • A wallet with sufficient Sepolia ETH on Ethereum and zkSync Era Testnet to pay for deploying smart contracts. You can get Sepolia ETH from the network faucets.
  • You know how to get your private key from your MetaMask walletopen in new window.

Tutorial

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:

yarn add -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:

  1. Navigate to the root of your project.
  2. Create a folder named contracts.
  3. 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;
    }
}

Compilation

Before we continue with the deployment, we must include hardhat-zksync-solc plugin in order to compile our contracts.

yarn add -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.

Configuration

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.
    },
  },
};

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.

Execute the following command in your terminal to run the compilation:

yarn 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

Here are the steps to create deploy script with hardhat-zksync-deploy plugin.

  1. Navigate to your project's root directory.
  2. Create a new folder named deploy.
  3. 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 default network in our hardhat.config.ts, run command:

yarn hardhat deploy-zksync

If you prefer to explicitly select network name within a command, you can run:

yarn 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 Sepolia testnet block exploreropen in new window 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-ethersopen in new window SDK that gives additional methods to use for faster development.

Installation

In the root directory of your project, execute this command:

yarn add -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.

Note

You should store your private key in .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.

hardhat-zksync-ethers does not require deploy folder

These plugins don't use the hardhat-zksync-deploy plugin, so the 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.

  1. Navigate to your project's root directory.
  2. Navigate to your project's scripts directory.
  3. 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:

yarn 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.

  1. Navigate to your project's root directory.
  2. Navigate to your project's scripts directory.
  3. 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:

yarn 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.

  1. Navigate to your project's root directory.
  2. Navigate to your project's scripts directory.
  3. 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:

yarn 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.