Jumping into Solidity — The ERC721 Standard (Part 3)

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.

Design choices

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

  1. 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.

Scalability

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

mapping(uint256 => address) internal owners;

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.

c = a + b 
c = a.add(b);

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

}

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

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.

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

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

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.

Codeslinger. Melbourne based Solidity developer for hire.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store