Implementing an Interface for Non-Fungible Tokens
October 11, 2021
Overview
Non-fungible tokens (NFTs) are tokens that represent proof of ownership of unique items. Each token is non-divisible and has a unique identifier linked to one owner. In general, NFTs conform to a set of standards that are governed by smart contracts stored on a blockchain. The most common standard is ERC-721.
ERC-721
ERC-721 is an open standard that describes how to build non-fungible tokens on the Ethereum blockchain. The standard describes the transfer, approval and information retrieval of tokens. Every ERC-721-compliant contract must implement the ERC721 and ERC165 interfaces, as outlined in EIP-721.
The following functions (corresponding to the relevant interface) must be implemented:
- interface ERC721
balanceOf(owner)
ownerOf(tokenId)
transferFrom(from, to, tokenId)
approve(to, tokenId)
getApproved(tokenId)
setApprovalForAll(operator, _approved)
isApprovedForAll(owner, operator)
safeTransferFrom(from, to, tokenId)
safeTransferFrom(from, to, tokenId, data)
- interface ERC165
supportsInterface(interfaceId)
- interface ERC721Enumerable (optional)
totalSupply()
tokenOfOwnerByIndex(owner, index)
tokenByIndex(index)
- interface ERC721Metadata (optional)
name()
symbol()
tokenURI(tokenId)
The following events (corresponding to the relevant interface) must be implemented:
- interface ERC721
Transfer(from, to, tokenId)
Approval(owner, approved, tokenId)
ApprovalForAll(owner, operator, approved)
Metadata
Most NFT collections choose to store the underlying assets that they represent off-chain, usually with a decentralised storage provider. This helps reduce gas fees related to storage and the processing of transactions. The details of these assets may be described using the metadata interface extension (interface ERC721Metadata
).
In order to link the contract with the details of the underlying asset, contracts utilise the tokenURI
function (specific to a given token) to return a JSON file that contains any necessary information regarding the collection and the assets held off-chain. The original metadata JSON schema was proposed as follows:
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}
Current implementations of the metadata interface are more flexible and don't excatly conform to this schema, but the general principle remains the same.
ABI
In order to interact with smart contracts, you need to know it's Application Binary Interface (ABI) - a low-level interface that defines the properties exposed by a smart contract. An ABI is used to encode and decode binary data at a low level, providing a mechanism to interact with smart contracts at a high level. Data is encoded according to its type and then decoded according to its schema, as specified by the interface.
All ERC-721-compliant contracts will expose an almost identical ABI, with an option to further extend the interface with additional functionality. The ABI of a smart contract is specified in JSON format given by an array of function, event and error descriptions.
An ABI usually results in a chicken-and-egg problem, where we need the smart contract to know the interface it exposes but we need the ABI to interact with the smart contract. This is especially true in smart contracts that extend the standard interface.
Function Objects
The format of a function object:
type
function
constructor
receive
fallback (default)
name
inputs
- an array of objectsname
type
components
outputs
stateMutability
pure
- function does not read blockchain stateview
- function does not modify blockchain statenonpayable
- function does not accept Ether (default)payable
- function accepts Ether
Event Objects
The format of an event object:
type
event
name
inputs
- an array of objectsname
type
components
indexed
- true if the field is part of the log's topics, false if it one of the log's data segment
anonymous
Error Objects
The format of an error object:
type
error
name
inputs
- an array of objectsname
type
components
Example ABI
For example, consider the basic contract below written in Solidity:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract ExampleContract {
constructor() { b = hex"74645678901234000890123456789321"; }
function ExampleFunction(uint256 arg1) public pure { }
event ExampleEvent(uint256 indexed arg1, bytes32 arg2);
error ExampleError(uint256 arg1);
bytes32 exampleConstant;
}
The corresponding ABI for this contract is:
[
{
"type": "function",
"inputs": [{ "name": "arg1", "type": "uint256" }],
"name": "ExampleFunction",
"outputs": []
},
{
"type": "event",
"inputs": [
{ "name": "arg1", "type": "uint256", "indexed": true },
{ "name": "arg2", "type": "bytes32", "indexed": false }
],
"name": "ExampleEvent"
},
{
"type": "error",
"inputs": [{ "name": "arg1", "type": "uint256" }],
"name": "ExampleError"
}
]
ERC-721 ABI
Given that all ERC-721-compliant contracts implement the same interface, the following ABI will be identical (but not exhuastive) for all ERC-721 smart contracts:
[
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
}
]