How to test smart contracts with Hardhat

Learn how to test your smart contracts locally using era-test-node and Hardhat

This how-to guide explains how to test smart contracts on ZKsync using Hardhat.

What you'll learn:

  • How to install era-test-node.
  • How to write local tests for smart contracts.
GitHub

Tools:

  • - zksync-ethers
  • - hardhat-zksync-deploy
  • - hardhat-chai-matchers
  • - era-test-node
smart contractshow-totesting
Last Updated: May 09, 2024

This tutorial provides a step-by-step guide on testing smart contracts using the hardhat-chai-matchers plugin in conjunction with the ZKsync Era Test Node on your local machine. To facilitate this process of running tests on the ZKsync Era Test Node, you'll also utilize the hardhat-zksync plugin.

Prerequisites

  • Node.js installed (version 14.x or later)
  • Either yarn or npm installed

Era-test-node plugin

In this tutorial, the contract functionality is tested using the ZKsync Era Test Node. To start local node we use the hardhat-zksync plugin to integrate this functionality within the Hardhat project.

During the alpha phase, ZKsync Era Test Nodes are currently undergoing development, wherein certain functionalities might not be fully supported or operational.

Installation

First, initialize a new Hardhat TypeScript project:

npx hardhat init

Select the Create a TypeScript project option and install the sample project's dependencies: hardhat and @nomicfoundation/hardhat-toolbox.

To install the hardhat-zksync plugin, execute the following command:

npm i -D @matterlabs/hardhat-zksync

Once installed, add the plugin at the top of your hardhat.config.ts file.

hardhat.config.ts
import "@matterlabs/hardhat-zksync";

Starting the ZKsync Era Test Node

You can now safely run the ZKsync Era Test Node with the following command:

npx hardhat node-zksync
We'll want to verify the correctness of our installations and test if we can run a ZKsync Era Test Node, without further use of this command in the tutorial.

You should see output similar to this:

09:04:44  INFO Account #9: 0xe2b8Cb53a43a56d4d2AB6131C81Bd76B86D3AFe5 (1_000_000_000_000 ETH)
09:04:44  INFO Private Key: 0xb0680d66303a0163a19294f1ef8c95cd69a9d7902a4aca99c05f3e134e68a11a
09:04:44  INFO Mnemonic: increase pulp sing wood guilt cement satoshi tiny forum nuclear sudden thank
09:04:44  INFO
09:04:44  INFO ========================================
09:04:44  INFO   Node is ready at 127.0.0.1:8011
09:04:44  INFO ========================================

Since we've confirmed that the ZKsync Era Test Node is functioning properly with the help of the hardhat-zksync plugin, we can shut it down and continue with the tutorial.

Integration with Hardhat

To enable the usage of ZKsync Era Test Node in Hardhat, update the version of Solidity to 0.8.26 in the hardhat.config.ts file, add the zksync:true option to the hardhat network, and add the latest version of zksolc:

solidity: "0.8.26",
zksolc: {
    version: "latest",
  },
networks: {
    hardhat: {
      zksync: true,
    },
  },

hardhat-chai-matchers plugin

To leverage ZKsync-specific capabilities within the Chai assertion library for testing smart contracts, it's necessary to use the hardhat-chai-matchers plugin.

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

npm i -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6

After installing it, add the plugin at the top of your hardhat.config.ts file:

hardhat.config.ts
import "@nomicfoundation/hardhat-chai-matchers";

With the previous steps completed, your hardhat.config.ts file should now be properly configured to include settings for local testing.

hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@matterlabs/hardhat-zksync";
import "@nomicfoundation/hardhat-chai-matchers";

const config: HardhatUserConfig = {
  solidity: "0.8.26",
  zksolc: {
    version: "latest",
  },
  networks: {
    hardhat: {
      zksync: true,
    },
  },
};

export default config;

Smart contract example

To set up the environment for using chai matchers and writing tests, you'll need to create some contracts.

Inside the contracts folder, rename the example contract file to Greeter.sol.

mv contracts/Lock.sol contracts/Greeter.sol

Now replace the example contract in Greeter.sol with the new Greeter contract below:

Greeter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Greeter {
    string private greeting;
    bool private greetingChanged;
    constructor(string memory _greeting) {
        greeting = _greeting;
        greetingChanged = false;
    }
    function greet() public view returns (string memory) {
        return greeting;
    }
    function setGreeting(string memory _greeting) public {
        require(bytes(_greeting).length > 0, "Greeting must not be empty");
        greeting = _greeting;
        greetingChanged = true;
    }
    function isGreetingChanged() public view returns (bool) {
        return greetingChanged;
    }
}

Write Test Cases

Now you can create a test with the hardhat-chai-matchers plugin:

Inside the /test folder, rename the example test file to test.ts.

mv test/Lock.ts test/test.ts

Replace the old test with this example showcasing the functionalities of the contract:

import * as hre from "hardhat";
import { expect } from "chai";
import { Wallet, Provider, Contract } from "zksync-ethers";
import { Deployer } from "@matterlabs/hardhat-zksync";
import { ZkSyncArtifact } from "@matterlabs/hardhat-zksync-deploy/src/types";

const RICH_PRIVATE_KEY = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";

describe("Greeter", function () {
  let provider: Provider;
  let deployer: Deployer;
  let artifact: ZkSyncArtifact;
  let contract: Contract;

  before(async function () {
    // Creation of a provider from a network URL adjusted specifically for the ZKsync Era Test Node.
    provider = new Provider(hre.network.config.url);
    // To ensure proper testing, we need to deploy our contract on the ZKsync Era Test Node, for more info check hardhat-zksync-deploy plugin documentation.
    deployer = new Deployer(hre, new Wallet(RICH_PRIVATE_KEY));
    artifact = await deployer.loadArtifact("Greeter");
    contract = await deployer.deploy(artifact, ["Hello, world!"]);
  });
  it("should work on Era Test node", async function () {
    const netVersion = await provider.send("net_version", []);
    expect(netVersion === 260);
  });
  it("greet should return a string", async function () {
    expect(await contract.greet()).to.be.a("string");
  });
  it("is deployed address valid", async function () {
    expect(await contract.getAddress()).to.be.properAddress;
  });
  it("greet should say Hello", async function () {
    expect(await contract.greet()).to.match(/^Hello/);
  });
  it("setGreeting should throw when passed an invalid argument", async function () {
    await expect(contract.setGreeting("")).to.be.revertedWith("Greeting must not be empty");
  });
  it("isGreetingChanged should return true after setting greeting", async function () {
    expect(await contract.isGreetingChanged()).to.be.false;
    const tx = await contract.setGreeting("Changed");
    await tx.wait();
    expect(await contract.greet()).to.match(/^Changed/);
    expect(await contract.isGreetingChanged()).to.be.true;
  });
});

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

npx hardhat test
When you execute this command, the contract will be automatically compiled, eliminating the need for manual compilation. However, if you prefer to compile manually, simply run the following command npm hardhat compile / yarn hardhat compile.

The hardhat-zksync plugin overrides the default behavior of the Hardhat test task. It starts the ZKsync Era Test Node before running tests, executes the tests, and then automatically shuts down the node after the test cases are completed.

Additionally, the plugin generates a log file named era_test_node.log, which indicates the node's activity and transactions made during the tests. Whenever you re-run the test command, the content of era_test_node.log is refreshed.

This setup ensures that your tests are executed against a controlled environment, mimicking the behavior of a live network but in a local sandboxed context. It's a common practice to ensure that smart contracts behave as expected under various conditions before deploying them to a live network.

era_test_node.log file example:

10:53:11  INFO
10:53:11  INFO EthToken System Contract
10:53:11  INFO   Topics:
10:53:11  INFO     0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
10:53:11  INFO     0x0000000000000000000000000000000000000000000000000000000000008001
10:53:11  INFO     0x00000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049
10:53:11  INFO   Data (Hex): 0x000000000000000000000000000000000000000000000000000028e0ec2a9900
10:53:11  INFO
10:53:11  INFO Call: SUCCESS
10:53:11  INFO Output: "0x0000000000000000000000000000000000000000000000000000000000000001"
10:53:11  INFO === Console Logs:
10:53:11  INFO === Call traces:

Made with ❤️ by the ZKsync Community