Deploying an ERC20 Token Using Hardhat: A Complete Guide

·

This guide provides a step-by-step walkthrough for creating and deploying a custom ERC20 token on the Ethereum blockchain using the Hardhat development environment. Hardhat offers a robust toolkit for compiling, testing, and deploying smart contracts efficiently.


Setting Up the Development Environment

To begin, ensure you have Node.js and npm installed. Open your terminal and navigate to your project directory. Initialize a new Node.js project by running:

npm init -y

This command generates a package.json file with default settings. Next, install Hardhat as a development dependency:

npm install --save-dev hardhat

After installation, initialize Hardhat in your project folder:

npx hardhat

Select the default options when prompted. This generates essential files and directories:

Install additional dependencies for secure contract templates and testing:

npm install @openzeppelin/contracts chai

OpenZeppelin provides audited, secure smart contract implementations, while Chai is used for writing assertion-based tests.


Creating the ERC20 Token Contract

In the contracts/ directory, delete the default Lock.sol file and create a new file named MyToken.sol. Paste the following Solidity code:

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";

contract MyToken is ERC20Capped, ERC20Burnable {
    address payable public owner;
    uint256 public blockReward;

    constructor(uint256 cap, uint256 reward) 
        ERC20("MyToken", "MYT") 
        ERC20Capped(cap * (10 ** decimals())) 
    {
        owner = payable(msg.sender);
        _mint(owner, 50000000 * (10 ** decimals()));
        blockReward = reward * (10 ** decimals());
    }

    function _mintMinerReward() internal {
        _mint(block.coinbase, blockReward);
    }

    function _beforeTokenTransfer(address from, address to, uint256 value) internal virtual override {
        if (from != block.coinbase && to != block.coinbase && block.coinbase != address(0)) {
            _mintMinerReward();
        }
        super._beforeTokenTransfer(from, to, value);
    }

    function _mint(address account, uint256 amount) internal virtual override(ERC20Capped, ERC20) {
        require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
        super._mint(account, amount);
    }

    function setBlockReward(uint256 reward) public onlyOwner {
        blockReward = reward * (10 ** decimals());
    }

    function destroyContract() public onlyOwner {
        selfdestruct(owner);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }
}

Understanding the Contract Structure

This contract creates a capped, burnable ERC20 token named "MyToken" (symbol: MYT). Key features include:


Configuring the Solidity Compiler

Specify the Solidity version in hardhat.config.js to avoid compilation errors:

require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.17",
};

Compile the contract using:

npx hardhat compile

Successful compilation generates an artifacts/ directory containing the contract’s ABI and bytecode.


Writing Unit Tests for the Token

In the test/ directory, replace the default test file with MyToken.js. Use the following code to test token functionality:

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

describe("MyToken Contract", function () {
  let Token, myToken, owner, addr1, addr2;
  const tokenCap = 100000000;
  const tokenBlockReward = 50;

  beforeEach(async function () {
    Token = await ethers.getContractFactory("MyToken");
    [owner, addr1, addr2] = await hre.ethers.getSigners();
    myToken = await Token.deploy(tokenCap, tokenBlockReward);
  });

  describe("Deployment", function () {
    it("Should set the correct owner", async function () {
      expect(await myToken.owner()).to.equal(owner.address);
    });

    it("Should assign the total supply to the owner", async function () {
      const ownerBalance = await myToken.balanceOf(owner.address);
      expect(await myToken.totalSupply()).to.equal(ownerBalance);
    });

    it("Should set the correct capped supply", async function () {
      const cap = await myToken.cap();
      expect(Number(hre.ethers.utils.formatEther(cap))).to.equal(tokenCap);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      await myToken.transfer(addr1.address, 50);
      expect(await myToken.balanceOf(addr1.address)).to.equal(50);

      await myToken.connect(addr1).transfer(addr2.address, 50);
      expect(await myToken.balanceOf(addr2.address)).to.equal(50);
    });

    it("Should fail if sender has insufficient balance", async function () {
      await expect(
        myToken.connect(addr1).transfer(owner.address, 1)
      ).to.be.revertedWith("ERC20: transfer amount exceeds balance");
    });
  });
});

Run the tests with:

npx hardhat test

All tests should pass, confirming the contract works as intended.


Configuring Deployment Settings

Create a .env file to store sensitive data:

PRIVATE_KEY=your_metamask_private_key
INFURA_SEPOLIA_ENDPOINT=your_infura_sepolia_url

Update hardhat.config.js to use the Sepolia testnet:

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

module.exports = {
  solidity: "0.8.17",
  networks: {
    sepolia: {
      url: process.env.INFURA_SEPOLIA_ENDPOINT,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};

Deploying the Contract to Sepolia

Create a deployment script in scripts/deploy.js:

const hre = require("hardhat");

async function main() {
  const MyToken = await hre.ethers.getContractFactory("MyToken");
  const myToken = await MyToken.deploy(100000000, 50);
  await myToken.deployed();
  console.log("Token deployed to:", myToken.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Deploy using:

npx hardhat run --network sepolia scripts/deploy.js

Note the deployed contract address for verification and interaction.


Verifying the Contract on Etherscan

  1. Visit Sepolia Etherscan.
  2. Paste the contract address into the search bar.
  3. Confirm the contract details and transaction history.

Importing the Token into MetaMask

  1. Open MetaMask and click "Import Tokens".
  2. Paste the contract address. The token symbol and decimals should auto-populate.
  3. Click "Add Custom Token" to view your balance.

To transfer tokens:


Frequently Asked Questions

What is an ERC20 token?
ERC20 is a technical standard for fungible tokens on the Ethereum blockchain. It defines a common set of rules for token transactions, balances, and supply, ensuring compatibility across wallets and exchanges.

Why use Hardhat for deployment?
Hardhat simplifies smart contract development with built-in testing, debugging, and deployment tools. It supports multiple networks and offers extensive plugin integration for streamlined workflows.

How do I get test ETH for deployment?
Use faucets like Sepolia Faucet to obtain free test ETH for deploying and testing contracts on the Sepolia network.

What is the purpose of a capped supply?
A capped supply ensures a maximum number of tokens can ever be created, preventing inflation and promoting scarcity. This is common in utility or governance tokens.

How can I make my contract more secure?
Use audited libraries like OpenZeppelin, implement access controls, and conduct thorough testing. Always verify contracts on Etherscan for transparency.

Can I deploy this on the Ethereum mainnet?
Yes, but replace the Sepolia configuration with a mainnet RPC endpoint and ensure you have sufficient ETH for gas fees. Always test extensively on testnets first.


Conclusion

This guide covered the end-to-end process of deploying a custom ERC20 token using Hardhat. From environment setup and contract creation to testing and deployment, these steps provide a foundation for Ethereum smart contract development. For further learning, explore more strategies on advanced contract optimization and security practices.