DEV Community

fangjun
fangjun

Posted on • Edited on

A Concise Hardhat Tutorial: Part 2 - ERC20 Token

This concise hardhat tutorial has 3 sections and this is section 2.

1 . Installation and sample project
2 . Write ERC20 token with OpenZeppelin
3 . Write ERC72 NFT token with on-chain SVG image

Hardhat is an Ethereum development tool suite to compile, unit test, debug and deploy smart contracts.


2. Write an ERC20 Token with OpenZeppelin

In the hardhat official tutorial at https://hardhat.org/tutorial/ , it write an ERC20 Token smart contract from scratch. Here we will write one using OpenZeppelin library instead. You can find more information about OpenZeppelin at: https://docs.openzeppelin.com/contracts/4.x/ . The sample contract is adapted from OpenZeppelin documents.

Compile-Test-Deploy circle


Step 1: Install OpenZeppelin

First, let's install OpenZeppelin contracts.

yarn add @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

Step 2: Write an ERC20 Token with OpenZeppelin

You can write an ERC20 Token smart contract by inheriting OpenZeppelin ERC20 implementation. OpenZeppelin Wizard is a helpful tools to setup an ERC20 token contract.

Image description

Our smart contract GLDToken.sol goes as follows:

// contracts/GLDToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract GLDToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("GLDToken", "GLD") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
}
Enter fullscreen mode Exit fullscreen mode

Compile the contract:

yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

Step 3: Write a deploy script

Create a deploy script which is adapted from https://docs.openzeppelin.com/learn/deploying-and-interacting . You can find a detailed explanation there.

 // scripts/deploy_GLDToken.ts

import { ethers } from "hardhat";

async function main() {
  const initialSupply = 10000;

  const GLDToken = await ethers.getContractFactory("GLDToken");
  const token = await GLDToken.deploy(initialSupply);

  await token.deployed();

  const totalSupply = await token.totalSupply()

  console.log(
    `GLDToken deployed to ${token.address} with an initialSupply ${totalSupply}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Using this script, we deploy GLDToken with 10000.0 GLD minted to the contract owner.

Try to deploy it to in-process local blockchain:

yarn hardhat run scripts/deploy_GLDToken.ts
//Output: 
GLDToken deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 with an initialSupply 10000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Step 4: Write Unit Test

In the hardhat official tutorial, there is a detailed explanation of unit test at https://hardhat.org/tutorial/testing-contracts.html . We adapted the test script in it with several necessary changes.

Create file GLDToken.test.ts:

// We import Chai to use its asserting functions here.
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("GLDToken", function () {
    // We define a fixture to reuse the same setup in every test.
    // We use loadFixture to run this setup once, snapshot that state,
    // and reset Hardhat Network to that snapshot in every test.

    const initialSupply = 10000;

    async function deployGLDTokenFixture() {

      // Contracts are deployed using the first signer/account by default
      const [owner, otherAccount] = await ethers.getSigners();

      const GLDToken = await ethers.getContractFactory("GLDToken");
      const token = await GLDToken.deploy(initialSupply);

      return { token, owner, otherAccount };
    }

    describe("Deployment", function () {
      it("Should assign the total supply of tokens to the owner", async function () {
        const { token, owner } = await loadFixture(deployGLDTokenFixture);
        const total = await token.totalSupply();
        expect(total).to.equal(await token.balanceOf(owner.address));
      });

    });

    describe("Transaction", function () {

        it("Should transfer tokens between accounts", async function () {
            const { token, owner, otherAccount } = await loadFixture(deployGLDTokenFixture);

            const ownerBalance = await token.balanceOf(owner.address);

            await token.transfer(otherAccount.address, 50);
            const addr1Balance = await token.balanceOf(otherAccount.address);
            expect(addr1Balance).to.equal(50);

            const ownerNewBalance = await token.balanceOf(owner.address);
            expect(ownerNewBalance).to.equal(ownerBalance.sub(50));
        });

        it("Should fail if sender doesn’t have enough tokens", async function () {
            const { token, owner, otherAccount } = await loadFixture(deployGLDTokenFixture);

            // Transfer 10001 GLD tokens from owner to otherAccount
            await expect(
             token.transfer(otherAccount.address, ethers.utils.parseEther('10001'))
            ).to.be.revertedWith("ERC20: transfer amount exceeds balance");
        });        

      });

});

Enter fullscreen mode Exit fullscreen mode

Run unit test:

yarn hardhat test  test/GLDToken.test.ts
Enter fullscreen mode Exit fullscreen mode

Output:

  GLDToken
    Deployment
      ✔ Should assign the total supply of tokens to the owner (597ms)
    Transaction
      ✔ Should transfer tokens between accounts
      ✔ Should fail if sender doesn’t have enough tokens

  3 passing (630ms)
Enter fullscreen mode Exit fullscreen mode

Step 5: Deploy smart contract to localhost and interact with it

By compiling, testing and deploying in in-process blockchain, we now have a smart contract which works correctly. We would like to deploy it to stand-alone localhost blockchain and interact with it interactively from Hardhat console.

Open another terminal and run in the project directory:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

In your working terminal, deploy the contract to localhost by running:

yarn hardhat run scripts/deploy_GLDToken.ts --network localhost 
//Output:
// GLDToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

Please note that you need to set localhost network in hardhat.config.ts.

Run hardhat console connecting to localhost:

yarn hardhat console --network localhost
Enter fullscreen mode Exit fullscreen mode

Interact with GLDToken smart contract instance in hardhat console as follows. You need address and ABI to interact with a contract. The ABI(Application Binary Interface) is retrieved by Hardhat-ethers plugin's ethers.getContractAt("GLDToken", address) using the contract name here.

Get a smart contract instance and retrieve read-only data:

fromWei = ethers.utils.formatEther;
toWei = ethers.utils.parseEther;

const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const token = await ethers.getContractAt("GLDToken", address);

const accounts = await hre.ethers.getSigners();
owner = accounts[0].address;
toAddress = accounts[1].address;

await token.symbol()
//'GLD'

totalSupply = await token.totalSupply();
fromWei(totalSupply)
//10000.0
Enter fullscreen mode Exit fullscreen mode

Some explanation on how to get contract instance:

  • We can get contract instance using Ethers.js directly(docs): new ethers.Contract( address , abi , signerOrProvider ). By this way, we need to get abi from the compile output (in artifacts directory).

  • We can use hardhat-ethers plugin's helper function ethers.getContractAt("GLDToken", address) (docs) as we do in the above code snippet.

Once we get an instance of the ERC20 contract, we can call its functions (ERC20 docs by OpenZeppelin).

You can also transfer token from your current address to another address by calling transfer(recipient, amount). This is a state-changing function of the ERC20 contract.

await token.transfer(toAddress, toWei('100'))

ownerBalance = await token.balanceOf(owner);
fromWei(ownerBalance);
//'9900.0'

toBalance = await token.balanceOf(toAddress);
fromWei(toBalance)
//'100.0'
Enter fullscreen mode Exit fullscreen mode

Note for playing with Public testnet

  1. Create an Alchemy Account and get API key
  2. Get some test ETH for Sepolia testnet from free faucet: Sepolia faucet
  3. Configure the network part in hardhat.config.ts for Sepolia testnet
  4. Deploy the GLDToken ERC20 contract to Sepolia testnet
  5. Play with your GLDToken contract using Hardhat console.

Btw, Alchemy also provides some handy APIs for standard ERC20. For example, there is alchemy_getTokenBalances link to get token balance of address.


Similarly, we can write an ERC721 token smart contract using OpenZeppelin. And in the next section, we will write a loot-like ERC721 NFT with on-chain SVG image.


If you feel this tutorial helpful and would like to know more, follow me Twitter: @fjun99 . DM is open.

Top comments (0)