How to test smart contracts with 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.
Tools:
- - zksync-ethers
- - hardhat-zksync-deploy
- - hardhat-chai-matchers
- - era-test-node
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.
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.
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
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:
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.
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:
// 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
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: