Jumping into Solidity — The ERC721 Standard (Part 3)

Andrew Parker
8 min readMay 3, 2018

--

So far in this series, I’ve introduced the basics of Non-Fungibles and ERC721, then looked at the Standard interface and some of it’s requirements. In this article we’ll make some design decisions about our ERC721 contract and actually begin writing it.

“Birds flying around a half-built building and construction site” by 贝莉儿 NG on Unsplash

Design choices, scalability and security

As discussed in Part 1 of this series, the ERC721 Standard is for contracts that manage non-fungible assets on the Ethereum blockchain. The assets that your ERC721 tokens represent will influence some of the design choices for how your contract works, most notably how new tokens are created.

For example, in the game Cryptokitties, players are able to “breed” their Kitties, which creates new Kitties (tokens). However, if your ERC721 tokens represent something more tangible, like concert tickets, you may not want token holders to be able to create more tokens. In some instances you may even want token holders to be able to “burn” their tokens, effectively destroying them.

The ERC721 standard has no restrictions as to how tokens are created or burned, or who can create them, so you are free to make these decisions according to your needs.

Design choices

In my contract, for both scalability and simplicity, tokens will be created in 2 ways:

  1. An initial supply of tokens will be defined during token creation, all of which will initially belong to the contract creator.
  2. A function, which is only callable by the contract creator, will issue more tokens when called. These new tokens will also initially belong to the contract creator.

You’ll see how this lends itself to scalability shortly. As for burning, there will be a function that burns a given token which is only callable by the token owner (or an authorised operator).

Scalability

As each token has one owner, I’ll have a mapping for tracking token ownership, defined as:

mapping(uint256 => address) internal owners;

The standard says that tokens can’t belong to the zero address (0x0), so in the owners mapping, we can let 0x0 fall back to the contract creator. I’ll go into more detail when we’re writing the ownerOf function, but it means that if we issue 10 tokens we don’t have to explicitly set the ownership of each one to the contract creator.

To keep things simple, because tokens that belong to 0x0 fall back to the contract creator, I won’t allow the token contract to be transferred to another owner.

Security

The last thing we need to cover before we get started is extremely important — protecting against overflows. If you don’t know about overflows, an integer type has a maximum and minimum value it can hold. In the case of uint256, the minimum value it can hold is 0, and the maximum it can hold is 115792089237316195423570985008687907853269984665640564039457584007913129639935.

If you have a uint256 whose value is 0 and you subtract 1 from it, it’s value will be that big crazy number. And if you add 1 to that, it will be 0 again. Basically the uint can’t count any higher so it just starts from the bottom again. You get similar behaviour when multiplying two numbers whose result is bigger than the maximum value a variable can hold.

Malicious (or stupid) people can exploit this behaviour to do all sorts of nasty things with your code if you aren’t careful. Just last week a number of ERC20 tokens were removed from trading on a number of exchanges due to a vulnerability in their token contracts related to overflows. Somebody clever found a way to send himself a crazy number of tokens that he didn’t actually have, and then quickly sold them. For people affected it was absolute chaos.

But don’t despair, the kind people over at OpenZeppelin have written a library to help prevent such attacks. SafeMath.sol is used to double check that overflows aren’t occurring during important calculations.

By declaring using SafeMath for uint256; at the top of your contract, it means the following calculation:

c = a + b 

which is at risk of overflows, becomes:

c = a.add(b);

which will throw if it overflows. It’s a little more costly than a regular addition, so we should save SafeMath operations for when there’s a possibility of overflow (from unpredictable user input — including from the contract owner/creator!).

Note: The only time I use SafeMath in this contract is for issuing extra tokens. So if your contract is designed differently you may not need to use this library; however, it’s a very useful item to have on your tool belt.

The Contract

We’re finally ready to begin writing our contract, so let’s get straight to it. We’ll be using Solidity 0.4.22, and I’ll list the dependencies.

pragma solidity ^0.4.22;import "./CheckERC165.sol";
import "./standard/ERC721.sol";
import "./standard/ERC721TokenReceiver.sol";
import "./libraries/SafeMath.sol";

A quick note on ERC721.sol (the ERC721 Standard interface), I’ve made some slight modifications to the mutability and visibility of some functions. But as mentioned in the previous article, this is allowed by the standard. I’ll discuss the changes as they come up, and all files are available on my GitHub.

Next we declare our contract, remembering to extend the ERC721 interface and our implementation of ERC165, and include SafeMath for uint256.

contract TokenERC721 is ERC721, CheckERC165{
using SafeMath for uint256;

Variables

It’s always good practice to declare your contract variables at the top of your contract, as it makes it much easier to read. I’ll blitz them all now, with a brief explanation for each, but I’ll go into more detail when we actually use them. Note: I’m using internal visibility rather than private in order that contracts which extend our token contract (like the “Metadata” and “Enumerable” variants) have access to them.

// The address of the contract creator
address internal
creator;
// The highest valid tokenId, for checking if a tokenId is valid
uint256 internal
maxId;
// A mapping storing the balance of each address
mapping
(address => uint256) internal balances;
// A mapping of burnt tokens, for checking if a tokenId is valid
// Not needed if your token can't be burnt
mapping
(uint256 => bool) internal burned;
// A mapping of token owners
mapping
(uint256 => address) internal owners;
// A mapping of the "approved" address for each token
mapping
(uint256 => address) internal allowance;
// A nested mapping for managing "operators"
mapping
(address => mapping (address => bool)) internal authorised;

The constructor

Next we’ll define the constructor. Depending on what your token needs to do, your constructor will most likely differ from mine. But you should still include the ERC165 part regardless. I’ll discuss a few quirks after.

constructor(uint _initialSupply) public CheckERC165(){
// Store the address of the creator
creator = msg.sender;

// All initial tokens belong to creator, so set the balance
balances[msg.sender] = _initialSupply;
// Set maxId to number of tokens
maxId = _initialSupply;
//Add to ERC165 Interface Check
supportedInterfaces[
this.balanceOf.selector ^
this.ownerOf.selector ^
bytes4(keccak256("safeTransferFrom(address,address,uint256"))^
bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes"))^
this.transferFrom.selector ^
this.approve.selector ^
this.setApprovalForAll.selector ^
this.getApproved.selector ^
this.isApprovedForAll.selector
] = true;

}

Some of you may have seen maxId = _initialSupply and immediately thought “Is this a typo? Or do tokenIds start at 1 and not 0? What kind of a sociopath starts indices at 1?!?”

No, this isn’t a typo. The reason I chose to start tokenIds at 1 is because 0 is quite a handy one to have when you start adding extra layers to your contract (beyond the scope of the ERC721 standard). Since uints default to 0, and using the delete operation on a variable gives a gas refund, it can be a shorthand way of saying a reference to a token is invalid. And actually the tokenId isn’t an index. The standard just requires that each token have its own unique uint that never changes, it doesn’t much care about how you determine them. To quote from the standard:

“While some ERC-721 smart contracts may find it convenient to start with ID 0 and simply increment by one for each new NFT, callers SHALL NOT assume that ID numbers have any specific pattern to them, and MUST treat the ID as a ‘black box’.”

You may have also noticed that for the interfaceId, I used the pattern bytes4(keccak256(“... for two of the function signatures. This is because the ERC721 Standard interface overloads safeTransferFrom, so just calling this.safeTransfeFrom.selector will result in a TypeError.

Valid tokens

All of the functions with _tokenId as an argument require that we check if this tokenId corresponds to a valid token. So let’s write a reusable internal function that does that check.

function isValidToken(uint256 _tokenId) internal view returns(bool){
return _tokenId != 0 && _tokenId <= maxId && !burned[_tokenId];
}

This will return false if a given _tokenId is 0, greater than maxId, or corresponds to a burnt token. If your token doesn’t allow burning, you can remove the && !burned[_tokenId] part.

Balance and owners

Next let’s cover two of the basic getter functions, balanceOf, and ownerOf. They’re pretty simple; one returns the balance of a given address, the other returns the owner of a given token.

balanceOf is simple, it just reads a value from our balances mapping:

function balanceOf(address _owner) external view returns (uint256){
return balances[_owner];
}

ownerOf is a little more complex:

function ownerOf(uint256 _tokenId) public view returns(address){
require(isValidToken(_tokenId));
if(owners[_tokenId] != 0x0 ){
return owners[_tokenId];
}else{
return creator;
}
}

First we use our isValidToken function from before to check that it’s a valid token. Because I’m making tokens with an owner of 0x0 revert to the contract creator, I also have to add a check for that. But depending on your contract design, you may be able to omit this check and just return owners[_tokenId].

Notice also that I’ve changed the visibility both here and in the interface, from external to public. This is because our ownerOf function ends up getting called fairly often from within our contract, and that’s cheaper if it’s not external.

Issue and Burn

The last thing I want to add today is just my implementation’s issue and burn functions. Unless you are copying my implementation (which you are welcome to do), it is unlikely that these will be useful. However, in order that I can devote all of the next article to covering approvals, operators and transfers, I’ll cover them now:

function issueTokens(uint256 _extraTokens) public{ 
// Make sure only the contract creator can call this
require(msg.sender == creator);
balances[msg.sender] = balances[msg.sender].add(_extraTokens);

//We have to emit an event for each token that gets created
for(uint i = maxId.add(1); i <= maxId.add(_extraTokens); i++){
emit Transfer(0x0, creator, i);
}

maxId += _extraTokens; //<- SafeMath for this operation
// was done in for loop above
}

Notice that in both the cases of addition I’ve used the add function from SafeMath. Otherwise, a large enough value for _extraTokens could cause all sorts of havoc.

function burnToken(uint256 _tokenId) external{
address owner = ownerOf(_tokenId);
require ( owner == msg.sender
|| allowance[_tokenId] == msg.sender
|| authorised[owner][msg.sender]
);
burned[_tokenId] = true;
balances[owner]--;
emit Transfer(owner, 0x0, _tokenId);
}

Don’t worry too much about allowance or authorised yet, I’ll explain them fully in the next article. We’ll see this exact pattern there as well, so I’ll be able to explain it with a little more context.

Wrapping up

Today we covered some design choices, as well as learning about the dangers of overflows and how to prevent them. If you’ve been writing your own contract along with me, it should be starting to take shape now. We’ve basically set up the framework and all that’s left to do next is add the moving parts.

In the next article, we’ll finish off our contract by adding approvals, operators and the different variants of the transfer function.

Next: Jumping into Solidity — The ERC721 Standard (Part 4)

--

--

Andrew Parker
Andrew Parker

Written by Andrew Parker

Codeslinger. Melbourne based Solidity developer for hire.

Responses (4)