What the standard defines
EIP-20 was created on November 19, 2015 by Fabian Vogelsteller and Vitalik Buterin. It specifies a standard interface for tokens so any token on Ethereum can be reused by other applications, from wallets to decentralized exchanges.[1]
The required interface is short: totalSupply, balanceOf, transfer, transferFrom, approve and allowance, plus two events, Transfer and Approval. The standard marks name, symbol and decimals as OPTIONAL, methods that improve usability but that other contracts must not expect to exist.[1]
Fungible means every unit is interchangeable, like dollars. One USDC is the same as any other. Non-fungible tokens use a different standard, ERC-721, where each token ID is unique.[2]
The events matter as much as the functions. Transfer must fire on every token movement, including transfers of 0, and Approval must fire on every successful approve call. Indexers, tax tools and wallets build their token histories from these two events, so a token that skips them looks broken to the entire ecosystem even if balances are correct.[1]
Decimals and base units
The EVM has no decimal numbers. Balances are stored as integers, and the decimals value tells software where to place the decimal point. EIP-20's own example: if decimals is 8, divide the token amount by 100,000,000 to get the user-facing amount.[1]
OpenZeppelin's ERC20 uses 18 decimals by default, which mirrors ETH, where 1 ETH equals 10^18 wei. The value can be overridden per token, so a wallet reads it from the contract rather than assuming it.[4]
A missed decimals value is a common integration bug. Sending "100" to a function that expects base units of an 18-decimal token transfers 0.0000000000000001 tokens, not 100.
OpenZeppelin's guide works through this with a sample token called GLD. If you want to send 5 GLD and decimals is 18, the contract call actually carries 5 x 10^18. Its ERC20 implementation uses 18 decimals by default, and a token that needs another value must override the decimals function.[4]
A worked example: approve, then transferFrom
Suppose Alice holds 250 units of an 18-decimal token and wants a DEX to swap 100 of them. On chain her balance is 250 x 10^18 = 250,000,000,000,000,000,000 base units.
Step one: Alice calls approve(dex, 100 x 10^18). The contract records an allowance and must emit an Approval event. Step two: the DEX contract calls transferFrom(alice, pool, 100 x 10^18). The contract checks the allowance, moves the balance, reduces the allowance to zero, and emits a Transfer event.[1]
That is two transactions and two gas payments. ERC-2612 adds a permit function so the approval can be a signed message instead of a transaction, letting the app submit approval and transfer together. ERC-2612 describes this as a way for users to interact with a token without first holding ETH for the approval.[3]
EIP-20 also warns about changing an allowance directly from one non-zero value to another, because a spender watching the mempool could spend both the old and new amounts. The standard says interfaces SHOULD set the allowance to 0 first, then to the new value.[1]
Under the hood, ERC-2612 adds three functions to the token: permit, nonces and DOMAIN_SEPARATOR. The signed message is structured under EIP-712, so wallets can show the owner, spender, value and deadline in readable form before the user signs.[3]
| Function or event | Required? | What it does |
|---|---|---|
| totalSupply() | Yes | Returns the total token supply |
| balanceOf(owner) | Yes | Returns an account's balance |
| transfer(to, value) | Yes | Sends tokens from the caller |
| approve(spender, value) | Yes | Lets a spender use up to value |
| transferFrom(from, to, value) | Yes | Spender moves approved tokens |
| allowance(owner, spender) | Yes | Returns the remaining allowance |
| name, symbol, decimals | Optional | Display information for wallets |
What happens under the hood of a transfer
A wallet does not call a function by name. The Solidity ABI specification says the first four bytes of call data select the function: the first four bytes of the Keccak-256 hash of its signature. For a token transfer the signature is transfer(address,uint256), followed by the recipient and the amount, each encoded as a 32-byte word.[5]
So a transfer of 100 tokens with 18 decimals is a transaction to the token contract, not to the recipient, carrying 0 ETH and call data that encodes the recipient address and the integer 100,000,000,000,000,000,000. The token contract updates two balances and must emit a Transfer event.[1]
This explains a common confusion. On a block explorer the transaction's "to" field shows the token contract, and the real recipient appears only in the decoded input and in the Transfer event log. Wallet activity screens reconstruct token transfers from those logs.
It also explains fees. Because a token transfer runs contract code and writes storage, it costs more gas than a plain ETH transfer, and on Ethereum the fee is paid in ETH, never in the token being sent.
Where ERC-20 falls short
A plain ERC-20 transfer does not notify the receiver. If you send tokens directly to a smart contract that was not built to handle them, the contract has no way to react, and the tokens can be stuck forever.[2]
Ethereum.org puts a number on it: as of June 20, 2024, at least $83,656,418 worth of ERC-20 tokens had been lost this way. It adds that a pure ERC-20 implementation is prone to the problem unless extra safeguards are implemented.[2]
Approvals are the second weak point. Many apps request an unlimited allowance to save a transaction later. If that app's contract is exploited, the attacker can pull every approved token. Reviewing and revoking old approvals is basic hygiene.
Ethereum.org names the most common version of this mistake: sending tokens to the token contract's own address, such as USDT deposited to the USDT contract. It recommends that token authors make transfer revert in that case, with a check that the recipient is not the contract itself.[2]
It also warns contract developers to always assume ERC-20 tokens can end up in their contract even if it is never supposed to receive any, because there is no way to prevent or reject a plain transfer on the receiving side.[2]
See also: Token approval explained · Revoke.cash · Etherscan
How tokens are created
A token creator deploys a contract that implements the interface. Most use audited code such as OpenZeppelin's ERC20 contract instead of writing the logic from scratch. OpenZeppelin's documentation shows a token built by inheriting ERC20 and minting an initial supply in the constructor.[4]
When new tokens are created, EIP-20 says the contract SHOULD emit a Transfer event with the from address set to 0x0. That is how block explorers and indexers detect minting.[1]
The same standard is used on every EVM chain: Arbitrum, Base, Polygon and BNB Chain tokens follow the ERC-20 interface. A token's address on one chain is not the same asset on another unless it was issued or bridged there.
Supply rules are up to the contract. Some tokens mint a fixed supply once in the constructor; others expose a mint function restricted to an owner or a governance contract. The standard only requires totalSupply to report the current number, so read the contract or its documentation to learn whether supply can grow.[1]
For builders, OpenZeppelin's ERC20 also sets decimals to 18 by default, with an override available when a token needs a different value such as 6.[4]
See also: OpenZeppelin · Best software wallets · MetaMask
The bottom line
ERC-20 is a 2015 interface of six functions and two events that lets wallets and apps handle any fungible token the same way. As a user, the two habits that matter are checking where you send tokens and revoking approvals you no longer need. As a builder, read decimals from the contract, use audited code, and support permit where your app handles approvals. And when a token behaves oddly, read its verified source before trusting its name or symbol: anyone can deploy a contract that calls itself USDC.
Educational content, not financial advice. Crypto assets are volatile; do your own research.