Another common standard is ERC-721, a token standard for Non-Fungible Tokens (NFTs), most commonly known for collectibles like CryptoKitties. It’s not a replacement for ERC-20, but more of an addition. The difference between ERC-20 and ERC-721 is that ERC-721 tokens are unique, or as the name suggests, non-fungible. They can’t be copied or replaced with another ERC-721 token.
Lets continue to use CryptoKitties as an example. Each CryptoKitty is a unique token and has its own properties, like type, color, traits, etc. Since these properties are different for each CryptoKitty, you cannot replace one for another. One DAI token, on the other hand, can be replaced with another DAI token since they are all the same.
Gaming and collectibles are just a few applications of ERC-721 tokens, but they can be used for many things, including ownership of physical objects that are “unique.” Here are some examples:
- (Digital) art
- Real estate
The ERC-721 standard defines multiple interfaces, but below you’ll find the main ERC-721 interface.
At first glance, ERC-721 may appear similar to ERC-20. One of the big differences is the use of the
tokenId parameter. Since each NFT is unique, each has its own token identifier, used to represent an individual token.
Note: “NFT” and “ERC-721 token” are used interchangeably here.
Another big difference is
safeTransferFrom, which is a safety measure of the ERC-721 standard, more specifically, the
ERC721TokenReceiver interface. It checks if the target address is a smart contract and will try to call the
onERC721Received function if that’s the case. This function (if it exists) must return
0xf0b9e5ba, which is equal to
bytes4(keccak256("onERC721Received(address,uint256,bytes)")). If it doesn’t return this exact value or if the function does not exist, the contract will throw an error and the transaction will be reverted. This prevents anyone from accidentally sending their NFTs to a smart contract that doesn’t support them. The
transferFrom function won’t check if the target address is capable of receiving ERC-721 tokens and should not be used unless the caller (the smart contract calling the function or person sending the transaction) explicitly checks this before transferring.
ERC-721 contracts must also implement ERC-165, a standard used to detect what interfaces a smart contract implements. Each interface/standard has an identifier and the
supportsInterface function can be queried to see if the contract implements that specific interface ID. We won’t go into much detail on ERC-165 in this article, but if you’re interested and want to learn more about it, I suggest you check it out here.
ERC-721 contracts also have the
approve function just like ERC-20. It can be used to approve transferring a single token (specified by the token ID). That’s why there’s also the
setApprovalForAll function, which would allow the
operator to send all tokens owned by the address.
How NFTs Are Stored Internally
This explanation is based on the ERC-721 implementation by OpenZeppelin, found here.
In a simple ERC-20 contract, tokens are stored in a mapping, with the address and the balance. Since every individual NFT has it’s own properties, storing NFTs in a smart contract is not as simple as keeping a list of the number of NFTs per address. As mentioned above, each NFT has it’s own token ID, so a smart contract needs to keep track of all the token IDs and their respective owners.
The easiest way to accomplish this is to use a mapping, which maps the token ID to the address of the owner.
mapping (uint256 => address) private _tokenOwner;
Since the ERC-721 standard has a function to get the number of NFTs for an address (
balanceOf), a second mapping can be used to keep track of that.
mapping (address => uint256) private _ownedTokensCount;
Alternatively, the contract could go over all the token IDs in the first mapping and check if the address is the address being queried. The problem with this is that loading an item from a mapping consumes gas. Even though this mapping is primarily used for querying (which doesn’t consume gas), it’s still not possible to go over the block gas limit when querying a smart contract.
Transferring a token works by calling
transferFrom), after which both mappings are updated.
The token with ID 1 is transferred to the address 0xabcd…1234, so this address now owns 2 tokens.
Creating NFTs and Storing Token Metadata
When an ERC-20 token is being minted, we can simply increase the total supply and add the tokens to an address. For ERC-721 tokens, however, we need to keep track of the token metadata somehow. In the case of CryptoKitties, that includes things like the breed, colour, etc.
Storing data on the blockchain is expensive. If you read my previous article on transaction input data, you may know that every non-zero byte of data consumes 68 units of gas, just to send it to the blockchain without storing it. That means sending a file of 100 kB (e.g. a small image) to the blockchain, costs about 6,800,000 units of gas. Assuming a gas price of 5 Gwei, that would cost you about 0.034 Ether, or about $5.78 at the time of writing, for one image, and again, that’s without even storing the image on the blockchain. That’s why developers often look at alternatives for storing data in a secure, decentralized way instead.
ERC-721 does define a method for storing token metadata, but this method is not a requirement. Developers are free to implement their own mechanisms. It uses URIs (Uniform Resource Identifiers) which reference a JSON file, which conforms to the “ERC-721 Metadata JSON Schema”.
As you may see, this schema is not very flexible. Some token developers choose to use IPFS (InterPlanetary File System), a decentralized protocol to share files. Then the IPFS hash can be stored in the token metadata.
mint function is called (e.g. when a new CryptoKittie is born), the URI or IPFS hash of the file with the metadata is specified and saved in the contract.