This concise hardhat tutorial has 3 sections and this is section 3.
1 . Installation and sample project
2 . Write ERC20 token with OpenZeppelin
3 . Write ERC72 NFT token with on-chain SVG image
Hardhat is an Ethereum development tool suite to compile, unit test, debug and deploy smart contracts.
3. Write a Loot-like NFT Token with SVG image on-chain
In this section, we will write a smart contract for an ERC721 NFT Token. Instead of storing metadata(image and properties) on your server or IPFS, we will store SVG images on-chain like the Loot project. SVG image format is supported by Opensea's storefront and you can view the NFT in it after the contract is deployed to ethereum blockchain mainnet, testnet or Polygon PoS chain.
If you want to know more about metadata, OpenSea provides a good explanation in its tutorial: https://docs.opensea.io/docs/metadata-standards . Please note that if you want your NFT token smart contract to work with Opensea marketplace correctly, you'll need to make it ownable
and add proxyRegistryAddress
property. Find out more in Opensea documents.
In this tutorial named "A Concise hardhat Tutorial", let's focus on the usage of Hardhat.
In step 2-4, we will compile, test, deploy. Then we will add metadata which has an image in SVG format.
Step 1: Install OpenZeppelin Contracts
If OpenZeppelin is not installed yet, install it by running:
yarn add @openzeppelin/contracts
Step 2: Write an ERC721 NFT Token with OpenZeppelin
We inherit an ERC721 contract from OpenZeppelin ERC721 token contract.
Some explanations about our ERC721 NFT contract:
- TokenID starts at 1 and auto-increments by 1.
- Everyone can mint a NFT token by calling
mintTo(to)
with Token ID.
Metadata will be added in the following steps.
// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract BadgeToken is ERC721 {
uint256 private _currentTokenId = 0;//Token ID here will start from 1
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
}
/**
* @dev Mints a token to an address with a tokenURI.
* @param _to address of the future owner of the token
*/
function mintTo(address _to) public {
uint256 newTokenId = _getNextTokenId();
_mint(_to, newTokenId);
_incrementTokenId();
}
/**
* @dev calculates the next token ID based on value of _currentTokenId
* @return uint256 for the next token ID
*/
function _getNextTokenId() private view returns (uint256) {
return _currentTokenId+1;
}
/**
* @dev increments the value of _currentTokenId
*/
function _incrementTokenId() private {
_currentTokenId++;
}
}
Compile by running:
yarn hardhat compile
Step 3: Write the deploy script
We Write a deploy script scripts/deploy_BadgeToken.ts
like this:
// scripts/deploy_BadgeToken.ts
import { ethers } from "hardhat";
async function main() {
const BadgeToken = await ethers.getContractFactory("BadgeToken");
console.log('Deploying BadgeToken ERC721 token...');
const token = await BadgeToken.deploy('BadgeToken','Badge');
await token.deployed();
console.log("BadgeToken deployed to:", token.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run the deploy script:
yarn hardhat run scripts/deploy_BadgeToken.ts
Step 4: Write Unit Test
Again, we write a simple unit test for our BadgeToken ERC721 token:
- Check name and symbol
- Mint 2 NFTs
The unit test script is test/BadgeToken-test.js
:
// We import Chai to use its asserting functions here.
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
describe("BadgeToken", function () {
async function deployBadgeTokenFixture() {
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();
const BadgeToken = await ethers.getContractFactory("BadgeToken");
const token = await BadgeToken.deploy('BadgeToken','Badge');
return { token, owner, otherAccount };
}
describe("Deployment", function () {
it("Should has the correct name and symbol", async function () {
const { token, owner } = await loadFixture(deployBadgeTokenFixture);
const total = await token.balanceOf(owner.address);
expect(total).to.equal(0);
expect(await token.name()).to.equal('BadgeToken');
expect(await token.symbol()).to.equal('Badge');
});
});
describe("Mint NFT", function () {
it("Should mint a token with token ID 1 & 2 to account1", async function () {
const { token, owner, otherAccount } = await loadFixture(deployBadgeTokenFixture);
const address1=otherAccount.address;
await token.mintTo(address1);
expect(await token.ownerOf(1)).to.equal(address1);
await token.mintTo(address1);
expect(await token.ownerOf(2)).to.equal(address1);
expect(await token.balanceOf(address1)).to.equal(2);
});
});
});
Run unit test:
yarn hardhat test test/BadgeToken.test.ts
Output:
GLDToken
Deployment
✔ Should has the correct name and symbol (731ms)
Mint NFT
✔ Should mint a token with token ID 1 & 2 to account1
2 passing (758ms)
✨ Done in 3.30s.
Step 5: Add metadata: name, description and svg image
Note: Step 5/6 is an advanced topic. You can also refer to my other tutorial: Web3 Tutorial: Build an NFT marketplace DApp like OpenSea
We need to do base64 encoding in Solidity. The SVG format image is encodes with base64 and then included in the metadata. Metadata is also encoded with base64.
We use the base64.sol library to conduct base64 encode adapted from the Loot project. The original base64 library by Brecht Devos is: https://github.com/Brechtpd/base64 . 0xMoJo7 wrote a on-chain SVG generation tutorial on dev.to, you can also refer to this link: https://dev.to/0xmojo7/on-chain-svg-generation-part-1-2678 .
Metadata is returned by ERC721 API function tokenURI
. Make some changes to the contract:
- Import
base64.sol
and OpenZeppelin UtilsStrings.sol
. - Implement function
tokenURI(tokenId)
. We create a SVG format image with black background and white tokenId. Name is set as Badge+tokenId ("Badge #1" for example). Description is set as "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look."
// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./base64.sol";
contract BadgeToken is ERC721 {
uint256 private _currentTokenId = 0;//Token ID here will start from 1
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
}
/**
* @dev Mints a token to an address with a tokenURI.
* @param _to address of the future owner of the token
*/
function mintTo(address _to) public {
uint256 newTokenId = _getNextTokenId();
_mint(_to, newTokenId);
_incrementTokenId();
}
/**
* @dev calculates the next token ID based on value of _currentTokenId
* @return uint256 for the next token ID
*/
function _getNextTokenId() private view returns (uint256) {
return _currentTokenId+1;
}
/**
* @dev increments the value of _currentTokenId
*/
function _incrementTokenId() private {
_currentTokenId++;
}
/**
* @dev return tokenURI, image SVG data in it.
*/
function tokenURI(uint256 tokenId) override public pure returns (string memory) {
string[3] memory parts;
parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';
parts[1] = Strings.toString(tokenId);
parts[2] = '</text></svg>';
string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2]));
string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Badge #', Strings.toString(tokenId), '", "description": "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
output = string(abi.encodePacked('data:application/json;base64,', json));
return output;
}
}
Compile, test and deploy:
yarn hardhat compile
yarn hardhat test
yarn hardhat run scripts/deploy_BadgeToken.ts
Step 6: Play with the NFT contract
Let's run a stand-alone hardhat network and deploy our contract to it. Now we can play with the NFT contract from hardhat console.
In another terminal, run command line in tutorial directory:
yarn hardhat node
Back to the current terminal, run deployment:
yarn hardhat run scripts/deploy_BadgeToken.ts --network localhost
//Output: Deploying BadgeToken ERC721 token...
// BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Open console:
yarn hardhat console --network localhost
In the terminal, run:
const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const token721 = await ethers.getContractAt("BadgeToken", address);
const accounts = await hre.ethers.getSigners();
owner = accounts[0].address;
toAddress = accounts[1].address;
await token721.symbol()
//'Badge'
Mint NFT and view it in online tools.
//mint NFT tokenId 1
await token721.mintTo(toAddress)
//mint NFT tokenId 2
await token721.mintTo(toAddress)
//mint NFT tokenId 3
await token721.mintTo(toAddress)
await token721.balanceOf(toAddress)
//3
Get the metadata by calling tokenURI
:
await token721.tokenURI(3)
//Output:
//'data:application/json;base64,eyJuYW1lIjogIkJhZGdlICMzIiwgImRlc2NyaXB0aW9uIjogIkEgY29uY2lzZSBIYXJkaGF0IHR1dG9yaWFsIEJhZGdlIE5GVCB3aXRoIG9uLWNoYWluIFNWRyBpbWFnZXMgbGlrZSBsb29rLiIsICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SW5oTmFXNVpUV2x1SUcxbFpYUWlJSFpwWlhkQ2IzZzlJakFnTUNBek5UQWdNelV3SWo0OGMzUjViR1UrTG1KaGMyVWdleUJtYVd4c09pQjNhR2wwWlRzZ1ptOXVkQzFtWVcxcGJIazZJSE5sY21sbU95Qm1iMjUwTFhOcGVtVTZJREUwY0hnN0lIMDhMM04wZVd4bFBqeHlaV04wSUhkcFpIUm9QU0l4TURBbElpQm9aV2xuYUhROUlqRXdNQ1VpSUdacGJHdzlJbUpzWVdOcklpQXZQangwWlhoMElIZzlJakV3SWlCNVBTSXlNQ0lnWTJ4aGMzTTlJbUpoYzJVaVBqTThMM1JsZUhRK1BDOXpkbWMrIn0='
We will use the online base64 decoder https://www.base64decode.org/ to get the original data.
We need to conduct two decode processes: first, decode the output data; second, decode the SVG image data.
In the first decode process, we get the following result. You can read the name and description in it.
{"name": "Badge #3", "description": "A concise Hardhat tutorial Badge NFT with on-chain SVG images like look.", "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj48c3R5bGU+LmJhc2UgeyBmaWxsOiB3aGl0ZTsgZm9udC1mYW1pbHk6IHNlcmlmOyBmb250LXNpemU6IDE0cHg7IH08L3N0eWxlPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIiAvPjx0ZXh0IHg9IjEwIiB5PSIyMCIgY2xhc3M9ImJhc2UiPjM8L3RleHQ+PC9zdmc+"}
The SVG data is still in base64. Let's decode it online:
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">3</text></svg>
You can take a look at the image in online SVG viewers such as https://www.svgviewer.dev/.
The image is a SVG image with tokenId in it.
As you can see we successfully write an ERC72 token smart contract with metadata and on-chain SVG image.
Note for deploying this NFT contract to public chain
You may try to deploy this NFT contract to public chain such as Polygon Mainnet and view it on marketplaces like Opensea. You will need an Alchemy Account and APIKEY to deploy contract to Polygon as well as $MATIC asset for paying gas.
Here is an outline:
- Configure the network part in
hardhat.config.ts
for Polygon Mainnet. (Get RPC URL in your Alchemy dashboard. Use a new account with just a small amount $MATIC for this test purpose and DO NOT use your main account for assets.) - Deploy the BadgeToken ERC721 contract to Polygon Mainnet
- Play with your ERC721 contract using Hardhat console: mint, transfer and etc.
- View and transfer NFT using marketplaces like Opensea.
Alchemy also provides some handy NFT APIs to pull NFT metadata. When your NFT contract is deployed to its supported network such as Polygon, you can try these APIs.
This is the end of "A concise hardhat tutorial" with 3 sections.
1 . Installation and sample project
2 . Write ERC20 token with OpenZeppelin
3 . Write ERC72 NFT token with on-chain SVG image
If you feel this tutorial useful, follow me at Twitter: @fjun99. DM is open.
Top comments (3)
Hi!, Great Tutorial. However I stuck before minting. Please look at the log below.
Did I missed something? The deployment address is not a contract account. What does that mean?
I don't understand why, in tokenURI(), parts is declared as
string[17] memory parts
when only the first 3 elements are accessed. Why isn't it string[3]??
Should be string[3].
I take it from loot's contract for quick and dirty implementation. And forget to change the details.
Thanks.