- Getting Started
- Deploying Your First Contract with HardHat
- Programmatic Smart Contract Verification
- Interacting with your Smart Contract
- HardHat Tasks
- HardHat Node
- Interacting with Networks via CLI
- Testing
- Additional HardHat Features
A smart contract development framework makes it easy to create smart contracts fast. We have previously gone through the Solidity fundamentals and how to deploy Smart Contract with Ethers.js, but now we want to speed up the process of creating a smart contracts with Hardhat. We are choosing Hardhat because it is one of the most used smart contract development framework at the time of writing this article.
Getting Started
- Create a folder for a new project (e.g.
mkdir hardhat-smart-contract-example
) - Run
yarn init
- Add Hardhat package:
yarn add --dev hardhat
- Create Hardhat project:
yarn hardhat
. You can click on enter for all the values. It creates:contracts
folder: this is where you put your Smart Contracts.scripts
folder: this is where you put your scripts for deploying contracts or interacting with them..gitignore
: git file for avoiding committing some file or folders.hardhat.config.js
: this is where you place the HardHat configuration, plugins, and tasks.README.md
: this is to document your project.test
folder: this is where you will create your smart contract tests.
Deploying Your First Contract with HardHat
You can deploy a contract with HardHat by just writing a few lines compare to the JavaScript vanilla version.
contracts/HelloWorld.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract HelloWorld {
string from;
function helloWorld() public view returns (string memory) {
if (bytes(from).length == 0) {
return "Hello World!";
}
return string(abi.encodePacked("Hello World from ", from, "!"));
}
function updateFrom(string memory _from) public {
from = _from;
}
}
scripts/deploy.js
const { ethers } = require("hardhat")
async function main() {
const helloWorldFactory = await ethers.getContractFactory("HelloWorld")
console.log("Deploying...")
const helloWorld = await helloWorldFactory.deploy()
await helloWorld.deployed()
console.log(`Deployed contract to: ${helloWorld.address}`)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
as you can see you don’t even have to specify the provider URL, private key or the smart contract location - HardHat is doing all of that for you 🪄
Then you can run yarn hardhat run scripts/deploy.js
.
HardHat comes with an built-in testing environment called HardHat Network that is running in the background. You can use this dev environment for testing and debugging your scripts and smart contracts. Of course, you can change the default network and define new networks with its own values on the hardhat.config.js
as shown below.
hardhat.config.js
:
require("@nomicfoundation/hardhat-toolbox")
require("dotenv").config()
const GOERLI_RPC_URL = process.env.GOERLI_RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
defaultNetwork: "hardhat",
networks: {
// chainId from https://goerli.net
goerli: { url: GOERLI_RPC_URL, accounts: [PRIVATE_KEY], chainId: 5 },
},
solidity: "0.8.8",
}
You can now specify the network name on the CLI yarn hardhat run scripts/deploy.js --network goerli
.
Remember to install
env
:yarn add --dev dotenv
.
Now go to the Goerli Etherscan site and paste the printed address, so you can see how the contract was deployed 🚀
Programmatic Smart Contract Verification
There are three ways of verifying a smart contract, going to the Etherscan website and manually verifying the contract, calling the Etherscan API from your deploying script or using a HardHat plugin. We are going to use the hardhat-etherscan plugin.
Next, we need an API key for Etherscan so we can use the Etherscan API. Create an account if you don’t have one. Now you can go to your account and the API keys sections and Create API Key (Add button).
Install the plugin yarn add --dev @nomiclabs/hardhat-etherscan
and add it to your HardHat config file:
hardhat.config.js
:
// other imports
require("@nomiclabs/hardhat-etherscan")
// other env variables
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY
module.exports = {
// other configuration
etherscan: {
apiKey: ETHERSCAN_API_KEY
}
}
Now let’s create a verify()
function on our deploying script:
scripts/deploy.js
:
const { ethers, run, network } = require("hardhat")
async function main() {
const helloWorldFactory = await ethers.getContractFactory("HelloWorld")
console.log("Deploying contract...")
const helloWorld = await helloWorldFactory.deploy()
await helloWorld.deployed()
console.log(`Deployed contract to: ${helloWorld.address}`)
if (network.config.chainId === 5 && process.env.ETHERSCAN_API_KEY) {
await helloWorld.deployTransaction.wait(6) // make sure the contract is already on Etherscan
await verify(helloWorld.address, [])
}
}
async function verify(contractAddress, args) {
console.log("Verifying contract...")
try {
await run("verify:verify", {
address: contractAddress,
constructorArguments: args,
})
} catch (e) { // we continue in case something goes wrong
if (e.message.toLowerCase().includes("already verified")) {
console.log("Already verified!")
} else {
console.log(e)
}
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
As you can imagine you can not run the verify function against the HardHat development environment, since there is no https://hardhat.etherscan.io
. This means we have to filter out the HardHat network. We can do it by checking the chainId
as you can see above.
Again, run yarn hardhat run scripts/deploy.js --network goerli
and after a while you should see on the console something like:
Successfully submitted source code for contract
contracts/HelloWorld.sol:HelloWorld at 0x1CCd4f9a2838DBb19116224921E1B054C297E73f
for verification on the block explorer. Waiting for verification result...
Successfully verified contract HelloWorld on Etherscan.
https://goerli.etherscan.io/address/0x1CCd4f9a2838DBb19116224921E1B054C297E73f#code
You can check the provided link to make sure the contract is actually verified.
Interacting with your Smart Contract
We can call the smart contract function in the same fashion as we did on the vanilla JavaScript Smart Contract deployment script.
scripts/deploy.js
// imports
async function main() {
// main function code
const result = await helloWorld.helloWorld()
console.log(result)
// update hello world
const transactionResponse = await helloWorld.updateFrom("Sergio")
await transactionResponse.wait(1)
const updatedResult = await helloWorld.helloWorld()
console.log(updatedResult)
}
// other functions
When an error is thrown you might need to delete the existing artifacts and cache (
yarn hardhat clean
) and retry deploying again.
HardHat Tasks
You can define HardHat tasks in addition to the existing ones that are shown when running yarn hardhat
. Tasks are usually created as plugins, whereas scrips are for your development.
Tasks are defined on the hardhat.config.js
file.
require("@nomicfoundation/hardhat-toolbox");
task("my-task", "This is a custom task").setAction(async () => {console.log("Hello Task!")});
// other stuff
now run yarn hardhat
and you will see my-task This is a custom task
was added to the list of tasks. Now run it yarn hardhat my-task
and Hello Task!
will be printed out.
The convention is to create tasks on their own folder:
task/my-task.js
, then import those tasks on thehardhat.config.js
file (require("./tasks/my-task")
).
You can also do more advance stuff like printing out the chainId
.
tasks/chain-id.js
:
const { task } = require("hardhat/config")
task("chain-id", "Prints Chain ID").setAction(async (taskArgs, hre) => {
console.log(hre.network.config.chainId)
})
module.exports = {}
hre
provides a wide range of operations that allows you to access network configuration, interact with Ethers.js and other cool stuff.
HardHat Node
You can spin up your own node, similar to the Ganache node, by running yarn hardhat node
. At the time of writing these lines the node comes with 20 accounts.
You can give it a try by running the node and configuring an additional network.
hardhat.config.js
:
module.exports = {
defaultNetwork: "hardhat",
networks: {
// other networks
localhost: {url: "http://127.0.0.1:8545/", chainId: 31337}
},
solidity: "0.8.8",
// other stuff
}
now when you run your deploying script yarn hardhat run scripts/deploy.js --network localhost
the contract will be created on the local node.
Contract deployment: HelloWorld
Contract address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Transaction: 0xa0a3c483d79a4dc77d602a8d7a4bb18768990570cebfe9f43856adbd077392e4
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Value: 0 ETH
Gas used: 135067 of 135067
Block #1: 0xeb8fa4f7d50f4c1069756a939e8624823c89114a5854d69f88f87bdb90ab4663
...
Contract call: HelloWorld#helloWorld
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Interacting with Networks via CLI
Run: yarn hardhat console --network localhost
(assuming the local node network name is localhost
) or yarn hardhat console --network goerli
.
You can invoke the functions as the ones you run on your scripts.
Testing
HardHat works with Mocha, which is a JavaScript framework.
Test are placed on the /test
folder.
test/test-deploy.js
const { ethers } = require("hardhat")
const { expect, assert } = require("chai")
describe("HelloWorld", function () {
let helloWorldFactory, helloWorld
beforeEach(async function () {
helloWorldFactory = await ethers.getContractFactory("HelloWorld")
helloWorld = await helloWorldFactory.deploy()
})
it("Should print Hello World", async function () {
const result = await helloWorld.helloWorld()
const expected = "Hello World!"
// assert.equal(result, expected)
expect(result).to.equal(expected)
})
it("Should update Hello World with from", async function () {
await helloWorld.updateFrom("Sergio")
const result = await helloWorld.helloWorld()
const expected = "Hello World from Sergio!"
expect(result).to.equal(expected)
})
})
You can run it with yarn hardhat test
.
Run a particular test with yarn hardhat test --grep Hello
, which would run all the tests that contain Hello
on their name. Alternatively, you can add the only()
keyword to run a particular test (it.only("Should print Hello World", async function () {}
).
Additional HardHat Features
Gas Reporter
Add the HardHat Gas Reporter plugin to see how much gas your are using.
yarn add --dev hardhat-gas-reporter
enable it:
hardhat.config.js
:
// other imports
require("hardhat-gas-reporter")
// other constants
const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
// other stuff
gasReporter: {
enabled: process.env.REPORT_GAS ? true : false,
outputFile: "gas-report.txt",
noColors: true,
currency: "EUR",
coinmarketcap: COINMARKETCAP_API_KEY,
token: "BNB", // deploying to Binance. Default is ETH
},
}
As you can see in the previous code snippet we are using CoinMarketCap so we can get the gas price in EUR
. You can get an API key signing up here.
Now run your tests: REPORT_GAS=true yarn hardhat test
. It will save into a gas-report.txt
file something like this:
·-----------------------------|----------------------------|-------------|-----------------------------·
| Solc version: 0.8.8 · Optimizer enabled: false · Runs: 200 · Block limit: 30000000 gas │
······························|····························|·············|······························
| Methods · 13 gwei/gas · 254.64 eur/bnb │
···············|··············|··············|·············|·············|···············|··············
| Contract · Method · Min · Max · Avg · # calls · eur (avg) │
···············|··············|··············|·············|·············|···············|··············
| HelloWorld · updateFrom · - · - · 44997 · 1 · 0.15 │
···············|··············|··············|·············|·············|···············|··············
| Deployments · · % of limit · │
······························|··············|·············|·············|···············|··············
| HelloWorld · - · - · 381167 · 1.3 % · 1.26 │
·-----------------------------|--------------|-------------|-------------|---------------|-------------·
so we would spend €1.26 if we deploy our contract to the Binance network. The call to updateFrom
has a cost of €0.15.
Code Coverage
Add the Solidity HardHat code coverage plugin:
yarn add --dev solidity-coverage
then run:
yarn hardhat coverage
and it will generate a coverage.json
file and print something like this:
-----------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------|----------|----------|----------|----------|----------------|
contracts/ | 100 | 100 | 100 | 100 | |
HelloWorld.sol | 100 | 100 | 100 | 100 | |
-----------------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
-----------------|----------|----------|----------|----------|----------------|
This is important so you make sure your code is fully tested.