The ERC-20 standard is the foundation of the fungible token economy. Every USDC, LINK, UNI, ARB, and thousands of other tokens you've interacted with implements this standard. Proposed by Fabian Vogelsteller in November 2015 and formally accepted as Ethereum Improvement Proposal #20, ERC-20 defines a common interface that all fungible tokens must implement — enabling wallets, DEXes, and any other smart contract to interact with any ERC-20 token without needing custom integration for each one. Understanding how ERC-20 tokens work gives you deeper insight into what happens when you deploy a token on Arbitrum, what the functions in your contract do, and why certain design decisions matter.

This guide covers the ERC-20 standard from first principles: the required functions, how balances and allowances work, what events are emitted, how transfers execute at the code level, and what optional extensions like minting and burning add. You don't need to write Solidity to benefit from this knowledge — understanding your token's capabilities makes you a better project creator.

What ERC-20 Actually Defines

ERC-20 is an interface specification — a set of functions and events that a compliant token contract must implement. The standard itself doesn't dictate how you implement the logic internally, only what public functions must be available and how they must behave. This creates interoperability: a DEX can call transfer() on any ERC-20 token and know exactly what to expect.

The ERC-20 standard requires six functions and two events. That's the complete minimum requirement. Everything else — minting, burning, pausing, ownership — are optional extensions built on top of this foundation.

The Six Required Functions

  • totalSupply() — Returns the total number of tokens in existence (in base units, accounting for decimals).
  • balanceOf(address) — Returns the token balance of a specific wallet address.
  • transfer(address to, uint256 amount) — Moves tokens from the caller's address to the recipient. Returns a boolean.
  • allowance(address owner, address spender) — Returns how many tokens a spender is allowed to spend on behalf of the owner.
  • approve(address spender, uint256 amount) — Grants a spender permission to use up to the specified amount of the caller's tokens.
  • transferFrom(address from, address to, uint256 amount) — Moves tokens from one address to another, using allowance. Used by DEXes and other contracts to spend tokens on your behalf.

The Two Required Events

  • Transfer(address indexed from, address indexed to, uint256 value) — Emitted on every token transfer, including minting (from = zero address) and burning (to = zero address).
  • Approval(address indexed owner, address indexed spender, uint256 value) — Emitted when approve() is called.

Events are stored in the transaction log on the blockchain. Block explorers like Arbiscan read these events to display token transfer history, and indexers like The Graph use them to build queryable databases of on-chain activity.

How Token Balances Work

Unlike physical coins, ERC-20 tokens don't "live" in wallets. They exist as entries in a mapping inside the smart contract. The contract maintains a ledger: a Solidity mapping(address => uint256) that records how many base units each address holds.

// Internal balance storage in a standard ERC-20 contract mapping(address => uint256) private _balances; // balanceOf() simply reads this mapping function balanceOf(address account) public view returns (uint256) { return _balances[account]; }

When you check your token balance in MetaMask, the wallet is calling balanceOf(yourAddress) on the contract. The contract returns the raw number stored in the mapping. MetaMask then divides by 10^decimals to show you a human-readable number. If you hold 1,000,000,000,000,000,000 base units and the token has 18 decimals, MetaMask shows "1.0" token.

The Decimals Convention

The decimals function (technically optional but universally implemented) returns the number of decimal places used for display. With 18 decimals, one token = 1,000,000,000,000,000,000 (10^18) base units. This granularity allows extremely precise fractional ownership. Someone holding 0.000000000000000001 of your token holds exactly 1 base unit.

This matters for tokenomics: a "1 million token supply" with 18 decimals means 1,000,000 × 10^18 base units are minted. All supply caps, transfer amounts, and allowances are expressed in base units at the contract level. Your deployment tool handles this conversion automatically.

Deploy Your Own ERC-20 on Arbitrum

Put your knowledge into practice. Create and deploy a compliant ERC-20 token on Arbitrum One in minutes — no coding required.

🚀 Create Token Now

How Token Transfers Work

A token transfer is a state change in the contract's balance mapping. When Alice transfers 100 tokens to Bob, the contract:

  1. Checks that Alice has at least 100 tokens (reverts if not)
  2. Subtracts 100 (in base units) from Alice's balance entry
  3. Adds 100 (in base units) to Bob's balance entry
  4. Emits a Transfer event recording the sender, recipient, and amount

No tokens actually "move" — it's purely a ledger update inside one smart contract. This is why token transfers are so fast and cheap: it's just two storage writes and an event log entry.

Direct Transfer vs. Approved Transfer

There are two ways to move ERC-20 tokens:

  • transfer(to, amount) — The token holder calls this directly to send tokens to someone. You're the msg.sender, you control your own tokens.
  • approve() + transferFrom() — Two-step process where you first authorize a third party (like a DEX), then that third party can move your tokens. This is how Uniswap, lending protocols, and staking contracts work.

The approve/transferFrom pattern is crucial for DeFi composability. When you "approve" a DEX to spend your tokens, you're adding an entry to the allowances mapping that says "this spender can use up to X of my tokens." The DEX then calls transferFrom() to execute the swap. Without this pattern, you'd need to manually transfer tokens to every contract before interacting with it — far less user-friendly.

The Allowance System

Allowances are tracked in a nested mapping: mapping(address => mapping(address => uint256)) private _allowances. The outer key is the token owner, the inner key is the spender, and the value is the approved amount.

// Granting an allowance _allowances[ownerAddress][spenderAddress] = approvedAmount; // Checking an allowance function allowance(address owner, address spender) public view returns (uint256) { return _allowances[owner][spender]; } // Spending within allowance function transferFrom(address from, address to, uint256 amount) { require(_allowances[from][msg.sender] >= amount, "Insufficient allowance"); _allowances[from][msg.sender] -= amount; _balances[from] -= amount; _balances[to] += amount; emit Transfer(from, to, amount); }

A common user mistake is approving a very large allowance (type(uint256).max — essentially unlimited) to save gas on future transactions. This is convenient but means the approved contract can drain your entire balance if compromised. Serious users approve only the exact amount needed and re-approve as necessary.

Standard ERC-20 Extensions

The OpenZeppelin library — used by virtually every serious ERC-20 deployment including those from createarbitrumtoken.com — provides battle-tested extensions that add functionality beyond the base standard.

ERC20Burnable

Adds burn(uint256 amount) and burnFrom(address account, uint256 amount) functions. burn() lets a holder permanently destroy their own tokens, reducing totalSupply(). This is used in buyback-and-burn mechanisms, deflationary token models, and any system where tokens are consumed as a fee. Burning emits a Transfer event with the zero address as the recipient.

ERC20Mintable (via Ownable)

Adds a mint(address to, uint256 amount) function restricted to the contract owner. Minting creates new tokens, increasing totalSupply(). Emits a Transfer event with the zero address as the sender. Common in reward distribution contracts, staking systems, and tokens with elastic supply.

ERC20Pausable

Adds pause() and unpause() functions (owner-only) that halt all transfers when active. The transfer() and transferFrom() functions check the paused state and revert if paused. Used as an emergency stop mechanism in case of security vulnerabilities or exploits. The tradeoff is significant centralization — a malicious or hacked owner could freeze all token movement indefinitely.

ERC20Permit (EIP-2612)

A modern extension that allows approval via signed messages (off-chain), eliminating the need for a separate approve() transaction. With ERC20Permit, a user signs a message off-chain authorizing a spender, then the spender submits the signature in the same transaction as the transferFrom(). This reduces the common "two-transaction approval flow" to a single transaction, saving gas and improving UX.

Events, Logs, and the Graph

Events are a crucial but often overlooked aspect of ERC-20 tokens. Every Transfer and Approval emits an event that gets stored in the transaction receipt's log. These logs are:

  • Permanently stored on the blockchain (Ethereum/Arbitrum full nodes keep them)
  • Indexed by block explorers (Arbiscan displays your token's transaction history from Transfer events)
  • Used by subgraphs (The Graph protocol indexes events to enable efficient on-chain data queries)
  • Read by wallets to determine token balances and transaction history

This is why when you deploy a new token and check Arbiscan, you'll immediately see the initial mint transaction in the token's transfer history — even though no one "sent" anything, the minting emitted a Transfer event from the zero address to your wallet.

Security Considerations in ERC-20

The simplicity of ERC-20 belies some subtle security considerations that have caused real losses in production contracts.

Approve Race Condition

If you change an allowance from X to Y, there's a window where a watching spender can spend the old allowance X and then the new allowance Y, effectively spending X+Y. The safe pattern is to approve 0 first, then approve the new amount. ERC20Permit solves this elegantly by replacing allowances with nonces.

Transfer-on-Fee Tokens

Some tokens take a fee on every transfer (deflationary tokens, reflection tokens). This breaks the assumption that transferring X tokens results in the recipient receiving X tokens. DEXes and protocols that assume no-fee transfers may behave incorrectly with these tokens. Standard ERC-20 contracts from createarbitrumtoken.com don't include transfer fees — this is a deliberate design choice for compatibility.

Integer Overflow (Historical)

In early Solidity (pre-0.8.0), integer overflow was a real attack vector. An unsigned integer maxing out at 2^256-1 would wrap around to zero on the next increment. Modern Solidity 0.8.0+ includes automatic overflow/underflow checks that revert on overflow. All modern ERC-20 deployments use Solidity 0.8.x and are safe from this class of bug.