Solidity Made Easy with Hardhat

Dec 27th, 2022
6 min read

As a Solidity Developer who started with Remix, I wondered if there was a better way to create a smart contract.

And I believe I’ve found the solution with Hardhat.

image

In my Web 3.0 article, I mentioned Smart Contracts as one of the most powerful tools for achieving Web 3.0 and as one of the factors that have led to Ethereum’s current success.

Disclaimer: This article will not go into the nitty-gritty technical details of Solidity. The goal is to show how to use Hardhat as the Solidity development environment.

Before Hardhat, there was Remix

Let’s talk a bit about Remix first.

What exactly is Remix? Remix is a smart contract development IDE. It’s likely the first thing you learn when learning smart contract development.

image
remix.ethereum.org

It has served its purpose admirably, whether for learning, prototyping, or development. Even though it is adequate, I want to look for something that better suits my needs.

Hardhat!

Hardhat is a development environment that streamlines compiling, deploying, debugging, and, my personal favorite, testing smart contracts.

This article will walk you through the steps of creating, compiling, testing, deploying, and verifying smart contract projects using Hardhat.


Article Outline

  1. Creating Project with Hardhat
  2. Implementing Smart Contract
  3. Automated Test with Hardhat
  4. Deploying with Hardhat Script
  5. Verifying Code on Blockchain Explorer (withHardhat Script)

1. Creating the project

Let’s start with an npm project, then install hardhat and create a TypeScript project with it.

npm init npm install --save-dev hardhat npx hardhat
image
Select TypeScript when creating a sample project

Then the project structure should be like this:

image
Project Structure

The meaning of each important files

  • hardhat.config.ts Configuration file (Learn more here)
  • contracts/ Directory for Smart Contract source code
  • scripts/ Directory for scripts, e.g., deploy scripts
  • test/ Directory for automated test files

Change the test command in package.json to this:

1"scripts": {
2    "test": "hardhat test"
3}

Environment file

Create .env by duplicate .env.example

image
The real .env

Configure .env to the real values. I’ll be using Ethereum Ropsten as the network for this article. (For the ROPSTEN_URL using this site: rpc.info is a good way to find one)

This article was written when Ropsten was still alive. But it will be deprecated and shut down soon (Q4 2022), so you might need to use another network.
Read more

🏁 Checkpoint #1: Project created

At this point, there should be a directory containing sample code, which you can test by running the following command:

npm run test
image
Test results

2. Implementing the contract

For this article, I’ll make a LoveLetter contract to send a lovely message (with money attached) to a recipient.

Create LoveLetter.sol in contracts/

image
LoveLetter.sol in contracts

The requirements:

  • Send letters containing Ether.
  • Receive letters with Ether. It should only be callable by the receiver.
  • Read the message contained within a letter. (We won’t be preventing others from reading the message.)
⚠️ Challenge1: Before reading the code below, try implementing it with the help from contracts/Greeter.sol.

🏁 Checkpoint #2: Contract implemented

At this point, the LoveLetter.sol is finished. Here are my implementations:

1//SPDX-License-Identifier: Unlicense
2pragma solidity ^0.8.0;
3
4import "hardhat/console.sol";
5
6contract LoveLetter {
7    uint256 public totalLetters;
8    mapping(uint256 => address) public senders;
9    mapping(uint256 => address) public receivers;
10    struct Letter {
11        string message;
12        uint256 etherAmount;
13        bool opened;
14    }
15    mapping(uint256 => Letter) public letters;
16
17    event Sent(
18        address indexed from,
19        address indexed to,
20        uint256 indexed id,
21        uint256 amount
22    );
23    event Opened(
24        address indexed from,
25        address indexed to,
26        uint256 indexed id,
27        uint256 amount
28    );
29
30    constructor() {
31        totalLetters = 0;
32    }
33
34    function send(address to, string memory message)
35        external
36        payable
37        returns (uint256 id)
38    {
39        id = totalLetters;
40        senders[id] = msg.sender;
41        receivers[id] = to;
42        letters[id] = Letter({
43            message: message,
44            etherAmount: msg.value,
45            opened: false
46        });
47        console.log("[send]", id, msg.value);
48        totalLetters++;
49        emit Sent(msg.sender, to, id, msg.value);
50    }
51
52    function open(uint256 id) external returns (string memory message) {
53        require(receivers[id] == msg.sender, "Not receiver");
54        require(!letters[id].opened, "Already opened");
55        message = letters[id].message;
56        letters[id].opened = true;
57        uint256 amount = letters[id].etherAmount;
58        console.log("[open]", id, amount);
59        if (amount > 0) {
60            payable(msg.sender).transfer(amount);
61        }
62        emit Opened(senders[id], msg.sender, id, amount);
63    }
64
65    function readMessage(uint256 id)
66        external
67        view
68        returns (string memory message)
69    {
70        message = letters[id].message;
71    }
72
73    function checkOpened(uint256 id) external view returns (bool opened) {
74        opened = letters[id].opened;
75    }
76
77    function getEtherAmount(uint256 id)
78        external
79        view
80        returns (uint256 etherAmount)
81    {
82        etherAmount = letters[id].etherAmount;
83    }
84
85    function getSender(uint256 id) external view returns (address sender) {
86        sender = senders[id];
87    }
88
89    function getReceiver(uint256 id) external view returns (address receiver) {
90        receiver = receivers[id];
91    }
92}

Check the contract by running this command:

npx hardhat compile

The result should be something like this:

image

Next, let’s use my favorite features of Hardhat. Automated Test!


3. Testing the contract

Create the test file loveletter.ts in test/.

image
loveletter.ts in test/

Implement tests for these use cases:

  • The contract can be deployed successfully.
  • Send the letter successfully, then read the correct values from it.
  • The letter can’t be opened by someone that’s not the receiver.
  • The receiver opens the letter successfully, receiving the Ether within.
⚠️ Challenge2: Before reading the code below, try implementing it with the help from test/index.ts.
1import { expect } from "chai";
2import { Signer, utils } from "ethers";
3import { ethers } from "hardhat";
4import { LoveLetter } from "../typechain";
5
6describe("LoveLetter", () => {
7  let love: LoveLetter;
8  let sender: Signer;
9  let receiver: Signer;
10  let stranger: Signer;
11  before(async () => {
12    const LoveLetterFactory = await ethers.getContractFactory("LoveLetter");
13    love = await LoveLetterFactory.deploy();
14    await love.deployed();
15    const accounts = await ethers.getSigners();
16    sender = accounts[0];
17    receiver = accounts[1];
18    stranger = accounts[2];
19  });
20
21  it("Should deployed and initiated", async () => {
22    expect(await love.totalLetters()).to.equal(0);
23  });
24
25  it("Should send successfully", async () => {
26    expect(
27      await love.connect(sender).send(await receiver.getAddress(), "Love chu", {
28        value: utils.parseEther("1"),
29      })
30    ).to.emit(love, "Sent");
31    expect(await love.readMessage(0)).to.equal("Love chu");
32    expect(await love.checkOpened(0)).to.equal(false);
33    expect(await love.getSender(0)).to.equal(await sender.getAddress());
34    expect(await love.getReceiver(0)).to.equal(await receiver.getAddress());
35    expect(await love.getEtherAmount(0)).to.equal(utils.parseEther("1"));
36  });
37
38  it("Should error if open by stranger", async () => {
39    expect(love.connect(stranger).open(0)).to.revertedWith("Not receiver");
40  });
41
42  it("Should open successfully", async () => {
43    const before = await receiver.getBalance();
44    const tx = await love.connect(receiver).open(0);
45    expect(tx).to.emit(love, "Opened");
46    const gas = (await tx.wait()).gasUsed.mul(tx.gasPrice || 0);
47    const after = await receiver.getBalance();
48    expect(after.sub(before).add(gas)).to.equal(utils.parseEther("1"));
49  });
50});

Running tests

Use the command:

npm run test

The result will be something like this:

image
The result
[send] 0 1000000000000000000 ... What is this?

And that would beconsole.log() for solidity!

This console.log is inside send function in my implementation of the contract. Read more about console.log

🏁 Checkpoint #3: Contract tested

At this point, the contract has been tested. Next, let’s deploy it onto the network!

⚠️ Challenge3: Add two test cases:
1. Revert the transaction if the receiver tries to open an opened letter.
2. Send a letter (which would has an id of 1) successfully.
My implementation will be in the GitHub repository at the end of this article.

4. Deploying the contract

⚠️ Challenge4: Try deploying the contract on Remix first!

Deploy with Hardhat script

Create the script at scripts/deploy-loveletter.ts

⚠️ Challenge5: Try implementing the script with scripts/deploy.ts as an example.
1import { ethers } from "hardhat";
2
3async function main() {
4  const LoveLetter = await ethers.getContractFactory("LoveLetter");
5  const loveLetter = await LoveLetter.deploy();
6
7  await loveLetter.deployed();
8
9  console.log("LoveLetter deployed to:", loveLetter.address);
10}
11
12main().catch((error) => {
13  console.error(error);
14  process.exitCode = 1;
15});

Then run the command below for deploying:

npx hardhat run scripts/deploy-loveletter.ts --network ropsten
image
Result of the deployment

🏁 Checkpoint #4: Contract deployed

At this point, the smart contract is deployed on the blockchain.

You can see your deployed contract on https://ropsten.etherscan.io/ (or whatever blockchain explorer for the network of your choice)

image
Contract is on ropsten.etherscan.io !

A thing might be a little different for you, namely the Contract tab without the green checkmark.

The checkmark indicates that the contract has been verified with readable source code. So let’s go and do that!


5: Verifying the source code

Even though anyone can deploy smart contracts on the blockchain (Permissionless) and see the code of all smart contracts. There’s a catch.

The code is a bytecode, which is not human-readable.

image
Bytecode of the contract

If contract developers want others to be able to read the source code, they will have to verify the contract with the original source code first.

And with hardhat, doing so is easier than ever!

npx hardhat verify --network ropsten {{contractAddress}}

Now, anyone will be able to read the code!

image
Verified source code
⚠️ Challenge6: Try verify the code on blockchain explorer itself.

🏁 Checkpoint #5: Contract code verified

At this point, we have a tested, deployed, and verified contract on the network!

⚠️ Challenge7: Try Deploy & Verify on mainnet. The process is exactly the same, just changing things in .env and sometinkering with hardhat.config.ts

🏁 Done! 🏁

To recap, here are the things we’ve done in this article:

  1. Creating Project with Hardhat
  2. Implementing Smart Contract
  3. Automated Test with Hardhat
  4. Deploying with Hardhat Script
  5. Verifying Code on Blockchain Explorer (with Hardhat Script)

Thank you for reading, see you later in the next article!

Resources:

GitHub - aikdanai/solidity-hardhat-101

Contribute to aikdanai/solidity-hardhat-101 development by creating an account on GitHub.github.com
embedImage

Hardhat's tutorial for beginners | Ethereum development environment for professionals by Nomic Foundation

Ethereum development environment for professionals by Nomic Foundationhardhat.org
embedImage
author's profile image
Aikdanai

Senior Software Engineer