Introduction
Among this "web3" and blockchain hype, I decided to venture into the dev side of the hype. Initialized hardhat project, opened my editor & struck the keyboard keys until I got to a working thingy & in this post I'll be walking you through how to make one & share what I learned along the way.
Let's get started!
Pre-Requisites
A short note to all my beginner friends, this post is totally suited for you & I'll be walking you through, *LIKE A BABY** on how to write these smart contracts*
Before jumping on to the code, let us just zoom out for a second & ask:
"What are smart contracts"
"What are NFTs" and
"What are Tokens anyways"
A Smart Contract is simply some code, written in the solidity programming language (Solidity is for Ethereum Blockchain. Other blockchains may require something different if they have smart contracts), deployed on the Ethereum Blockchain to perform certain operations defined in the code itself. You see, the Ethereum Blockchain consists of thousands of decentralized compute nodes sitting & crunching numbers to keep the blockchain active. When you deploy code on the blockchain, you're required to provide incentives or fees to encourage nodes to process your deployment transaction.
Also, anyone (a user with some wallet address) calling your smart contract function which causes some state change in your code (like modifying the value of a variable in your code), will have to give a fee.
This fee is known as GAS FEE, similar to you providing π° to AWS for their lambda functions.
A Token on the other hand is SOMETHING on the blockchain, with which these smart contracts interact, creating, manipulating, or destroying them. That SOMETHING can itself be anything saved in a smart contract's state, some text, number, etc. while REPRESENTING numerous things like an image, currency, or even a virtual girlfriend! the possibilities are endless!
Let's see an example of a smart contract written in solidity, which maintains a record of "who called the smart contract how many times?"
In this solidity code or Ethereum smart contract, we have a map (a data structure that is used to maintain key-value pairs in solidity, here the key is user address & value is a positive 256-bit integer) contractCalls
which can be viewed publicly & a public functionincrementState
which grabs the address of the user who called the function by using msg.sender
and increments its value in that map.
[Note: when grabbing a non-existing msg.sender
address with contractCalls[msg.sender]
, solidity defaults to 0 (zero) for an unsigned int (uint)]
If we call contractCalls[msg.sender]
again to see the number accumulated by the user, THAT number, THAT UINT256, IS THE TOKEN!
That's it!
Also, looking at the solidity code, if you've ever worked with any object-oriented language, the contract Test { ... }
syntax along with that public
keyword may look similar to you, because it is. Solidity, like any other OOP language, supports visibility modifiers (public
, private
, internal
& external
) and concepts like inheriting from other contracts.
Yeah, I know, it was an absurd way of telling someone "what is a token?" by teaching them how to write an almost hello world version of a new language, but hey! what if, I would've shown you this instead
TOKENS are digital assets defined by a project or smart contract and built on a specific blockchain. Token can be UTILITY TOKENS or SECURITY TOKENS. UTILITY TOKENS are also called consumer or incentive tokens
Now, let us discuss, "What is an NFT?"
NFT, short for Non-Fungible Tokens (which I think, even a 3rd grader knows), is a class of tokens that have a standard protocol of creating, transferring, and burning/destroying them. A smart contract should follow the rules laid down by ERC721 (standard for Non-fungible tokens) in order to even count as an NFT.
Code
When talking about developing & testing smart contracts, the first thing which comes to me is Remix IDE, an online smart contract compiler. Remix makes writing & testing smart contracts extremely convenient. But Remix is not THE GO-TO choice when developing big projects with solidity.
To develop complex & big projects in solidity, you need the Hardhat Toolchain which helps you efficiently write smart contracts, compile them, deploy them on testnet or even mainnet and perform automated tests.
I'll be talking in detail about the whole hardhat dev workflow in another post, as it sometimes makes things LOOK complex while they aren't and in this post, I target to cover our beginner friends too π
For our cute little NFT smart contract, let us, for now, open up the Remix IDE, create a new file in the contracts
folder, and write the following stuff in there:
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyToken is ERC721 {
uint256 public totalMints = 0;
uint256 public mintPrice = 1 ether;
uint256 public maxSupply = 50;
uint256 public maxPerWallet = 5;
string public URI = "https://bafybeifqmgyfy4by3gpms5sdv3ft3knccmjsqxfqquuxemohtwfm7y7nwa.ipfs.dweb.link/metadata.json";
mapping(address => uint256) public walletMints;
constructor() ERC721("MyToken", "MTK") {}
function safeMint(address to) internal {
uint256 tokenId = totalMints;
totalMints++;
_safeMint(to, tokenId);
}
function mintToken(uint256 quantity_) public payable {
require(quantity_ * mintPrice == msg.value, "wrong amount sent");
require(walletMints[msg.sender] + quantity_ <= maxPerWallet, "mints per wallet exceeded");
walletMints[msg.sender] += quantity_;
safeMint(msg.sender);
}
function getMyWalletMints() public view returns (uint256) {
return walletMints[msg.sender];
}
}
The first line is used to tell the solidity compiler what version of solidity we're using, this is important as this language is relatively new & thus has ongoing changes (and, don't worry, even I don't know what the hell "pragma" means π€‘). And, due to the immutable nature of these smart contracts running on the blockchain, THERE IS NO UPDATING YOUR CODE, thus we need to specify, what version was used when this code was written.
Next, comes the import statement to import the ERC721 contract from open zeppelin into our contract to add the basic functionality of an NFT on top of our sh*tty code. There are some really important reasons, why we need to import this rather making our own buggy implementation π:
- Too lazy to write all of these functions (see implementation docs here)
- The implementations provided by open zeppelin are tested hundreds of thousands of times & thus can be trusted more than our own code, also, their code has been optimized to require as low gas fees as possible while providing maximum functionality when run on actual Ethereum nodes
Our contract MyToken
inherits all internal and public variables & functions of the ERC721
contract provided by open zeppelin, this means that now we have functions like _safeMint()
that could handle all the functionality of assigning someone a token with some ID, we just have to control who takes how much & what's the total supply for the NFT.
Now, in the contract let's define some state variables:
-
totalMints
is a publicly visible positive integer that we'll be using to count how many tokens have been minted so far. that we'll also use to increment & assign a new & unique number to each token or NFT.
uint256 public totalMints = 0;
-
mintPrice
is a public uint which has a value of1 ether
, solidity has a special suffixether
for integers which are to be represented as some ETH value.
uint256 public mintPrice = 1 ether;
-
maxSupply
is the total number of NFTs we decide to mint (public uint) so as to limit mints.
uint256 public maxSupply = 50;
-
maxPerWallet
is the maximum NFTs a single wallet/user/address can have (public unit).
uint256 public maxPerWallet = 5;
-
URI
is the URL which points to JSON data containingname
,description
&image
for this NFT. This JSON data is also known as "metadata" & it should at least have at least these 3 properties to correctly show the image in your wallet (Metamask or any other wallet). Also, I uploaded the JSON metadata file for this NFT on https://web3.storage which is a decentralized file system to store & host your uploaded files (see the video below for your "how to's"). You can choose a centralized service or even host it yourself but what's the point of all this if you want to use centralized servers in the end to host the metadata. Notice when looking at the JSON pasted, I used the image which seems to be coming from a web server & you're correct, which I can justify by saying, "Yeah... π" but you would want to host the image on it or any other IPFS π¬.
string public URI = "https://bafybeifqmgyfy4by3gpms5sdv3ft3knccmjsqxfqquuxemohtwfm7y7nwa.ipfs.dweb.link/metadata.json";
-
walletMints
which is a map datatype to contain key-value pairs of addresses mapped to uint (token ids). We'll be using it to limit mints per wallet.
mapping(address => string) public walletMints;
Now coming to the contract's constructor, a special function that runs only when the contract is deployed. When declaring the constructor, we call the ERC721 contract's constructor to initialize the inherited state from ERC721 with ERC721("TokenName", "TKNSYMBL")
.
constructor() ERC721("MyToken", "MTK") {}
Notice, that after calling ERC721 contract's constructor, we left the body of our contract's constructor empty and that is because, FOR NOW, we don't need to perform any operations when our contract is deployed.
Next, comes this function
function safeMint(address to) internal {
uint256 tokenId = totalMints;
totalMints++;
_safeMint(to, tokenId);
}
It uses the _safeMint
function inherited from the ERC721
contract which mints or assigns an address a token ID. And before doing so, makes sure that the token id is not owned by anyone. There is also a function _mint
that does NOT check for ownership and just assigns the address to the given token id irrespective of whether the token id is already assigned to some address or not (which is almost never recommended... unless you want to shoot yourself in the foot).
[see a full list of all functions provided by the ERC721 contract here]
Also, while these functions like _safeMint
& _mint
are callable from our inherited contract, users cannot directly call them as internal
functions of a contract are usable by their inherited contract but can't be called from external wallet addresses. If they were callable, anyone could have minted unlimited tokens for free by calling it on their address and different token ids!
OUR safeMint
function is marked as internal
which means users can't directly call this function externally. This is what we want because the "pay to mint" functionality is still missing from our contract & we will be creating a new function to ensure users follow the rules laid down by us when buying this NFT(e.g following the limits for max supply & mints per wallet).
To ensure that users are paying for this NFT, we will be creating a function mintToken
which will be calling our internal
function safeMint
along with performing some additional checks. It'll take a positive integer as its parameter quantity_
, requiring users to pay the amount of mintPrice * quantity_
in ethers (e.g exactly 2 ether for 2 NFT purchases) and check if this purchase does not exceed the maxPerWallet
limit for the wallet calling the function.
function mintToken(uint256 quantity_) public payable {
require(quantity_ * mintPrice == msg.value, "wrong amount sent");
require(walletMints[msg.sender] + quantity_ <= maxPerWallet, "mints per wallet exceeded");
walletMints[msg.sender] += quantity_;
safeMint(msg.sender);
}
Notice the payable
keyword in front of the visibility modifier public
which indicates to the solidity compiler that, specifically this function mintToken
CAN accept any ether value sent to it while calling it.
"What!? these contracts can store my ether?!" Yup! when someone purchases your NFT, they send this ether to the contract & are stored on the contract itself until you decide to add code to send that balance to yourself, or else the ether is just stuck there FOREVER!
For checking that the limits are being followed, we use the inbuilt require
function to check a condition, if it turns out to be false
, the function will throw an error with the specified message (in the 2nd argument in the require(condition, "error message")
function), reverting the whole function call transaction along with the sent ether.
If everything goes fine, it will accept the sent ether, increment the walletMints
for the user's wallet address and call the safeMint
function on the user's address (referenced by msg.sender
) to actually mint the NFT to them.
The same thing could be done in this way too, where you pass one more parameter in the mintToken
function named recipient
whom to mint the NFT. Both ways are totally right & its your call what to implement.
function mintToken(uint256 quantity_, address recipient) public payable {
require(quantity_ * mintPrice == msg.value, "wrong amount sent");
require(walletMints[recipient] + quantity_ <= maxPerWallet, "mints per wallet exceeded");
walletMints[recipient] += quantity_;
safeMint(recipient);
}
Now our NFT contract is almost finished with the functionality to purchase & transfer the tokens, let's test it on remix ide
Great! now let's write one more function to actually withdraw your "hard-earned" ether from the contract's balance.
address owner;
constructor() ERC721("MyToken", "MTK") {
owner = msg.sender;
}
function withdraw() public {
payable(owner).transfer(address(this).balance);
}
Here, we declare a variable owner
of a special datatype address
which will store a public wallet address. In the constructor, we assign it to the address of the person who called the constructor, or the person who deployed the contract (by referencing msg.sender
).
The withdraw
function is publicly callable & will transfer all the ether balance present in the contract (use address(this).balance
to get the balance of the contract) to the owner's address.
Syntax for sending ETH from contracts to wallet addresses:
payable(sender_address).transfer(X ether)
Aaaand... that's a wrap!
Like this post, if you liked it π
But if you loved it? man you gotta follow me on Twitter π
Want to show some more love? here's my public wallet address
0x9d1092f5cd0459eDaff0bcA6943dC6A6F8F85F38
Feel free to donate 1,2,3,4,5 ETH π
Top comments (1)
Hi, thanks for great tutorial.
Just don't understand
How do you add
"Name" and "Description" ?