Jumping into Solidity — The ERC721 Standard (Part 4)

In my last article, we started writing our ERC721 contract. There was plenty of preamble and explanation in the first three articles (Part 1, Part 2, Part 3), so I’m going to assume you’re playing along at home and jump straight back into the code.

Photo by Rémi Jacquaint on Unsplash

To the code!

Towards the end of the last article, in my function, you may have noticed me referencing two mappings that we declared at the start of our contract, and . Both of these are used for managing approvals, which is basically a system for giving control of your tokens to someone else, while retaining ownership of it.

There are two forms of approvals, one which grants someone permission to control one specific token (), and the other which grants them control over of all of your tokens (). Both types of approval have their own functions, and they work quite differently from each other, so we’ll cover them separately.

Authorised

Authorised is described in the ERC721 standard by two functions,

function setApprovalForAll(address _operator, bool _approved) external;

which sets who is authorised to control all your tokens (lets call these people operators), and

function isApprovedForAll(address _owner, address _operator) external view returns (bool) {
return authorised[_owner][_operator];
}

which reads that data. This is where the mapping which we declared at the start of our contract comes in handy. In case you’ve forgotten, that mapping was declared as:

mapping (address => mapping (address => bool)) internal authorised;

If you’ve never seen a nested mapping before, it’s actually pretty simple. In this case, each owner address maps to its own mapping, which maps operator addresses to a bool stating whether or not they’re actually an operator.

So reading from this nested mapping just takes the following structure,

authorised[owner][operator]

which will evaluate to true if is an operator for , and false otherwise.

In fact, this is precisely how our function works, it simply returns a value from this mapping:

function isApprovedForAll(address _owner, address _operator) external view returns (bool) {
return authorised[_owner][_operator];
}

Similarly, our function just sets a value in this mapping. The only other requirement is that our function “emits the ApprovalForAll event”, which is defined in the Standard interface as

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

So our function is simply,

function setApprovalForAll(address _operator, bool _approved) external {
emit ApprovalForAll(msg.sender,_operator, _approved);
authorised[msg.sender][_operator] = _approved;
}

Allowance

Allowance is also described by two functions,

function approve(address _approved, uint256 _tokenId) external payable;

which sets who is approved for a given token, and

function getApproved(uint256 _tokenId) external view returns (address);

which reads this data. The most important thing to note is that for any given token, there can only ever be one authorised address at any given time. If you approve someone for a token, and then approve someone else, then the first person will no longer be authorised for that token.

Similar to our mapping for dealing with operators, we have for dealing with single-token approvals. To refresh your memory:

mapping (uint256 => address) internal allowance;

There are no fancy tricks here, it’s just a mapping of each to its approved address. If it hasn’t got one, it evaluates to .

The two requirements of our function are that it “throws if is not a valid NFT” and that it returns “the approved address for this NFT, or the zero address if there is none”, which gives us:

function getApproved(uint256 _tokenId) external view returns (address) {
require(isValidToken(_tokenId));
return allowance[_tokenId];
}

And to round off approvals, we have our function. Note that I’ve changed the mutability from payable to implicit non-payable — I made the same change in my copy of the interface too. We don’t need to take payment from this function.

function approve(address _approved, uint256 _tokenId)  external{

The standard has the following condition for our function,

“Throws unless `msg.sender` is the current NFT owner, or an authorised operator of the current owner.”

We begin by getting the result of and storing it in a temporary variable. If you remember from earlier, is a little more complex than just reading a variable, so this way it’s cheaper than re-calling each time we need the owner’s address.

address owner = ownerOf(_tokenId);

then we check if the is the owner or operator as per the requirements. Note that I’m directly reading the array rather than calling , this is just another gas-saving measure.

require( owner == msg.sender                    
|| authorised[owner][msg.sender]
);

Lastly we have an event to emit, and we have to actually update the mapping.

emit Approval(owner, _approved, _tokenId);
allowance[_tokenId] = _approved;

The transfer functions

The last and arguably most important part of our contract is the transfer functions. There are technically three, but they all basically do the same thing, except two of them have some extra features. Let’s start with the most simple one,

transferFrom

As the name suggests, the function is used for transferring a token from one address to another. Before we go any further, note that I’ve changed the mutability from payable to implicit non-payable (because we don’t need to take payments with this one), and I’ve also changed the visibility from external to public (because will be re-used by our other transfer functions, so making it public will save gas).

function transferFrom(address _from, address _to, uint256 _tokenId) public {

Next there are a bunch of requirements for this function that will cause it to throw if they’re not met. Let’s start by getting the token’s because we’ll be using that a lot in our checks, and our function actually contains a check for if a is valid which is one of ’s requirements:

address owner = ownerOf(_tokenId);

Next we have “Throws unless `msg.sender` is the current owner, an authorised operator, or the approved address for this NFT.”, which is:

require ( owner == msg.sender
|| allowance[_tokenId] == msg.sender
|| authorised[owner][msg.sender]
);

Again I’m directly accessing the mappings rather than using the approval functions we wrote before because it‘s cheaper.

Next, “Throws if `_from` is not the current owner.” is just:

require(owner == _from);

And our last check, “Throws if `_to` is the zero address.”:

require(_to != 0x0);

Personally, I like to emit any events immediately after doing checks, but it doesn’t really matter whether you do them now or at the end.

emit Transfer(_from, _to, _tokenId);

Next we update our mapping to reflect the new owner,

owners[_tokenId] = _to;

and adjust the balances of the and addresses,

balances[_from]--;
balances[_to]++;

Note there’s no need for SafeMath in a case like this, our contract logic ensures that nobody with a balance of 0 can get to this point, meaning they can’t overflow their balance.

The only thing left to do is reset the for this token. Now that the token has changed hands, the new owner needs to decide who has control of it.

if(allowance[_tokenId] != 0x0){
delete allowance[_tokenId];
}

The event implies that we reset the allowance, so there ‘s no need to emit the event too.

safeTransferFrom

The functions are almost identical to except that they check whether the recipient is a valid ERC721 receiver contract, and if it is, let you pass some data to that contract. There’s a 3-parameter version and a 4-parameter version of this function, but the 3-param one just calls the 4-param one with the last parameter blank, so we’ll start with the 4-param.

Once again, notice I’ve modified the mutability and visibility from external payable to public and implicit non-payable for gas reasons and because we don’t need to take payment.

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) public{

The first thing we’ll do is just call our function, this will do most of our checks for us, and transfer the token.

transferFrom(_from, _to, _tokenId);

Next we have to meet the added conditions of ,

When transfer is complete, this function checks if `_to` is a smart contract (code size > 0). If so, it calls `onERC721Received` on `_to` and throws if the return value is not `bytes4(keccak256(“onERC721Received(address,address,uint256,bytes)”))`.

We’re going to have to use a little bit of assembly here, luckily there’s an opcode which returns the size of the code at a given address. I’ve you’ve never used assembly before literally all this is doing is checking the code size of and storing it in . Then we return safely to the bounds of Solidity.

uint32 size;
assembly {
size := extcodesize(_to)
}

If the size is 0, then this address doesn’t belong to a contract. But if it does, we need to call on this contract and require that it give us the correct response:

if(size > 0){
ERC721TokenReceiver receiver = ERC721TokenReceiver(_to);
require(receiver.onERC721Received(msg.sender,_from,_tokenId,data) == bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")));
}

That’s the only difference between and . The only thing left is the 3-parameter version, which simply calls the 4-parameter version with the final argument as an empty string. Notice once again that I’ve changed the mutability from payable to implicit non-payable:

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external {
safeTransferFrom(_from,_to,_tokenId,"");
}

Wrapping up

Voila! You‘ve just finished written your own ERC721 implementation contract, nice one! A copy of this contract is available on my GitHub for you to use if you haven’t been playing along at home. I hope you’ve found this series informative so far, and don’t despair because there’s more to come!

Next time we’ll be testing our contract, because code is no good if it hasn’t been tested, and I’m also going to run through the Metadata and Enumerable extensions in a future article as well.

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

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