Skip to content

Tokens: ERC‐721

Ananthan edited this page May 25, 2024 · 9 revisions

The origin of Ethereum non-fungible tokens is related to the CryptoKitties DApp on Ethereum. CryptoKitties is a game where players can purchase, sell, exchange, and breed unique digital cats. This uniqueness makes the CryptoKitties amazingly collectible, as somebody could check out the characteristics of cats and wish to own many of them.

Such rare, collectable things like cryptokitties can be represented with Ethereum tokens. These tokens follow an Ethereum community standard ERC-721 (Ethereum Request for Comments 721)

ERC-721

ERC-721 tokens can be utilized in any trade, however their worth is an aftereffect of the uniqueness and extraordinariness related to each token. Each token generated by an NFT contract is identified by a unique token ID.

function balanceOf(address _owner) external view returns (uint256)

function ownerOf(uint256 _tokenId) external view returns (address);

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

function approve(address _approved, uint256 _tokenId) external payable;

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

balanceOf

This function is used to find the number of tokens that a given address owns.

ownerOf

This function returns the address of the owner of a token. Because each ERC-721 token is non-fungible and, therefore, unique, it’s referenced on the blockchain via a unique ID. We can determine the owner of a token using its ID.

safeTransferFrom

There are two functions with same names, but with different arguments. These functions internally call transferFrom function and also perform some additional checks to ensure a secure transfer. They check whether the caller is the actual owner or authorized operator and whether the recipient's address is valid for receiving the token or not.

transferFrom

It also transfers the ownership of an NFT from one address to another. Unlike the safeTransferFrom function, here, the caller is the one responsible for confirming that the receiver of the NFT is capable of receiving NFTs. If this is not ensured by the caller, there is a chance for the NFTs to get lost once the function execution is complete.

approve

This function is used to change or conform the approved address for an NFT.

setApprovalForAll

This function allows the owner of the NFT to allow or prevent certain third parties to manage all of owner's NFTs. Such authorised third parties are termed as operators.

getApproved

This function returns the approved address for a given NFT.

isApprovedForAll

This function returns a boolean value that indicates whether a specific operator is authorised on behalf of a given owner.

Events in ERC-721

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);
  • Transfer: This event is emitted whenever the ownership of any NFT is changed from one address to another.

  • Approval: This event is emitted whenever an approved address for a particular NFT is updated or confirmed.

  • ApprovalForAll: This event is fired when an operator is allowed or prevented to use the NFTs of a particular owner.

Contract

Every ERC-721 compliant contract should implement the ERC721 and ERC165 interfaces. ERC165 interface is used to publish and detect interfaces used in a smart contract.

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

The supportsInterface function receives a single argument representing interfaceID (bytes4) of an interface and returns true (bool) if that interface is supported.

We can use some additional interfaces to add more features to the ERC 721 token standard. Following are the optional interfaces.

ERC721TokenReceiver: A wallet-based application must implement this wallet interface. It will take up safe transfers.

interface ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

ERC721Metadata: This interface allows you to represent NFTs with the name and other details of the assets.

interface ERC721Metadata {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string);
}

ERC721Enumerable: This makes your contract publish its list of NFTs and make them discoverable.

interface ERC721Enumerable {
    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 _index) external view returns (uint256);
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

ERC721 Contract

Now let us take a look at the implementation of ERC-721 by OpenZeppelin. We are going to inherit multiple contracts in this contract. You can find out inherited contracts from here.

Here we will be importing 8 solidity files.

We can copy these codes from the respective links, create local files in the Remix IDE and import them in the code.

import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";

Here Address.sol is used for confirming whether an address is a contract or an externally owned account. Context.sol contract provides information about the current execution context, including the sender of the transaction and data through msg.sender and msg.data. If we need to perform any string operations in our contract, we can use Strings.sol library. Next, we can inherit our imported contracts to the ERC721 contract and we can define mappings and state variables used in this contract.

contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
    using Address for address;
    using Strings for uint256;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to owner address
    mapping (uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping (address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping (uint256 => address) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping (address => mapping (address => bool)) private _operatorApprovals;
}

The _owners mapping is created for retrieving the owner's address using token ID. Using _balances mapping, we can query token count using the owner's address. The _tokenApprovals mapping provides an approved address with respect to token ID. The _operatorApprovals is used by owners to provide a privilege to an account for operating tokens. This mapping is used for checking these operator privileges.

Next, we have a constructor for setting token name and token symbol at the time of deployment.

constructor (string memory name_, string memory symbol_) {
    _name = name_;
    _symbol = symbol_;
}

Now, let us go through the functions used in the ERC-721 contract implementation.

supportsInterface

We already covered supportInterface under the ERC165 interface. This function will check whether or not the provided interface-id is implemented in that contract.

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
      return interfaceId == type(IERC721).interfaceId
          || interfaceId == type(IERC721Metadata).interfaceId
          || super.supportsInterface(interfaceId);
}

balanceOf

This function will take an address as an argument and return the uint256 value. ie, this function gives token balance with respect to the address we provide.

function balanceOf(address owner) public view virtual override returns (uint256) {
      require(owner != address(0), "ERC721: balance query for the zero address");
      return _balances[owner];
}

ownerOf

This function returns the owner of a given token.

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

name

The name function just returns the name of your token contract.

function name() public view virtual override returns (string memory) {
      return _name;
}

symbol

The symbol-function just returns the symbol of our token contract.

function symbol() public view virtual override returns (string memory) {
      return _symbol;
}

tokenURI

This function takes token ID as its argument and returns URI which represents metadata about that particular token. You can manually set the token's URI depending on our token implementation.

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
      require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
      string memory baseURI = _baseURI();
      return bytes(baseURI).length > 0
          ? string(abi.encodePacked(baseURI, tokenId.toString()))
          : '';
}

_baseURI

This function is used for computing tokenURI. It is empty by default and can be overridden in child contracts.

function _baseURI() internal view virtual returns (string memory) {
    return "";
}

approve

This function allows the owner to map approval to the desired spender.

function approve(address to, uint256 tokenId) public virtual override {
    address owner = ERC721.ownerOf(tokenId);
    require(to != owner, "ERC721: approval to current owner");
    require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
       "ERC721: approve caller is not owner nor approved for all"
    );
    _approve(to, tokenId);
}

getApproved

This function returns the approved address for a token ID. The transaction will be reverted if the token ID does not exist.

function getApproved(uint256 tokenId) public view virtual override returns (address) {
    require(_exists(tokenId), "ERC721: approved query for nonexistent token");
    return _tokenApprovals[tokenId];
}

setApprovalforAll

This function sets or resets the approval of a given operator.

function setApprovalForAll(address operator, bool approved) public virtual override {
    require(operator != _msgSender(), "ERC721: approve to caller");
    _operatorApprovals[_msgSender()][operator] = approved;
    emit ApprovalForAll(_msgSender(), operator, approved);
}

isApprovedForAll

It checks whether the operator is under the specific owner. It returns a true/false value.

function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
    return _operatorApprovals[owner][operator];
}

transferFrom

This function is used for transferring a token from one address to another.

function transferFrom(address from, address to, uint256 tokenId) public virtual override {
    //solhint-disable-next-line max-line-length
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
    _transfer(from, to, tokenId);
}

safeTransferFrom

This function is similar to transferFrom function, except that they check whether the receiver is a valid ERC-721 receiver contract.

function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
    safeTransferFrom(from, to, tokenId, "");
}

Here there are two functions with similar names, but one version has an extra data parameter. The choice will depend on the available input parameters.

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
    _safeTransfer(from, to, tokenId, _data);
}

_safeTransfer

This function safely transfers tokens from sender to receiver and confirms that the contract receivers are aware of the ERC-721 standard.

function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
    _transfer(from, to, tokenId);
    require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
}

_exists

It checks that a specific token ID exists or not.

function _exists(uint256 tokenId) internal view virtual returns (bool) {
    return _owners[tokenId] != address(0);
}

_isApprovedOrOwner

This function checks whether the spender account of that specific token ID is the owner or approved operator.

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

_safeMint

This function safely mints token ID and transfers it to the provided address.

function _safeMint(address to, uint256 tokenId) internal virtual {
    _safeMint(to, tokenId, "");
}

_mint

It mints token with given tokenId and sends it to the specified address:

function _mint(address to, uint256 tokenId) internal virtual {
    require(to != address(0), "ERC721: mint to the zero address");
    require(!_exists(tokenId), "ERC721: token already minted");
    _beforeTokenTransfer(address(0), to, tokenId);
    _balances[to] += 1;
    _owners[tokenId] = to;
    emit Transfer(address(0), to, tokenId);
}

_burn

If you want to remove a specific tokenId, you can trigger this function with that particular token ID.

function _burn(uint256 tokenId) internal virtual {
    address owner = ERC721.ownerOf(tokenId);
    _beforeTokenTransfer(owner, address(0), tokenId);
    // Clear approvals
    _approve(address(0), tokenId);
    _balances[owner] -= 1;
    delete _owners[tokenId];
    emit Transfer(owner, address(0), tokenId);
}

_transfer

This is an internal function to transfer ownership of a given token ID to another address. Unlike transferFrom function, this does not impose any checks on caller.

function _transfer(address from, address to, uint256 tokenId) internal virtual {
    require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
    require(to != address(0), "ERC721: transfer to the zero address");
    _beforeTokenTransfer(from, to, tokenId);
    // Clear approvals from the previous owner
    _approve(address(0), tokenId);
    _balances[from] -= 1;
    _balances[to] += 1;
    _owners[tokenId] = to;
    emit Transfer(from, to, tokenId);
}

_approve

This function approves the given address to operate on the given token ID.

function _approve(address to, uint256 tokenId) internal virtual {
    _tokenApprovals[tokenId] = to;
    emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}

_checkOnERC721Received

This is an internal function that checks if the recipient implements IERC721receiver interface. This function ensures that the destination address can accept the ERC-721 tokens.

function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) {
    if (to.isContract()) {
        try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
            return retval == IERC721Receiver(to).onERC721Received.selector;
        } catch (bytes memory reason) {
            if (reason.length == 0) {
                revert("ERC721: transfer to non ERC721Receiver implementer");
            } else {
                // solhint-disable-next-line no-inline-assembly
                assembly {
                   revert(add(32, reason), mload(reason))
                }
            }
        }
    } else {
        return true;
    }
}

_beforeTokenTransfer

This is a hook called before any token transfer. Hooks are functions that are called before or after some actions take place. This function is useful for adding any additional checks or actions that will be executed before a token transfer occurs.

function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { }

The complete contract is given below.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";

contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
    using Address for address;
    using Strings for uint256;

    string private _name;

    string private _symbol;

    mapping (uint256 => address) private _owners;

    mapping (address => uint256) private _balances;

    mapping (uint256 => address) private _tokenApprovals;

    mapping (address => mapping (address => bool)) private _operatorApprovals;

    constructor (string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return interfaceId == type(IERC721).interfaceId
            || interfaceId == type(IERC721Metadata).interfaceId
            || super.supportsInterface(interfaceId);
    }

    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

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

    function name() public view virtual override returns (string memory) {
        return _name;
    }

    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0
            ? string(abi.encodePacked(baseURI, tokenId.toString()))
            : '';
    }

    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }

    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");

        return _tokenApprovals[tokenId];
    }

    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(operator != _msgSender(), "ERC721: approve to caller");

        _operatorApprovals[_msgSender()][operator] = approved;
        emit ApprovalForAll(_msgSender(), operator, approved);
    }

    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        _transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        _safeTransfer(from, to, tokenId, _data);
    }

    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
        _transfer(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

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

    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }

    function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual {
        _mint(to, tokenId);
        require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId);

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }

    function _transfer(address from, address to, uint256 tokenId) internal virtual {
        require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
        require(to != address(0), "ERC721: transfer to the zero address");

        _beforeTokenTransfer(from, to, tokenId);

        // Clear approvals from the previous owner
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
    }

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
        private returns (bool)
    {
        if (to.isContract()) {
            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
                return retval == IERC721Receiver(to).onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ERC721: transfer to non ERC721Receiver implementer");
                } else {
                    // solhint-disable-next-line no-inline-assembly
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { }
}


Clone this wiki locally