Skip to content

ERC-20 Tokens: Creating Your Own Cryptocurrency

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn the ERC-20 token standard including total supply, transfers, allowances, and deploying your own fungible token on Ethereum. Why it matters: ERC-20 is the most widely adopted token standard, powering thousands of cryptocurrencies, stablecoins, and DeFi tokens with trillions in market capitalization. By the end, you'll deploy your own ERC-20 token with OpenZeppelin.

ERC-20 is a technical standard for fungible tokens on Ethereum, defining a common interface that all token contracts must implement to ensure compatibility with wallets, exchanges, and DeFi protocols.

Understanding the ERC-20 Standard

The ERC-20 interface defines six required functions and two events that every compliant token must implement.

Function Description Returns
totalSupply() Total token supply uint256
balanceOf(address) Token balance of an address uint256
transfer(address, uint256) Send tokens to an address bool
allowance(address, address) Amount spender can use from owner uint256
approve(address, uint256) Authorize spender to use tokens bool
transferFrom(address, address, uint256) Transfer on behalf of owner bool
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Minimal ERC-20 implementation (for learning)
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
flowchart TD
  A[ERC-20 Token Contract] --> B[State Variables]
  A --> C[Core Functions]
  A --> D[Events]
  B --> E[_totalSupply]
  B --> F[_balances mapping]
  B --> G[_allowances mapping]
  C --> H[totalSupply]
  C --> I[balanceOf]
  C --> J[transfer]
  C --> K[approve]
  C --> L[allowance]
  C --> M[transferFrom]
  D --> N[Transfer event]
  D --> O[Approval event]

Creating a Token with OpenZeppelin

The safest way to create an ERC-20 token is using OpenZeppelin's audited and battle-tested contracts.

npm install @openzeppelin/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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

contract MyToken is ERC20, Ownable {
    uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18;  // 1 billion tokens
    
    constructor() ERC20("MyToken", "MTK") {
        // Mint initial supply to deployer
        _mint(msg.sender, 100_000_000 * 10**18);  // 100 million
    }
    
    function mint(address _to, uint256 _amount) external onlyOwner {
        require(totalSupply() + _amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(_to, _amount);
    }
    
    function burn(uint256 _amount) external {
        _burn(msg.sender, _amount);
    }
    
    // Override decimals to 18 (default)
    function decimals() public pure override returns (uint8) {
        return 18;
    }
}

Deploy this contract and interact with it. Expected behavior:

  • Token name: "MyToken", symbol: "MTK", decimals: 18
  • Initial supply: 100 million MTK minted to deployer
  • Owner can mint up to MAX_SUPPLY (1 billion)
  • Anyone can burn their own tokens

Testing Token Transfers

Test the transfer, approve, and transferFrom workflow.

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

describe("MyToken", function () {
  let token, owner, spender, recipient;
  
  beforeEach(async function () {
    [owner, spender, recipient] = await ethers.getSigners();
    const MyToken = await ethers.getContractFactory("MyToken");
    token = await MyToken.deploy();
    await token.waitForDeployment();
  });
  
  it("should have correct name and symbol", async function () {
    expect(await token.name()).to.equal("MyToken");
    expect(await token.symbol()).to.equal("MTK");
  });
  
  it("should assign initial supply to owner", async function () {
    const ownerBalance = await token.balanceOf(owner.address);
    expect(ownerBalance).to.equal(ethers.parseEther("100000000"));
  });
  
  it("should transfer tokens between accounts", async function () {
    await token.connect(owner).transfer(recipient.address, ethers.parseEther("1000"));
    expect(await token.balanceOf(recipient.address)).to.equal(ethers.parseEther("1000"));
  });
  
  it("should handle approve and transferFrom", async function () {
    // Owner approves spender to use 500 tokens
    await token.connect(owner).approve(spender.address, ethers.parseEther("500"));
    expect(await token.allowance(owner.address, spender.address))
      .to.equal(ethers.parseEther("500"));
    
    // Spender transfers from owner to recipient
    await token.connect(spender).transferFrom(
      owner.address,
      recipient.address,
      ethers.parseEther("500")
    );
    
    expect(await token.balanceOf(recipient.address)).to.equal(ethers.parseEther("500"));
    expect(await token.allowance(owner.address, spender.address)).to.equal(0);
  });
});

Expected test output:

MyToken
     should have correct name and symbol (56ms)
     should assign initial supply to owner (42ms)
     should transfer tokens between accounts (38ms)
     should handle approve and transferFrom (65ms)

  4 passing (312ms)

Adding Token Features

Extend your token with additional features like pausing, capped minting, and snapshots.

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

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

contract AdvancedToken is ERC20Capped, ERC20Burnable, ERC20Pausable, Ownable {
    constructor(uint256 _cap)
        ERC20("AdvancedToken", "ADV")
        ERC20Capped(_cap * 10**18)
        Ownable(msg.sender)
    {}
    
    function mint(address _to, uint256 _amount) external onlyOwner {
        _mint(_to, _amount);
    }
    
    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }
    
    function _update(address from, address to, uint256 value)
        internal
        override(ERC20, ERC20Capped, ERC20Pausable)
    {
        super._update(from, to, value);
    }
}

Expected behavior: Tokens can only be minted up to the cap. Pausing prevents all transfers. Burning permanently removes tokens from circulation.

Deploying to a Testnet

Deploy your token to Sepolia testnet and verify on Etherscan.

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

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying token with account:", deployer.address);
  
  const AdvancedToken = await ethers.getContractFactory("AdvancedToken");
  const cap = 1_000_000_000; // 1 billion cap
  const token = await AdvancedToken.deploy(cap);
  await token.waitForDeployment();
  
  const address = await token.getAddress();
  console.log("Token deployed to:", address);
  
  // Auto-verify on Etherscan
  if (network.name !== "hardhat") {
    await token.deploymentTransaction().wait(5);
    await hre.run("verify:verify", {
      address: address,
      constructorArguments: [cap],
    });
    console.log("Contract verified on Etherscan");
  }
}

main().catch(console.error);

Expected output:

Deploying token with account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Token deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Contract verified on Etherscan

Common Errors and Misunderstandings

1. Decimal Mismatch

Token decimals default to 18. Frontends must use the same decimals for display. Different decimals (6 for USDC) require parseUnits and formatUnits.

2. Approve + TransferFrom Order

Users must approve before a contract can transferFrom. The approve sets the allowance, then the contract calls transferFrom within its own transaction.

3. Infinite Approval

Many dApps request max uint256 approval to avoid repeated approve transactions. While convenient, this grants unlimited spending power. Audit the contract before granting.

4. Transfer to Contract

Sending tokens to a contract that doesn't implement ERC-20 handling results in permanent loss. Use safeTransfer or check the contract supports tokens.

5. Reentrancy in transferFrom

While ERC-20 transfers don't make external calls by default, custom implementations or hooks (ERC-777) can introduce reentrancy. Always use checks-effects-interactions.

Practice Questions

  1. What six functions must an ERC-20 token implement? totalSupply(), balanceOf(), transfer(), allowance(), approve(), and transferFrom(). These form the core ERC-20 interface.

  2. How does the approve + transferFrom workflow work? Token owner calls approve(spender, amount) to authorize spending. The spender then calls transferFrom(owner, recipient, amount) within the allowed limit. The allowance decreases by the transferred amount.

  3. What is the purpose of the decimal field? Decimals define the smallest unit of the token. 18 decimals (default) means 1 token = 10^18 smallest units. USDC uses 6 decimals for efficiency.

  4. Why use OpenZeppelin's ERC-20 instead of writing from scratch? OpenZeppelin contracts are audited, battle-tested, and handle edge cases correctly. Writing from scratch risks bugs, overflow errors, and security vulnerabilities.

  5. What happens when you transfer tokens to the zero address? It permanently burns the tokens. The ERC-20 standard doesn't prevent this. Most implementations explicitly forbid transfers to address(0).

Challenge

Create a custom ERC-20 token with a tax mechanism: every transfer deducts 1% as a fee that goes to a treasury address, implements a maximum wallet holding limit of 2% of supply, and includes a cooldown period between transfers. Test all edge cases.

Real-World Task

Launch a community token for a fictional project using OpenZeppelin and Hardhat. Deploy to Sepolia testnet, verify on Etherscan, add liquidity on a testnet Uniswap instance, create a token vesting contract for team members, and build a simple React frontend showing balances and transfers.

Frequently Asked Questions

What is the ERC-20 token standard?

ERC-20 is a standard interface for fungible tokens on Ethereum. It defines six required functions (totalSupply, balanceOf, transfer, allowance, approve, transferFrom) and two events (Transfer, Approval) ensuring all tokens work seamlessly with wallets, exchanges, and DeFi protocols.

Can I create an ERC-20 token without code?

You can use platforms like TokenFactory or OpenZeppelin's Contracts Wizard web interface, which generates Solidity code based on your selected features. However, understanding the code is essential for security and customization.

How much does it cost to deploy an ERC-20 token?

Deployment gas varies by complexity. A basic ERC-20 costs approximately 500,000-800,000 gas. At 20 gwei gas price, that's roughly 0.01-0.016 ETH. Complex tokens with extensions can cost 1.5-3 million gas.

Next Steps

After deploying your ERC-20 token, explore NFTs (ERC-721 and ERC-1155) for non-fungible tokens, then dive into DeFi protocols for lending and swapping.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro