---
name: "token_standards"
description: "Field guide to ERC-20 / ERC-721 / ERC-1155 interfaces, events, and transfer semantics; use whenever an agent must identify which token standard a contract implements or read and interpret token state on-chain."
metadata: { "includeInPrompt": true }
---

# Token Standards

## Purpose

Identify which token standard (ERC-20, ERC-721, ERC-1155) a contract implements and read its state correctly — via interface probes, not guesses from names or symbols.

## Workflow

1. **Probe with ERC-165 first.** Call `supportsInterface(bytes4)` with `0x80ac58cd` (ERC-721) and `0xd9b67a26` (ERC-1155). Save this as `probe.sh`-style usage and run it with curl (executed 2026-09-22 against Base mainnet):
   ```bash
   # usage: probe <contract> <interfaceId> <rpc>
   probe() {
     DATA="0x01ffc9a7${2}00000000000000000000000000000000000000000000000000000000"
     curl -s -X POST -H "Content-Type: application/json" \
       --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_call\",\"params\":[{\"to\":\"$1\",\"data\":\"$DATA\"},\"latest\"]}" "$3"
   }
   probe 0x03c4738Ee98aE44591e1A4A4F3CaB6641d95DD9a 80ac58cd https://base-rpc.publicnode.com
   # => result 0x...01 = true. (Basenames on Base, verified 2026-09-22: true for 721, true for 0x01ffc9a7 itself.)
   ```
   A result ending in `...01` is true, `...00` is false. **Encoding trap:** a `bytes4` argument is LEFT-aligned in its 32-byte word (value first, zeros after) — right-aligning it silently queries a different, usually-reverting interface.
2. **Handle the ERC-20 case separately.** ERC-20 predates ERC-165, so a contract with no `supportsInterface` can still be ERC-20. Confirm with a second signal: a `Transfer(address,address,uint256)` event in its logs, or a successful `balanceOf(address)` call returning one word.
3. **Derive (don't memorize) any other interface ID.** An ERC-165 interface ID is the XOR of the 4-byte keccak selectors of the interface's functions. Save this as `iface.py` and run it locally (executed 2026-09-22; outputs `0x80ac58cd 0xd9b67a26`):
   ```python
   def keccak256(b):
       RC=[0x0000000000000001,0x0000000000008082,0x800000000000808a,0x8000000080008000,
           0x000000000000808b,0x0000000080000001,0x8000000080008081,0x8000000000008009,
           0x000000000000008a,0x0000000000000088,0x0000000080008009,0x000000008000000a,
           0x000000008000808b,0x800000000000008b,0x8000000000008089,0x8000000000008003,
           0x8000000000008002,0x8000000000000080,0x000000000000800a,0x800000008000000a,
           0x8000000080008081,0x8000000000008080,0x0000000080000001,0x8000000080008008]
       RT=[[0,36,3,41,18],[1,44,10,45,2],[62,6,43,15,61],[28,55,25,21,56],[27,20,39,8,14]]
       st=[0]*25; d=b+b'\x01'
       while (len(d)*8)%1088!=1080: d+=b'\x00'
       d+=b'\x80'
       for o in range(0,len(d),136):
           blk=d[o:o+136]
           for i in range(17): st[i]^=int.from_bytes(blk[i*8:i*8+8],'little')
           for r in range(24):
               C=[st[x]^st[x+5]^st[x+10]^st[x+15]^st[x+20] for x in range(5)]
               D=[C[(x-1)%5]^(((C[(x+1)%5]<<1)&0xFFFFFFFFFFFFFFFF)|(C[(x+1)%5]>>63)) for x in range(5)]
               A=[st[x+y*5]^D[x] for y in range(5) for x in range(5)]
               B=[0]*25
               for x in range(5):
                   for y in range(5):
                       v=A[x+y*5]; n=RT[x][y]
                       B[y+((2*x+3*y)%5)*5]=(((v<<n)&0xFFFFFFFFFFFFFFFF)|(v>>(64-n)))
               for x in range(5):
                   for y in range(5): st[x+y*5]=B[x+y*5]^((~B[(x+1)%5+y*5])&B[(x+2)%5+y*5])
               st[0]^=RC[r]
       return b''.join(s.to_bytes(8,'little') for s in st)[:32]
   def iface(sigs):
       x=0
       for s in sigs: x^=int(keccak256(s.encode()).hex()[:8],16)
       return '0x%08x'%x
   print(iface(["balanceOf(address)","ownerOf(uint256)","safeTransferFrom(address,address,uint256,bytes)",
    "safeTransferFrom(address,address,uint256)","transferFrom(address,address,uint256)",
    "approve(address,uint256)","setApprovalForAll(address,bool)","getApproved(uint256)",
    "isApprovedForAll(address,address)"]))
   print(iface(["balanceOf(address,uint256)","balanceOfBatch(address[],uint256[])",
    "setApprovalForAll(address,bool)","isApprovedForAll(address,address)",
    "safeTransferFrom(address,address,uint256,uint256,bytes)",
    "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)"]))
   ```
4. **Read state with the right overload.** `balanceOf(address)` is ERC-20/721; `balanceOf(address,uint256)` is ERC-1155 (an extra token id). ERC-721 ownership is per token: `ownerOf(tokenId)` (reverts for nonexistent ids). ERC-20 amounts are in base units — divide by `10**decimals` (usually 18; USDC uses 6 — always read `decimals()` first). `totalSupply` exists on 20 and most 721s; 1155 has `totalSupply(id)` only as an optional extension — don't assume it's there.
5. **Interpret transfers by standard.**
   - ERC-20: `Transfer(from,to,amount)`. No receiver check — tokens sent to a contract with no way to move them are typically stuck.
   - ERC-721: `Transfer(from,to,tokenId)`; `safeTransferFrom` requires the receiver contract to implement `onERC721Received`, otherwise the tx reverts.
   - ERC-1155: `TransferSingle(operator,from,to,id,value)` for one id, `TransferBatch(operator,from,to,ids[],values[])` for batches — `ids` and `values` arrays must be the same length. `safeTransferFrom`/`safeBatchTransferFrom` require `onERC1155Received`/`onERC1155BatchReceived` on contract receivers.
6. **Apply the approval gotchas before any move.**
   - ERC-20 approvals are per spender + amount: `approve`/`allowance`. The classic pattern is approve-to-zero before changing a nonzero allowance; some contracts skip the allowance check entirely, so verify against the contract's own code, not assumptions.
   - ERC-721/1155 have no per-amount approvals. `approve(spender,tokenId)` (721 only) grants one token; `setApprovalForAll(operator,true)` grants an operator control of ALL of your tokens of that standard on that contract. Treat operator approval as high-trust.

## Output Contract

For any token contract the agent analyzes, the final result must contain:
1. **Standard verdict** — which of ERC-20 / ERC-721 / ERC-1155 the contract implements, with the probe evidence (which `supportsInterface` calls returned true/false, plus the confirming second signal for ERC-20).
2. **Read reference** — the applicable read functions and event signatures for that standard (the exact forms from the Workflow, not paraphrases).
3. **Gotcha checklist applied** — receiver-check behavior, approval model (allowance vs operator), decimals handling, and any ambiguity (e.g. contract supports multiple IDs) — with a note on how each was resolved or what still needs the explorer.

## Operating Rules

1. Never declare a standard from a name, symbol, or explorer label alone — probe `supportsInterface` and back it with a second signal (event shape or a working read call).
2. Absence of `supportsInterface` says nothing about ERC-20: it predates ERC-165 and usually doesn't implement it.
3. Encode `bytes4` arguments left-aligned; a right-aligned interface ID queries garbage and usually reverts.
4. Quote a contract address only if you re-verified it the same day (explorer or `eth_getCode`); otherwise write "verify on the explorer". (Example addresses above were verified on Base 2026-09-22.)
5. When a claim can't be checked on-chain from here, soften it ("typically", "confirm on the explorer yourself") instead of stating it.
6. This skill covers interfaces and semantics only — how to navigate an explorer UI belongs to `reading_block_explorers`, and trading or DeFi strategy doesn't belong here at all.
