ERC20 is a standard interface for fungible tokens on Ethereum. Any wallet, exchange, or smart contract that supports ERC20 can automatically handle your token without custom integration. This compatibility is why ERC20 dominates.
The ERC20 Standard: Required Functions
ERC20 defines six mandatory functions and two optional events. A contract that doesn't implement all six is not ERC20-compliant and will fail on major exchanges.
Core functions
- balanceOf(address account): Returns token balance of an account. Backed by a mapping: address to uint256.
- transfer(address to, uint256 amount): Sends tokens from caller to recipient. Decrements caller's balance, increments recipient's balance. Emits Transfer event.
- approve(address spender, uint256 amount): Approves a spender (typically an exchange or contract) to transfer up to amount tokens on the caller's behalf. Does not transfer immediately; just grants permission. Critical for dApps and exchange integration.
- transferFrom(address from, address to, uint256 amount): Requires prior approval. Transfers tokens from one address to another. Used when a contract acts on your behalf (e.g., a DEX swap).
- allowance(address owner, address spender): Returns the remaining amount the spender is approved to transfer from owner.
- totalSupply(): Returns total tokens in existence.
Required events
- Transfer(address indexed from, address indexed to, uint256 value): Emitted whenever tokens move.
- Approval(address indexed owner, address indexed spender, uint256 value): Emitted when approval changes.
Indexed parameters allow off-chain systems to filter events. Exchanges listen for Transfer events to detect deposits.
Writing the Contract in Solidity
Use Solidity 0.8 or higher (adds built-in overflow protection, eliminating the need for SafeMath).
Minimal ERC20 implementation
| pragma solidity ^0.8.0; contract MyToken { string public name = "MyToken"; string public symbol = "MTK"; uint8 public decimals = 18; 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); constructor(uint256 initialSupply) { totalSupply = initialSupply * 10 ** uint256(decimals); balanceOf[msg.sender] = totalSupply; } function transfer(address to, uint256 value) public returns (bool) { require(to != address(0), "Invalid address"); require(balanceOf[msg.sender] >= value, "Insufficient balance"); balanceOf[msg.sender] -= value; balanceOf[to] += value; emit Transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) public returns (bool) { allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) public returns (bool) { require(balanceOf[from] >= value, "Insufficient balance"); require(allowance[from][msg.sender] >= value, "Allowance exceeded"); balanceOf[from] -= value; balanceOf[to] += value; allowance[from][msg.sender] -= value; emit Transfer(from, to, value); return true; } } |
Key design decisions
Decimals: Ethereum internally uses integers. Decimals define the display scale. 18 decimals is standard (1 token = 10^18 wei, matching Ether). Set this once in the constructor; it cannot be changed.
Initial supply: Decide whether to mint all tokens at once (constructor) or add a mint() function. Minting at deploy is simpler and clearer. Adding mint() requires an owner address and minting restrictions to prevent inflation.
Overflow protection: Solidity 0.8+ automatically reverts on overflow/underflow. Earlier versions required OpenZeppelin's SafeMath library.
Common Extensions
Most production tokens add features beyond the six core functions:
- Ownership: Add an owner and onlyOwner modifier to control who can mint, burn, or freeze accounts.
- Burning: burn() and burnFrom() reduce supply and total supply.
- Pausing: pause/unpause emergency stops all transfers.
- Capping: Enforce a maximum total supply cap.
- Freezing: Blacklist addresses (e.g., for regulatory compliance).
Use OpenZeppelin Contracts library instead of writing these yourself. Audited, production-tested, and upgradeable.
Example with OpenZeppelin:
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyToken is ERC20, Ownable { constructor() ERC20("MyToken", "MTK") {} function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } function burn(uint256 amount) public { _burn(msg.sender, amount); } } |
Deployment and Integration
Deployment
- Write the contract in Solidity.
- Compile with Hardhat or Truffle: npx hardhat compile
- Deploy to testnet (Sepolia for Ethereum, Mumbai for Polygon): npx hardhat run scripts/deploy.js , network sepolia
- Verify on Etherscan so exchanges and wallets can read your code.
- After testing, deploy to mainnet with the same process.
Gas costs: A simple ERC20 deploy costs ~1.2M gas (~0.02 ETH at 20 gwei). Mainnet deployment is irreversible.
Integration with exchanges and wallets
Once deployed and verified on Etherscan, most wallets and DEXs automatically recognize your token. To list on Uniswap:
- Create a liquidity pool (requires 2 tokens: yours and a base token like USDC or WETH).
- Deposit initial liquidity. The price is determined by the ratio of tokens in the pool.
- List on Uniswap's front-end if desired (optional; liquidity alone makes it tradable).
Risks and Considerations
- Supply control: If you can mint infinitely, the token becomes worthless (hyperinflation). Cap supply or require governance to mint.
- Approval attack: Front-running the approve function before increasing allowance is possible. Recommend using increaseAllowance() instead of approve() to change existing allowances.
- Reentrancy: If your token interacts with external contracts in transfer hooks, ensure no state inconsistency. Use checks-effects-interactions pattern.
- Regulatory: In many jurisdictions, tokens are securities if they represent investment contracts (claims on profits/revenue). Consult legal counsel.
Summary
ERC20 is straightforward: implement six functions, emit two events, and ensure basic invariants (balance and allowance consistency). Most complexity comes from extensions (minting, burning, pausing). Use OpenZeppelin's tested implementations rather than writing from scratch. Test thoroughly on a testnet before mainnet deployment; contract code is immutable once live.

