logoAcademy

Intro ERC-721 Tokens

Non-Fungible Token Standard

Previously, we explored the ERC-20 interface, a contract standard which allowed for the implementation of a token contract which the majority of smart contract users are able to easily call. As a result of the ERC-20 standard, many protocols were able flourish without having to worry about incompatible token interfaces. However, there is one thing that the ERC-20 standard did not fix... To understand the biggest shortcoming of the ERC-20 standard, we will first digress from talking about the functionality of the ERC-20 to discussing the concept of fungibility.

Fungibility

Fungibility refers to the ability of an item to be converted into another item (without any difference in value). The most basic example of fungibility is currency:

  • Dollar bills are considered fungible because if we swap one dollar bill for another dollar bill without any change in value
  • For a given $100 bill, we can convert it into 100 $1 bills. Therefore, we can consider a $100 bill to be fungible
  • For a $2 bill, we can convert it into 200 pennies. Therefore, $2 bills are fungible. Relating this to ERC-20 tokens, notice that 1 unit of some ERC-20 token A will always be equivalent to 1 unit of the same ERC-20 token A. Therefore, ERC-20 tokens are fungible. However, this also implies that for a use case that requires for 1 unit of some ERC-20 A to be different in value than 1 unit of the same ERC-20 token A, the ERC-20 token standard is not sufficient for such a use case.

ERC-721

To make clear a particular that ERC-20 cannot support, imagine we wanted to represent an art collection within a smart contract. The smart contract would contain the following functionality in a similar fashion to an ERC-20 token:

  • The ability to query the "balance" (i.e. the art holdings) of a particular user
  • The ability to transfer art pieces from one account to another
  • The ability to get information about the art collection
  • The biggest different between an arbitrary ERC-20 token contract and an art collection is the fungibility of individual items - ERC-20 tokens are inherently fungible with items in the art collections are not (since the items each vary in value).

To account for uses cases like an art collection, the ERC-721 standard was introduced. Formally, ERC-721 is a standard for non-fungible tokens. Below is the interface of the ERC-721 token from its original proposal :

interface ERC721 {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

Implementation Design

Notice that for an ERC-20 token, we can store the balance of all token holders with the following data structure:

mapping(address => uint) balances;

In the case of the ERC-721 token standard, this is not enough, since using just this data structure would imply that all tokens of an ERC-721 contract are fungible. Therefore, we want to use the following data structures:

mapping(address => uint) balances;
mapping(uint => address) holders;

The balances data structure will map accounts to the number of non-fungible tokens they hold. The data structure holders, meanwhile, will map the ID of each non-fungible token to the address which holds the token. Therefore, by adding just another mapping, we've just allowed our token contract to incorporate non-fungible tokens!

On this page