DEV Community

Jamiebones
Jamiebones

Posted on

ERC 721 The NFT Standard

ERC 721 represent the standard for the minting and transfer of Non Fungible Token otherwise known as NFT. A non fungible token is a token that is unique and different from another. It cannot be broken down into smaller pieces unlike the ERC 20 tokens that are divisible to smaller pieces.

NFT are revolutionary right now and they represent the future. An NFT could represent anything from a media file, to a digital art, certificate, title deeds or a lease agreement. The use of NFT is enormous and knowing how they work is the goal of this blog post.

ERC 721 Standard

The ERC 721 is the standard that defines the methods, events and state variables that a non fungible token (NFT) should posses to be compliant.

Interface of ERC 721

The ERC 721 standard for NFT is more complicated than the one of ERC 20. It spans several interface and contracts with some marked as core while others are optional. Below is the OpenZepplin implementation of the contract interface.

 // SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;

interface IERC721 is IERC165 {

    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);
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

Enter fullscreen mode Exit fullscreen mode

Interface of ERC 165

The ERC 721 interface inherits from ERC 165. The ERC 165 is a contract that is used for standard interface detection. It allows the client/caller to check if a function is available for call in the target contract.

The interface IERC 165 which ERC 165 inherits from defines the following function:

function supportsInterface(bytes4 interfaceId) external view returns (bool)
Enter fullscreen mode Exit fullscreen mode

Variables and state in ERC 721 standard

The following are the state variables defined in the standards implementation by OpenZepplin.

Token owner mapping

This is a mapping of uint256 to an address. It is used to find the owner of a given NFT ID

  mapping(uint256 => address) private _tokenOwner;
  function ownerOf(uint256 tokenId) public view returns (address){
    address owner = _tokenOwner[tokenId];
    require(owner != address(0);
    return owner;
}
Enter fullscreen mode Exit fullscreen mode

The _tokenOwner mapping is private preventing direct access. To get value from the _tokenOwner mapping, the function ownerOf which accepts a uint256 as a tokenId is used to find the address associated with an NFT.

Approved address mapping

The owner of a NFT can give approval to another address. Once the approval is given, that address will be able to transfer the NFT from the owner to themselves or another address. This approvals is managed by a mapping called _tokenApprovals.

 mapping(uint256 => address) private _tokenApprovals;
 function getApproved(uint256 tokenId) public view returns (address) {
   require(_exists(tokenId));
   return _tokenApprovals[tokenId];
}
Enter fullscreen mode Exit fullscreen mode

The mappings _tokenApprovals is used to store the approval given by a token owner to an address. This approval allows that address to transfer the token on behalf of the owner. This mapping is private and the function getApproved is used to pull out the address that is allowed to transfer a particular NFT on behalf of an owner. The getApproved function uses an internal function _exists to check if the provided tokenid is a valid NFT ID on the smart contract.

Number of token owned per owner

This state variable is used to track and store the number of token owned by an address in a contract. It consist of a mapping with an address as the key and uint256 as the number of tokens owned.

  mapping(address => uint256) private _ownedTokensCount;
  function balanceOf(address _owner) public view returns (uint){
     require(_owner != address(0));
     return _ownedTokensCount[_owner];
  }
Enter fullscreen mode Exit fullscreen mode

Operator approval

An operator is a third party that is trusted and can be approved by an owner to have access to all the NFTs owned by them. If an owner gives permission to an operator, that operator is allowed to withdraw any NFT from that owner's address and they are allowed to transfer it to themselves or others.

The operator approval is stored in a nested mapping data structure.

The operator approval is different from the tokenApproval. The operator approval grants the right to an operator to be able to withdrawal potentially all the NFTs from an owner's address while in the tokenApproval, the owner gives an address permission to be able to withdraw just a particular NFT.

  mapping(address => mapping(address => bool)) private _operatorApproval;
function setApprovalForAll(address to, bool approved) public {
   require(to != msg.sender);
   _operatorApproval[msg.sender][to] = approved;
   emit ApprovalForAll(msg.sender, to, approved);
}

function isApprovedForAll(address owner, address operator) public view returns (bool) {
  return _operatorApproval[owner][operator];
}
Enter fullscreen mode Exit fullscreen mode

The function setApprovalForAll is used to grant approval to an address (operator like Open Sea) access to manage your NFTs. The function checks and ensure that the to address is not the same as msg.sender. Then it proceeds to save the approval on the state variable _operatorApproval. The owner of the NFTs is the caller of this function.

The next function isApprovedForAll checks if an operator has approval to an owner's NFTs. It returns the value stored in the state variable _operatorApproval.

ERC 721 standard functions

The standard contains some useful functions for the management and transfers of NFTs. Below is the implementation of some of this functions by OpenZepplin

Constructor

  constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }
Enter fullscreen mode Exit fullscreen mode

OpenZepplin implementation of ERC 721 standard also inherits from IERC721Metadata interface that stipulates that an NFT contract should have a name and a symbol. The constructor accepts two string parameters that are used to set the name and symbol of the NFTs contarct.

balanceOf

This is used to check how many NFTs does someone own.

ownerOf

This is used to check the owner of a particular NFT.

  function ownerOf(uint256 tokenId) public view returns (address) {
     address owner = _owners[tokenId];
     require(owner != address(0), "ERC721: owner query for nonexistent token");
        return owner;
    }
Enter fullscreen mode Exit fullscreen mode

approve

The approve function is used to grant approval by the NFT owner of a particular NFT token to an address for the purpose of helping to administer that NFT. The function checks firstly, if the msg.sender is the token owner or has approval to the token. if all checks clears, the _tokenApprovals state variable is updated with the tokenId pointing to the to address. ( this means the to address can manage that token )

   function approve(address to, uint256 tokenId) public virtual override {
        address owner = ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");
        require(msg.sender == owner || isApprovedForAll(owner, 
        msg.sender),
            "ERC721: approve caller is not owner nor approved for 
        all");
        _tokenApprovals[tokenId] = to;
        emit Approval (owner, to, tokenId);
    }
Enter fullscreen mode Exit fullscreen mode

getApproved function

This function implementation is defined above. It is used to return the address that is approved to manage an NFT.

setApproval for all

This function is used to grant an operator full access to all the NFTs available in an owner's address.

isApprovedForAll

This function returns a boolean showing if an operator has access or not to an owner NFts.

transferFrom

This is a public function used to transfer the NFT from the owner's account to the receiver's account. For this function to work, approval must have been given prior by the owner of the NFT to an address for the purpose of managing the NFT.

The function accepts three parameters which are :

from : address of the NFT owner
to: the address of the receiver
tokenId: the ID of the NFT to transfer.

Before the transferFrom function is called, the NFT owner must first approve that msg.sender calling the function has the approval to transfer that NFT. This is done by the owner by calling the approve function.

Implementation of transferFrom function

  function transferFrom(address from, address to, uint256 tokenId) public {
  require(_isApprovedOrOwner(msg.sender, tokenId);
  _transfer(from, to, tokenId);
}

function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }
Enter fullscreen mode Exit fullscreen mode

The function checks if the msg.sender is the owner of the NFT, or has approval to manage the NFT. If it does the transfer is executed by the internal function _transfer.

safeTransferFrom

This is a public function that is used to safely transfer the NFT from the owner's account to the recipient account. Safely means that, if the recipient is a contract, you will need to ensure, that the contract has callbacks registered to let it know when a NFT was successfully transferred to it.

The function takes the following parameters :

from: the address of the current owner of the NFT
to: the recipient of the NFT
tokenId: the ID of the NFT to be transferred
_data : the bytes data that is forwarded if to is a contract.

Implementation of safeTransferFrom

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
   _transfer(from, to, tokenId);
   require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
}
Enter fullscreen mode Exit fullscreen mode

The safeTransferFrom function has two overloads. One overload function is used to transfer NFT to an EOA address (externally owned account ), that is an account address in our wallet. This overload for transferring to an EOA receives only three (3) parameters. The last _byte parameter is missing.

The other overloaded of the safeTransferFrom function is used to transfer NFT to a contract address. For a contract to be able to receive NFT, it must implement the IERC721Receiver interface. This interface specifies that a contract that wants to accept NFTs must implement the function named onERC721Received.

   function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
Enter fullscreen mode Exit fullscreen mode

_mint internal function

The _mint internal function is used to mint a new NFT at the given address. The function takes two arguments which are:

to: the address of the owner for which the NFT is minted
tokenId: the new unique tokenId for the token to be minted

When the _mint function is called a Transfer event is emitted.

_burn internal function

This internal function is used to destroy or burn a given NFT from the owner's address. It accepts two parameters which are:

owner : the address of the owner whose token is to be burned
tokenId : the tokenId of the NFT to be burned

Summary

The ERC 721 standard has a lot of functions, events that is necessary for the transfer of NFTs. I just covered a bit of them here to prevent this blog post for becoming too long. In my next blog post, I will show how to create a contract to mint NFTs.

Until then, thanks for reading.

Happy coding!

Top comments (0)