Skip to content

Token and signing standards

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.

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.

A token is a contract whose database is address → balance. “Owning USDC” means a row in Circle’s contract.

  1. transfer(to, amount) moves the caller’s own tokens.
  2. approve(spender, amount) records an allowance: “this spender may pull up to N of my tokens”.
  3. 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 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.

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.

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 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.origin checks that were used to mean “the caller is an EOA with no code”.
  • 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.

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
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
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

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

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.