How to Compile an ERC-721 Using Vyper

written by
Chris Cao
Date Last Updated
June 6, 2022
Download Project Files

This is tutorial is designed to help you build your first NFT ERC-721 contract.


This tutorial aims 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 calledtokenId, which represents each token. ThetokenIdcan be used to reference unique metadata for each token via a link to an external location, which is usually immutable storage. Proof of ownership can also be performed by 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 for keeping track of the created token on Ethereum.

Getting started

Step 1: Activate your virtualenv (ape)

source ~/virtualenvs/ape/bin/activate

Step 2: Personalize the project with ape-template

$ ape plugins install template
$ ape template [email protected]:ApeAcademy/ERC721.git

Step 3: Compile your project

NOTE: You must have ape-vyper plugin installed to compile a vyper contract.

$ ape plugins list
$ ape plugins install vyper -y
$ ape compile


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 only register your ERC721 NFT if you implement ERC165 as well.

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 the NFT, tokenID of the NFT, and any extra data 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.

ERC165 Interface

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 interfaces

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 implement this extension, as displaying the metadata is a valuable part of what NFTs are used for. When you add an extension, you must also add it to the interfaces 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 an 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 are a way to capture and log all the actions in the smart contract and put them 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 specific address.

event Transfer

Logs when the ownership of an NFT change by any mechanism. Such as sending to another owner, being newly created, or destroyed.

event Approval

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.

event ApprovalForAll

Since an owner can own multiple NFTs.ApprovalForAllallows an operator to be authorized to transfer any NFTs that a particular owner owns in this contract.

Standard Methods: 

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 users to add any data they 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 manage 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. 

Optional functions:


The function returns the address of the owner.


Is a hashMap for operators addresses that are approved 


Is a hashmap for tokenId to an Address

Permit (ERC4494)

This is a new ERC. It is a way to creategasless approvalsfor 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 toward its 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 a 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, 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. Two DApps may 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

_transferFrom (internal)

Execute transfer of NFT.

transferFrom (external)

Calls the internal _transferFrom with the correct params

Fun Fact:

A unique uint256 ID identifies every NFT inside the ERC-721 smart contract, and this identifying number SHALL NOT change for the life of the contract. The pair (contract address, uint256 tokenId) will 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 an NFT contract. If you would like some help with it. Please join us in discord and say hello! Or, if you want to add more functionality, let’s discuss it! I plan on adding more optional functions to the project.


Ape Academy EIP-721 Template:


ERC Sample:





Developer Tip