iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

ERC-20 (tokens)

ERC-20 is the fungible-token standard on Ethereum. Six required functions + two events. Implement it once with OpenZeppelin; you get wallet support, DEX listings, and indexers for free.

Standard, OpenZeppelin, extensions

EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 1) Standard interface — what every ERC-20 must implement
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);
}

// 2) OpenZeppelin implementation — the right starting point
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor(uint256 initialSupply)
        ERC20("My Token", "MTK")
        Ownable(msg.sender)
    {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

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

// 3) Useful extensions
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";

contract GovToken is ERC20, ERC20Burnable, ERC20Permit, ERC20Votes, Ownable {
    constructor() ERC20("Gov", "GOV") ERC20Permit("Gov") Ownable(msg.sender) {
        _mint(msg.sender, 1_000_000 * 10 ** decimals());
    }

    // Required overrides when combining extensions
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes)
    { super._update(from, to, value); }

    function nonces(address owner) public view override(ERC20Permit, Nonces)
        returns (uint256) { return super.nonces(owner); }
}

// 4) Decimals — usually 18, never call them "the token amount"
// 1 token = 1 * 10^18 raw units
// transfer(to, 1 ether) sends 1 whole token if decimals == 18

// 5) Approve + transferFrom flow — the 2-step pattern that powers DEXes
//
// 1. User holds 100 MTK
// 2. User calls MTK.approve(dexAddress, 100 ether) — allows DEX to spend up to 100
// 3. DEX calls MTK.transferFrom(user, dex, 50 ether)
//
// Allowance race condition: change approval from non-zero to non-zero in one tx — risky.
// Fix: ERC20Permit (EIP-2612) — sign approval, submit in same tx; or approve(0) then approve(new).

// 6) Permit — gasless approvals via EIP-712 signatures
// Client signs:
//   { owner, spender, value, nonce, deadline }
// Contract verifies signature in `permit()`, sets allowance, then transferFrom — one tx.

// 7) Read + interact off-chain (ethers v6)
import { ethers } from 'ethers';
import ABI from './MyToken.abi.json';

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer   = new ethers.Wallet(PRIVATE_KEY, provider);
const token    = new ethers.Contract(ADDRESS, ABI, signer);

const balance  = await token.balanceOf(myAddress);
const decimals = await token.decimals();
console.log(ethers.formatUnits(balance, decimals));

const tx = await token.transfer(toAddress, ethers.parseUnits('1.5', decimals));
await tx.wait();

// 8) Listen for transfers
token.on('Transfer', (from, to, amount, event) => {
    console.log(`${from} → ${to}: ${ethers.formatUnits(amount, decimals)}`);
});

// 9) Common bugs + safety
//   • DON'T send ETH to an ERC-20 contract address — usually lost
//   • DON'T forget the OpenZeppelin _update override when stacking extensions
//   • DON'T trust other ERC-20s blindly — some have transfer fees or non-standard returns
//   • Use SafeERC20 (`token.safeTransfer`) when calling other tokens you don't control

// 10) Audit checklist
//   • Use OpenZeppelin (battle-tested) — don't write transfer logic from scratch
//   • Pin OpenZeppelin version; review the upgrade diff before bumping
//   • Cap supply (ERC20Capped) if minting is finite
//   • Pausable + Ownable behind a multisig — emergency stop
//   • Verify on Etherscan; publish ABI; reproducible builds (hardhat / foundry)
//   • Get a real audit before mainnet for non-trivial tokens

Why it matters

Build ERC-20s on top of OpenZeppelin — not by copy-pasting the interface. Battle-tested implementations cover the allowance race, reentrancy, and edge cases (zero amounts, self-transfers) that custom code gets wrong.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1_000_000 ether);
    }
}
Try it Yourself »

Exercise

OpenZeppelin ERC-20 import path.

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

Test yourself

Q1. ERC-20 is the standard for…
Q2. ERC-721 is the standard for…
Q3. Audited reference implementations come from…

Discussion

Loading…