Skip to content
Tech Interview Prep home
Technical interview guide

Token Standards (ERC-20, ERC-721, ERC-1155)

The interface standards that let wallets, exchanges, and other contracts interact with any token the same way — fungible, non-fungible, and multi-token.

Read
45 min
Practice MCQs
25
Interview QA
25
Edition
v3
Editorial status
Reviewed

Scope: ERC-20, ERC-165, ERC-721, ERC-1155, ERC-2612, ERC-2981, and ERC-4906 specifications; OpenZeppelin Contracts 5.x documentation reviewed 2026-09-04.

Overview

Curated: · Written: · Reviewed:

A token standard is an interoperability contract, not an asset guarantee

The mental model: a shared wire format, nothing more

An ERC is a promise about call signatures and event shapes, not about what a token is worth or how its contract behaves internally. ERC-20 says: if I call transfer(address,uint256) on your contract, it takes two arguments and emits a Transfer event. That is the whole guarantee. Everything a candidate needs to reason about — value, custody, supply policy, safety — lives in the implementation, and the standard is silent on all of it.

Why this matters enough to have three major standards: wallets, DEXs, marketplaces and lending protocols integrate once against the interface and then work with any compliant token. Uniswap does not ship custom code for each of the thousands of tokens it lists; it ships one pair contract that calls transferFrom and listens for Transfer. ERC-20 is the composable base unit DeFi is built from — the reason a new token can be tradable and collateralizable on day one is that the integration cost is zero.

The three standards divide by asset shape:

standardmodelsidentitytypical use
ERC-20fungible quantitiescontract addresscurrencies, governance, LP shares
ERC-721individually unique tokenscontract address + uint256 token IDart, deeds, one-of-one claims
ERC-1155many token types in one contractcontract address + token IDgame items, semi-fungible editions

Conformance makes calls predictable. It does not prove economic value, legal ownership, metadata truth, issuer solvency, transferability under every state, or implementation safety. Say that sentence in the interview before the interviewer asks.

ERC-20 from the inside: who calls what

The interface is small enough to hold in your head:

// Solidity 0.8.x pseudocode of the ERC-20 surface (ERC-20, EIP final 2015)
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);

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

The actor mapping is where interviews catch people:

  • transfer — the holder moves their own funds. msg.sender is the owner.
  • approve + transferFrom — a contract moving user funds goes through this pair, never transfer. The user calls approve(vault, 100e18); later the vault calls transferFrom(user, vault, 100e18) and the token contract checks the vault's remaining allowance. This two-step delegation is the entire permission model DeFi runs on.
  • Transfer to the zero address — by convention, a mint; from the zero address, a burn. The standard does not define who may do this.

Amounts are plain integers in base units. The optional decimals value is a display convention only: USDC and USDT use 6, most long-tail tokens use 18, and some use neither. It changes no arithmetic and makes nothing floating point. A protocol must read decimals() per token, normalize explicitly, and define rounding direction for deposits, shares, fees and prices. Assuming 18 is one of the most common production integration bugs.

The allowance model and its defects

Allowance is delegated transfer authority: the spender may move up to amount of the owner's balance, and approve overwrites it wholesale.

The overwrite race. Suppose an allowance of 100 exists and the owner wants to raise it to 200. If the owner's approve(200) sits in the mempool, a watching spender can front-run it: transferFrom 100 against the old allowance, then the replacement lands, then transferFrom another 200 — 300 total instead of 200. The standard's own note recommends setting the allowance to zero first, waiting for that to confirm, then setting the new value. OpenZeppelin's increaseAllowance/decreaseAllowance mitigate by never racing a nonzero overwrite, at the cost of two transactions for a reset.

Infinite approvals persist because each approve costs the user a transaction (~$1–20 in gas depending on congestion) and a confirmation wait, so wallets and marketplaces default to approve(type(uint256).max) for convenience. The trade is real: one signature versus a standing drain on the whole balance. If the spender — or its upgrade authority — is ever compromised, the owner's entire balance is gone in one call. Good UX surfaces existing allowances and makes revocation one click.

ERC-2612 permit replaces the approval transaction with an EIP-712 signature the user signs off-chain and a relayer submits: permit(owner, spender, value, deadline, v, r, s). Verification binds the owner, spender, amount, current nonce, deadline, contract and chain domain; the nonce is consumed exactly once, so a captured signature cannot be replayed after use or on another chain. Permit saves a transaction; it does not transfer tokens, constrain what the spender does with the allowance afterward, or make anything revocable after execution.

Worked example: unlimited approve is the whole balance

USDC on Ethereum, 6 decimals. Owner holds 1,200 USDC. The marketplace spender is compromised a week later.

approval setcompromised spender callsUSDC leaving the wallet
approve(spender, type(uint256).max)one transferFrom1,200
approve(spender, 50e6), revoked after usesame50, then 0
ERC-2612 permit, nonce N, 10-minute deadlinereplay of the signature after use0 — nonce N is consumed

The first row is why "unlimited approval" is a standing transfer grant, not a UI checkbox. The nonzero-overwrite race above is the natural follow-up.

What ERC-20 does not standardize

Everything economically interesting is left to the implementation:

  • decimals — optional, display-only, varies (6 for USDC/USDT, 18 for most others). Never a float, never a unit of account between contracts; convert to base units at the boundary.
  • name and symbol — optional, unverified, and not unique. Two contracts can both call themselves "USDC"; one of them is a scam.
  • mint authority — no cap, no backing, no policy. A Transfer from the zero address can be a legitimate mint or an insider printing.
  • fees, pausing, blacklists — USDT can freeze an address and block transfers; a fee-on-transfer token delivers less than requested; a rebase token changes balances without any transfer event.

The consequence: a protocol must state which asset semantics it supports, and reject the rest. Accounting the requested amount instead of the received amount is how vaults go insolvent against fee-on-transfer tokens.

Non-compliant and hostile tokens, and the two defences

The standard says transfer returns bool, but the ecosystem predates strict enforcement, so production tokens do all of the following:

  • return nothing (older tokens like some pre-standard deployments — the ABI decoder reverts on the missing return unless handled);
  • return false instead of reverting on failure, so a plain high-level call succeeds from Solidity's perspective while the transfer silently failed;
  • fee-on-transfer: recipient receives amount − fee;
  • rebasing: balanceOf changes with no event at all.

Two defences, used together:

  1. SafeERC20-style low-level handling (OpenZeppelin's SafeERC20): drop to a low-level call, accept empty return data as success, treat an explicit false as failure, revert otherwise. This handles the return-value zoo without knowing which token you are facing.
  2. Balance-difference accounting: snapshot balanceOf(this) before and after, credit the difference — never the requested amount. This is the only correct method for fee-on-transfer tokens and a cheap invariant check for everything else.

A token that is deliberately hostile can still lie in balanceOf or re-enter your contract from the transfer hook, so state updates follow checks-effects-interactions order and the token allowlist is a real security boundary, not a formality.

ERC-721: identity, approvals, and the safe-transfer hook

Each NFT is the pair (contract address, uint256 token ID). Neither half alone is unique — the same ID exists in every collection.

Two distinct authorization questions:

  • token approval — one address may move one specific token;
  • operator approval — one address may move every token the owner holds in that contract. Marketplaces ask for this, and users grant it without reading; a compromised or upgraded marketplace operator drains the whole collection.

Transfers clear the token-specific approval, and off-chain quotes can be stale by execution time, so re-check ownerOf and authorization at execution.

safeTransferFrom calls onERC721Received on the recipient when it is a contract and requires the exact selector back. This prevents the classic "sent my NFT to a contract that can't handle it, locked forever" failure — but it hands an adversarial contract a callback after token state has changed, so receiver and sender code must be reentrancy-safe. transferFrom skips the hook and puts recipient capability on the caller. "Safe" means protocol compatibility, never commercial safety or authenticity.

Metadata and enumeration are optional extensions. A tokenURI can point to mutable or dead content; centralized hosting can change what users see without touching ownership. Content addressing (IPFS-style) proves bytes match an identifier, not that anyone will keep serving them. Treat metadata as untrusted content: scheme allowlist, size and timeout caps, sanitization of anything renderable, and provenance on cached copies.

ERC-2981 standardizes the question a marketplace asks — royaltyInfo(tokenId, salePrice) returning a receiver and amount — and nothing about the answer being paid. Marketplaces can ignore it, peer-to-peer transfers never see it, and a discovered royalty is not protocol-enforced revenue or a legal entitlement.

ERC-1155: many tokens, one contract, batched

Balances are a mapping of (account, token ID) → quantity inside one contract. IDs can be fungible (a currency), non-fungible (quantity-1 items) or semi-fungible; the interface does not declare which, so the application must.

balanceOfBatch and safeBatchTransferFrom amortize per-call overhead — one transaction moves a whole game inventory instead of forty. Batch arrays must have matching lengths and stay gas-bounded; a batch with a length mismatch or an oversized array is a real production failure mode.

Operator approval is all-token authority for that owner within the contract — broader than any single ERC-20 allowance — so interfaces must disclose its breadth.

Safe transfers update balances, emit TransferSingle or TransferBatch, then invoke onERC1155Received / onERC1155BatchReceived on contract recipients and require the correct selector. Same rule as ERC-721: the callback is an adversarial external call after state change; restore invariants before it, guard reentry, and treat forwarded data as unbounded input.

ERC-1155 was designed so event history alone reconstructs balances — including mints and burns via the zero address. An indexer built from canonical logs, with reorg rollback and duplicate handling, should reconcile with a live balanceOfBatch query; disagreement is an incident signal, not a reason to silently overwrite history.

ERC-165 and the lying contract

supportsInterface lets a contract report which interface IDs it implements, within a bounded gas budget. It is discovery, not proof: an adversarial contract can claim support and then behave however it likes, and a proxy upgrade can change reported support without changing the address. Use it to pick a code path, then still handle calls and return data defensively.

Choosing, and when not to tokenize

  • ERC-20 when units are interchangeable and you need the whole DeFi toolchain for free.
  • ERC-721 when individual identity and per-token provenance are the point.
  • ERC-1155 when one contract must track many asset types or you need batch economics.

Do not tokenize merely to add a market. Before shipping, document issuer obligations, upgrade and freeze powers, custody, privacy, metadata retention, user recovery, regulation and exit. The standard supplies the wire contract; the application owns trust and economics.

What changes at scale

  • Indexing: enumeration is optional in ERC-721; at collection sizes in the millions, on-chain enumeration is unworkable and event-derived indexes with reorg handling are mandatory.
  • Finality policy: crediting a deposit the moment a receipt appears loses money to reorgs; credit after a risk-based confirmation depth and reconcile against canonical state.
  • Upgrade risk: a proxy keeps its address while its behavior changes, so integrations monitor implementation, role, pause and metadata events where exposure is material — not just balances.
  • Idempotency: deposits keyed by (chain, contract, block, transaction, log index) survive replays and reorgs; anything keyed by symbol or name will collide.

Misconceptions interviewers probe

  • "ERC-20 compliance means the token is safe" — it means the ABI matches. Weak answer; strong answer separates interface conformance from implementation risk.
  • "decimals is part of the arithmetic" — display convention only.
  • "approve is a checkbox" — it is standing transfer authority; the overwrite race and infinite-approval blast radius are the follow-ups.
  • "safeTransferFrom means the trade is safe" — it means the receiver acknowledged receipt via the hook.
  • "royalties are enforced on-chain" — ERC-2981 is a lookup, payment is voluntary.
  • "the token ID identifies my NFT" — only within (chain, contract).

Likely follow-ups: how do you integrate a fee-on-transfer token into a vault (balance-diff accounting, or reject it); how does permit prevent replay (domain separator + nonce); how would you detect a token that lies about its interface (ERC-165, then verify actual behavior); what breaks in your indexer on a reorg.

Production checklist

Bind every operation to canonical token identity (chain, contract, token ID). Validate amounts and recipients. Constrain approvals and surface revocation. Make deposits and withdrawals idempotent. Handle receiver callbacks reentrancy-safely. Inspect canonical receipts and wait for policy finality. Reconcile balances, supply and events. Monitor role, pause, upgrade, mint, burn, allowance and metadata changes. Test: nonstandard return values, fee and rebase behavior, malicious receivers, wrong decimals, zero values, batch boundaries, proxy upgrades, reorgs and dependency outages.