How to build and use ERC-721 tokens in 2021

An intro for devs to the uniquely identifying token standard and its future

The ERC-721 standard has been around for a while now. Originally made popular by blockchain games, it's more and more used for other applications like Defi.

But what exactly is it?

NFT Meme

A non-fungible token (NFT) is a uniquely identifying token. The word non-fungible implies you cannot just replace one NFT with another one. Every NFT is unique and different. In contrast to any fungible token like Bitcoin or ERC-20 where it doesn't matter which specific coin/token you receive, they all have the same value.

On Ethereum Cryptokitties helped to make this into the now popular 721 standard.

The Features of ERC-721

Let's explore the exact definition of the standard, all features and functions that are part of it.

1. The NFT implementation

At the core we have a mapping from uint256 => address. This is the reverse of a fungible token (e.g. ERC-20) mapping which is from address => uint256.

Why this reverse mapping?

In the fungible tokens we map from owner to balance. While in 721 we actually also do this in a second mapping (see balanceOf below), we more importantly need a way to uniquely identify tokens. By mapping from uint256 to address, we can give every minted token an id and map this id to exactly one owner. When creating a new NFT token, you create a new id (most commonly you just start at id 1 and count + 1 any following id) and set  the owner as ownerMapping[id] = receiverAddress.

Let's look at the actual 721 functions:

A. Getters

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

Returns the owner of the NFT token with the given id. In the 721 implementation we would just return ownerMapping[id] here.

function balanceOf(address owner) external view returns (uint256);

Returns the balance of an owner, meaning the total amount of how many NFT tokens this address owns. In our implementation we will need to keep track of this when minting or transferring an NFT.

B. Transfer

function safeTransferFrom(address from, address to, uint256 tokenId) external payable;

The transfer function comes in three forms. This one here is probably the most commonly used one. It will transfer the token with the given id to the receiving address. It will only work if the from address is the owner or approved (see below for approvals) and if the receiver is either not a smart contract or a smart contract that implements the 721 receiver interface (see below for ERC-165 and how to receive tokens). Otherwise the call will revert.

The other two variants for the transfer functions are simply one with an additional bytes data field to pass to the receiver hook and another non-safe transfer function that doesn't invoke the transfer hook.

C. Approvals

function approve(address approveTo, uint256 tokenId) external payable;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool isApproved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);

The approval functions allow someone to approve another address for either one specific token or all tokens. Think of the approve for all functions as something where you would approve a trusted smart contract, so it cannot just steal all your tokens. Once you approve another address, it has full transfer control over it.

2. Metadata (optional)

The metadata extension is optional for ERC-721 smart contracts. It includes a name and symbol just like they exist in many other token standards including ERC-20. Further a tokenURI can be defined for a token. Each unique token should have its own URI.

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

This tokenURI links to a metadata file. In the case of a game, the file could just be hosted on the servers of the game provider. In the spirit of decentralization, in many cases IPFS is used to store this file. Just make sure to use IPFS pinning not to loose the file. The metadata file has a defined name, description and image. It might look like this:

{
  "name": "Buzz",
  "description": "Paper collage, using salvaged and original watercolour papers", 
  "image": "https://ipfs.infura.io/ipfs/QmWc6YHE815F8kExchG9kd2uSsv7ZF1iQNn23bt5iKC6K3/image"
}

3. Enumeration (optional)

Another additional optional interface is the enumeration. It contains functions for counting and receiving tokens by the index.

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

With the totalSupply function you can determine how many NFT's in total exist currently, excluding the burnt ones of course. The other two functions will return the n'th token from the list of all tokens (tokenByIndex) or from the list of the tokens of that owner (tokenOfOwnerByIndex).

You might have noticed, if you never burn tokens and count token ids from 1 incrementally, the first two functions would just reflect the token ids.

4. Receiver hook and ERC-165 support

If you don't know what EIP-165 is yet, see my tutorial here. In short it's a way for smart contracts to define what interfaces they support. So for example, does the smart contract support receiving ERC-721 tokens? If it does, the respective supportsInterface function must exist and return true for that contract.

In the ERC-721 a smart contract receiver must implement the onERC721Received function. You could use the onERC721Received function to implement some further receiving logic or just confirm the receiver supports the 721. But be aware that in the case of the non-safe transfer function, the hook is not called! In the hook you always need to return the magic value as confirmation.

How can I deploy a 721 NFT?

Now let's see how you would implement a 721 token contract. We will be using the Openzeppelin contracts to help us.

This is a basic NFT example contract which allows the owner to mint new tokens. The syntax for the imports assumes using Remix, adjust it in case you installed the Openzeppelin contracts via npm.

// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Counters.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";

contract NftExample is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("NFT-Example", "NEX") {}

    function mintNft(address receiver, string memory tokenURI) external onlyOwner returns (uint256) {
        _tokenIds.increment();

        uint256 newNftTokenId = _tokenIds.current();
        _mint(receiver, newNftTokenId);
        _setTokenURI(newNftTokenId, tokenURI);

        return newNftTokenId;
    }
}

Consider using some other minting mechanism depending on your use case. The _setTokenURI is of course optional.

You can also choose to use the Preset available here. A simple contract MyContract is ERC721PresetMinterPauserAutoId will be all you need to get an NFT contract that is preset to have

  • minting, pausing and admin roles using the Openzeppelin Access Control mechanism
  • enabled pause and unpause functionality

I highly recommend checking out the Openzeppelin 721 contracts and documentation for further details.

How can I receive an NFT in a contract?

As mentioned before, if you use the safeTransferFrom function to send an NFT to a smart contract, it will revert unless the contract specifically added support for receiving 721 tokens

 Let's see how you would add this kind of support:

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721Holder.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/introspection/ERC165.sol";

contract Receiver721Example is IERC721Receiver, ERC165, ERC721Holder {
    constructor() {
        _registerInterface(IERC721Receiver.onERC721Received.selector);
    }
    
    function doSomethingWith721Token(IERC721 nftAddress, uint256 tokenId) external {
        // do something here
    }
}

The available ERC721Holder is implementing a basic onERC721Received function which returns the magic value to confirm the support for receiving 721 tokens. Make sure to actually implement a function that can handle the token accordingly.

Use cases for NFT and ERC-721

CryptoKitties

Most commonly people use the standard for blockchain games like the original Cryptokitties. It's well suited for this case as you can easily have in-game items represented as NFT. The additional metadata file is particularly useful to add more in-game information to the NFT.

But the usage goes beyond just games, and we are seeing it come into existence more and more now. Take a look at Rarible to see many other use cases. Digital art seems to be a major driver for NFT's at this point. Domains can be purchased or utilities, e.g., something that gives you digital access rights. More marketplaces include OpenSeaSuperRare, and Axie Infinity.

I find those in the space of Defi very interesting. For example you can purchase certain insurances, e.g., using yinsure (from yearn). The NFT gives you the permission for the defined insurance and claims are handled and paid out by the decentralized governance.

We even considered using the 721 standard for our derivatives as tokenized positions at Injective Protocol, however we ultimately decided against it as it wasn't a perfect fit. In theory it would have been possible though and might be something to add later on if people show interest.

In general any uniquely identifying possession could be turned into an NFT. We will see much more use cases in the future.

Future and Interchain NFT's

Interchain

As mentioned we will probably see much more use cases for NFT's in the future on Ethereum and other blockchains. There is a new standard in the making called Interchain NFTs. The Interchain Foundation from the Cosmos universe is developing this standard for multiple networks dealing with 721 NFTs. It will be a useful standard for any blockchain once they have working bridges.

What are you looking forward to? Do you own any NFT's? Have you developed your own?

Let me know in the comments.


Markus Waas

Solidity Developer

More great blog posts from Markus Waas

  • Uniswap

    Using the new Uniswap v2 as oracle in your contracts

    How does the Uniswap v2 oracle function and how to integrate with it

    We've covered Uniswap previously here . But let's go through the basics first again. What is UniSwap? If you're not familiar with Uniswap yet, it's a fully decentralized protocol for automated liquidity provision on Ethereum. An easier-to-understand description would be that it's a decentralized...

  • People making fun

    Smock: The powerful mocking tool for Hardhat

    Features of smock and how to use them with examples

    We’ve covered mocking contracts before , but now there’s an additional great tool available: smock . It simplifies the mocking process greatly and also gives you more testing power. You’ll be able to change the return values for functions as well as changing internal contract storage directly!...

  • Set Protocol

    Trustless token management with Set Protocol

    How to integrate token sets in your contracts

    With Set Protocol you can create baskets of tokens that give users different levels of exposure to underlying assets (currently only ERC-20 tokens). Set Protocol and their TokenSet functionality is the perfect example for making use of the new paradigm of Defi and composability. You can let...