Skip to main content

ERC-20: Fungible Token Standard

EIP
20
Status
Final
Type
Standards Track (ERC)
Authors
Fabian Vogelsteller, Vitalik Buterin
Created
November 19, 2015

Overview

ERC-20 defines a standard interface for fungible tokens on Ethereum and EVM-compatible chains. Every token is identical and interchangeable; one USDC is the same as any other USDC. The standard specifies how tokens are transferred, how balances are queried, and how third parties can be authorized to spend tokens on behalf of a holder.

ERC-20 is the foundation of onchain finance. Lending, borrowing, swaps, liquidity pools, governance, stablecoins, all of it is built around ERC-20 compatibility. Any new token standard that wants to work with existing DeFi infrastructure typically extends or maintains compatibility with ERC-20.

We wrote a broader overview of the ERC standards we think matter most in our ERC Token Standards blog post.

Interface Specification

The ERC-20 interface defines six mandatory functions, two mandatory events, and three optional metadata functions.

Events

Transfer

event Transfer(address indexed from, address indexed to, uint256 value);

Emitted when tokens move between addresses. This includes minting (where from is the zero address) and burning (where to is the zero address). Every token movement onchain emits this event, it's what indexers like Hgraph use to track transfer history and compute balances.

Approval

event Approval(address indexed owner, address indexed spender, uint256 value);

Emitted when an owner authorizes a spender to transfer tokens on their behalf via the approve function.

Mandatory Functions

totalSupply

function totalSupply() external view returns (uint256);

Returns the total number of tokens in existence. Note: this may not reflect circulating supply. Tokens sent to burn addresses (like 0x000...dead) are still counted unless the contract explicitly decrements the supply.

balanceOf

function balanceOf(address account) external view returns (uint256);

Returns the token balance of the given address. Balances are stored in a mapping(address => uint256). This means you can look up any individual balance, but you cannot enumerate all holders from the contract itself. Reconstructing a full holder list requires indexing every Transfer event from contract deployment.

transfer

function transfer(address to, uint256 amount) external returns (bool);

Sends amount of tokens from the caller's address to to. Must emit a Transfer event. Reverts if the caller doesn't have enough balance.

allowance

function allowance(address owner, address spender) external view returns (uint256);

Returns how many tokens spender can still pull from owner's balance, the unspent portion of a previous approve call.

approve

function approve(address spender, uint256 amount) external returns (bool);

Authorizes spender to transfer up to amount of the caller's tokens. Must emit an Approval event.

transferFrom

function transferFrom(address from, address to, uint256 amount) external returns (bool);

Transfers amount tokens out of from's balance and into to, deducting from the caller's allowance. The caller must have been approved for at least amount by from. Must emit a Transfer event.

The approve + transferFrom pattern is how DeFi works. When you deposit tokens into a lending protocol or swap on a DEX, you first approve the contract to spend your tokens, then the contract pulls them via transferFrom. This two-step delegation is what makes composability possible.

Optional Metadata Functions

These are not part of the core spec but are implemented by virtually every ERC-20 token in practice:

function name() external view returns (string memory);     // e.g. "USD Coin"
function symbol() external view returns (string memory); // e.g. "USDC"
function decimals() external view returns (uint8); // e.g. 6 for USDC, 18 for UNI

decimals are critical for display and arithmetic. Token amounts are stored as integers. A balance of 1000000 for a token with 6 decimals represents 1.0 tokens. Common values: 18 (most tokens), 6 (USDC, USDT), 8 (WBTC).

Function Selectors

For low-level integrations and ABI encoding:

FunctionSelector
name()0x06fdde03
symbol()0x95d89b41
decimals()0x313ce567
totalSupply()0x18160ddd
balanceOf(address)0x70a08231
allowance(address,address)0xdd62ed3e
transfer(address,uint256)0xa9059cbb
approve(address,uint256)0x095ea7b3
transferFrom(address,address,uint256)0x23b872dd

Notable ERC-20 Tokens

TokenSymbolDecimalsEthereum Mainnet Address
USD CoinUSDC60xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Tether USDUSDT60xdAC17F958D2ee523a2206206994597C13D831ec7
ChainlinkLINK180x514910771AF9Ca656af840dff83E8264EcF986CA
UniswapUNI180x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Wrapped EtherWETH180xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

Risks and Gotchas

Front-Running on approve

The approve function has a well-known race condition. If Alice has approved Bob for 100 tokens and wants to change it to 50, Bob can front-run the transaction: he calls transferFrom for the original 100 before Alice's new approve(50) is mined, then calls transferFrom again for 50, extracting 150 total.

Mitigations:

  • Set allowance to 0 first, then set to the desired value (two transactions)
  • Use EIP-2612 permit() for signature-based approvals

Missing Return Value

The spec says transfer, approve, and transferFrom must return bool. Several major tokens, most notably USDT, return nothing. This causes standard Solidity calls to revert at the ABI decoder level even when the transfer succeeds.

Mitigation: Always use OpenZeppelin's SafeERC20 library, which handles both compliant and non-compliant tokens.

Fee-on-Transfer Tokens

Some ERC-20 tokens deduct a fee on transfer. If your contract does transferFrom(user, address(this), 100), the contract may receive fewer than 100 tokens.

Mitigation: Always check the actual balance change after the transfer, not the amount parameter you passed in.

Rebasing Tokens

Tokens like stETH automatically adjust balances across holders. A stored balanceOf value can become stale between blocks.

Mitigation: Wrap rebasing tokens before integrating them (e.g., use wstETH instead of stETH) to get a non-rebasing ERC-20 interface with a stable balance.

Decimals Are Not Always 18

USDC and USDT use 6 decimals. WBTC uses 8. Assuming 18 decimals is a common source of bugs.

Mitigation: Always read decimals() from the contract and handle arithmetic accordingly. Never hardcode 18.

Infinite Approvals

Setting an allowance to type(uint256).max is a common UX pattern to avoid repeated approvals. Some implementations don't decrement this value on transferFrom (an intentional gas optimization). Infinite approvals persist through proxy upgrades. If the implementation contract changes, all approved funds are at risk.

Mitigation: Use finite approvals for the exact amount needed per transaction. If infinite approvals are unavoidable, regularly audit and revoke stale allowances using tools like Revoke.cash.

ERC-20 Data on Hgraph

Hgraph indexes ERC-20 token data on Hedera (production) and Ethereum (beta) including metadata, holder balances, and full transfer history. The schema below describes the Hedera side; the Ethereum schema differs and is documented separately.

DataFields
Token metadataname, symbol, decimals, total_supply, token_evm_address, contract_type, metadata_reliability_score, created_timestamp
Holder balancesaccount_id, token_evm_address, balance, balance_timestamp
Transfer historysender_evm_address, receiver_evm_address, amount, consensus_timestamp, transaction_hash, transfer_type

For full GraphQL schema details and example queries (token search, holder lookups, transfer history, and analytics), see the ERC Indexer query examples.

Want to integrate ERC-20 data into your application? Get a free API key.

External References