Jumping into Solidity — The ERC721 Standard (Part 2)

In the first article of this series, I introduced the concept of a Non-Fungible Token (NFT), and the need for the ERC721 (draft) Standard. In this article, we’ll be taking a first look at the ERC721 Standard interface, and breaking down some of its requirements. There will also be a brief but important detour to talk about the ERC165 Standard.

“A displayed content of a toolkit displayed showing tools like hammer, axe, box cutter, iron and flashlight” by Todd Quackenbush on Unsplash

Interfaces and the ERC165 Standard

The ERC721 Standard states that:

“Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces”

If you’ve never had to write a Solidity contract that works with contracts written by other developers, you may be wondering, “What’s an interface?”. And regardless you’re probably also wondering, “What’s the ERC621 interface?”. So let’s answer both those questions.

Interfaces

An interface is basically an abstract contract, but the only thing you can define are unimplemented functions. It’s an outline, written in Solidity code, which ensures contracts written by other developers all work well together, without having to know each other’s code base.

For example, if an interface defines the function balanceOf as

function balanceOf(address _owner) external view returns (uint256);

then you know that any contract which implements that interface will have a function called balanceOf which takes one argument (address) and returns one value (uint256). You also know that the mutability of this function is view, meaning that the function is able to read the contract state, but not modify it, and that it has the external modifier, which has implications for gas consumption, and how that function should be called. If your contract function’s modifiers or the returns don’t match those defined in the interface, it will cause your compiler to give TypeErrors and fail to compile.

So by just using an interface, you’re telling other developers about some of the functions your contract has, and how to use them. Easy!

But this raises the question, without looking at your contract code, how can other developers know you’ve used a given interface? The whole point of this interface thing was so we don’t have to know each other’s code bases. The answer: The ERC165 Standard.

The ERC165 Standard

“What?!? Another ERC Standard?? How deep does this rabbit hole go?”

Don’t worry, this is the only other one we’ll be talking about, and it’s very simple — it only has one function! The ERC165 Standard is just a way of checking if your contract’s fingerprints match the fingerprint of any given interface. Let’s have a look at the ERC165 Standard interface in its entirety:

interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as
/// specified in ERC-165
/// @dev Interface identification is specified in
/// ERC-165. This function uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID`
/// and `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

So your contract has to have a function, supportsInterface, which takes a single argument representing the interfaceID (bytes4) of an interface and returns true (bool) if that interface is supported, where the interfaceID is defined in the ERC165 Standard as “the XOR of all function selectors in the interface”.

Or in plain English, you can give this function any interface’s fingerprint (the interfaceID), and it will tell you whether that matches any of your contract’s fingers.

As for getting function selectors, there are two easy ways to do it. I’ll use the balanceOf function from earlier as an example. To save you scrolling up, it was defined as:

function balanceOf(address _owner) external view returns (uint256){
//...
};

Our final ERC721 contract will have implemented this function, which is why I added the curly braces, but when it comes to function selectors it doesn’t actually matter — you’ll see why. The two ways to get the function selector of the above function are:

this.balanceOf.selector

or manually with

bytes4(keccak256("balanceOf(address)"))

Both will return 0x70a08231 and while the first one looks cleaner in my opinion, we occasionally need the second one if an interface uses overloaded functions. You’ll notice that the function selector doesn’t care about parameter names, modifiers, mutability, returns or the content of the function. Just the function name and the parameter types. That’s why I said it didn’t matter if the function was implemented or not.

But we needed “the XOR of all function selectors in the interface” for the interfaceID. Lets assume an interface consists of three functions: function1(), function2() and function3().

The interfaceID is just:

interfaceID = this.function1.selector ^ this.function2.selector ^ this.function3.selector;

Pretty simple! Now remember, the reason we began this ERC165 discussion was because our ERC721 contract needs to implement the ERC165 interface, so let’s write our ERC165 implementation (which our ERC721 contract will inherit) to wrap this up.

When it comes to Solidity, we want to minimise the gas used. Doing unnecessary calculations costs users money and it wastes the network’s resources. The ERC165 standard actually requires that the supportsInterface function “use less than 30,000 gas”. So rather than re-calculating interfaceIDs every time someone calls supportsInterface, lets keep our supported interfaceIDs in a mapping.

Start our contract, CheckERC165, like this:

contract CheckERC165 is ERC165 {

mapping (bytes4 => bool) internal supportedInterfaces;

This way, the supportsInterface function just has to return a value from the mapping, here’s that whole function implemented:

function supportsInterface(bytes4 interfaceID) external view returns (bool){
return supportedInterfaces[interfaceID];
}

Great! Our contract now implements all the functions in the ERC165 interface (there’s only one). Let’s add the ERC165 interfaceID to supportedInterfaces and bring this thing full circle.

Solidity Version 0.4.22 was recently released, giving us the easier constructor function name for our constructor. So let’s make a constructor, and in it, add the interfaceID of the ERC165 interface.

constructor() public {
supportedInterfaces[this.supportsInterface.selector] = true;
}

Now if anyone calls supportsInterface with the interfaceID of the ERC165 Standard interface ( 0x01ffc9a7), it will return true.

And that’s it for ERC165! The complete contract is available on my GitHub. We’ll be using this later with our ERC721 implementation, and it’s also a handy one to have in the bag when dealing with interfaces in general.

The ERC721 Interfaces

Now that we know about interfaces, let’s move back to ERC721. You’ll notice that the ERC721 Standard actually contains four different interfaces. One main one for a general ERC721 contract, one for contracts that can receive ERC721 tokens, and two for optional extensions which add extra functionality. We’ll ignore the extensions for now, and just have a quick look at the main interface and the receiver.

Payable Functions and Mutability

Something that immediately stood out to me was that four of the functions in the ERC721 interface have the payable modifier. Namely the two safeTransferFrom functions, transferFrom and approve. It didn’t really make sense that an ERC721 contract should always take payment every time a token is transferred, or control of a token is granted.

You can probably imagine situations where this may be the case — if you build a marketplace for your NFTs, then no doubt you’ll be dealing with payments — but situations where a token owner has to pay just to grant control of a token to someone else is harder to imagine. And in fact, the reason for the payable modifiers isn’t very sexy, it all comes down to mutability.

From the caveats section of the ERC721 Standard:

“Mutability guarantees are, in order weak to strong: payable, implicit nonpayable, view, and pure.”

So by marking these functions as payable, it was simply a way for the authors to say that there is no restriction on mutability required. In this particular instance, payable is just a way of explicitly saying nothing is restricted. Your implementations may be stricter, but not weaker.

There are two ways to deal with this, you can either:

  1. keep these functions as payable, and just return any Ether that gets sent with transactions (or keep it if that’s your design), or
  2. remove the payable modifier, but you’ll have to do the same thing in your copy of the ERC721 interface in order to avoid throwing a TypeError.

Remember from earlier that function selectors aren’t affected by modifiers, so this won’t interfere with the interfaceID. The same is true for functions marked external, you should feel free to change them to public as needed.

ERC721TokenReceiver

Before I wrap up I want to quickly cover the ERC721TokenReceiver interface. This isn’t something our token contract needs to inherit. As the name implies, it’s an interface for contracts that can receive ERC721 tokens.

Since I won’t be covering making a wallet contract in this series, lets quickly make a couple of dummy wallets that we’ll use for testing later. One valid receiver, and one invalid receiver.

For our purposes, the only important bit of information is that a valid ERC721TokenReceiver will implement the function

function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);

and return

bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))

An invalid one will either not implement that function, or return literally anything else. So lets define our two receivers as follows:

contract ValidReceiver is ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4){
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}

}

and

contract InvalidReceiver is ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4){
return bytes4(keccak256("some invalid return data"));
}

}

We won’t use these until much later when we’re doing tests, so just save them and keep them in your pocket. I just wanted to cover them now while we’re doing an overview of the interfaces.

Wrapping up

So after all that, you should be familiar with interfaces, and have an ERC165 implementation which you can use for indicating which interfaces are implemented by your contract. We also covered mutability guarantees in the ERC721 standard, and quickly wrote a couple of dummy wallet contracts for testing later.

In the next post, we’ll use everything we’ve covered so far, and start writing the actual ERC721 token contract.

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

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.