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 |
|---|---|
| ERC20Burnable | Allows token holders to destroy (burn) their tokens, reducing total supply |
| ERC20Capped | Enforces 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 |
| ERC20Votes | Adds vote delegation and checkpointing for governance tokens |
| ERC20Snapshot | Captures 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.