TechLead
Lesson 7 of 20
5 min read
Web3 & Blockchain

ERC-20 Token Standard

Understand and implement the ERC-20 token standard for creating fungible tokens on Ethereum

What is ERC-20?

ERC-20 (Ethereum Request for Comments 20) is the most widely adopted token standard on Ethereum. It defines a common interface that all fungible tokens must implement, enabling wallets, exchanges, and other contracts to interact with any ERC-20 token in a uniform way. Fungible means every token is identical and interchangeable — just like dollars, where one dollar equals any other dollar.

ERC-20 Interface

  • totalSupply(): Returns the total number of tokens in existence
  • balanceOf(account): Returns the token balance of a specific address
  • transfer(to, amount): Transfers tokens from the caller to another address
  • approve(spender, amount): Grants permission for another address to spend tokens on your behalf
  • allowance(owner, spender): Returns how many tokens the spender is allowed to spend
  • transferFrom(from, to, amount): Transfers tokens using the allowance mechanism

Implementing an ERC-20 Token

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

/// @title MyToken — A complete ERC-20 implementation from scratch
contract MyToken {
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    error InsufficientBalance(uint256 available, uint256 required);
    error InsufficientAllowance(uint256 available, uint256 required);

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        decimals = 18;
        totalSupply = _initialSupply * 10 ** uint256(decimals);
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        return _transfer(msg.sender, to, amount);
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 currentAllowance = allowance[from][msg.sender];
        if (currentAllowance < amount) {
            revert InsufficientAllowance(currentAllowance, amount);
        }
        allowance[from][msg.sender] = currentAllowance - amount;
        return _transfer(from, to, amount);
    }

    function _transfer(address from, address to, uint256 amount) internal returns (bool) {
        require(to != address(0), "Transfer to zero address");
        if (balanceOf[from] < amount) {
            revert InsufficientBalance(balanceOf[from], amount);
        }
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

Using OpenZeppelin (Production Approach)

In production, you should use the battle-tested OpenZeppelin Contracts library rather than writing token logic from scratch. It has been audited extensively and is used by the majority of production tokens.

// 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/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @title GovernanceToken — Production ERC-20 with extensions
contract GovernanceToken is ERC20, ERC20Burnable, ERC20Permit, Ownable {
    uint256 public constant MAX_SUPPLY = 100_000_000 * 10 ** 18; // 100M tokens

    constructor()
        ERC20("Governance Token", "GOV")
        ERC20Permit("Governance Token")
        Ownable(msg.sender)
    {
        // Mint initial supply to deployer
        _mint(msg.sender, 10_000_000 * 10 ** 18); // 10M initial
    }

    /// @notice Mint new tokens (only owner, respects max supply)
    function mint(address to, uint256 amount) external onlyOwner {
        require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(to, amount);
    }
}

The Approve/TransferFrom Pattern

The approve + transferFrom pattern is fundamental to DeFi. When you want a smart contract (like Uniswap or Aave) to spend your tokens, you first approve the contract to spend a certain amount. Then, the contract calls transferFrom to move your tokens. This two-step process prevents contracts from taking tokens without your explicit permission.

import { ethers } from 'ethers';

const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);
const dexContract = new ethers.Contract(dexAddress, dexABI, signer);

// Step 1: Approve the DEX to spend 100 tokens on your behalf
const approvalTx = await tokenContract.approve(
  dexAddress,
  ethers.parseUnits('100', 18)
);
await approvalTx.wait();
console.log('Approved DEX to spend 100 tokens');

// Step 2: The DEX contract calls transferFrom internally when you swap
const swapTx = await dexContract.swapTokens(
  tokenAddress,
  ethers.parseUnits('50', 18),  // Spend 50 tokens
  otherTokenAddress,
  0 // Minimum output (set properly in production!)
);
await swapTx.wait();
console.log('Swap completed');

// Check remaining allowance
const remaining = await tokenContract.allowance(myAddress, dexAddress);
console.log('Remaining allowance:', ethers.formatUnits(remaining, 18));

ERC-20 Extensions

Common Extensions

Extension Purpose
ERC20BurnableAllows token holders to destroy (burn) their tokens, reducing total supply
ERC20CappedEnforces a maximum total supply that cannot be exceeded
ERC20Permit (EIP-2612)Gasless approvals via off-chain signatures — users sign a permit message instead of sending an approve transaction
ERC20VotesAdds vote delegation and checkpointing for governance tokens
ERC20SnapshotCaptures token balances at specific points in time for airdrops or governance snapshots

Common Pitfalls

  • Double-spend on approval: Setting allowance from 50 to 100 has a race condition. Use increaseAllowance/decreaseAllowance or set to 0 first.
  • Missing decimals handling: Always account for decimals. USDC uses 6 decimals, not 18.
  • Transfer to contract address: Tokens sent to a contract that cannot handle them are lost forever. Consider ERC-777 or safe transfer checks.
  • Fee-on-transfer tokens: Some tokens deduct a fee during transfers, breaking assumptions in DeFi protocols.

Continue Learning