ERC721 Using Hardhat: An Updated Comprehensive Guide To NFTs
The ERC721 standard is used for representing ownership of non-fungible tokens (NFTs). Each NFT is unique and is therefore complements use cases such as collectibles, property titles, identity documents, among many others.
Much like the ERC721, this guide will be heavily influenced by a separate guide on ERC20 which is the standard for fungible tokens. To this end, I have compiled an end-to-end guide on how to create your own ERC721 token using Hardhat. This includes:
- Hardhat Environment Setup & Configuration
- Extending OpenZeppelin’s ERC721 Contract
- Deploying Token Contract to Goerli Testnet (getting test ETH, connecting to node, private key exporting)
- Interacting Programatically with a Local Contract Instance
- Sending Created ERC721 Tokens on Goerli Testnet via Metamask
Although targeted at those starting out in the space, this guide requires some basic familiarity with Node.js and Solidity concepts. All the code used in this guide can be found on github.
Related Guides:
Hardhat Setup
We first need to crate a project directory and install Hardhat:
mkdir ERC721
cd ERC721
npm install --save-dev hardhat
Once the hardhat
package has been installed, we can then run npx hardhat
which will bring up some options for bootstrapping the project:
For this tutorial, we will select the Create a JavaScript project
option. You will be prompted with a series of questions which you can continuously select enter to. The last of which will install the project dependencies.
npm install --save-dev @nomicfoundation/hardhat-toolbox@^1.0.1
Following the npx hardhat
command, the project will have 3 folders:
contracts/
is where the source files for your contracts should be.test/
is where your tests should go.scripts/
is where simple automation scripts go.
In addition a hardhat.config.js
file will have been generated. This file will manage the plugins and dependencies viewable by hardhat. Plugins and dependencies will have to be installed first followed by requiring it in the hardhat.config.js
file.
The npx hardhat
command will have also created a sample Lock contract which we will not require for this guide so you can go ahead and remove them:
rm contracts/Lock.sol test/Lock.js
Now that Hardhat has been setup, we can begin creating our ERC721 token.
ERC721 Contract
OpenZeppelin, which spearheads the advancement of the ERC721 standard, provides a comprehensive library for secure smart contract development which we will be using to implement our token. To do this, we need to first install the OpenZeppelin contracts package.
npm install @openzeppelin/contracts
We can then import the OpenZeppelin contracts into our own contract by prefixing its path with @openzeppelin/contracts/...
.
Before starting our first ERC721 token contract, we need to complete the most important step: naming your token collection! This differs from the ERC20 implementation where the name is given to the token instead of a token collection. For ERC721 tokens which are non-fungible, each NFT has to have a different identifier but we can still group them together under the same collection. Hence, for ERC721 tokens, the smart contract represents the token collection where new NFTs from the collection can be minted by passing a unique identifier to the token collection contract.
The standard practice is to match the smart contract file name with that of your token collection. For this guide, I will be naming the token collectionNonFunToken
(non-fungible token) with a symbol of NONFUN
and hence my smart contract file name will be NonFunToken.sol
. Feel free to choose your own collection name but do remember to replace any instance of NonFunToken
with your own specially crafted name.
The new contract will be placed in the contracts/
folder:
touch contracts/NonFunToken.sol
Open NonFunToken.sol
in your code editor and add the following code. The code will utilise the Solidity’s constructor function to give the token collection a name
and symbol
upon deployment.
We will be importing OpenZeppelin’s ERC721 contract via the path @openzeppelin/contracts/token/ERC721/ERC721.sol
. You can find the path for the contract via OpenZeppelin’s docs or their github.
Do note that we are also using the Ownable
contract to ensure that on the contractOwner
can mint NFTs. Save the NonFunToken.sol
contract and we can now compile the completed contract.
Compiling The Contract
For the Ethereum Virtual Machine (EVM) to run our code, we need to compile our Solidity code into the EVM compatible EVM bytecode. To ensure there are no versioning issues, we can specify a Solidity version in the provided hardhat.config.js
file.
To compile the NonFunToken
contract, we can use Hardhat’s handy compile command.
npx hardhat compile
Hardhat will have compiled the contract into a newly created artifacts/
folder.
Deploying The Contract Locally
Before deploying the contract to a public network, it is best practice to test the contract on a local blockchain first. Hardhat simplifies the process of setting this up by having an in-built local blockchain which can be easily run through a single line of code:
npx hardhat node
Run the above command in a separate command window.
The Hardhat network will print out the address as well as a list of locally generated accounts. Do take note that this local blockchain only stores the interactions until the console is closed hence the state is not preserved between runs. Additionally, take the time to read through the details of the accounts as it is important that you do not use these sample accounts for sending any real money. With the local blockchain up and running, we can then start writing our deployment scripts. Hardhat currently does not have a native deployment system and hence the need for scripts.
For our deployment script, we will be making use of the hardhat-ethers
plugin. You can find the documentation for the plugin here. To use it in the project, we need to first install it:
npm install --save-dev @nomiclabs/hardhat-ethers ethers
Navigating to the scripts/
directory, you will be able to see that a deploy.js
file was previously created. We can replace the main()
function with the following:
Hardhat will deploy the contract using the first account created when we started up the node above. The mintCollectionNFT()
function in NonFunToken.sol
will allow us to externally call the contract in order to mint NONFUN
NFTs. To access the array of accounts created, we can use the getSigners()
helper function provided by the hardhat-ethers
plugin.
Save the file and we are now ready to deploy our NonFunToken
contract!
npx hardhat run --network localhost scripts/deploy.js
As mentioned, we are deploying the contract to our localhost
. Do take note of the deployed contract address as we will need it in the next section for interacting with our contract programatically.
Interacting With The Contract
This section is optional but it is a good introduction to how to interact with smart contracts using the ethers.js package. Through this interaction, you will also be able to also experience for yourself the set of default functions which your ERC721 token collection has inherited from the OpenZeppelin contract.
Getting the deployed contract
In order to get the instance of our deployed contract, you will need the contract address returned from the deployment command. You can replace the contractAddress
in the code below with the one provided by your local machine.
With that, we have set up our nonFunToken
object which is an abstraction of a contract deployed on the Ethereum network, in this case a local network. This nonFunToken
object enables a simple way to serialize calls and transactions to an on-chain contract and deserialize their result logs. You can refer to the ethers documentation for more info.
The sections below showcases how to call the default public functions which our NonFunToken
contract has inherited from OpenZeppelin’s ERC721 contract. Remember to add each section to the main()
function in order to call the contract. To run the interact.js
script, enter the following command into your console:
npx hardhat run --network localhost scripts/interact.js
name()
name()
returns the name of the token collection used in theNonFunToken.sol
constructor.
symbol()
symbol()
returns the token collection symbol used in the NonFunToken.sol
constructor.
mintCollectionNFT(address collector, uint 256 tokenId) — Custom mint implementation
The core set of ERC721 contracts are unopinionated thereby allowing developers to expose the internal functions as external functions in their preferred way. As such, we require a custom function, mintCollectionNFT()
, to implement NFT minting for the collection using the ERC721 internal function _safeMint()
.
balanceOf(address owner)
balanceOf()
returns the total NFT token count from the collection belonging to the owner.
ownerOf(uint256 tokenId)
ownerOf()
returns the owner of the tokenId
.
safeTransferFrom(address from, address to, uint256 tokenId)
safeTransferFrom()
moves the NFT with tokenId
from the from
address ot the to
address. If the caller of safeTransferFrom()
does not match the from
, the caller must have approval to move the token (see below)
approve(address to, uint256 tokenId)
approve()
gives permission to transfer the tokenId
to another account. There can only be a single approved account per tokenId
. Approval is cleared once the token is transferred.
getApproved(uint256 tokenId)
getApproved()
returns the account that has been approved to transfer the tokenId
NFT on behalf of the owner.
safeTransferFrom() with valid approve()
An example of calling safeTransferFrom()
from a valid approved account. In this case the contractOwner
has been approved and is able to transfer NFT1
from the collector
account.
setApprovalForAll(address operator, bool _approved)
setApprovalForAll()
adds/removes the operator
privilege to spend all of the caller
NFTs from the collection. This privilege can be turned off/on by modifying the bool
parameter.
isApprovedForAll(address owner, address operator)
isApprovedForAll()
returns if the operator
is allowed to manage all of the owner
NFTs from this collection.
safeTransferFrom() with valid isApprovedForAll()
An example of safeTransferFrom()
when the collector
has approval to move all of the contractOwner
NFTs. In this case, the collector
transfers all of the NFTs in the contractOwner
wallet to themselves.
Testing The Contract
Given that the NFTs will have a monetary value, writing automated tests is crucial to ensure that the contract is performing how we expect it to. We will be making use of the chai
and hardhat-chai-matchers
package to quickly write up our own tests.
$ npm install --save-dev chai
$ npm install --save-dev @nomicfoundation/hardhat-chai-matchers
Once installed, we can then create the test script file:
touch test/NonFunToken.test.js
As most of the contract functions have been implemented in the above section, the code below is meant more as a starting point for you to then write a comprehensive set of tests.
Upon saving the above into NonFunToken.test.js
, we can then run the tests using:
npx hardhat test
If all goes well, you should expect to see the results of the test printed to your console:
Do play around with the various test scenarios which are suitable for your token. You can even try implementing negative test cases which were not included in the sample above.
Deploying the Contract Publicly
Now that we are confident that the contract is working as expected, it’s finally time to deploy NonFunToken
onto a public network! This is the exciting part, as anybody connected to the network will be able to interact with your NFT collection. For the purpose of this guide, we will be deploying the contract onto the Goerli testnet. The Goerli testnet functions identical to the mainnet minus the dollar value attached to ETH. As such, it is the perfect environment to experiment with your contracts prior to deploying to the mainnet. Do note that we are also deploying to Goerli as many of the other testnets (Ropsten, Rinkeby, Kiln) will be stopped following ETH merge.
In order to deploy to Goerli, we will need to first source some Goerli ETH, hereon referred to just ETH. This is required as transactions on the public network need to be processed by a miner which demands a gas fee to be paid. So far, we have been testing the contract on our local network where all the accounts were locally generated with a set amount of ETH. On public networks, all the new ETH generated are minted to the miners and hence we need to source ETH in order to run our contract code.
The easiest way to procure some ETH is via faucets. Faucets are community funded websites where users can request for ETH to be sent to a privately owned wallet. Do note, it is best to send these funds to a separate dev wallet as the private keys will be needed in plain text later. Faucets are used in the test environment as a way to circulate testnet ETH for developers to use. You can easily find such sites by doing a search but I have linked 2 such sites below:
- goerlifaucet.com: Requires an Alchemy account
- goerli-faucet.pk910.de: Uses computing power to mitigate spam
Once you’ve got some ETH, let’s go ahead and add the network to our hardhat.config.js
so that Hardhat knows where to deploy to.
In order for Hardhat to deploy the contract to Goerli, it needs two additional things:
- A Goerli node which it can connect to in order to send the transaction to the network
- The wallet which will be used for deploying the contract
For the API to connect to the node, you can generate one by signing up on alchemy.io, creating a new app and copying the API KEY under the “VIEW KEY” on the app dashboard. Do note, you only need to sign up for the free account so you can just skip the payment information.
Next up, we will need the private key of the account that is going to pay for the deployment of the contract. In order to get the private key for the account which received the faucet ETH, you can export it directly from Metamask under the “Account details”. Copy and paste that private key into the config file.
With that, we are now ready to to deploy the contract by using the following command:
npx hardhat run scripts/deploy.js --network goerli
You would have noticed that, in contrast to our local environment, transaction confirmation was not instant and this is because the transaction had to be processed by the Goerli testnet. This gives you a taste of how time-consuming the dev cycle can be if not for a local environment. Also note that the ETH balances in the specified account has decreased by a small amount due to gas fees.
Once deployed, your console should output the newly deployed contract address:
Remember that the NonFunToken
contract requires us to mint NFTs from the collection after the contract has been deployed. To achieve this, we will write a separate script, mint.js
, to mint the NFTs via code. Do take note of the deployed address as we will need it in our mint script.
touch scripts/mint.js
We can then add the following code:
Save the file and we can run the script using:
npx hardhat run --network goerli scripts/mint.js
For the initial collection, we will mint 10 NFTs to the wallet which deployed the contract hence in order to view the token, we will need to add our contract to Metamask. Open Metamask and navigate to the assets tab where you will see an “Import tokens” link. Select the link and enter the contract address, the “Token Symbol” field should be auto-populated but you will need to enter 0
for the decimal field.
Once imported, you should now be able to see 10 NONFUN
NFTs in your wallet!
You might have noticed that unlike the ERC20 implementation, the send button for our ERC721 NFT has been disabled. Unfortunately, Metamask desktop extension does not currently support additional NFT functionalities. In order to see our NFT being transferred, we can either interact with our contract via NFT wallets which support such functions or create another script to interact programmatically. As an example of the latter, transfer.js
showcases a simple transfer of an NFT from the collection.
touch scripts/transfer.js
npx hardhat run --network goerli scripts/transfer.js
Running the above, NFT 1
has been sent to my other dev wallet:
All these transactions are publicly viewable hence you can use tools such as Etherscan to view and analyse transactions. For example, the transaction which I just sent can be found here:
For NFTs, you can also view all the NFTs in the collection by using the token tracker which can be accessed from the main contract overview page.
Congrats, you have successfully created your very own ERC721 NFT collection! Do remember that the NFT is only as valuable as what it enables users to do. As such, continue building use cases which utilizes your NFT as a base for uncensorable and secure digital ownership. All the best out there!
Thanks for staying till the end. Would love to hear your thought/comments so do drop a comment. I’m active on twitter @AwKaiShin if you would like to receive more digestible tidbits of crypto-related info or visit my personal website if you would like my services :)