ERC-721 Non-fungible token (NFT) standard (Non-Fungible Token Standard)
Final · NFTs & multi-token · created 2018-01-24
Defines a common way to track and transfer distinguishable assets by owner inside a smart contract. It lets wallets and brokerage and auction apps handle any NFT through the same functions.
Why it is needed
Many assets in the world, such as houses, paintings and game items, are each different from one another. The existing ERC-20 token standard assumes all tokens are identical and only counts quantities, so it cannot record ownership of such assets one by one. As a result, each project built its contract in its own way. The source cites CryptoPunks as an example: its auction feature is built into the contract and punks is baked into the function names, which makes it hard to generalize. In this situation, wallets and brokerage and auction apps must integrate with each contract separately, and users have difficulty knowing where it is safe to send an asset. This standard unifies the function and event names for counting distinguishable assets, finding their owners, transferring them, and giving others management rights, so that any app can work with any NFT right away.
What to implement
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);: Emitted when NFT ownership changes. On creation _from is 0, and on destruction _to is 0.event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);: Emitted when the approved address of a single NFT is changed or reaffirmed. The zero address means no approval.event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);: Emitted when an owner enables or disables an operator.function balanceOf(address _owner) external view returns (uint256);: Returns the number of NFTs owned by _owner. Throws if asked about the zero address.function ownerOf(uint256 _tokenId) external view returns (address);: Returns the owner address of an NFT. Throws if the NFT is invalid.function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;: Transfers the NFT, and if the recipient is a contract, calls onERC721Received and reverts if the return value is not the magic value. data is passed to the recipient unchanged.function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;: Same as the function above, but with data left empty.function transferFrom(address _from, address _to, uint256 _tokenId) external payable;: Transfers the NFT without a receipt check. The caller is responsible for making sure _to can receive it, otherwise the NFT may be lost permanently.function approve(address _approved, uint256 _tokenId) external payable;: Changes or reaffirms the approved address of a single NFT. Only the owner or an operator can call it.function setApprovalForAll(address _operator, bool _approved) external;: Enables or disables an operator to manage all of the caller's NFTs.function getApproved(uint256 _tokenId) external view returns (address);: Returns the approved address of a single NFT. It is the zero address if there is none.function isApprovedForAll(address _owner, address _operator) external view returns (bool);: Tells whether _operator is an operator of _owner.function supportsInterface(bytes4 interfaceID) external view returns (bool);: Reports whether the interface is implemented, using the ERC-165 identifier. It runs with less than 30,000 gas.function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);: Implemented by a contract that receives NFTs. It must return the magic value for the transfer to complete, and throwing rejects it.function name() external view returns (string _name);: Returns the name of this contract's NFT collection.function symbol() external view returns (string _symbol);: Returns the abbreviated name of the NFT.function tokenURI(uint256 _tokenId) external view returns (string);: Returns the unique URI for each asset. Throws if the NFT is invalid.function totalSupply() external view returns (uint256);: Returns the number of valid NFTs tracked by this contract.function tokenByIndex(uint256 _index) external view returns (uint256);: Returns the token number of the NFT at index _index. The sort order is not specified.function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);: Returns the token number of the _index-th NFT among those owned by _owner.
In depth
Required interface and ERC-165 identifier
A contract that follows ERC-721 must implement both the ERC721 interface and the ERC165 interface. The ERC-165 identifier of the ERC721 interface is 0x80ac58cd. ERC165's supportsInterface(bytes4 interfaceID) returns true if the contract implements that interface and interfaceID is not 0xffffffff, and false otherwise, and it runs with less than 30,000 gas. Each optional extension also has an identifier. The receiver interface ERC721TokenReceiver is 0x150b7a02, the metadata extension ERC721Metadata is 0x5b5e139f, and the enumeration extension ERC721Enumerable is 0x780e9d63. The source explains that ERC-165 was chosen so that the interfaces a contract supports can be known from outside, and notes that if a global interface registry appears in the future, the implementation of extensions could be delegated to a separate contract.
The token identifier is a uint256 black box
Every NFT is identified within a contract by a unique uint256 ID, and this number must not change for as long as the contract exists. The pair (contract address, uint256 tokenId) becomes a globally unique identifier pointing to a specific asset on the Ethereum chain. Some contracts may find it convenient to assign IDs starting at 0 and incrementing by 1, but callers must not assume any particular rule for IDs and must treat them as a black box. An NFT can also be destroyed and become invalid. uint256 was chosen because UUIDs and sha3 hashes can be converted directly to uint256, which suits a wide range of applications. An NFT assigned to the zero address is considered invalid, and calling balanceOf with the zero address or asking ownerOf about an invalid token throws.
Three transfer functions and their throw conditions
There are three transfer functions: safeTransferFrom with a bytes data argument, safeTransferFrom without it (equivalent to passing empty data), and transferFrom, which has no safety check. All three must throw if msg.sender is not the current owner, an approved address or an authorized operator, if _from is not the current owner, if _to is the zero address, or if _tokenId is not a valid NFT. After the transfer completes, safeTransferFrom calls onERC721Received if the code size of _to is greater than 0, and throws if the return value is not bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")). transferFrom performs no such check, so the caller must confirm that _to can receive NFTs themselves, otherwise the NFT may be lost permanently. The specification only defines the conditions under which a transfer must throw, and an implementation may throw in other situations as well. The original text gives these examples: blocking transfers while the contract is paused, blocking receipt by particular addresses, forbidding unsafe transfers, charging fees on both sides, and read-only registries. Minting and burning are not covered by the specification and are left to the implementation.
Approval scheme: per-token approval and operators
approve(_approved, _tokenId) changes or reaffirms the approved address for a single NFT. The zero address means there is no approved address, and the call throws unless msg.sender is the current owner or an authorized operator of the owner. setApprovalForAll(_operator, _approved) enables or disables a third party (the operator) to manage all of msg.sender's assets, and emits the ApprovalForAll event. A contract must allow multiple operators per owner. getApproved throws for an invalid _tokenId, and isApprovedForAll returns a bool indicating whether the address is an operator. An authorized operator can also set the approved address of an NFT. When a transfer happens, the approved address of that NFT is cleared automatically. There is no quantity allowance like ERC-20's allowance. Because an NFT has a quantity of either 0 or 1, the original text explains that the standard keeps the benefits of the original ERC-20 design without the problem that arose when an allowance was changed later (OpenZeppelin issue #438).
The three events and responsibility for minting and burning
The Transfer event is emitted whenever ownership of an NFT changes by any mechanism. _from is 0 when an NFT is created, and _to is 0 when it is destroyed. As an exception, during contract creation any number of NFTs may be created and assigned without emitting Transfer. When Transfer is emitted, it also means the approved address of that NFT has been reset. The Approval event is emitted when the approved address of an NFT is changed or reaffirmed, and the zero address means no approval. The ApprovalForAll event is emitted when an operator is enabled or disabled. All arguments of the three events are indexed. Minting and burning themselves are outside the specification, but the event rules above must be followed when NFTs are created or destroyed.
Receiver contract interface and the magic value
Wallet, broker and auction contracts that want to accept safe transfers must implement the ERC721TokenReceiver interface. After a transfer, the NFT contract calls onERC721Received(_operator, _from, _tokenId, _data) on the recipient. _operator is the address that called safeTransferFrom, and _from is the previous owner. This function can reject the transfer by throwing, and if it returns anything other than the magic value, the transaction must be reverted. The message sender is always the NFT contract address. The reason for using a magic value instead of a simple true is the Solidity DelegateCallReturnValue bug. In that bug, a previously deployed contract that did not implement the function returned 1 (true) in certain situations. Checking for the magic value makes it possible to tell a genuine positive response from an empty true. The _operator argument exists because when an operator sends a token, it acts on its own will rather than on behalf of the owner, so both the operator and the previous owner are meaningful to the recipient.
Optional extension 1: Metadata
ERC721Metadata is an optional extension that provides name(), symbol() and tokenURI(uint256 _tokenId). name is a descriptive name for the collection and symbol is its abbreviated name, and an empty string is a valid response for both. tokenURI returns a unique URI (RFC 3986) for each asset and throws for an invalid _tokenId. The URI may point to a JSON file that conforms to the ERC721 Metadata JSON Schema, which defines three properties: name (identifies the asset), description (describes the asset) and image (a URI pointing to an image/* resource). Images are recommended to be 320 to 1080 pixels wide with an aspect ratio between 1.91:1 and 4:5, and this recommendation is taken from Instagram. The URI may change. For an NFT that represents a house, for example, the photos or occupant information can naturally change. Because metadata is returned as a string, it is currently meant to be called from web3 and not to be read by other contracts. Any contract can use the same name and symbol, so deciding which contract is the canonical one is outside the scope of the standard.
Optional extension 2: Enumeration, and Caveats
ERC721Enumerable provides totalSupply(), tokenByIndex(_index) and tokenOfOwnerByIndex(_owner, _index). totalSupply is the number of valid NFTs, each of which has an owner other than the zero address and can be queried. tokenByIndex throws if _index is greater than or equal to totalSupply(), and tokenOfOwnerByIndex throws if _index is greater than or equal to balanceOf(_owner) or if _owner is the zero address. The sort order is not specified. The original text advises implementations that may grow large to avoid for/while loops, and it deployed XXXXERC721, which tracks 2^128 tokens, to a testnet to show that every function runs with less gas than an ENS lookup. The Caveats that were added because of Solidity 0.4.20 syntax limits must also be followed. Function mutability guarantees get stronger in the order payable, nonpayable, view, pure, and an implementation must be as strong as or stronger than the interface. Functions marked external may be implemented as public. An implementation of ERC721Metadata or ERC721Enumerable must also implement ERC721. For more ERC-20 compatibility, a decimals function returning uint8(0) can be added, but it is not required. The original text notes that trying to preserve privacy by blocking enumeration gains nothing, because an attacker can simply call ownerOf for every tokenId.