ERC-721

ERC-721 is the standard interface for non-fungible tokens on Ethereum. Where ERC-20 tracks a balance per address, ERC-721 tracks an owner per token ID — so a transfer names a specific token rather than an amount.

The interface#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

// IERC165 supplies supportsInterface(bytes4) -> bool.
interface IERC721 is IERC165 {
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);

    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;
    function setApprovalForAll(address operator, bool approved) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    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);
}

The is IERC165 is not decoration. supportsInterface(0x80ac58cd) returning true is how a marketplace, a receiver contract, or a router decides it is looking at an ERC-721 at all, and the spec makes it mandatory. Note also that the declarations above follow OpenZeppelin’s IERC721; the EIP itself marks the transfer and approve functions payable. Payability is not part of the selector, so this makes no difference at the call site — but it will look like a typo if you diff against the spec.

Two things in that list differ from ERC-20 in ways that bite:

  • safeTransferFrom versus transferFrom. When to is a contract, the safe variant calls onERC721Received on it and requires the magic return value IERC721Receiver.onERC721Received.selector (0x150b7a02) — a recipient that implements the function but returns anything else still reverts. When to is an ordinary account neither variant checks anything. The bare variant never checks, which is how tokens end up stranded at contracts that cannot move them.
  • setApprovalForAll is collection-wide. It authorises an operator for every token the caller owns in that contract, including ones minted later, until explicitly revoked.

Metadata lives behind tokenURI(uint256), defined in the optional IERC721Metadata extension rather than the core interface.

ERC-1155 holds many token IDs in one contract, each with a supply, so it covers fungible and non-fungible cases together. ERC-2981 adds a royalty query — a number a marketplace may read and may ignore, since like every ERC it binds nobody.