ERC-20, ERC-721, and ERC-1155 ABI JSON (Copy-Paste Reference)

The ERC-20 ABI in JSON, plus ERC-721, ERC-1155, and WETH9, each with the human-readable viem form, function selectors, and what's optional versus required.

evmquery team··14 min read
Share
ERC-20, ERC-721, ERC-1155, and WETH9 ABI JSON reference

The ERC-20 ABI is the same nine functions and two events on every compliant token, which means you shouldn’t have to retype it, paste it from a random gist, or ask an LLM to reconstruct it from memory. This page has the ERC-20 ABI as JSON, plus ERC-721, ERC-1155, and WETH9, each in both JSON and viem’s human-readable format, generated directly from viem 2.55.11’s own exports and checked against real deployed selectors.

Standard ABI key facts

  • ERC-20, ERC-721, and ERC-1155 all ship as ready-made exports in viem: erc20Abi, erc721Abi, and erc1155Abi. There’s no wethAbi export; WETH9 isn’t an ERC standard, it’s one specific deployed contract.
  • import { erc20Abi } from "viem" bundles the required core interface with the optional metadata extension (name, symbol, decimals) in one array, since almost every real ERC-20 implements all of it.
  • USDT’s transfer, approve, and transferFrom return nothing, not bool. Decoding them with a standard ERC-20 ABI that declares returns (bool) throws a decode error, verified live against Etherscan’s own ABI for USDT’s contract.
  • viem 2.55.11’s erc721Abi mislabels the Enumerable extension’s second function tokenByIndex when it should be tokenOfOwnerByIndex, same name as the other overload, wrong selector. See the ERC-721 section below before copying it as-is.

TL;DR

Import erc20Abi, erc721Abi, and erc1155Abi straight from viem instead of hand-typing them; there’s no bundled WETH9 ABI so that one’s below as JSON. Watch for two gotchas: USDT doesn’t return bool from transfer/approve, and viem’s shipped erc721Abi has one mislabeled function in the Enumerable extension.

ERC-20

The ERC-20 ABI JSON below is exactly viem’s erc20Abi export, generated by running JSON.stringify(erc20Abi) against the installed viem@2.55.11 package, not retyped. It combines the required core interface (transfer, transferFrom, approve, allowance, balanceOf, totalSupply, and the Transfer/Approval events) with the optional metadata extension (name, symbol, decimals). EIP-20 marks metadata as optional, but viem includes it because nearly every real-world token implements it.

[
{ "type": "event", "name": "Approval", "inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
] },
{ "type": "event", "name": "Transfer", "inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
] },
{ "type": "function", "name": "allowance", "stateMutability": "view",
"inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }],
"outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "approve", "stateMutability": "nonpayable",
"inputs": [{ "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" }],
"outputs": [{ "type": "bool" }] },
{ "type": "function", "name": "balanceOf", "stateMutability": "view",
"inputs": [{ "name": "account", "type": "address" }],
"outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "decimals", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "uint8" }] },
{ "type": "function", "name": "name", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "string" }] },
{ "type": "function", "name": "symbol", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "string" }] },
{ "type": "function", "name": "totalSupply", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "transfer", "stateMutability": "nonpayable",
"inputs": [{ "name": "recipient", "type": "address" }, { "name": "amount", "type": "uint256" }],
"outputs": [{ "type": "bool" }] },
{ "type": "function", "name": "transferFrom", "stateMutability": "nonpayable",
"inputs": [{ "name": "sender", "type": "address" }, { "name": "recipient", "type": "address" }, { "name": "amount", "type": "uint256" }],
"outputs": [{ "type": "bool" }] }
]

Human-readable, for viem’s parseAbi:

import { parseAbi } from "viem";
const erc20Abi = parseAbi([
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address account) view returns (uint256)",
"function transfer(address recipient, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function transferFrom(address sender, address recipient, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
"event Approval(address indexed owner, address indexed spender, uint256 value)",
]);

Or just import the built-in:

import { erc20Abi } from "viem";
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
const balance = await client.readContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
abi: erc20Abi,
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});

Ethers doesn’t ship a built-in ERC-20 ABI constant; pass the JSON above (or the human-readable strings) straight to new ethers.Interface(...) or new ethers.Contract(address, abi, provider).

ERC-721

The ERC-721 ABI below is viem’s erc721Abi export. It bundles the required core interface with the optional Metadata extension (name, symbol, tokenURI) and the optional Enumerable extension (totalSupply, tokenByIndex, and the owner-scoped index lookup). It does not include supportsInterface. That’s ERC-165, technically a separate standard NFTs are expected to implement, and it’s covered on its own further down.

viem 2.55.11's erc721Abi has a mislabeled function

The Enumerable extension defines two different functions: tokenByIndex(uint256) and tokenOfOwnerByIndex(address,uint256). In viem’s shipped erc721Abi, both entries are named tokenByIndex. The second one, which takes (address, uint256), should be named tokenOfOwnerByIndex. Because the ABI encodes function selectors from the name plus argument types, this isn’t cosmetic: calling it as shipped computes selector 0xcb60f1bf, but the real tokenOfOwnerByIndex(address,uint256) on a deployed contract is 0x2f745c59. Calling tokenOfOwnerByIndex through viem’s unmodified erc721Abi reverts against a real NFT contract. The JSON block below has this corrected; if you import { erc721Abi } from "viem" directly, rename the second tokenByIndex entry yourself before using it, or use the human-readable list below instead.

[
{ "type": "event", "name": "Approval", "inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "approved", "type": "address" },
{ "indexed": true, "name": "tokenId", "type": "uint256" }
] },
{ "type": "event", "name": "ApprovalForAll", "inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "operator", "type": "address" },
{ "indexed": false, "name": "approved", "type": "bool" }
] },
{ "type": "event", "name": "Transfer", "inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": true, "name": "tokenId", "type": "uint256" }
] },
{ "type": "function", "name": "approve", "stateMutability": "payable",
"inputs": [{ "name": "to", "type": "address" }, { "name": "tokenId", "type": "uint256" }], "outputs": [] },
{ "type": "function", "name": "balanceOf", "stateMutability": "view",
"inputs": [{ "name": "owner", "type": "address" }], "outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "getApproved", "stateMutability": "view",
"inputs": [{ "name": "tokenId", "type": "uint256" }], "outputs": [{ "type": "address" }] },
{ "type": "function", "name": "isApprovedForAll", "stateMutability": "view",
"inputs": [{ "name": "owner", "type": "address" }, { "name": "operator", "type": "address" }], "outputs": [{ "type": "bool" }] },
{ "type": "function", "name": "name", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "string" }] },
{ "type": "function", "name": "ownerOf", "stateMutability": "view",
"inputs": [{ "name": "tokenId", "type": "uint256" }], "outputs": [{ "name": "owner", "type": "address" }] },
{ "type": "function", "name": "safeTransferFrom", "stateMutability": "payable",
"inputs": [{ "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "tokenId", "type": "uint256" }], "outputs": [] },
{ "type": "function", "name": "safeTransferFrom", "stateMutability": "nonpayable",
"inputs": [{ "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "tokenId", "type": "uint256" }, { "name": "data", "type": "bytes" }], "outputs": [] },
{ "type": "function", "name": "setApprovalForAll", "stateMutability": "nonpayable",
"inputs": [{ "name": "operator", "type": "address" }, { "name": "approved", "type": "bool" }], "outputs": [] },
{ "type": "function", "name": "symbol", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "string" }] },
{ "type": "function", "name": "tokenByIndex", "stateMutability": "view",
"inputs": [{ "name": "index", "type": "uint256" }], "outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "tokenOfOwnerByIndex", "stateMutability": "view",
"inputs": [{ "name": "owner", "type": "address" }, { "name": "index", "type": "uint256" }], "outputs": [{ "name": "tokenId", "type": "uint256" }] },
{ "type": "function", "name": "tokenURI", "stateMutability": "view",
"inputs": [{ "name": "tokenId", "type": "uint256" }], "outputs": [{ "type": "string" }] },
{ "type": "function", "name": "totalSupply", "stateMutability": "view",
"inputs": [], "outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "transferFrom", "stateMutability": "payable",
"inputs": [{ "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "tokenId", "type": "uint256" }], "outputs": [] }
]

Human-readable, corrected:

import { parseAbi } from "viem";
const erc721Abi = parseAbi([
"function balanceOf(address owner) view returns (uint256)",
"function ownerOf(uint256 tokenId) view returns (address)",
"function safeTransferFrom(address from, address to, uint256 tokenId, bytes data)",
"function safeTransferFrom(address from, address to, uint256 tokenId)",
"function transferFrom(address from, address to, uint256 tokenId)",
"function approve(address to, uint256 tokenId)",
"function setApprovalForAll(address operator, bool approved)",
"function getApproved(uint256 tokenId) view returns (address)",
"function isApprovedForAll(address owner, address operator) view returns (bool)",
// Metadata extension (optional)
"function name() view returns (string)",
"function symbol() view returns (string)",
"function tokenURI(uint256 tokenId) view returns (string)",
// Enumerable extension (optional)
"function totalSupply() view returns (uint256)",
"function tokenByIndex(uint256 index) view returns (uint256)",
"function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)",
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
"event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)",
"event ApprovalForAll(address indexed owner, address indexed operator, bool approved)",
]);

import { erc721Abi } from "viem" still works fine for everything except the mislabeled Enumerable overload, which most integrations never call anyway.

ERC-1155

The ERC-1155 ABI below is viem’s erc1155Abi export, sourced from OpenZeppelin’s reference implementation rather than the bare EIP-1155 interface, so it includes the required core (balanceOf, balanceOfBatch, setApprovalForAll, isApprovedForAll, safeTransferFrom, safeBatchTransferFrom), the optional MetadataURI extension (uri), supportsInterface (ERC-165), and OpenZeppelin’s custom revert errors (ERC1155InsufficientBalance, ERC1155InvalidApprover, and similar). The errors aren’t part of the EIP itself, so a hand-rolled ERC-1155 contract won’t necessarily revert with these, but they don’t hurt to include if you’re decoding OpenZeppelin-based tokens.

[
{ "type": "function", "name": "balanceOf", "stateMutability": "view",
"inputs": [{ "name": "account", "type": "address" }, { "name": "id", "type": "uint256" }],
"outputs": [{ "type": "uint256" }] },
{ "type": "function", "name": "balanceOfBatch", "stateMutability": "view",
"inputs": [{ "name": "accounts", "type": "address[]" }, { "name": "ids", "type": "uint256[]" }],
"outputs": [{ "type": "uint256[]" }] },
{ "type": "function", "name": "isApprovedForAll", "stateMutability": "view",
"inputs": [{ "name": "account", "type": "address" }, { "name": "operator", "type": "address" }],
"outputs": [{ "type": "bool" }] },
{ "type": "function", "name": "safeBatchTransferFrom", "stateMutability": "nonpayable",
"inputs": [
{ "name": "from", "type": "address" }, { "name": "to", "type": "address" },
{ "name": "ids", "type": "uint256[]" }, { "name": "values", "type": "uint256[]" }, { "name": "data", "type": "bytes" }
], "outputs": [] },
{ "type": "function", "name": "safeTransferFrom", "stateMutability": "nonpayable",
"inputs": [
{ "name": "from", "type": "address" }, { "name": "to", "type": "address" },
{ "name": "id", "type": "uint256" }, { "name": "value", "type": "uint256" }, { "name": "data", "type": "bytes" }
], "outputs": [] },
{ "type": "function", "name": "setApprovalForAll", "stateMutability": "nonpayable",
"inputs": [{ "name": "operator", "type": "address" }, { "name": "approved", "type": "bool" }], "outputs": [] },
{ "type": "function", "name": "supportsInterface", "stateMutability": "view",
"inputs": [{ "name": "interfaceId", "type": "bytes4" }], "outputs": [{ "type": "bool" }] },
{ "type": "function", "name": "uri", "stateMutability": "view",
"inputs": [{ "name": "id", "type": "uint256" }], "outputs": [{ "type": "string" }] },
{ "type": "event", "name": "ApprovalForAll", "inputs": [
{ "indexed": true, "name": "account", "type": "address" },
{ "indexed": true, "name": "operator", "type": "address" },
{ "indexed": false, "name": "approved", "type": "bool" }
] },
{ "type": "event", "name": "TransferBatch", "inputs": [
{ "indexed": true, "name": "operator", "type": "address" },
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "ids", "type": "uint256[]" },
{ "indexed": false, "name": "values", "type": "uint256[]" }
] },
{ "type": "event", "name": "TransferSingle", "inputs": [
{ "indexed": true, "name": "operator", "type": "address" },
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "id", "type": "uint256" },
{ "indexed": false, "name": "value", "type": "uint256" }
] },
{ "type": "event", "name": "URI", "inputs": [
{ "indexed": false, "name": "value", "type": "string" },
{ "indexed": true, "name": "id", "type": "uint256" }
] }
]

Human-readable:

import { parseAbi } from "viem";
const erc1155Abi = parseAbi([
"function balanceOf(address account, uint256 id) view returns (uint256)",
"function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])",
"function setApprovalForAll(address operator, bool approved)",
"function isApprovedForAll(address account, address operator) view returns (bool)",
"function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes data)",
"function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] values, bytes data)",
// MetadataURI extension (optional)
"function uri(uint256 id) view returns (string)",
// ERC-165
"function supportsInterface(bytes4 interfaceId) view returns (bool)",
"event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)",
"event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)",
"event ApprovalForAll(address indexed account, address indexed operator, bool approved)",
"event URI(string value, uint256 indexed id)",
]);

import { erc1155Abi } from "viem" gives you this exact array, errors included.

WETH9

WETH isn’t an ERC standard, so neither viem nor ethers ships a built-in constant for it. There’s nothing to import. The WETH ABI below is the canonical WETH9 contract deployed at 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 on Ethereum, hand-transcribed from its verified source (the same contract every L2’s canonical WETH mirrors). It’s an ERC-20 with two extra functions: deposit() (wrap ETH into WETH 1:1) and withdraw(uint256) (unwrap back to ETH), plus Deposit and Withdrawal events alongside the standard Transfer and Approval.

[
{ "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "stateMutability": "view", "type": "function" },
{ "constant": false, "inputs": [{ "name": "guy", "type": "address" }, { "name": "wad", "type": "uint256" }], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" },
{ "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" },
{ "constant": false, "inputs": [{ "name": "src", "type": "address" }, { "name": "dst", "type": "address" }, { "name": "wad", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" },
{ "constant": false, "inputs": [{ "name": "wad", "type": "uint256" }], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" },
{ "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" },
{ "constant": true, "inputs": [{ "name": "", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" },
{ "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "stateMutability": "view", "type": "function" },
{ "constant": false, "inputs": [{ "name": "dst", "type": "address" }, { "name": "wad", "type": "uint256" }], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" },
{ "constant": false, "inputs": [], "name": "deposit", "outputs": [], "stateMutability": "payable", "type": "function" },
{ "constant": true, "inputs": [{ "name": "", "type": "address" }, { "name": "", "type": "address" }], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" },
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{ "anonymous": false, "inputs": [{ "indexed": true, "name": "src", "type": "address" }, { "indexed": true, "name": "guy", "type": "address" }, { "indexed": false, "name": "wad", "type": "uint256" }], "name": "Approval", "type": "event" },
{ "anonymous": false, "inputs": [{ "indexed": true, "name": "src", "type": "address" }, { "indexed": true, "name": "dst", "type": "address" }, { "indexed": false, "name": "wad", "type": "uint256" }], "name": "Transfer", "type": "event" },
{ "anonymous": false, "inputs": [{ "indexed": true, "name": "dst", "type": "address" }, { "indexed": false, "name": "wad", "type": "uint256" }], "name": "Deposit", "type": "event" },
{ "anonymous": false, "inputs": [{ "indexed": true, "name": "src", "type": "address" }, { "indexed": false, "name": "wad", "type": "uint256" }], "name": "Withdrawal", "type": "event" }
]

Human-readable:

import { parseAbi } from "viem";
const weth9Abi = parseAbi([
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function allowance(address, address) view returns (uint256)",
"function approve(address guy, uint256 wad) returns (bool)",
"function transfer(address dst, uint256 wad) returns (bool)",
"function transferFrom(address src, address dst, uint256 wad) returns (bool)",
"function deposit() payable",
"function withdraw(uint256 wad)",
"event Approval(address indexed src, address indexed guy, uint256 wad)",
"event Transfer(address indexed src, address indexed dst, uint256 wad)",
"event Deposit(address indexed dst, uint256 wad)",
"event Withdrawal(address indexed src, uint256 wad)",
]);

selectors for deposit() and withdraw(uint256) are 0xd0e30db0 and 0x2e1a7d4d respectively, both confirmed against viem’s toFunctionSelector and the actual deployed WETH9 bytecode’s dispatch table.

Non-standard tokens: USDT’s missing return values

USDT is ERC-20 in spirit but not in exact interface. Its transfer, approve, and transferFrom return nothing at all, not even a bare bool that always resolves to true. This is confirmed directly from Etherscan’s own ABI JSON for the live USDT contract (0xdAC17F958D2ee523a2206206994597C13D831ec7), where all three entries carry "outputs":[].

If you call one of these functions using the standard erc20Abi above, which declares returns (bool), decoding the result throws:

AbiDecodingZeroDataError: Data was empty when decoding function output

viem’s readContract/writeContract and ethers’ Contract methods will surface this as a decode failure, not a revert. The call itself succeeds on-chain. The fix is to give USDT its own ABI variant with outputs: [] on those three functions, or use a permissive interface library that tolerates missing return data. This is also why the original OpenZeppelin SafeERC20 wrapper exists: it checks returndata.length == 0 || abi.decode(returndata, (bool)) rather than assuming every token returns a bool.

Getting a specific token’s own ABI

The standards above cover the interface every compliant token implements, but real contracts often add extra functions: minting, pausing, blacklists, rebasing logic, permit signatures. For a verified contract, the complete ABI (write functions and events included) sits on its Etherscan or Sourcify page. If you only need to read from it, paste the address into Get Contract ABI: it returns the contract’s view and pure functions as JSON, resolves proxies to their implementation, and works on unverified contracts too.

To read live values instead of just the interface shape, the ERC-20 Inspector, ERC-721 Inspector, and ERC-1155 Inspector tools on this site call these exact standards against any address you give them, no code required.

For the concepts behind an ABI itself (what it is, how it maps to calldata, where it comes from), see Smart Contract ABI Explained.

If you’re writing this kind of decoding logic for your own agent or automation pipeline, pulling the ABI once and caching it beats re-fetching or re-typing it on every run.

Next steps

Share

Skip the copy-paste and query by address

evmquery resolves any contract's real ABI, standard or not, and lets you call it in one request. No credit card for the free tier.