In this project we will build a basic NFT smart contract, that will serve us as the backbone or foundation for almost any future NFT project.
Why? Because NFTs are not just jpegs, they carry many superpowers that could improve our lives in many ways.
What is an NFT? NFT stands for Non-Fungible Token. It is basically a crypto token that is unique and cannot be replaced, because there is no other token that is identical in utility and therefore value.
So, what can we do with them? Since they live on the blockchain and are unique, we can use them to verify ownership of a real world asset (real estate deeds, art), to represent a membership (club membership, fan club), to track real world items in a supply chain (medicine, food, clothing), to use as tickets (movies, events, concerts, VIPs) and many more use cases we have yet to discover.
But, what is it with all the hype around them? As well as being unique, they have the ability to carry properties inside them. The value inherent to blockchains and the fact ownership can be transferred, leads to people attaching value to them. This has created huge opportunities for marketplaces to buy and sell these assets.
Now that we know they can be much more than just an image, what are we waiting for!
By the end of this lesson we are going to learn a lot of things. A simple breakdown of the steps to get there is:
like the pros. For that we are going to set up our working environment in our computer, making sure we have all the necessary tools to create, debug and deploy smart contracts.
To begin our project, we should open a console (check your Operating System options on how to use one).
We will be using npm
, the package manager from Node, and its command npx
that allows us to run or execute a package or one of its scripts. If you are
comfortable with another package manager, feel free to use it.
Let’s create and cd
into our D_D Academy projects folder, and create a
folder for our NFT project:
# create a folder for all our D_D Academy projects mkdir d_d_academy cd d_d_academy # create a folder for this project mkdir projectNFT cd projectNFT # initialize our folder as an npm package npm init -y # install hardhat (and its dependencies) npm install --save-dev hardhat
The --save-dev
flag used in the last command, lets the project know it's a
development dependency (not needed in production). You can view what
dependencies are needed in the package.json
file in the root of the project.
Once we have our environment set up, we need to create a project.
We run the npx hardhat
command to create a basic project:
After you enter your npx hardhat
command, what information can you see? What
are those ten lines and what is?
// 888 888 888 888 888 // 888 888 888 888 888 // 888 888 888 888 888 // 8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 // 888 888 "88b 888P" d88" 888 888 "88b "88b 888 // 888 888 .d888888 888 888 888 888 888 .d888888 888 // 888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. // 888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 👷 Welcome to Hardhat v2.11.1 👷 ? What do you want to do? … ❯ Create a JavaScript project Create a TypeScript project Create an empty hardhat.config.js Quit
We choose Create a Javascript project
and hardhat will create an example
project for us. It will give us 3 prompts for options. Choosing the defaults is
ok for us. Here's what mine asked me:
✔ What do you want to do? · Create a JavaScript project ✔ Hardhat project root: · ~/d_d_academy/projectNFT ✔ Do you want to add a .gitignore? (Y/n) · y ✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
The project asked us to install @nomicfoundation/hardhat-toolbox
in the
last prompt. If they didn’t install or we accidentally chose ‘n’, we can always
install them manually with:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers
We delete some files so we start fresh:
rm contracts/*.sol rm scripts/*.js rm test/*.js
In a Hardhat project, the default folders are supposed to be for:
default paths
contracts/
is where the source files for your contracts should be.scripts/
is where simple automation scripts go.test/
is where your tests should go.We now need to add our last dependency (OpenZeppelin contracts):
npm install @openzeppelin/contracts
Open Zeppelin developed a lot of standard contracts that are super powerful, widely used and fully tested and audited.
With our project folder ready, we can now fire up our Code Editor.
We are using VSCode for this one (we think it has a good balance of costs vs. benefits) but you can use whatever you feel comfortable with. A few of the more popular code editors are VSCode, Atom, Sublime, Vim, emacs, and many more.
Enough rambling, let’s start writing our contract!
In your console, type code .
and yes, with that dot at the end! Wait for the
magic.
Let’s create an empty file named ProjectNFT.sol
inside the contracts/
folder
and copy this code inside:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; contract ProjectNFT { }
If you’ve been through our first project, you’ll remember the first lines defines our copyright license, the second one defines the solidity version we are going to be using for this contract and the last two lines are how we declare a smart contract in solidity.
With an empty project, now we can start adding what we need to create our awesome NFT collection. In particular, ERC721 (the formal specification to follow for NFTs) have a lot of requirements to meet (they should have a numbered ID, functions to transfer them, approve others as delegates to transfer them, etc).
All of it is pretty much for our project, but luckily OpenZeppelin has developed a lot of contracts that implement these standards and more, they are widely used and audited and, the thing we love about them, they are open-source and we can use them to leverage our learning process. With that in our heads, we are going to use them inside our contract.
For this we are going to ‘inherit’ (think of using the properties of or extending for now) the ERC721 contract from OpenZeppelin. In the same way you inherit the DNA and characteristics from your family/relatives.
This is what we need in our code:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract ProjectNFT is ERC721 { constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} }
import
statement, that lets us use the
contracts in the ERC721.sol
file.is ERC721
to our contract ProjectNFT
statement. This lets us
inherit all the variables, functions, events, etc from ERC721
into the new
ProjectNFT
contract, where we can use them, as if they were its own, as well
as any new code we write.constructor
function. This is called only once when the
contract is first deployed. The ERC721
constructor
which we will inherit
has two parameters, so we need to assign them arguments i.e. an actual name
and symbol for our NFT. Therefore, we will create a new constructor, while
still inheriting the ERC721
’s. Our constructor
will receive 2 parameters
and call the ERC721
constructor with them. Composability, eh?We need to identify our NFTs, this is what gives each one its uniqueness on the blockchain.
We are going to use a state variable to keep track of the total supply of NFTs, which we can use as our ID for each new token created:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract ProjectNFT is ERC721 { uint256 private totalSupply; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} }
We made our totalSupply variable private and it will store exactly how many NFTs have been minted so far. In the next step we will use this variable to identify each new NFT that’s created.
Once we have a way to identify our NFTs, we only need a way to create them.
We will add a function to mint new items, and yes, that means creating a new NFT:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract ProjectNFT is ERC721 { uint256 private totalSupply; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public { totalSupply++; _safeMint(msg.sender, totalSupply); } }
What our mint
function does first is increment our totalSupply
state
variable by 1, because our collection will have one item more. Then it calls a
function from our inherited ERC721
contract called _safeMint
that allows a
new item to be added to our ProjectNFT contract.
As you can see, OpenZeppelin implementation takes care of all the heavy lifting, but later in our learning journey we will be able to understand how it all works inside the hood.
We can create unique NFTs by now, but we didn't add any media or utility to them just yet.
We are missing the last piece of the puzzle. For now they are just a numbered proof of ownership from our contract, stored on the blockchain. So here is where the magic starts.
We can make our NFT collection about anything we want: text, images, music, videos, anything you want. But how?
The ERC721
implementation we inherited has a function to store a tokenURI
.
What is this black magic? This is just a unique URI, a web address if you prefer, that points to something on the internet, whatever we want it to be. e.g. a video, an image.
OpenZeppelin ERC721 contract provides us a way to create unique URIs from the
IDs of the NFTs. It gives a way to define a base path for a web address and just
attaches the token ID at the end of it. So, if you wanted, you could upload your
NFT info (we'll look at how we store that later) to any web address, say
www.my-site.com/my-nft-collection
and number each info file with the corresponding ID (1, 2, 3, 4…).
We are not going to limit the size of our collection for this project.
So, to let our inherited contract know where our NFTs will be stored, we have to
override
an OpenZeppelin function. Actually, you are expected to do this if
you look into OpenZeppelin's documentation. There’s a good reason for
everything. Well, lots of things. And this is one of them!
Here's the modifications we will add and I'll explain them below:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract ProjectNFT is ERC721 { uint256 private totalSupply; string private ipfsBaseURI; constructor(string memory _name, string memory _symbol, string memory baseURI) ERC721(_name, _symbol) { ipfsBaseURI = baseURI; } function mint() public { totalSupply++; _safeMint(msg.sender, totalSupply); } function _baseURI() internal view override returns (string memory) { return ipfsBaseURI; } }
What have we done?
override
modifier to indicate
that this function replaces the one that is inside our inherited contract.Internally, when someone needs to consult an NFT's info, they call the
OpenZeppelin's tokenURI
function. That function only joins the return
value of our newly _baseURI
function with the ID of the token requested.
With that we can see the power of inheritance and overriding. We can inherit a whole set of functionalities and change only what we need to create a different experience.
We have most of our smart contract ready, so we are going to focus on the media that we want on our NFTs.
We propose a "Choose your own adventure" for this part of the project:
If you want to go quickly into deploying the contract, you can use the images and files that we prepared, and move on to the next section.
If you want to create your own images and have an NFT of your own, you'll have to take the winding road and learn a little about the standard used to identify NFTs and a solution to store files in a decentralized way.
You might remember this from the previous project, when we used Remix. Once we have our contract ready, we can compile and deploy. In remix, we had a bunch of buttons in the web interface, but now we are getting pro (a.k.a. doing it all ourselves, with the console).
Why did we deviate from Solidity? Because of the way we set our contract up. When we deploy we tell our contract where our files are stored. Therefore we had to store the files somewhere to get the address we need to run the deployment.
So, now that we have our media and JSONs uploaded, we can deploy our NFT contract!
Remember that scripts folder at the beginning of our project? That's where we are going to create our script to tell Hardhat the set of instructions to follow. We can create one for deploy, another one to mint or call functions of our contract. Hardhat uses javascript for this.
So, let's create a deploy.js
under the scripts/
folder and copy this code:
// import Hardhat to use it inside our script const hre = require('hardhat') // We define a variable with the name of our contract const CONTRACT_NAME = 'ProjectNFT' // The 3 parameters for our smart contract's constructor (CHANGE THIS!!!) const NFT_NAME = 'Academy' const NFT_DESCRIPTION = 'D_D Academy Basic NFT Collection' // Change this if you created your own images/JSONs: const NFT_BASE_URI = 'https://gateway.pinata.cloud/ipfs/QmSCNdaA5JBT5D6V5FbivfZonKjdsMPeZeTyTWzq1D3yrJ/' // We define a function with all we want Hardhat to run async function main() { // We get the contract factory from Hardhat const ContractFactory = await hre.ethers.getContractFactory(CONTRACT_NAME) // We deploy the contract (notice we pass 3 parameters that our contract's constructor needs) const contract = await ContractFactory.deploy( NFT_NAME, NFT_DESCRIPTION, NFT_BASE_URI, ) // We wait for it to be deployed to the blockchain await contract.deployed() // We print the contract's address to the console console.log(`${CONTRACT_NAME} deployed to: ${contract.address}`) // --> ( We'll add more stuff here later ) <-- } // We call the main function we created above (don't change this) main() .then(() => process.exit(0)) .catch((error) => { console.error(error) process.exit(1) })
The code has comments outlining what each section does, but it basically has 1 variable for our contract's name so the script knows which contract to deploy, and 3 more variables for the data our constructor needs: a Name, a Description and a Base URI for our files. With that info set at the top, the script deploys the contract, waits for it to be deployed and then prints the address of our deployed contract to the console.
Before we run our script, we need to tell Hardhat what solidity version our
contracts are using. For that, we need to go into the hardhat.config.js
file
in our root folder. Find the line that says solidity: '0.8.xx',
and replace
the 0.8.xx
for the pragma used in our contract: 0.8.12
.
We could have used a range of solidity versions e.g. ^0.8.0
in our contract,
but we like to promote best practices by choosing a fixed solidity version
and knowing beforehand nothing is going to change in our bytecode if a new
version in that range is released.
Now, to run the script we type this in the console:
npx hardhat run scripts/deploy.js
This is the output i got:
Notice that ‘Compiled 10 Solidity files successfully’? But we only wrote 1 Solidity file!
Well, we are actually inheriting some of OpenZeppelin's implementations, so the compiler also has to process everything we are importing in our contract.
Since we are in a development environment, Hardhat provides us with the tools to run this in a local blockchain, without the hassle of dealing with blockchain times, tokens to pay gas for our deployment, etc. This means our contract was deployed to a dummy blockchain Hardhat created, it all went well, and then the blockchain gets deleted.
Taking advantage of this, we are going to add a mint after the deploy to test if all goes well.
Let's add these lines after the console.log part, right where we left a placeholder before in our deploy.js file:
// --> ( We'll add more stuff here later ) <-- // We call the contract to mint 1 NFT const tx = await contract.mint() // We wait for the transaction to finish await tx.wait() // We ask for the tokenURI of the recently minted NFT // (we assume 1 is the tokenId since we only minted one) const tokenURI = await contract.tokenURI(1) // We print out the tokenURI console.log(`NFT 1 tokenURI: ${tokenURI}`) }
Let's run the script again and see what it tells us! Here's my output:
We got the URI of the token, right? So, now we know we can deploy it (and mint) to a real blockchain!
For us to get our contract out of our computers and into the real world, we are going to run the deploy again, but this time to a testnet.
What is a testnet? It is a basically a whole running blockchain, but it runs only so people can try stuff out. On it, you have eth, tokens, NFTs, but they have no monetary value. This way, you can develop and test your contracts without fear of losing real money. For the moment, we are choosing to do this on the Goerli (it's one of the many Ethereum testnets)
Before we go any further, let's take an extra step for precaution, until the
next project where we are going to learn how to use collaborative tools to store
our projects. But until then, let's open our main folder's .gitignore
file an
add this line anywhere:
hardhat.config.js
In order to do this we need:
First and foremost, security. We are exploring new grounds, experimenting and by doing so, we could make mistakes that we don't want to happen where we have our money.
We strongly recommend you use a separate wallet for developing, completely independent from the one that holds your personal assets on the blockchain, if you have any.
Wallets have a special number, called a private key. You should never, ever share your private keys with anyone. If anyone has that number, they can take full control of the wallet, and empty it of all that is in it!
To be able to deploy our contract, we are going to use that number, that's why we recommend, yet again, to use a separate wallet for developing.
We need to inform Hardhat what node are we using to connect to the blockchain/network
Once you have your wallet funded with test eth, you need sign up for one of the Ethereum RPC Node Providers. Alchemy and Infura are the most used, Ankr has a "community endpoint" without signing up that is not dedicated, to list a few options.
Once you sign up, you'll be asked to create an App, be sure to select the Goerli network there.
When the app is created, you'll see a "View Key" button, or similar. Press it and copy the HTTP link, we'll use it in our next step.
Once you have funded your wallet with fake eth, we can go ahead and change our
Hardhat configuration file. Go into your main project's folder and open
hardhat.config.js
.
We are going to replace our file with this:
require('@nomicfoundation/hardhat-chai-matchers') require('@nomiclabs/hardhat-ethers'); const WALLET_PRIVATE_KEY = 'YOUR-PRIVATE-KEY-DONT-SHARE' const RPC_API_KEY = 'YOUR-API-KEY-FROM-INFURA-OR-ALCHEMY' /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.12', networks: { goerli: { url: RPC_API_KEY, accounts: [WALLET_PRIVATE_KEY], }, }, }
And you need to fill those 2 global variables with your own for hardhat to communicate correctly to the network.
We have already gone through how to get your API KEY.
For your wallet's private key, the most sensitive data of the project, you need
to open Metamask, click on the three dots next to your Account Name, and then on
Account Details, then click on Export Private Key. It will ask for your Metamask
password (the one you use to open it, NOT your seed phrase). It also shows you a
notice so you know that you are entering the danger zone. Confirm and you'll be
able to copy your private key. Paste it in to our hardhat.config.js
Please, if you are already a developer and you plan to use git to store your
project, don't store your hardhat.config.js
on it, because you will have your
private key there.
Ok. We are ready, let's deploy to the Goerli testnet!
Now we need to run our deploy.js script and the deployment is going to cost us some eth (test eth, don't worry) from our wallet, since we are storing information (i.e. our contract) in the blockchain. We only have to add 2 words to the command to let Hardhat know we are doing the real thing now. Remember our deploy script will create our contract AND mint one NFT. If you only want to deploy, delete all the lines we added to test the minting.
Run:
npx hardhat run scripts/deploy.js --network goerli
This is the output i got in my console:
Remember we are deploying to the actual blockchain, a testnet, but a blockchain nonetheless.
Now that we have run the deploy script, delete the private key and the RPC API
key from hardhat.config.js
to minimize the handling of the wallet. In the next
project we will learn about tools to collaborate and store your projects online
So now we can go and explore the chain to find your contract (and mine too!). Just go to Goerli Etherscan (or Opensea) and search for the address of our deployed contract.
You can view my links as an example on Etherscan and Opensea
And that's it! You have created your own NFT collection from scratch! What a superstar!!
Developer DAO Foundation © 2023
Website content licensed under CC BY-NC 4.0.
Website code is licensed under MIT.