A Gentle Introduction to Hardhat

In this blog post, we learn about Hardhat and how we can set up our own local dev environment to compile, deploy and verify EVM-based smart contracts.

A Gentle Introduction to Hardhat
Hardhat Logo Credit: hardhat.org

Hi, fren! gm. ☀️

In this blog post, I'll provide a gentle intro to Hardhat, and we're going to cover different Hardhat tools while working with a basic smart contract.

So, without further ado, let's get cracking!


What is Hardhat?

Hardhat is a JavaScript-based development environment used to compile, deploy, test, and debug EVM-based smart contracts.

It helps manage and automate recurring tasks that are essential to building decentralized apps. One of the main tools that power the Hardhat environment is Ethers.js - a JavaScript library that allows us to deploy & interact with smart contracts.

In a nutshell, Hardhat provides us with a set of tools that will help us with the entire smart contract development journey – from creation, testing, and deployment to interacting with smart contracts.

Let's start by installing Hardhat.


Installation Requirements

To install Hardhat, we must have:

  • Node.js v16.0 or above installed on our machine. To check the Node.js version, run node --v in the command line. To install Node.js, follow this quick guide.
  • Yarn or npm package manager. In this article, I'm going to use Yarn, but feel free to use npm if you're comfortable with it. To install yarn, run: npm i -g yarn

Installing Hardhat and Creating a new project

Now, to create a new Hardhat project, create a new folder on your computer, and open it in VS Code. Then, open a new terminal in VS Code.

To create a Hardhat project:

  • Firstly, we'll initialize a package.json file by running yarn init. We will press Enter to fill in basic details.
  • Next, we will install Hardhat by running: yarn add --dev hardhat
  • Then, we will create a Hardhat project by running: yarn hardhat, then select Create a JavaScript project. We'll choose all the default options by pressing Enter.
    Once the project is created, our package.json should look something like this:
For name, instead of "hardhat-tutorial", it should show the name of your directory.

The Hardhat Directory Structure:

Our Hardhat project follows the following structure:

.
├── contracts/
├── scripts/
├── test/ 
├── hardhat.config.js 
Hardhat project directory structure
  • The contracts folder: where all our smart contracts will be kept. Hardhat automatically creates a sample contract Lock.sol for us to play with. For now, let's just delete it.
  • The scripts folder: where all our deploy scripts and other scripts will be kept. Again, Hardhat provides us with a sample deploy script.
  • The test folder: all the test scripts will be kept there. Hardhat also provides us with a sample test script with some basic tests for the Lock.sol contract.
  • The hardhat.config.js file: It's the Hardhat config file. It has all the configurations such as network settings, Solidity versions, plugin configs, private key configs, etc.
  • There's also a node_modules directory that contains all the installed packages Hardhat comes with out of the box.
💡
Before moving forward, delete all the files such as Lock.sol, deploy.js, and Lock.js files in the contracts, scripts, and test folders respectively. We don't need them. Make sure to only delete the files and not the entire directories.

Before we can explore Hardhat, let's create a sample smart contract to play with.

I'm using a SimpleStorage.sol contract. It's a simple contract that doesn't require prior knowledge of any complex concepts.

Here's the code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract SimpleStorage {
    uint256 favoriteNumber;

    struct People {
        uint256 favoriteNumber;
        string name;
    }

    People[] public people;

    mapping(string => uint256) public nameToFavoriteNumber;

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber;
    }

    function retrieve() public view returns (uint256) {
        return favoriteNumber;
    }

    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        people.push(People(_favoriteNumber, _name));
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}
SimpleStorage.sol
💡
Make sure that the Solidity version you specify in your smart contract is compatible with the Solidity version specified in the hardhat.config.js file. In my hardhat.config.js, the Solidity version is 0.8.17. Since my contract uses v0.8.9 or above, I don't face any issues while compiling my contract.

To compile our smart contracts, we can run: yarn hardhat compile

Deploying our Smart Contract with Hardhat

To deploy a smart contract with Hardhat, we can create a simple script called deploy.js in the scripts directory.

// 1 - import Ethers from Hardhat
const { ethers } = require('hardhat')

// 2 - async main function
async function main() {
    
  // create an instance of `SimpleStorage.sol` contract
  const SimpleStorageFactory = await ethers.getContractFactory('SimpleStorage')
  console.log('Deploying contract...⏳')

  // deploy the contract on the blockchain 
  const simpleStorage = await SimpleStorageFactory.deploy()

  // wait till the contract gets deployed
  await simpleStorage.deployed()

  console.log(`Contract deployed at: ${simpleStorage.address}`)
}

// 3 - call main function
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.log(error)
    process.exit(1)
  })
deploy.js

Deploying Contract to a local Network:

To run the deploy.js script, we can run: yarn hardhat run scripts/deploy.js

Using this command, Hardhat deploys our smart contract to the local Hardhat Network, a built-in local Ethereum network node similar to Ganache.

Deploying Contract to a Test Network:

We can add other testnets like Goerli in the hardhat.config.js file and then deploy our contract to the testnets.

To add the Goerli testnet to Hardhat, we need two things:
1 - An RPC URL: Sign up at Alchemy, go to the Dashboard, and create a new app.

Creating an App in Alchemy.

Then, open the App's dashboard and click on "View Key" and grab the HTTPS key:

Click on the "View Key" button and grab the HTTPS key.

2 - A Private Key: To grab a private key, open up Metamask, and select an Account. Click the three dots and go to account details and click on "Export Private Key".

💡
Make sure to create a Metamask account that you use specifically for development purposes. This account should never have any real tokens that have value in the real world. 

Now, that we have the RPC URL and a private key, let's create a .env file in the root directory, and put the keys in it:

RPC_URL = https://eth-goerli.g.alchemy.com/v2/ghT8CHssndeSss
PRIVATE_KEY = 0b0d4ssjw672mzakwlsrxmd3md9q2P8Xx7sksade5Xe8sjd
Don't worry! These are not my actual keys 😀

Now, let's import the RPC URL & private key to the hardhat.config.js file.

To import the keys from our .env file, we need to first install the dotenv package. To do so, run: yarn add --dev dotenv

Then, add require('dotenv').config() to the top of hardhat.config.js.

Then, import the RPC_URL and PRIVATE_KEY from .env file to hardhat.config.js. And also add a new networks entry for Goerli by providing an RPC URL, private keys and chain ID, like so:

require('@nomicfoundation/hardhat-toolbox')
require('dotenv').config()

const GOERLI_RPC_URL = process.env.RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY

module.exports = {
  solidity: '0.8.17',
  networks: {
    goerli: {
      url: GOERLI_RPC_URL,
      accounts: [PRIVATE_KEY],
      chainId: 5,
    },
  },
}
Your hardhat.config.js should look something like this ☝🏻

Now, we can finally deploy our smart contract to the Goerli testnet. A quick note before deploying, make sure your account in Metamask (the one whose private keys we're using here) should have some testnet ETH. To grab some, here's a faucet from where you can get some Goerli ETH.

Okay, finally it's time to deploy.

To deploy our contract to Goerli, run the same command but with a network flag this time:

yarn hardhat run scripts/deploy.js --network goerli

It will take some time since we're actually deploying to a testnet and some amount of mining needs to be done in order for the contract to be deployed. It will also deduct some gas fees from our Metamask account.

💡
The --network flag takes in a network name. In case of deploying to Goerli, we provide: --network goerli, or whatever network name is specified in the hardhat.config.js. In case of deploying to a local hardhat network, we can specify: --network hardhat.

So, initially when we ran yarn hardhat run scripts/deploy.js, it was equivalent to running yarn hardhat run scripts/deploy.js --network hardhat, Hardhat automatically added the --network hardhat part for us.

The Hardhat Architecture:

Hardhat is designed around the concept of tasks and plugins, with the majority of Hardhat's functionality coming from plugins.

Plugins are the backbone of Hardhat, and they're built using the same config API that we use in the Hardhat configuration. Here's a list of official plugins provided by Hardhat.

When we run Hardhat from the command line, we basically run a task. A task is a JavaScript async function with some associated metadata. This metadata is used by Hardhat to automate some things for you. Arguments parsing, validation, and help messages are all taken care of.

To see the list of available tasks in your project, run npx hardhat or yarn hardhat.


Verifying Smart Contracts

We can verify the source code of our contract on Etherscan using the Etherscan plugin.

The Etherscan plugin is pretty cool, and all it requires is the contract deployment address and constructor arguments and the plugin will detect which contract to verify.

To install the plugin, run: yarn add --dev @nomiclabs/hardhat-etherscan

Then, head over to the hardhat.config.js and import the plugin like so: require ('@nomiclabs/hardhat-etherscan')

In order to use the plugin, we'll also need an Etherscan API key. So, head over to Etherscan.io and sign up. And then grab your API key token. Once you have the API keys, you can add them to your .env file like so:

RPC_URL = https://eth-goerli.g.alchemy.com/v2/ghT8CHssndeSss
PRIVATE_KEY = 0b0d4ssjw672mzakwlsrxmd3md9q2P8Xx7sksade5Xe8sjd
ETHERSCAN_API = BMWUTESLADQXUVFECGSPINBZ7IVQQS9
Again, these are not my actual private or API keys. 

Then, add the Etherscan API keys to the hardhat.config.js file, like so:

require('@nomicfoundation/hardhat-toolbox')
require('dotenv').config()

const GOERLI_RPC_URL = process.env.RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY
const ETHERSCAN_API = process.env.ETHERSCAN_API

module.exports = {
  solidity: '0.8.17',
  networks: {
    goerli: {
      url: GOERLI_RPC_URL,
      accounts: [PRIVATE_KEY],
      chainId: 5,
    },
  },

  etherscan: {
    apiKey: ETHERSCAN_API,
  },
}

Now, there are two ways to verify the source code of our smart contract.

  1. Using the verify task in the command line.
  2. Programmatical verification by writing a verification script.

1- Using the Verify Task:

First, let's take a look at the 1st option: Verifying the contract using the verify task.

To run the verify task, run the following command, and pass in the deployed contract address, and a bunch of constructors arguments (if any):

yarn hardhat verify --network goerli CONTRACT_ADDRESS
Here on Etherscan, you can see the contract's source code has been verified! 🙌🏻

2- Programmatically verifying Smart Contracts:

To programmatically verify a smart contract, we can either write a verify function in our deploy script, or write a separate verify script. For now, let's just stick to doing it in the deploy script.

Similar to the verify task, we need to pass in two things to the verify function: contract address and constructor args (if any).

Since our smart contract is pretty simple and doesn't require any constructor args, all we need to care about is the contract deployment address.

// programmatically verify contract source code
async function verify(contractAddress, args) {
  console.log('Verifying Contract...✨')

  try {
    await run('verify:verify', {
      address: contractAddress,
      constructorArguments: args,
    })
  } catch (e) {
    if (e.message.toLowerCase().includes('already verified')) {
      console.log('Already verified!')
    } else {
      console.log(e)
    }
  }
}
The verify function

To call the Hardhat verify task from a script, we make use of the 'verify:verify' subtask.

Then, we can call the verify() function from our main() function to run the verification.

  // check if the network is goerli & Etherscan API exists
  if (network.config.chainId === 5 && process.env.ETHERSCAN_API) {
    console.log('Waiting for block confirmation... ⏳')
    await simpleStorage.deployTransaction.wait(5) // wait for 6 block confirmations

    await verify(simpleStorage.address, []) // run verification
  }

Now, run the deploy script to deploy and verify the smart contract code:

yarn hardhat run scripts/deploy.js --network goerli

Here's the entire deploy.js code:

// 1 - imports
const { ethers, run, network } = require('hardhat')

// 2 - async main function
async function main() {
  // create an instance of `SimpleStorage.sol` contract
  const SimpleStorageFactory = await ethers.getContractFactory('SimpleStorage')
  console.log('Deploying contract...⏳')

  // deploy the contract on the blockchain
  const simpleStorage = await SimpleStorageFactory.deploy()

  // wait till the contract gets deployed
  await simpleStorage.deployed()

  console.log(`Contract deployed at: ${simpleStorage.address}`)

  // check if the network is goerli & Etherscan API exists
  if (network.config.chainId === 5 && process.env.ETHERSCAN_API) {
    console.log('Waiting for block confirmation... ⏳')
    await simpleStorage.deployTransaction.wait(5) // wait for 6 block confirmations

    await verify(simpleStorage.address, []) // run verification
  }
}

// programmatically verify contract source code
async function verify(contractAddress, args) {
  console.log('Verifying Contract...✨')

  try {
    await run('verify:verify', {
      address: contractAddress,
      constructorArguments: args,
    })
  } catch (e) {
    if (e.message.toLowerCase().includes('already verified')) {
      console.log('Already verified!')
    } else {
      console.log(e)
    }
  }
}

// 3 - call main function
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.log(error)
    process.exit(1)
  })
deploy.js

Interacting with the Smart Contract

Once we have deployed our smart contract, the next step is to figure out how we can interact with it.

Let's say we want to call the store function from the SimpleStorage.sol contract. The store() function takes in an uint256 as an argument. So, in the deploy script, we can call the function like so:

const callStore = await simpleStorage.store(101)
await callStore.wait(1) // wait for 1 block confirmation

// call the retrieve func to see if we actually changed favoriteNumber to 101
const getFavNumber = await simpleStorage.retrieve()  
console.log(`New Fav Number: ${getFavNumber}`)

Here's the final deploy.js script code:

// 1 - imports
const { ethers, run, network } = require('hardhat')

// 2 - async main function
async function main() {
  // create an instance of `SimpleStorage.sol` contract
  const SimpleStorageFactory = await ethers.getContractFactory('SimpleStorage')
  console.log('Deploying contract...⏳')

  // deploy the contract on the blockchain
  const simpleStorage = await SimpleStorageFactory.deploy()

  // wait till the contract gets deployed
  await simpleStorage.deployed()

  console.log(`Contract deployed at: ${simpleStorage.address}`)

  const callStore = await simpleStorage.store(101)
  await callStore.wait(1) // wait for 1 block confirmation

  // call the retrieve func to see if we actually changed favoriteNumber to 101
  const getFavNumber = await simpleStorage.retrieve()
  console.log(`New Fav Number: ${getFavNumber}`)

  // check if the network is goerli & Etherscan API exists
  if (network.config.chainId === 5 && process.env.ETHERSCAN_API) {
    console.log('Waiting for block confirmation... ⏳')
    await simpleStorage.deployTransaction.wait(5) // wait for 6 block confirmations

    await verify(simpleStorage.address, []) // run verification
  }
}

// programmatically verify contract source code
async function verify(contractAddress, args) {
  console.log('Verifying Contract...✨')

  try {
    await run('verify:verify', {
      address: contractAddress,
      constructorArguments: args,
    })
  } catch (e) {
    if (e.message.toLowerCase().includes('already verified')) {
      console.log('Already verified!')
    } else {
      console.log(e)
    }
  }
}

// 3 - call main function
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.log(error)
    process.exit(1)
  })

Run the deploy script once again to deploy the contract either on the local network or on Goerli: yarn hardhat run scripts/deploy.js --network goerli

Here's the output:

CS-MBP:hardhat-tutorial shuriken$ yarn hardhat run scripts/deploy.js --network goerli
yarn run v1.22.15
$ /Users/shuriken/Desktop/Code/hardhat-tutorial/node_modules/.bin/hardhat run scripts/deploy.js --network goerli

Deploying contract...⏳
Contract deployed at: 0x2FC4C8De7b2a63832F4B4f59c71Bf502AcF700b4
New Fav Number: 101
✨  Done in 62.17s.

Running a Local Node with Hardhat

In Hardhat, we can spin up a local node (similar to Ganache) in our terminal. This local node is slightly different than the default Hardhat network in the sense that it still uses the Hardhat Runtime Environment (HRE) but it's considered its own separate network since it can live past the duration of the yarn hardhat run script.

The local node provides us with multiple accounts & private keys filled with plenty of test Ether.

To run our local node, run the following command in the Terminal: yarn hardhat node

and it will spin up a local node for us:

CS-MBP:hardhat-tutorial shuriken$ yarn hardhat node
yarn run v1.22.15
$ /Users/shuriken/Desktop/Code/hardhat-tutorial/node_modules/.bin/hardhat node
(node:8080) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

To interact with our local node and deploy contracts on it, we can add a new network in the hardhat.config.js file:

require('@nomicfoundation/hardhat-toolbox')
require('dotenv').config()

const GOERLI_RPC_URL = process.env.RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY
const ETHERSCAN_API = process.env.ETHERSCAN_API

module.exports = {
  solidity: '0.8.17',
  networks: {
    goerli: {
      url: GOERLI_RPC_URL,
      accounts: [PRIVATE_KEY],
      chainId: 5,
    },
    localhost: {
      url: 'http://127.0.0.1:8545/', 
      chainId: 31337,
    },
  },

  etherscan: {
    apiKey: ETHERSCAN_API,
  },
}

Now, we can run the deploy script on the localhost nodes by running the following command:

yarn hardhat run scripts/deploy.js --network localhost

Now, we can see a ton of logs like Contract Deployment, Contract Address, Value, Block info, etc in the terminal itself. This is incredibly powerful for quickly testing and deploying contracts and being able to see how our contract would interact on a real testnet.


That's it for now, fren!

We've covered a lot in this blog post. We have learned how to set up our own local development environment with Hardhat to compile, deploy, verify and interact with smart contracts.

The Hardhat development environment is pretty straightforward to use and scales pretty well. We can also extend it pretty easily, thanks to Hardhat's ability to work with an endless list of official plugins & custom tasks.

To learn more, feel free to check out the official Hardhat documentation.


If you enjoyed reading this article, please consider subscribing here so that you get a notification in your mail whenever I post new stuff. Subscribing is free, and it gives me a ton of motivation to keep going.

#WAGMI ✌🏻