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.
./install.sh token-standardsname: "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 }
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.
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.
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.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)"]))
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 10decimals (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.Transfer(from,to,amount). No receiver check — tokens sent to a contract with no way to move them are typically stuck.Transfer(from,to,tokenId); safeTransferFrom requires the receiver contract to implement onERC721Received, otherwise the tx reverts.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.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.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.For any token contract the agent analyzes, the final result must contain:
supportsInterface calls returned true/false, plus the confirming second signal for ERC-20).supportsInterface and back it with a second signal (event shape or a working read call).supportsInterface says nothing about ERC-20: it predates ERC-165 and usually doesn't implement it.bytes4 arguments left-aligned; a right-aligned interface ID queries garbage and usually reverts.eth_getCode); otherwise write "verify on the explorer". (Example addresses above were verified on Base 2026-09-22.)reading_block_explorers, and trading or DeFi strategy doesn't belong here at all.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.