ERC-721 Token

The ERC-721 Token standard is the Ethereum token standard which enables Non-Fungible Tokens (NFTs) on the Ethereum blockchain. Just like the ERC-20 Token standard helps in creating fungible tokens, the ERC-721 focuses on non-fungible asset design.

Create Your Own NFT on Ethereum Blockchain

Goal — We will explore the ERC-721 token standard, non-fungibility, and build a SIMPLE ERC-721 token.

What is an ERC 721 Token?

The ERC-721 Token standard is the Ethereum token standard which enables Non-Fungible Tokens (NFTs) on the Ethereum blockchain. Just like the ERC-20 Token standard helps in creating fungible tokens, the ERC-721 focuses on non-fungible asset design. Do you remember CryptoKitties? That is a non-fungible token (i.e. each Kitty asset is unique).

Fungibility

Before understanding non-fungibility, we need to understand what is fungibility. There are two main properties of fungibility:

Interchangeability — You can interchange or replace units of a fungible asset. Your $20 bill and my $20 bill represents the same thing and can be interchanged. Another example that 1kg gold bar is equal to another 1k gold bar. They represent the same value and can be interchanged.

Quantity — You can merge units of a fungible asset to get a higher value in quantity. The example you can add up multiple KGs of grains and now you have the same thing but in a higher quantity.

Nonfungibility

Let’s understand what is non-fungibility.

A non-fungible token represents a unique token which can’t be interchanged with another token.

You can’t divide or add up to get a higher quantity because every unit is unique and you cannot replace or interchange it with other assets. For example, two pieces of land. They are both non-fungible regarding their location.

Image source- Every Kitty is unique and represents a different token. Aren’t they cute? See the leftmost one, isn’t he shy? 😋

Example of Non-Fungible Tokens

There are many Non-Fungible Tokens. Some famous NFTs are CryptoKitties, Etheremons, Crypto Bots, and Blockchain Cuties. You can check a full list of ERC-721 tokens here.

The ERC-721 Standard

Now’s let deep dive into the ERC-721 standard. The ERC-721 standard defines a set of methods which help in identifying and interacting with a Non-Fungible Token.

Events- events which get emitted on Transfer, Approvals.

ERC-721 Functions

balanceOf — Will return the balance of an address.

ownerOf — Will return owner address of a token.

safeTransferFrom —Transfer token from one address to another with checks performed to make sure the recipient can accept the token, so it doesn’t get burned/lost.

transferFrom — Transfer token from one address to another address (note: the use of this function is discouraged). The Caller of the method is responsible to put the correct recipient address.

approve — Approve any other address to send a transaction from token owner account to any other account.

setApprovalForAll — Allow or disallow an operator (any address, mostly wallets and exchanges) to send all tokens from owner address to any other address.

getApproved — Return an address which is allowed to transfer token for owners. Return 0 if no address is set.

isApprovedForAll — Return true if given operator (any address) is approved by a given owner.

pragma solidity ^0.4.20;
interface ERC721 /* is ERC165 */ {
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);
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);
}

Building an ERC-721 Token using OpenZeppelin and Truffle

Now let’s build an ERC-721 token using the OpenZeppelin library and Truffle.

First, let’s setup Truffle:

mkdir simple
truffle init
npm install openzeppelin-solidity

Let’s create a new Contract for our SIMPLE Token:

pragma solidity ^0.4.24;

import "/openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol";
import "/openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract SIMPLEToken is ERC721Full, Ownable{
  
  constructor() 
  ERC721Full("SIMPLE", "SMPL")
  public {}
  
function mint(address to, uint256 tokenId) public onlyOwner {
   _mint(to, tokenId);
  }
function _mint(address to) public onlyOwner{
   mint(to, totalSupply().add(1));
  }
}

Let’s see what we are doing here. We are inheriting two contracts; ERC721FULL and Ownable.

Ownable — Using this contract, we can manage ownership of our contract and are able to mint tokens from the contract owner’s account only.

ERC721FULL — This is a standard implementation of ERC-721 interface we have mentioned above. Let’s see what happens inside this contract:

pragma solidity ^0.4.24;

import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "./ERC721Metadata.sol";
contract ERC721Full is ERC721, ERC721Enumerable, ERC721Metadata {
  constructor(string name, string symbol) ERC721Metadata(name, symbol)
    public
  {
  }
}

ERC721FULL internally inherits 3 contracts. We will look mainly into ERC721 to understand the implementation.

Let’s go through the main function one by one. Before that, we will understand, how tokens are getting stored:

// Mapping from token ID to owner
  mapping (uint256 => address) private _tokenOwner;
  
 
// Mapping from token ID to approved address
  mapping (uint256 => address) private _tokenApprovals;
  
// Mapping from owner to number of owned token
  mapping (address => uint256) private _ownedTokensCount;
  
// Mapping from owner to operator approvals
  mapping (address => mapping (address => bool)) private _operatorApprovals;

_tokenOwner — This mapping is needed to store a token against its owner. By using this we can know who is the owner for given tokenId.

_tokenApprovals — This mapping is needed to store tokenId, against an address which is approved by the token owner, to transfer a token on behalf of the owner.

_ownedTokenCount — This mapping is needed to know how many tokens an address owns. If we don’t create this mapping, we have to loop to get this information and looping takes lots of gas on EVM.

_operatorApprovals — Mapping of an owner and an operator (any address, mostly wallets, and exchanges) to check if the owner had given approval or not.

Now let’s see functions in this standard:

balanceOf — This will return the balance of an address. First, it checks for a valid address and then using _ownedTokensCount returns the count of the token.

function balanceOf(address owner) public view returns (uint256) {
    require(owner != address(0));
    return _ownedTokensCount[owner];
  }

OwnerOf — This will return the owner address for a given token using _tokenOwner mapping.

function ownerOf(uint256 tokenId) public view returns (address) {
    address owner = _tokenOwner[tokenId];
    require(owner != address(0));
    return owner;
  }

approve — This will approve an address to transfer a token on behalf of the owner. The function first checks if the owner called the function or if the call is approved by the owner to send all tokens. Then it updates the _tokenApprovals mapping if everything is correct.

function approve(address to, uint256 tokenId) public {
    address owner = ownerOf(tokenId);
    require(to != owner);
    require(msg.sender == owner || isApprovedForAll(owner, msg.sender));
    
_tokenApprovals[tokenId] = to;
    emit Approval(owner, to, tokenId);
  }

safeTransferFrom — There are two functions with similar names, but with different arguments. These functions internally call transferFrom function. Though they also perform one more important duty. They check if the recipient address is valid for receiving the token or not. This helps in the security of the token.

function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId,
    bytes _data
  )
    public
  {
    transferFrom(from, to, tokenId);
    require(_checkOnERC721Received(from, to, tokenId, _data));
  }

transferFrom — This is the main function to transfer a token from one address to another address. Let’s see what is it doing:

1- Check if the token is either owned by called or approved to the caller. Also, checks of a valid address.

2- Clear approval, remove current ownership and reduce the token count of the current owner.

3- Add token to recipient account and increase the token count for the recipient.

function transferFrom(
    address from,
    address to,
    uint256 tokenId
  )
    public
  {
    require(_isApprovedOrOwner(msg.sender, tokenId));
    require(to != address(0));
_clearApproval(from, tokenId);
    _removeTokenFrom(from, tokenId);
    _addTokenTo(to, tokenId);
emit Transfer(from, to, tokenId);
  }
  

setApprovalForAll — This function approves the address to transfer all tokens on behalf of the owner. It first checks if the called and to address are not the same, and then updates the _operatorApprovals mapping.

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

isApprovedForAll — This function checks if the owner approved the operator to transfer tokens or not.

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

getApproved — Returns the approved address for given tokenId.

function getApproved(uint256 tokenId) public view returns (address) {
    require(_exists(tokenId));
    return _tokenApprovals[tokenId];
  }

You can check the other helper functions and inherited contracts.

If you don’t understand something, please ask in comments! We’re here to help!

Testing ERC-721 Token

Now we will test our SIMPLE token and play around it. We will use a local built-in blockchain to deploy and test the contract.

truffle develop

This will give us the Truffle console.

Now let’s deploy our contract. Remember! You need to add a migration file. You can check the attached GitHub repo at the end of the article.

truffle compile
migrate --reset
SIMPLEToken.deployed().then((simple) => {token = simple;})

Now, let’s mint some tokens, and perform a test transfer & approval.

token._mint(web3.eth.accounts[0]) // will mint a new token

token.totalSupply() // check token's total supply

token.safeTransferFrom(web3.eth.accounts[0] , web3.eth.accounts[1], 1) // transfer token (token id 1) from 0'th account to 1st account

token.ownerOf(1) // check owner of token id 1

token._mint(web3.eth.accounts[0]) // will mint another token

token.approve(web3.eth.accounts[3] , 2) // approve token id 2 to  account[3]

token.safeTransferFrom(web3.eth.accounts[0] , web3.eth.accounts[1], 2 , {from:web3.eth.accounts[3]})   // Note that we are adding {from:web3.eth.accounts[3]}, this mean that we are invoking this function using account[3]

You can go ahead and test all methods (OpenZeppelin is a well-tested library so you don’t need to worry about the functionality provided by the library). You should instead be focusing on testing any functionality added by you 😎

Conclusion

Today we have created a Non-Fungible Token using Solidity on Ethereum. NFT tokens are mostly used as collectibles and have a variety of other use-cases, such as representing real-estate, artwork, certificates, loans, etc… There is a big use-case in gaming where one can use NFTs to represent items and collectibles across games and platforms. This is really exciting!

What’s Next?

In the next article, we will convert this into an upgradable contract using ZeppelinOS… So subscribe & stay tuned!

Let us know what you want to learn about in the comment section.👇


Need help with your project or have questions? Contact us via this form, on Twitter @QuickNode, or ping us on Discord!

About QuickNode

QuickNode is building infrastructure to support the future of Web3. Since 2017, we’ve worked with hundreds of developers and companies, helping scale dApps and providing high-performance access to 16+ blockchains. Subscribe to our newsletter for more content like this and stay in the loop with what’s happening in Web3! 😃