Jumping into Solidity — The ERC721 Standard (Part 6)

Well fellow codeslingers, by now we’ve learned about and written our basic ERC721 implementation, and for those of you who were brave enough to stick around for Part 5, we’ve even tested our code. Now it’s time to give our contract a little extra juice by adding the Metadata and Enumerable extensions. In this article we’ll cover the Metadata extension, and next time Enumerable.

“A man welding a steel sheet in a workshop” by Uğur Gürcüoğlu on Unsplash

So what are these extensions?

The Metadata and Enumerable extensions are small, optional additions to the standard which give your token a little bit of extra functionality. The Metadata extension gives your token contract a name and symbol (like an ERC20 token) and gives each individual token some extra data that makes it unique, while the Enumerable extension makes it easier to sort through your tokens (other than just retrieving based on tokenId).

As I said, these are optional, so you’re free to not include them at all if you don’t need them. And even if you want to add custom metadata and enumeration functionality which doesn’t conform to the extensions, you’re free to do that too, and it won’t be a breach of the basic ERC721 standard.

However, as always, the benefit of conforming to a standard means that nobody will need to know your codebase in order to interact with your contract. So unless you have a particular reason for not wanting to use the extensions, I suggest you adhere to them if adding metadata or enumeration to your token contract.

Anyway, let’s get straight to the biscuits.

The Metadata Extension

Before we start discussing its methods, we need to declare our contract as both an extension of our original TokenERC721.sol contract (to save us having to re-do all that hard work from the last 5 articles) and an implementation of the ERC721Metadata extension interface.

contract TokenERC721Metadata is TokenERC721, ERC721Metadata {

The Metadata extension comprises of three functions,

function name() external view returns (string _name);
function symbol() external view returns (string _symbol);
function tokenURI(uint256 _tokenId) external view returns (string);

The name and symbol functions just return the name and symbol of your token contract. Until a few weeks ago, the mutability of these functions in the standard interface was set to pure. This matters, because as discussed in Part 1, the mutability of functions can be stricter than that required by the standard, but not looser, and pure is the strictest mutability.

So what? So that means you had to choose between either hard-coding the name and symbol into your token contract, or creating a contract that didn’t fully conform to the ERC721 Metadata standard extension. The rationale behind the pure mutability was that it meant the metadata was guaranteed not to change for the life of the contract, which has benefits for caching and indexing by third parties, but it also meant you couldn’t declare this metadata during contract creation.

But fear not my friends, because I raised the issue with the ERC721 standard people (remember, it’s still not finalised), and after a pretty efficient discussion and review process, the decision was made to change the mutability for these two functions to view.

Why am I telling you this rather than just saying “they’re view”? Because within this lies a fairly important lesson. If you’re reading these articles you’ve probably looked at at least one other ERC721 implementation or tutorial, and because the standard is not finalised, and because there are many implementations out there that for one reason or another don’t bother to fully adhere to the standard, there’s a good chance that something you learned was wrong.

The lesson isn’t that these articles are the absolute authority on ERC721. When I finish writing this I’m going to bed, and they could change something in the standard and I’d have no idea, plus I make mistakes just like anybody.

The lesson is that when writing a token to an ERC standard, your first and foremost resource should be the standard’s EIP. This will tell you exactly what you do and don’t need to do, and exactly how your contract must work in order to comply. If you just copy someone else’s implementation, you risk picking up their mistakes, and unless your needs completely overlap with theirs, your contract will probably include some stuff it didn’t need too (which is a waste of gas!).

Anyway, now that you’ve all bookmarked the EIPs page we can get back to our contract. But as a side note, since I suggested a change to the standard and it was accepted, I’m now a contributor! Woo!

So with the name and symbol functions now being view, we have a couple of options. Although it’s not explicitly stated anywhere in the ERC721 standard, the idea is that these two probably shouldn’t change for the life of the contract. With that in mind, it’s best to either set them during contract creation (with some extra parameters in your constructor), or hard-code them in their respective functions. Given that it makes our contract a little more reusable, I’ve opted for the former.

We’ll need to store the name and symbol in strings, so let’s declare them first. I’m also going to declare something called uriBase which I’ll discuss in a minute, but bare with me for now because it’s much nicer to declare all our contract’s variables at the start.

string private __name;
string private __symbol;
bytes private __uriBase;

I’ve used the double underscores because the name and symbol methods use versions with no underscore and only one underscore, and we don’t want any collisions… Also double underscores look cool.

The constructor

Now that we have all our variables ready, let’s write the new constructor. We’re still going to take _initialSupply as an argument and pass it to the constructor of our TokenERC721 contract, and also take an argument for each of the variables declared above and set them. We’ll also add the new interface to supportedInterfaces(check out Part 2 if you don’t know what that’s all about).

constructor(uint _initialSupply, string _name, string _symbol, string _uriBase) public TokenERC721(_initialSupply){
__name = _name;
__symbol = _symbol;
__uriBase = bytes(_uriBase);

//Add to ERC165 Interface Check
supportedInterfaces[
this.name.selector ^
this.symbol.selector ^
this.tokenURI.selector
] = true;
}

You’ll notice that I’m taking _uriBase as a string and immediately converting it to bytes. I’ll cover why later, but basically every time it gets used it has to be converted to bytes first, so this is just saving energy down the track.

The name and token functions

With those variables set, the name and token symbols are now simple, the interface declares a variables that will be returned at the end of the function, so we just have to set these variables equal to the ones we declared before and the rest works like magic:

function name() external view returns (string _name){
_name = __name;
}

function symbol() external view returns (string _symbol){
_symbol = __symbol;
}

A quick note on these functions, if you want to use the Metadata extension but for whatever reason don’t like these functions, it’s perfectly acceptable to just return an empty string.

Now that they’re done, let’s see what this URI business is all about…

The tokenURI function

The tokenURI function takes a tokenId as its only argument, and returns a URI which points to metadata about that specific token. Depending on your implementation and its specific needs, you may want to manually set each token’s URI whenever it’s minted, or you may want some authority to be able to change it in the future. You may even want to allow token holders to change the URI.

There’s no right or wrong way for how this should work, the only requirements are that when this function is called with a given tokenId, it returns a string with the URI pointing to that token’s metadata, or throws if there’s no token with that tokenId.

When I designed my implementation of the metadata extension, I was aiming to maximise scalability. I was also designing for a situation where all my tokens’ metadata would be stored in a single location. Some of you may be screaming “but that’s not decentralised!”, and yep, that’s true. Unless your tokenURIs point to some sort of completely decentralised storage, this is going to be a point of centralisation to a certain extent.

But that may not matter. The reason for your ERC721 implementation will probably have some degree of centralisation, whether it’s a DApp with a frontend hosted on AWS, or even if it represents deeds to a house. Nobody is going to honour your blockchain-based house-trading system if society breaks down and we’re all looting (or something less extreme affects the housing market) —so let’s not go overboard with this decentralisation talk. But as I said before, you’re free to write the tokenURI function as you please. The important thing is that our ERC721 token contract has decentralised ownership of the tokens.

With the scalability and centralised storage in mind, my token implements the tokenURI function by having a URI base and appending a tokenId to get the token’s full URI. This means issuing new tokens doesn’t use any extra gas for the metadata extension, which is important if your implementation will have thousands of tokens!

If the URI of the token with the tokenId of 19 were:

http://www.someTokenImplementation.com/tokens/19

Then the uriBase would be:

http://www.someTokenImplementation.com/tokens/

Note the “/” at the end.

Our tokenURI function just concatenates the tokenId onto the end of this string and returns that. Sounds easy, but actually in Solidity it’s a little complex. Our tokenId is a uint256 while our uriBase is a string, and there are no explicit or implicit conversions between the two.

But we can convert between string and bytes pretty easily, in Solidity they’re basically the same thing. So what we’re going to do is divide the tokenId by 10, take the remainder, find the number that represents that digit in UTF-8 encoding, store that number in a byte array, and repeat until we’ve seen all the digits. (Note that for digits 0–9, the corresponding UTF-8 codes are 48–57 respectively. So we just have to add remainder+48 to get the UTF-8 code.)

Then we’ll create a new byte array the length of the uriBase + the length length of the digits byte array, then iterate through both arrays, adding their values to the new byte array, until finally we have a byte array with our token’s full URI. We then just explicitly convert that to a string and return it.

This may all sound confusing, but hopefully it makes a little more sense when you see the code:

function tokenURI(uint256 _tokenId) external view returns (string){
require(isValidToken(_tokenId));

//prepare our tokenId's byte array
uint maxLength = 78;
bytes memory reversed = new bytes(maxLength);
uint i = 0;
    //loop through and add byte values to the array
while (_tokenId != 0) {
uint remainder = _tokenId % 10;
_tokenId /= 10;
reversed[i++] = byte(48 + remainder);
}
    //prepare the final array
bytes memory s = new bytes(__uriBase.length + i);
uint j;
    //add the base to the final array
for (j = 0; j < __uriBase.length; j++) {
s[j] = __uriBase[j];
}
    //add the tokenId to the final array
for (j = 0; j < i; j++) {
s[j + __uriBase.length] = reversed[i - 1 - j];
}
    //turn it into a string and return it
return string(s);
}

The reason 78 was chosen for the maxLength of our tokenId’s byte array is because that’s the length of the largest possible uint256. For the curious, that number is:
115792089237316195423570985008687907853269984665640564039457584007913129639935

That’s a big number!

Also note that I’ve changed the visibility from external to public, which is allowed by the standard. The reason for doing this is that Solidity is a little less relaxed with arguments for external functions, so it throws and error if you try to manipulate the tokenId variable.

Regardless of how you decide to implement your tokenURI function, your first line should always do something similar to mine, make sure you check if it’s a valid tokenId!

And there’s our tokenURI function. The URI can point to whatever you’d like, however the ERC721 people make some recommendations regarding formatting. They suggest that it point to a JSON file which conforms to the “ERC721 Metadata JSON Schema”. That just means it follows this structure:

{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents",
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents",
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive.",
}
}
}

This is just a recommendation, so you’re free to ignore it, or add extra data. But as always, the benefit of conforming means it’ll be easier for others to use your data.

Wrapping up

So there you have it, your ERC721 token now has metadata! You also hopefully bookmarked the EIPs repository too, and considered some of the practicalities of how decentralised your DApp will be.

Next time we’ll be writing our implementation of the Enumerable extension; it’s not as sexy as the Metadata extension, but for many applications arguably far more important.

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

Like what you read? Give Andrew Parker a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.