This is tutorial is designed to help you build your first NFT ERC-721 contract.
The goal of this tutorial is to teach you the foundations of a compliant NFT smart contract so you can build your own.
ERC-721 is a standard for NFT(non-fungible tokens). Every token in the collection described by the smart contract is unique. The tokens are represented by an integer value called tokenId, which represents each individual token.. The tokenId can be used to reference unique metadata for each token via an link to an external location, which is usually immutable storage. A proof of ownership can be also performed from checking a signature from the account that holds a token, which could be compared against the contract on the chain for the latest owner. The ERC-721 contract is responsible to keep track of the created token on Ethereum.
Step 1: activate your virtualenv (ape)
Step 2: personalize the project with ape-template
Step 3: Compile your project
NOTE: You must have ape-vyper plugin installed to compile a vyper contract.
You are done! You are ready to mint your first NFT token on a test network! Follow the next tutorial on how to do it!
The next part of this tutorial is understanding the ERC-721 Contract. Stick around if you would like to figure it out together.
Reading the contract
So the first couple of lines of code are about implementing interfaces. The ERC721 interface includes all the functions and events that are a part of the standard. Additionally, according to the ERC721 standard, every contract MUST also implement the ERC165 interface. This is important, because some markets like OpenSea will not register your ERC721 NFT if you do not also implement ERC165.
ERC721 Receiver Interface
When a contract is the target of a safeTransferFrom call, the standard specifies that the target contract must handle the receipt of an NFT through a callback specified by this interface. It passes along the operator, owner of NFT, and tokenID of the NFT, as well as any extra data that has been passed through the call to handle in the callback. At the end of the callback, it is expected to submit back a particular value, which the ERC721 contract must check for to ensure the callback was handled correctly.
The ERC165 interface publishes what interfaces the contract supports. By implementing this interface, other contracts can utilize the published information to avoid calling it via unsupported functions. We do not want to make function calls with an invalid input as it may revert or have unexpected results. You can read more about it here.
ERC721 Metadata Extension
This interface is OPTIONAL; it exposes and gives the user the name and symbol of the ERC721 contract, as well as the tokenURI of each NFT in the contract. This is how markets like OpenSea display what collections each NFT series is a part of, as well as displaying the individual metadata for each token. Most NFTs end up implementing this extension as displaying the metadata is a very useful part of what NFTs are used for. When you add an extension, you must also add it to the set of interfaces that are supported in this contract via ERC165.
ERC Enumerable Extension (OPTIONAL)
Total Supply Count NFTs tracked by this contract
TokenByIndex: Enumerate valid NFTs
tokenOfOwnerByIndex Enumerate NFTs assigned to owner
With these OPTIONAL extensions: You have to be more explicit about it. For example ERC721 Metadata helps the user VIEW the information stored in name, symbol, and tokenURI and NOT change it. That is why we are explicit with the VIEW keyword.
The View and Pure call will use a STATICCALL ensuring no storage can be altered during execution.
Events way for you to capture and log all the actions in the smart contract and put it into an ETH node. Some of the parameters in the event are indexed for search and filter purposes. For example you want to view all the events that happen with a certain address.
Logs when the ownership of a NFT changes by any mechanism. Such as sending to another owner, being newly created, or destroyed.
Logs when an operator for a particular NFT is changed or reaffirmed. Note ZERO address indicates no approved address or an approval has been revoked, such as when the owner transfers it to a new owner, or the approval is used by the approved operator.
Since an owner can own multiple NFTs. ApprovalForAll allows an operator to be authorization to transfer any NFTs that a particular owner owns in this contract.
These are the standard functions for ERC-721.
function balanceOf(address _owner) external view returns (uint256)
BalanceOf counts all NFTs assigned to the owner and puts in an efficient HashMap.
function ownerOf(uint256 _tokenId) external view returns (address)
- Finds the owner assigned to an NFT based on tokenId. Zero Address means no owner. Queries should throw an error.
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable
- Transfer the ownership of an NFT from one address to another. This function will
- Throws if `owner` is not the current owner.
- Throws if `receiver` is the zero address.
- Throws if `tokenId` is not a valid NFT.
- If `receiver` is a smart contract, it calls `onERC721Received` on `receiver` and throws if the return value is not`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
- NOTE: bytes4 is represented by bytes32 with padding
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable
- The difference between this function and the one before is the data parameter. The data parameter allows the user to add any data you want in the function call. It essentially does nothing. Sometimes the contract specifically requests non standard data so you must include it in the call.
function transferFrom(address _from, address _to, uint256 _tokenId) external payable
- TransferFrom without safe validation, means the contract does not check whether the recipient is a valid ERC721 receiver contract. If the receiver is not valid but you call transferFrom anyways. It will be lost to the ether. It assigned a new owner that is not valid.
function approve(address _approved, uint256 _tokenId) external payable
- Change or reaffirm the approved address for an NFT.
function setApprovalForAll(address _operator, bool _approved) external
- Enable or disable approval for an operator to mange all of the msg.sender assets.
function getApproved(uint256 _tokenId) external view returns (address)
- Get the approved address for a single NFT
function isApprovedForAll(address _owner, address _operator) external view returns (bool)
- Query if an address is an authorized operator for another address. It is a hashmap for address and mapping of operator address and whether or not the operator has auth or not.
function returns the address of the owner.
is a hashMap for operators addresses that are approved
is a hashmap for tokenId to an Address
This is a new ERC. It is a way to create gasless approvals for ERC721 tokens so that contracts can get approvals to trade them on our behalf. However, this ERC is still in Draft, meaning it can change at any time, so be careful to watch out for changes in this ERC as it moves towards it’s Final implementation. Also note that there could be unforeseen security consequences of using this function, as its intended behavior has not been widely studied nor has it seen much use in production.
EIP 4494 Interface and Implementation of Permit
Function to approve by way of owner signature.
Permit returns the nonce of an NFT by taking address of spender, tokenId, deadline(expiry), and signature to approve the operator.
Permit must check that the signer is not a zero address.
Deadline is the time less than or equal to current blocktime.
Owner of tokenId must not be Zero Address
Sig is a valid secp256k1 (EIP-2098) from the owner of tokenId. It is made with spender address, tokenId, nonce, and deadline
Domain Separator should be unique to the contract and chain to prevent replay attacks from other domains.
Nonce MUST be incremented upon any transfer of tokenId
NOTE that we do not refer msg.sender. Permit caller can be any address.
Rational on why we implemented Permit:
The permit function is sufficient for enabling a safeTransferFrom transaction to be made without the need for an additional transaction.
The format avoids any calls to unknown code.
The nonces mapping is given for replay protection.
A powerful function to convert uint to string. It is not native in vyper or any smart contract language to convert uint to string. So thanks to @skellet0r we have beautiful function to help us with that. It converts bytes to char and contacts char to a string with the max length of a string78 based on the max uint.
This function helps us create the readable tokenURI. Which is a concatenation of baseURI and stringify of tokenURI.
is made with BaseURI and the stringify of tokenURI
The domain separator prevents collision of otherwise identical structures. It is possible that two DApps come up with an identical structure like Transfer(address from,address to,uint256 amount) that should not be compatible. By introducing a domain separator the DApp developers are guaranteed that there can be no signature collision.
Support Interface (EIP-165)
Comes from EIP- 165 to verify interfaces
Returns the address of the owner of the NFT.
Throws if `tokenId` is not a valid NFT.
tokenId The identifier for an NFT.
NOTE it is a view function not a state change function
Returns whether the given spender can transfer a given token ID
It is gas efficient since it returns a bool.
spender address of the spender to query
tokenId uint256 ID of the token to be transferred
bool whether the msg.sender is approved for the given token ID, is an operator of the owner, or is the owner of the token
We have an internal transferFrom and an external transferFrom
Execute transfer of NFT
Calls the internal _transferFrom with the correct params
Every NFT is identified by a unique uint256 ID inside the ERC-721 smart contract. This identifying number SHALL NOT change for the life of the contract. The pair (contract address, uint256 tokenId) will then be a globally unique and fully-qualified identifier for a specific asset on an Ethereum chain.
The End and Thank You
These are all the functions that we decided to implement on the core of a NFT contract. If you would like some help with it. Please join us in discord and say hello! Or if you would like to add some more functionality let’s discuss it! I plan on adding more optional functions to the project.
Ape Academy EIP-721 Template: https://github.com/ApeAcademy/ERC721