Please make sure you have the following software installed:
- Node.js v14.17.6 LTS or higher, for installing HardHat and other node packages.
- MetaMask for interacting with the blockchain.
- Once you have installed MetaMask, add a connection with the Polygon Mumbai testnet.
- Remember to get your account funded with testnet MATIC tokens using the Polygon faucet.
When we deploy our contract to the blockchain (either mainnet or testnet), it is a good practice to verify the code of our smart contract after deploying it. If our smart contract is verified, then the smart contract code will be visible on the block explorer and users will be able to interact with the smart contract directly from the block explorer (such as Polygonscan). Verifying the source code is highly encouraged as it makes our project more transparent, and users are more likely to interact with it.
Using a HardHat plugin, smart contracts can be verified automatically during the deployment process. To do this, we will need a Polygonscan API key. Follow these steps to get your own API key:
- Open Polygonscan.
- Click on SignIn in the upper right corner of the page.
- If you already have an account, enter your username and password to login, or else create your new account by visiting https://polygonscan.com/register.
- Once you are logged in, go to the API-KEYs section on the left sidebar.
- Click on the "Add" button, give it a name and click on continue.
You now have an API key which will allow you to access the Polygonscan API features such as contract verification. This key will be same for both mainnet and testnet.
To install HardHat, run the command:
This will install HardHat globally so that later we can make use of the
npxcommand to create HardHat projects.
Now to create our project, we will use the following code
On typing the last command, something similar to this should appear on your screen:
We can start with a basic sample project so that it is easier to understand the project layout, so let's just press Enter. After this we will be asked to set our project root - Press Enter to keep the default value. Next it will ask if we want a .gitignore file. Press Enter again to keep the default value of "yes" or type n for "no". It will then ask if we want to install the dependencies for our sample project. Press Enter to accept the default of yes. Now HardHat will create a sample project for us and install the dependencies. Once complete, it will look something like this:
Congrats 🎊🎊🎊 you have created your first HardHat project!
Now let's open our project and take a look at what it contains. I will be using VSCode as my editor of choice but feel free to use any other code editor you are comfortable with.
What we get is a very simple project scaffold. The directory names are quite self explanatory. All our smart contracts, script files and test scripts are going to be kept in their respective directories (folders).
hardhat.config.jsis the file which contains all the configuration details specific to HardHat.
Before we start writing our smart contract, let's look at the
hardhat.config.jsfile which is the heart of our HardHat project. The contents of this file by default are:
We start by importing the
@nomiclabs/hardhat-wafflepackage which will give us access to the
hreclass. HRE is short for Hardhat Runtime Environment which is an object that contains all the functionality that HardHat exposes. You can think of it as "HardHat is hre".
NNext is where we define various tasks which can be run by typing
npx hardhat <TASK_NAME>
While developing our project we are also doing to write our own custom tasks.
At the end of the file is
module.export, this is where we are going to list various parameters like compiler version, networks to use, API keys, etc. Please note, here we have defined the solidity version as
While writing any program, we always prefer using various libraries so that we don't have to write everything from scratch. Since we are going to build an NFT based project, we will follow the standards defined in EIP-721. The best way to do this is to import the ERC721 contract present in the OpenZeppelin contracts library and only making the necessary changes for our project. To install this package, open a terminal and run the command:
Let's create a new file named
contractsdirectory. This is going to be our first smart contract which will help us in creating NFTs.
We start by defining the License of our smart contract. For this tutorial we are keeping it unlicensed. If we don't define the license, it will cause a warning during compile time. The
pragmakeyword is used to define the Solidity version used to compile the code. Make sure you are using the same Solidity version as defined in the
Next we are going to import the ERC721 smart contract from the OpenZeppelin library which we just installed. After the line defining the Solidity version and before defining the contract, import the ERC721 contract:
Make the following modifications to the code:
Here we are doing the following things:
- Inheriting the OpenZeppelin ERC721 smart contract into our
Artwork.solsmart contract using the
- The constructor is always the first function that is called while deploying a smart contract. Since we are inheriting another smart contract, we have to pass in the values for the constructor of that smart contract while defining our constructor. Here we take a name and symbol as constructor arguments and are passing them to the constructor of ERC721.
symbolare going to be the name and symbol of our NFT respectively.
NFTs are called Non-Fungible Tokens because each one is unique. What makes them unique is the token id assigned to them. We are going to define a global variable called tokenCounter and use it for calculating the token id. It will start with zero and increment by 1 for every new NFT that is created (or "minted"). The value of tokenCounter is set to 0 in the constructor.
Now we are going to define a mint function which can be called by any user in order to mint new NFTs. Each NFT will have certain data associated with it. In our case, we are using images or other collectibles as the basis of the NFT and hence the image should be somehow stored in the smart contract. Since storing data directly on a blockchain has an associated cost, it won't be financially feasible if the entire image and other associated data (metadata) is stored. So, we will need to host the image separately along with a JSON file containing all the details about the NFT. The image and JSON file can be hosted separately either decentralised (using IPFS) or centrally using traditional methods. The JSON file contains the link to the image as well. Once the JSON file is hosted, the link pointing to that JSON file is stored in the blockchain in the as tokenURI. URI stands for "Universal Resource Identifier". This is an example of centrally hosted tokenURI.
With that in mind, the mint function is how we create each NFT associated with the smart contract:
_safeMintis another function present in the OpenZeppelin ERC721 contract which is used to mint new NFTs. It takes two parameters:
- to: The first parameter is the address of an account which will own the NFT after it is minted.
- tokenId: The second parameter is the tokenId of the newly minted NFT.
msg.senderis a special keyword which returns the address of the account calling the smart contract. In this case it would return the account currently calling the mint function. Hence the account calling the mint function will be passed as first argument and therefore the minted NFT will be owned by this account.
_setTokenURI()function is not yet defined so just ignore it for the moment. This function will be used for setting the tokenURI for the minted NFT. This function was present in the ERC721 library but has been discontinued after Solidity version 0.8.0 and so we will need to implement it ourselves.
Once the token is minted and its tokenURI is set, we increment the tokenCounter by 1 so that the next minted token has a new token id.
Our NFT smart contract must store all the valid token id's with their respective tokenURI. For this we can use the
mappingdata type in Solidity. Mappings work similarly to hashmaps in other programming languages like Java. We can define a mapping from a
uint256number to a
stringwhich will signify that each token id is mapped to its respective tokenURI. Just after the declaration of the tokenCounter variable, define the mapping:
Now let's write the _setTokenURI function:
There are many new terms defined here so let's deal with them one-by-one:
- internal: The function is defined with internal keyword. It means this function can be called only by other functions in this smart contract or other smart contracts inheriting this smart contract. This function cannot be called by an external user.
- virtual: This keyword means that the function can be overriden by any contract that is inheriting this smart contract.
- require: The first thing inside the function body is the
requirekeyword. It takes in a conditional statement. If this statement returns true then the rest of the function body is executed. If the conditional statement returns false, then it will generate an error. The second parameter is the generated error message and it is optional.
- _exists(): This function returns true if there is a token minted with the passed tokenId, otherwise it returns false.
In summary: This function first makes sure that the tokenId for which we are trying to set the tokenURI is already minted. If it is, it will add the tokenURI to the mapping, along with the respective tokenId.
The last function which we have to create is the
tokenURI()function. It will be a publicly callable function which takes a tokenId as a parameter and returns its respective tokenURI. This is a standard function which is called by various NFT based platforms like OpenSea. Platforms like this use the tokenURI returned from this function to display various information about the NFT like its properties and the display image.
Let's write the tokenURI function:
- public: This function is public which means any outside user can call it.
- view: Since this function doesn't change the state of the blockchain, i.e. it doesn't change any value in the smart contract, executing this function will not require any Gas. Since no state change will take place, this function is defined as view.
- override: We already have a tokenURI() function in the ERC721 contract we have inherited which uses the concept of "baseURI + tokenId" to return the tokenURI. Since we need a different logic, we need to override the inherited function by using this keyword.
- returns(string memory): Since this function will return a string value we have to define it when declaring the function. The memory keyword defines where the information is stored.
This function first checks whether the tokenId passed was actually minted. If the token was minted, it returns the tokenURI from the mapping.
Putting all the functions together, the final smart contract will look like:
Now that our smart contract is ready, we must compile it. In order to compile a smart contract with HardHat, run the command:
If everything went as expected, you will be getting the message "Compilation finished successfully". If the contract doesn't compile successfully or there are errors, try to read the tutorial again to find out where it went wrong. Some of the possible mistakes are:
SPDX-License-Identifieris not provided
- There is a mismatch between the Solidity compiler version defined with the pragma keyword and the version defined in
- There is a version mismatch between the imported smart contract's Solidity version and the version used to write our smart contract. To solve this, double check the version of the OpenZeppelin contracts you are installing with npm. In my case, the npm package is version 4.3.2 and the smart contracts are written with solidity version 0.8.0.
At this point, we have written our smart contract and compiled it. However, a successfully compiled smart contract doesn't mean that it is correct! It is very important to write test cases to ensure that it passes all the intended use cases and some edge cases. Testing smart contracts becomes even more important since once a smart contract is deployed to the blockchain it cannot be modified.
We are going to use the
chailibrary for writing our tests. This library should have been already installed while creating our project, if not you can use the command
npm install --save-dev chai.
We are going to test our smart contract for the following:
- An NFT is minted successfully: After an account calls the mint function, an NFT is minted for that account and its balance increases.
- The tokenURI is set successfully: For two tokens minted with different tokenURIs, both tokens have their own respective tokenURI and the data can be retrieved properly.
testfolder there will be a script called
sample-test.js. We will delete this file and create a new file called
artwork-test.jsin the same
testdirectory. The filename is unimportant but to keep it organized the test file should be somehow related to the contract being tested. In this new file, add the following code:
- describe: This keyword is used to give a name to the set of tests we are going to perform.
- beforeEach: The function defined inside of
beforeEachwill be executed before every test case. We will be deploying the NFT contract here because the contract must be deployed before each test is run.
- it: This is used to write each test case. The
itfunction takes in a title for the test and a function which runs the test case.
NOTE: Unlike with Truffle, HardHat has no need to run
ganache-cliseparately for tests. Hardhat have it's own local testnet which we can use.
In order to deploy the smart contract we have to first get a reference to the compiled smart contract using
ethers.getContractFactory()and then we can use the
deploy()method to deploy the smart contract and pass in the arguments. We do this in the
To check whether the NFT is minted properly, we first get one of the default accounts created by HardHat. Then we call the mint function present in the smart contract with a random tokenURI. We check the balance of the account before and after minting and it should be 0 and 1 respectively. If the contract passes the test, it means that NFTs are minted properly.
To check whether a tokenURI is set properly, we take two random tokenURIs and set them from different accounts. Then we call the tokenURI() function to get the tokenURI for respective token, then match them with the passed argument to ensure that the tokenURIs are set correctly.
So finally, after putting all the test cases together, the content of
artwork-test.jsfile will be:
You can run the tests with the command:
This should output:
At this point we have learned how to write smart contracts and test them. Now its time we finally move towards deploying our smart contract to the Mumbai testnet so we can show off our newly learned skills to our friends 😎.
Before we move ahead and deploy our smart contract, we will need two additional npm packages:
- dotenv: Type
npm install dotenv. This will be used to manage environment variables, which are used for setting up access to our Polygonscan API key.
- @nomiclabs/hardhat-etherscan: Type
npm install @nomiclabs/hardhat-etherscan. This library is used to verify the smart contract while deploying it to the network. The process of verifying a smart contract on Etherscan and Polygonscan is identical.
Create a new file named
.envin the project root directory
Create a new entry in the
POLYGONSCAN_KEYand set it to equal the API key created in the beginning of the tutorial. Also add another entry
PRIVATE_KEYand set it to the private key of the Mumbai testnet account with MATIC in it.
In order to deploy a verified smart contract to the Mumbai testnet, we have to make certain changes in the
hardhat.config.jsfile. First, copy & paste this entire code into the file and then we will go over it step by step to understand what is happening:
Let's now understand what we are doing here:
- We have imported the
@nomiclabs/hardhat-etherscanpackage which is a hardhat plugin that enables us to verify our smart contract after we have deployed it to the network.
- For verifying the smart contract, we will need the Polygonscan API Key we created towards the start of the tutorial. That API key is passed with the
module.export. Please note that the object name is "etherscan", and the term "apiKey" is used to refer to the key we created.
- We also have created a new task for deploying our smart contract. To create the task, we use the task keyword and pass it a name and description. In this task we have written the code for deploying our smart contract. After deploying the contract, we call the verify:verify task, made available to us by the hardhat-etherscan plugin. We passed the contract address along with the parameters passed to the constructor while deploying the contract.
- Finally since we are going to deploy our contract to Mumbai testnet, we created a new entry in the network section and added the RPC url to Mumbai testnet along with the private key of the account which will be used to deploy the smart contract.
We are now ready to deploy our contract to testnet. Run the following command:
Now we have to wait for a few minutes as the contract is deployed and verified. 🤞🤞🤞
If everything is correct you should get an output similar to this:
You can now view your contract at the link provided in the output. You can check out my example of the deployed contract here.
If the private key you passed belongs to an account which doesn't have sufficient MATIC tokens to deploy the contract, you will get this error:
In this case, please remember to fund your deployment account using the faucet.
Invalid API Key
If the Polygonscan API Key you entered is missing or is not valid, you are going to get the following error message:
In this case please make sure you are entering the correct API key.
If we view our smart contract on Polygonscan, you can see that our contract is verified. This is indicated by a green checkmark in the Contracts tab. In the Contracts tab, click on
Write Contract. Now click on "Connect to Web3" and connect your Metamask account.
Now select the
mintaction and put in your tokenURI. I am using https://gambakitties-metadata.herokuapp.com/metadata/1 as my tokenURI. If you want, you can create your own tokenURI by hosting it using IPFS. Once you enter your tokenURI, click on the "Write" button. This will show a Metamask popup. Confirm the transaction and wait for it to be confirmed.
Congratulations🥳🥳🥳, Your NFT is successfully minted. You can visit the Opensea Testnet page and in the "My Profile" section you can now view your NFT. Checkout the example here.
In this tutorial, we learned some of the basics of HardHat. We wrote a smart contract that can be used to create NFTs, wrote tests for our smart contract and finally deployed it to the Mumbai testnet. We also verified our contract using a HardHat plugin and a Polygonscan API key. Using a similar procedure, we can build any number of DeFi projects and deploy to any EVM compatible network (Ethereum, Polygon, Binance Smart Chain, Avalanche, etc.).
Why stop here? Looking ahead, you can also learn how to build an NFT Marketplace contract where various NFTs can be listed, bought and sold. You can also dig deeper into other aspects of NFTs and learn new things 😎!
Hi, my name is Bhaskar Dutta and I am a blockchain Developer, Researcher and Freelancer. I am always looking forward to learning new things and I like to discuss Sitcoms. To know me better, you can check out my Github profile.
I found the following very helpful while learning: