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.
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.
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
- Creating Project with Hardhat
- Implementing Smart Contract
- Automated Test with Hardhat
- Deploying with Hardhat Script
- 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
Then the project structure should be like this:
The meaning of each important files
Configuration file (Learn more here)hardhat.config.ts
Directory for Smart Contract source codecontracts/
scripts/
Directory for scripts, e.g., deploy scriptstest/
Directory for automated test files
Change the
command in test
to this:package.json
1"scripts": {
2 "test": "hardhat test"
3}
Environment file
Create
by duplicate .env
.env.example
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
2. Implementing the contract
For this article, I’ll make a
contract to send a lovely message (with money attached) to a recipient.LoveLetter
Create
in LoveLetter.sol
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
is finished. Here are my implementations:LoveLetter.sol
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:
Next, let’s use my favorite features of Hardhat. Automated Test!
3. Testing the contract
Create the test file
in loveletter.ts
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:
[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
🏁 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)
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.
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!
⚠️ 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 withhardhat.config.ts
🏁 Done! 🏁
To recap, here are the things we’ve done in this article:
- Creating Project with Hardhat
- Implementing Smart Contract
- Automated Test with Hardhat
- Deploying with Hardhat Script
- Verifying Code on Blockchain Explorer (with Hardhat Script)
Thank you for reading, see you later in the next article!