Token and signing standards
In one line
Section titled “In one line”An ERC is a shared interface, like an API contract that every implementation agrees to expose. Tokens, vaults, signatures, wallets and proxies each have one, and each interface has a known way to hurt you.
Why it exists
Section titled “Why it exists”Without a common interface, every exchange and wallet would need custom code for every token. ERC-20 (proposed in November 2015) fixed that for fungible tokens: any contract that exposes the same handful of functions works everywhere. The same idea was then repeated for each new kind of thing.
| Year proposed | Standard | Gap it closed |
|---|---|---|
| 2015 | ERC-20 | One interface for fungible tokens |
| 2017 | EIP-712 | Signing structured data a human can read, bound to one contract and chain |
| 2017 | ERC-777 | Tokens with send and receive hooks |
| 2018 | ERC-721, ERC-1155 | Unique tokens (NFTs), then many token types in one contract |
| 2018 | ERC-1271 | Letting a contract wallet “sign” |
| 2019 | ERC-1967, ERC-1822 | Agreed storage slots for proxies, and the UUPS upgrade pattern |
| 2020 | ERC-2612, ERC-2535 | Approvals by signature, and diamond proxies |
| 2021 | ERC-4626, ERC-4337 | A common vault interface, and smart accounts without a protocol change |
| 2023 | ERC-7201 | Namespaced storage for upgradeable contracts |
| 2024 | EIP-7702 | Letting an ordinary wallet run contract code (live since Pectra, May 2025) |
The numbers are ticket numbers, not versions. See Basics.
How it works
Section titled “How it works”ERC-20: the balance table
Section titled “ERC-20: the balance table”A token is a contract whose database is address → balance. “Owning USDC” means a row in Circle’s contract.
transfer(to, amount)moves the caller’s own tokens.approve(spender, amount)records an allowance: “this spender may pull up to N of my tokens”.transferFrom(from, to, amount)lets the spender pull tokens within that allowance.
Steps 2 and 3 are how every DeFi protocol takes a deposit. They are also the standing risk: allowances are often unlimited and forgotten, and a contract that holds many users’ allowances is a target (see arbitrary external calls in Contract attacks).
ERC-721 and ERC-1155: tokens with hooks
Section titled “ERC-721 and ERC-1155: tokens with hooks”ERC-721 is the NFT interface: each token ID has one owner. ERC-1155 keeps many token types, fungible or not, in one contract. In both, the safeTransfer functions call a hook on the receiving contract. A hook is an external call into code you do not control, so it is a reentrancy entry point. ERC-777 has the same property through its send and receive hooks.
ERC-4626: vault shares
Section titled “ERC-4626: vault shares”A tokenised vault. You deposit an asset and get shares back, like units of a fund. The share price is total assets divided by total shares. Because that is integer division, an empty or nearly empty vault can be manipulated: the first-depositor inflation attack is described in Contract attacks.
EIP-712: signatures that say what they are for
Section titled “EIP-712: signatures that say what they are for”Typed structured signing. The signed hash includes a domain separator that binds name, version, chainId and the verifying contract. That prevents replay across chains and across contracts. The wallet can also show the fields to the user instead of an opaque hash.
ERC-2612 and Permit2: approvals by signature
Section titled “ERC-2612 and Permit2: approvals by signature”permit lets a user grant an allowance with an off-chain signature instead of a transaction, so the user pays no gas for the approval. Permit2 is a Uniswap contract that offers the same for any ERC-20. Phishing favourite: one signature drains a wallet, and signing a message feels harmless compared with sending a transaction.
ERC-1271: signatures from contracts
Section titled “ERC-1271: signatures from contracts”A contract has no private key. ERC-1271 defines isValidSignature, which a contract wallet (Safe, smart accounts) implements so that other contracts can ask it “do you approve this hash?”.
ERC-4337 and EIP-7702: smart accounts
Section titled “ERC-4337 and EIP-7702: smart accounts”- ERC-4337 is account abstraction without a protocol change. Users sign UserOperations, and bundlers submit them on-chain for a smart account.
- EIP-7702 lets an EOA delegate to contract code by signing an authorisation. These authorisations are a new phishing target, and they break
msg.sender == tx.originchecks that were used to mean “the caller is an EOA with no code”.
ERC-1967 and ERC-2535: proxy plumbing
Section titled “ERC-1967 and ERC-2535: proxy plumbing”- ERC-1967 fixes the storage slots where a proxy keeps its implementation, admin and beacon addresses, so tools and explorers can find them.
- ERC-2535 is the diamond: one proxy routes each selector to a facet, and upgrades go through
diamondCut.
Both are covered in Proxies and upgrades.
Compare
Section titled “Compare”Token interfaces
Section titled “Token interfaces”| Standard | What one contract holds | Unit | Receiver hook | Typical use | Trap |
|---|---|---|---|---|---|
| ERC-20 | One fungible token | Balance per address | No | Stablecoins, governance tokens | Unlimited approvals; non-standard behaviour (table below) |
| ERC-777 | One fungible token | Balance per address | Yes | Some older tokens | Hooks make every transfer a reentrancy entry point |
| ERC-721 | Unique tokens | One owner per token ID | Yes, on safeTransferFrom |
NFTs, position receipts | Reentrancy through the hook |
| ERC-1155 | Many token types | Balance per address per ID | Yes, on every safe transfer | Games, multi-asset contracts | Reentrancy through the hook |
| ERC-4626 | Shares of a pool of one ERC-20 | Share balance | No | Vaults, lending pools | First-depositor inflation, rounding |
ERC-20 tokens that do not behave
Section titled “ERC-20 tokens that do not behave”| Behaviour | Example | What breaks | Fix |
|---|---|---|---|
Returns no bool from transfer |
USDT | A call that expects a bool reverts or misreads | OpenZeppelin SafeERC20 |
| Fee on transfer | Some tokens take a cut | Received amount is less than the amount sent | Measure the balance before and after |
| Rebasing | Balances change without transfers | Cached balances go stale | Use a wrapped, non-rebasing version |
| Blocklist | USDC and USDT issuers can freeze addresses | Transfers to or from a blocked address revert | Pull payments; do not let one transfer block a batch |
| 6 decimals | USDC, USDT | Maths that assumes 18 decimals is off by 10^12 | Read decimals(); scale explicitly |
Ways to authorise spending
Section titled “Ways to authorise spending”| Mechanism | How the user grants it | Costs the user gas | Risk |
|---|---|---|---|
approve (ERC-20) |
A transaction | Yes | Unlimited allowances that are never revoked |
permit (ERC-2612) |
An EIP-712 signature | No | A phished signature is enough |
| Permit2 | One approval to Permit2, then signatures | Once | One signature can cover many tokens |
| EIP-7702 authorisation | A signed authorisation to run a contract’s code | No | The wallet now runs code the attacker chose |
| ERC-1271 | The contract wallet’s own logic | Depends on the wallet | Bugs in the wallet’s validation |
What breaks
Section titled “What breaks”The commands use cast from Foundry and assume --rpc-url or ETH_RPC_URL is set.
| Symptom | Cause | Check |
|---|---|---|
| A wallet is drained long after using an app | An old unlimited allowance to a contract that was later exploited | cast call $TOKEN "allowance(address,address)(uint256)" $OWNER $SPENDER |
| Deposits of USDT revert, other tokens work | The code expects transfer to return a bool |
Use SafeERC20; test against the real token on a fork |
| Amounts are wrong by a factor of 10^12 | 6-decimal token treated as 18 | cast call $TOKEN "decimals()(uint8)" |
| Accounting drifts from real balances | Fee-on-transfer or rebasing token | Compare balanceOf before and after the transfer |
| A signature valid on one chain is replayed on another | No chainId or contract address in the signed data | Check that the EIP-712 domain includes chainId and verifyingContract |
| A user signed “a message” and lost tokens | The message was a permit or Permit2 approval |
Decode the typed data before signing; wallets show the spender and amount |
| “Only EOAs” check is bypassed | EIP-7702 delegation | cast code $ADDR returns 0xef0100 followed by the delegate address |
| Vault shares round to zero for a new depositor | ERC-4626 inflation attack on an empty vault | Compare totalAssets() with totalSupply(); see Contract attacks |
Keywords
Section titled “Keywords”ERC-20, transfer, approve, transferFrom, allowance, unlimited approval, SafeERC20, fee-on-transfer, rebasing token, blocklist, decimals, ERC-721, NFT, ERC-1155, safeTransferFrom, onERC721Received, ERC-777, token hooks, ERC-4626, tokenised vault, shares, totalAssets, EIP-712, typed data, domain separator, chainId, verifyingContract, ERC-2612, permit, Permit2, gasless approval, ERC-1271, isValidSignature, contract wallet, Safe, ERC-4337, account abstraction, UserOperation, bundler, EIP-7702, set code, authorisation, ERC-1967, ERC-2535, diamond, diamondCut, ERC-1822, UUPS, ERC-7201.
Sources
Section titled “Sources”- ERC-20: Token Standard
- ERC-721: Non-Fungible Token Standard
- ERC-777: Token Standard
- ERC-1155: Multi Token Standard
- ERC-4626: Tokenized Vaults
- EIP-712: Typed structured data hashing and signing
- ERC-2612: Permit Extension for EIP-20 Signed Approvals
- ERC-1271: Standard Signature Validation Method for Contracts
- ERC-4337: Account Abstraction Using Alt Mempool
- EIP-7702: Set Code for EOAs
- ERC-1967: Proxy Storage Slots
- ERC-2535: Diamonds, Multi-Facet Proxy
- OpenZeppelin Contracts: ERC-20 API, including SafeERC20
- OpenZeppelin Contracts: ERC-4626 and the inflation attack
- Uniswap: Permit2 overview