Jumping into Solidity — The ERC721 Standard (Part 7)

By now our ERC721 implementation is really starting to take shape. We’ve written our basic implementation, the true codeslingers among you even tested it, and in the last article we learned about extensions and added Metadata to our contract. Today we’re going to round it off by implementing the Enumerable extension which will really give our contract some legs.

Photo by Sanwal Deen on Unsplash

A quick note for the long time readers

In my last article, I went on a slight tangent where I urged you all to bookmark the EIPs page, and always use that as your main reference when writing a contract to an ERC standard (in this case the one for ERC721). The reason being that other resources can have mistakes, and up until a few days ago ERC721 wasn’t even finalised.

onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data)

The Enumerable Extension

The Enumerable extension basically makes it easier for people to organise our NFTs. It bakes in some functions that let websites, apps, other contracts etc. sort them by index, which makes them easy to list without requiring any extra data.

function totalSupply() external view returns (uint256);function tokenByIndex(uint256 _index) external view returns (uint256);function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
contract TokenERC721Enumerable is TokenERC721, ERC721Enumerable {

Working with arrays in Solidity

Depending what programming languages you have experience with, you may be accustomed to being able to easily manipulate arrays without any stress at all.

uint[] a = [1,2,3,4];
delete a[1];
a = [1,0,3,4];
a = [1,4,3];

Contract variables and constructor

As you may have figured out by now, we’re actually adding two types of list. The first is just a list of all the tokens that have been made, which is what the tokenByIndex function deals with. There will only be one of these, managed by the following array:

uint[] internal tokenIndexes;
mapping(uint => uint) internal indexTokens;
mapping(address => uint[]) internal ownerTokenIndexes;
mapping(uint => uint) internal tokenTokenIndexes;
ownerTokenIndexes[ownerAddress][tokenIndex] = tokenId;
tokenTokenIndexes[tokenId] = tokenIndex;
constructor(uint _initialSupply) public TokenERC721(_initialSupply){
for(uint i = 0; i < _initialSupply; i++){
tokenTokenIndexes[i+1] = i;
ownerTokenIndexes[creator].push(i+1);
tokenIndexes.push(i+1);
indexTokens[i + 1] = i;
}

//Add to ERC165 Interface Check
supportedInterfaces[
this.totalSupply.selector ^
this.tokenByIndex.selector ^
this.tokenOfOwnerByIndex.selector
] = true;
}

totalSupply

I’m not sure just how intuitive any of this has been so far so let’s knock out some easy stuff while all that soaks in. The totalSupply function is extremely simple, we just return the length of our tokenIndexes array. Since every token is in there, the length of the array will naturally be the number of tokens.

function totalSupply() external view returns (uint256){
return tokenIndexes.length;
}

tokenByIndex

The tokenByIndex function is equally simple. The index just refers to the position in this array, so we just return whatever value is at that position. The only other thing we have to do is make sure the index being checked is less than the total number of tokens.

function tokenByIndex(uint256 _index) external view returns(uint256){
require(_index < tokenIndexes.length);
return tokenIndexes[_index];
}

tokenOfOwnerByIndex

Even this function is simple. It’s basically the same as the last one, except instead we check if the index is less than the owner’s balance, and return a value from the relevant ownerTokenIndexes array.

function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256){
require(_index < balances[_owner]);
return ownerTokenIndexes[_owner][_index];
}

Modifying transferFrom

I think the best way to explain this is with an example. Suppose we have two token owners, call them 0xAAA and 0xBBB.

ownerTokenIndexes[0xAAA] => [1, 2, 3, 4];
ownerTokenIndexes[0xBBB] => [5, 6, 7, 8];
tokenTokenIndexes[1] => 0;
tokenTokenIndexes[2] => 1;
tokenTokenIndexes[3] => 2;
tokenTokenIndexes[4] => 3;
tokenTokenIndexes[5] => 0;
tokenTokenIndexes[6] => 1;
tokenTokenIndexes[7] => 2;
tokenTokenIndexes[8] => 3;
  1. Move 4 from the end of ownerTokenIndexes[0xAAA] to fill the newly empty slot.
  2. Update tokenTokenIndexes[4] to reflect the new index of 4.
  3. Add 2 to the end of ownerTokenIndexes[0xBBB]
  4. Update tokenTokenIndexes[2] to reflect the new index of 2.
//Temporarily store the old index because we need to reference it a few times
uint
oldIndex = tokenTokenIndexes[2];
//Overwrite the value at oldIndex (2) with whatever is
// at the end of the ownerTokenIndexes[0xAAA] array (
4).
ownerTokenIndexes[0xAAA][oldIndex] =
ownerTokenIndexes[0xAAA][ ownerTokenIndexes[0xAAA].length - 1 ];
//Update tokenTokenIndexes for whatever token is
// now at ownerTokenIndexes[0xAAA][oldIndex] (
4).
tokenTokenIndexes[ownerTokenIndexes[0xAAA][oldIndex]] = oldIndex;
//Update tokentkenIndexes[2] to be the length of
// ownerTokenIndexes[0xBBB], since this will be its new position.
tokenTokenIndexes[2] = ownerTokenIndexes[0xBBB].length;
//Add 2 to the end of ownerTokenIndexes[0xBBB].ownerTokenIndexes[0xBBB].push(2);
//Decrement ownerTokenIndexes[0xAAA].length by 1ownerTokenIndexes[0xAAA].length--;
function transferFrom(address _from, address _to, uint256 _tokenId) public {
address owner = ownerOf(_tokenId);
require ( owner == msg.sender
|| allowance[_tokenId] == msg.sender
|| authorised[owner][msg.sender]
);
require(owner == _from);
require(_to != 0x0);

emit
Transfer(_from, _to, _tokenId);

owners[_tokenId] = _to;
balances[_from]--;
balances[_to]++;
if(allowance[_tokenId] != 0x0){
delete allowance[_tokenId];
}


//=== Enumerable Additions ===
uint oldIndex = tokenTokenIndexes[_tokenId];
//If the token isn't the last one in the owner's index
if(oldIndex != ownerTokenIndexes[_from].length - 1){
//Move the old one in the index list
ownerTokenIndexes[_from][oldIndex] = ownerTokenIndexes[_from][ownerTokenIndexes[_from].length - 1];
//Update the token's reference to its place in the index list
tokenTokenIndexes[ownerTokenIndexes[_from][oldIndex]] = oldIndex;
}
ownerTokenIndexes[_from].length--;
tokenTokenIndexes[_tokenId] = ownerTokenIndexes[_to].length;
ownerTokenIndexes[_to].push(_tokenId);
}

burnToken

The change to burnToken is pretty much the same as the change to transferFrom, except we don’t have to worry about steps 5 and 6 where we added our token to ownerTokenIndexes[_to].

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);
//=== Enumerable Additions ===
uint oldIndex = tokenTokenIndexes[_tokenId];
if(oldIndex != ownerTokenIndexes[owner].length - 1){ //Move last token to old index
ownerTokenIndexes[owner][oldIndex] = ownerTokenIndexes[owner][ownerTokenIndexes[owner].length - 1];
//update token self reference to new position
tokenTokenIndexes[ownerTokenIndexes[owner][oldIndex]] = oldIndex;
}
ownerTokenIndexes[owner].length--;
delete tokenTokenIndexes[_tokenId];
//This part deals with tokenIndexes
oldIndex = indexTokens[_tokenId];
if(oldIndex != tokenIndexes.length - 1){
//Move last token to old index
tokenIndexes[oldIndex] = tokenIndexes[tokenIndexes.length - 1];
}
tokenIndexes.length--;

}

issueTokens

Lastly we have our issueTokens function. As previously discussed, even if your contract allows issuing of new tokens, it may not do it in batches like mine. But no matter how you’re minting them, if you’ve been following this series to create your contract, your contract should do something like this whenever a new token is created:

uint newId = maxId.add(1); //Using SafeMath to prevent overflows.//Assign the new token its index in ownerTokenIndexes
tokenTokenIndexes[newId] = ownerTokenIndexes[msg.sender].length;
//Add the tokenId to the end of ownerTokenIndexes
ownerTokenIndexes[creator].push(newId);
//Add the token to the end of tokenIndexes and indexTokens
indexTokens[thisId] = tokenIndexes.length;
tokenIndexes.push(thisId);
function issueTokens(uint256 _extraTokens) public{
require(msg.sender == creator);
balances[msg.sender] = balances[msg.sender].add(_extraTokens);

uint thisId; //We'll reuse this for each iteration below.
for(uint i = 0; i < _extraTokens; i++){
thisId = maxId.add(i).add(1); //SafeMath to be safe!
//Assign the new token its index in ownerTokenIndexes
tokenTokenIndexes[thisId] = ownerTokenIndexes[creator].length;
//Add the tokenId to the end of ownerTokenIndexes
ownerTokenIndexes[creator].push(thisId);

//Add the token to the end of tokenIndexes
indexTokens[thisId] = tokenIndexes.length;
tokenIndexes.push(thisId);


emit Transfer(0x0, creator, thisId);
}
//Note: This used to be before the loop
// (loop was slightly different).
maxId = maxId.add(_extraTokens);

}

Wrapping up

So after all that, your token now has the Enumerable extension! We’ve implemented all the new functions, as well as modified our minting, burning and transfer functions. This should make it easier for other people to incorporate your token into their projects, which benefits everybody.

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