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.

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, runnode --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 runningyarn init
. We will pressEnter
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 selectCreate a JavaScript project.
We'll choose all the default options by pressingEnter
.
Once the project is created, ourpackage.json
should look something like this:

"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
- The
contracts
folder: where all our smart contracts will be kept. Hardhat automatically creates a sample contractLock.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 theLock.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.
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;
}
}
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)
})
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.

Then, open the App's dashboard and click on "View Key" 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".
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
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,
},
},
}
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.
--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
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.
- Using the
verify
task in the command line. - 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


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)
}
}
}
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)
})
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 ✌🏻
