I recently wrote a tutorial here teaching the fundamentals of NFTs and explaining the ERC-721 standard, the first and most important standard when it comes to NFT smart contracts. In today’s tutorial, we are going to advance our studies by going to practice, that is, programming our Smart Contract following the pattern. It is important to see this tutorial as part 2 and that you return to the first one if you do not have mastery of the subject yet.
For this tutorial, I’m using the Remix tool, but you won’t see anything specific to it during the tutorial. It’s just to abstract any aspect of the environment since it’s the rawest tool today. However, nothing prevents you from using the same codes to write your contract in toolkits like Truffle and HardHat, which I intend to do later on here on the blog and in the courses.
Then open Remix, create a new file called MyNFT.sol, and let’s program!
First, the basic structure
As explained in the standard’s official documentation, every NFT contract must implement the ERC721 and ERC165 interfaces. The first concerns the mandatory functions and events of all NFTs, while the second concerns how the contract notifies that it is compliant with certain standards/interfaces.
Although it is not mandatory (as far as I know) to have the interface declared in your code, it can be a good starting point to do so, to make sure it adheres to the standards mentioned above and helps with some functions such as the one required by ERC165 that we will see further. Then, at the top of your MyNFT.sol, declare the interfaces as provided by Ethereum.
With these interfaces in your code, the next step is to create the contract itself below, which should implement them.
Notice that I implemented the previously described interfaces with the keyword ‘is’. Now is the time to structure our contract’s state variables. It is necessary for controlling ownership of NFTs and others.
Here we have a first mapping that relates each existing NFT (the key is the tokenId, a uint256) with its owner (wallet address).
Then we have a second mapping that relates, for each owner’s wallet, how many NFTs he has.
The third variable is the mapping of tokenIds to approved operators. That is. It is the equivalent of the allowance of ERC-20 tokens, but binary (with or without permission).
And finally, we have a mapping of, given an owner (first address), we have the mapping of operators with full permissions in the NFT collection of that owner. Note that the first three variables have _ in the name because they are internal (for internal use in the contract). While isApprovedForAll is public and has the same name defined in the pattern, that is, one less function to write!
And before we get into the ERC-721 specific functions, let’s implement the only function required by ERC-165, supportsInterface.
This function returns a boolean indicating whether or not a given interface passed as a parameter is implemented by this contract. As our contract must implement the ERC721 and ERC165 interfaces, it is with them that I make the comparison to return whether or not the contract supports the informed interface.
Implementing the ownership and delegation functions
For the sake of organization, I’m going to break the contract roles into four groups: the ownership roles, the delegation roles, the transfer roles, and the optional, non-mandatory roles. Let’s start with the property functions, and within that group, with the two property read/check functions.
The ownerOf function, required by the interface, expects the id and a token and queries this id in the owners mapping to see who owns it. If the returned address is 0, I chose to return an error stating that the token in question does not exist. Also, notice that I didn’t use the return statement at the end of the function. Instead, I named the variable from the statement returns in the function signature, so it is “linked” to the variable of the same name in the body of the function.
The balanceOf function, also required by the interface, receives the address of a wallet that we use to look in the mapping of balances to return how many tokens that user has, returning an error if the informed wallet is zero.
In addition to ownership, ERC-721 states that we can delegate control of our tokens to other wallets, which is especially useful for brokers, marketplaces, etc. For that, we need to implement specific functions that act on the state variables that we defined before.
First, let’s talk about the setApprovalForAll function, which is mandatory in the standard and which starts by setting that, for the requester of the transaction (msg.sender, called owner), let’s define that the operator has full control (approved = true) or no control (approved = false) on all owner’s NFT collection. At the end of the execution of this transaction, we emit an event as required by the specification.
The second function, approve, do the same thing but for only one NFT of the owner. It starts by taking information about who owns the token that we are going to delegate control to, and if that owner is not the requestor or is not in full control of the collection, it will give an unauthorized error. If he has permission, then the informed operator (spender) is added as having approval on the id token also informed. Upon completion of the function, the Approval event is fired as required by the pattern.
The third function serves to return who is the approved operator/controller for a given token. If the token does not exist, an error is reported. If there is no approval for the token in question, address zero will be returned, as is usual in Solidity.
The fourth and last function of this group IS NOT of the ERC-721 standard. This function is very useful for some checks later on. It returns a boolean indicating whether a given wallet (spender) is the owner of the token or someone approved by the owner. Nothing else.
Implementing the transfer functions
One of the main advantages of using standardized NFTs is that not only can we register our creations and properties on the blockchain, but we can also transfer them to other people, which is accomplished through the three transfer functions required by the ERC-721 standard, the first one is transferFrom, which I’m going to implement slightly differently from the default, but I’ll explain why.
The transferFrom function expects you to inform who is the current owner of the token, who will be the new owner, and the token id. It does not assume that the requester automatically owns it because he may be the approved controller of it, not the original owner, but he still must know who owns it. This owner will be validated in the first line of the implementation, as well as whether the new owner is a valid wallet.
Afterward, we verify through another requirement if the msg.sender is the owner or has permission to make this transfer. Having it, the current owner’s balance is decreased, the new owner’s balance is incremented, and the ownership of the tokenId is changed in the owner mapping.
Finally, as per the default, all existing approvals for this token are revoked, and a transfer event is issued.
I implemented this function slightly different from the default one because I need a function for internal use in the contract in order to reuse the logic in the next functions. Even so, it is important that we have another transferFrom, this one, like sending the contract to be 100% adherent, as follows.
Then we have the external function with the name specified in the ERC721 and the internal one for use by the contract itself and who does the hard work. Now let’s talk about the two safeTransferFrom functions that are more secure versions of transferring NFTs.
Notice that safeTransferFrom has the same signature as the original transferFrom and that it even calls the other one internally. But also note that it is considered “safe” because of an additional validation at the end of it that prevents it from being transferred to an invalid container. But what would an invalid container be?
The first thing to test is whether the to.code, that is, the source code bytes linked to that address, are zero. If they are zero, it means that the ‘to’ address is a common account and not a contract, which is completely valid. If it is a smart contract (code.length > 0), then another test must be performed. We check whether the response to the onERC721Received function call indicates that the token was successfully received. Remember that even if this requires being after the transfer, if it is negative, the transaction as a whole will be undone.
And now, let’s talk about the second secure transfer function, which changes only one element in the signature.
Here we have one more parameter called data, and with that, an overload of function (overload), allowing to call safeTransferFrom with or without this last parameter that serves to pass additional data at the developer’s taste. In addition, it is worth mentioning that its storage location was defined as calldata, which is more economical than memory (in gas) but does not allow changing this parameter internally in the function (it is immutable).
And with that, we finish all the mandatory implementation of functions and events defined by ERC-721. With what we implemented, it is already possible to implement an NFT contract, and we can define in the contract constructor that when our NFT is created, it will already be transferred to its creator, that is, a single mint in the same deployment.
In this way, you already have all the NFT functionalities, but not in the way that commercial NFTs, large collections, etc, are usually done. We’ll talk about some optional/additional features that you’ve certainly missed in the next part of this tutorial.