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";
Uppercase.
Discussion
Loading…