Beginner45 minutes

Create Your First ERC-20 Token

Learn how to create, deploy, and interact with your own ERC-20 token on Ethereum. This tutorial covers everything from basic tokenomics to deployment on testnet.

1. Prerequisites

Before starting this tutorial, you should have:

  • Basic understanding of Solidity syntax
  • Node.js v18+ installed
  • A code editor (VS Code recommended)
  • MetaMask wallet with testnet ETH (Sepolia)

Pro Tip

Get free testnet ETH from the Sepolia Faucet

2. What is ERC-20?

ERC-20 is a technical standard for fungible tokens on the Ethereum blockchain. It defines a set of rules that all Ethereum tokens must follow, enabling seamless interoperability between tokens, wallets, and decentralized applications.

Required Functions

FunctionDescription
totalSupply()Returns total token supply
balanceOf(address)Returns balance of an account
transfer(to, amount)Transfers tokens to an address
approve(spender, amount)Approves spender to use tokens
allowance(owner, spender)Returns remaining allowance
transferFrom(from, to, amount)Transfers on behalf of owner

3. Project Setup

We will use Hardhat as our development framework. Create a new project:

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

Install OpenZeppelin contracts for secure, audited implementations:

npm install @openzeppelin/contracts

4. Basic Token Contract

Create a new file contracts/MyToken.sol:

contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MyToken is ERC20, Ownable {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) Ownable(msg.sender) {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

Understanding the Code

  • ERC20 Import: We inherit from OpenZeppelin's battle-tested ERC20 implementation that handles all standard functionality.
  • Ownable: Provides access control so only the contract owner can mint new tokens.
  • Constructor: Initializes token with name, symbol, and mints initial supply to deployer.
  • decimals(): Returns 18 by default. We multiply initialSupply by 10^18 to account for this.

5. Advanced Features

Let's add some useful features to our token:

contracts/AdvancedToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract AdvancedToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit {
    uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18; // 1 billion max

    mapping(address => bool) public blacklisted;

    event Blacklisted(address indexed account);
    event Unblacklisted(address indexed account);

    constructor()
        ERC20("Advanced Token", "ADV")
        Ownable(msg.sender)
        ERC20Permit("Advanced Token")
    {
        _mint(msg.sender, 100_000_000 * 10 ** decimals()); // 100M initial
    }

    function mint(address to, uint256 amount) public onlyOwner {
        require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(to, amount);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function blacklist(address account) public onlyOwner {
        blacklisted[account] = true;
        emit Blacklisted(account);
    }

    function unblacklist(address account) public onlyOwner {
        blacklisted[account] = false;
        emit Unblacklisted(account);
    }

    function _update(
        address from,
        address to,
        uint256 value
    ) internal override(ERC20, ERC20Pausable) {
        require(!blacklisted[from], "Sender is blacklisted");
        require(!blacklisted[to], "Recipient is blacklisted");
        super._update(from, to, value);
    }
}

Security Note

The blacklist feature is controversial in decentralized finance. Consider your use case carefully before implementing centralized controls.

6. Deployment

Create a deployment script at scripts/deploy.js:

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

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with:", deployer.address);

  const Token = await ethers.getContractFactory("MyToken");
  const token = await Token.deploy(
    "My Token",      // name
    "MTK",           // symbol
    1000000          // initial supply (1 million)
  );

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

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

Configure Hardhat for Sepolia testnet in hardhat.config.js:

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

module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};

Deploy to Sepolia:

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

7. Contract Verification

Verify your contract on Etherscan to allow users to read the source code:

npx hardhat verify --network sepolia <CONTRACT_ADDRESS> "My Token" "MTK" "1000000"

Verification Benefits

Verified contracts build trust with users, enable direct interaction through Etherscan, and make your project appear more professional.

8. Next Steps

Congratulations! You've created your first ERC-20 token. Here's what you can explore next: