Building an ERC-20 Token From A to Z (Hardhat + OpenZeppelin)

Published: (December 17, 2025 at 08:45 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Disclaimer
Educational purposes only. Not money, has no intrinsic value, and is not associated with any real‑world brand or stablecoin.

Why I Wrote This Article

Many people entering crypto ask the same questions:

  • “Why did a random token appear in my wallet?”
  • “Why do I see a balance but can’t sell it?”
  • “How are ERC‑20 tokens actually created?”
  • “Why does a token look real if it’s fake?”

The best way to truly understand this is simple: create your own ERC‑20 token once. When you do that, everything suddenly makes sense: balances, wallets, approvals, fake tokens, approvals.

What We Will Build

We will build a clean educational ERC‑20 token with:

  • OpenZeppelin ERC‑20 implementation
  • Role‑based access control
  • Pausable transfers
  • Minting
  • Deployment with Hardhat
  • Adding the token to MetaMask
  • Understanding how wallets detect balances

1. What Is ERC‑20 (Plain English)

ERC‑20 is not a currency. At minimum, an ERC‑20 token implements:

function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function totalSupply() external view returns (uint256);
function name() external view returns (string);
function symbol() external view returns (string);
function decimals() external view returns (uint8);

Important
A market and liquidity must exist for a token to have price and real value. No liquidity → no price → no real value.

2. Environment Setup

Requirements

  • Node.js (LTS)
  • npm
  • MetaMask wallet
  • Sepolia test ETH
  • RPC provider (Alchemy, Infura, etc.)

Create the Project

mkdir my-token
cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npx hardhat

When prompted, choose Create a JavaScript project.

3. Install OpenZeppelin

OpenZeppelin provides audited, production‑grade smart contracts.

npm install @openzeppelin/contracts

4. Writing the ERC‑20 Token Contract

Create contracts/MyToken.sol:

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

/*
    MyToken — an educational ERC-20 token.
    NOT money. NOT a stablecoin.
*/

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract MyToken is ERC20, AccessControl, Pausable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    constructor(uint256 initialSupply)
        ERC20("My Educational Token", "MYT")
    {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
        _grantRole(PAUSER_ROLE, msg.sender);

        _mint(msg.sender, initialSupply);
    }

    function mint(address to, uint256 amount)
        external
        onlyRole(MINTER_ROLE)
    {
        _mint(to, amount);
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function _update(address from, address to, uint256 amount)
        internal
        override
        whenNotPaused
    {
        super._update(from, to, amount);
    }
}

Key Concepts

  • name and symbol are just strings.
  • The contract stores balances.
  • Tokens exist only after they are minted.
  • If you don’t mint, the balance will be zero.

5. Hardhat Configuration

Create (or edit) hardhat.config.js:

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

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

6. Environment Variables

Create a .env file (never commit this file):

SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY

7. Deployment Script

Create scripts/deploy.js:

const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log("Deploying with address:", deployer.address);

  const initialSupply = hre.ethers.parseUnits("1000", 18);

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

  await token.waitForDeployment();

  console.log("Token deployed at:", await token.getAddress());
}

main().catch(console.error);

8. Compile and Deploy

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

You will receive a contract address.

9. Adding the Token to MetaMask

  1. Switch MetaMask to the Sepolia network.
  2. Go to Assets → Import Token.
  3. Paste the contract address. MetaMask auto‑fills the symbol and decimals.

If the balance shows 0, check:

  • Correct network
  • That the address you minted to actually received tokens
  • Decimals setting
  • Reload MetaMask (clear cache)

10. Why Tokens Can Appear “Out of Nowhere”

Wallets do not store tokens. A wallet simply calls:

balanceOf(yourAddress)

Therefore, anyone can mint tokens to your address without asking or permission, and your wallet will still display them.

11. Why Name, Symbol, and Icon Mean Nothing

  • No global registry of token names.
  • No trademark enforcement on‑chain.
  • No “official token” flag.

Anyone can deploy a contract with any name, symbol, decimals, or logo. The only reliable identifier of a token is its contract address.

12. The Core Security Lesson

Understanding these fundamentals makes many scams obvious:

  • “Free tokens” → meaningless balances
  • Fake stablecoins → just ERC‑20 contracts with a nice name
  • Approval traps → dangerous permissions

Knowledge beats fear.

Conclusion

ERC‑20 tokens are just smart contracts. Creating your own token once is the fastest way to understand balances, approvals, minting, and the security pitfalls that surround them.

Back to Blog

Related posts

Read more »