Fungible token with minting mechanism
In this course we will extend the ERC20 token we created in the previous course to include a minting mechanism. This course will also form the basis for our course on creating a faucet dApp (decentralized app).
Why would we want to mint tokens?
Honestly, there are not many really good reasons for minting tokens as in most cases minting tokens will lead to inflation. The best way to combat inflation is to have some kind of burning mechanism in place to combat the inflation. You can learn about burning tokens in our ERC20 burning course.
However, if you want to create a faucet dApp where users can get some of your tokens for free, then you will need to have a minting function in your token contract.
What is a minting mechanism?
Minting is the process of creating new tokens. This is done by calling a function in our smart contract that will add tokens to the total supply. If your token is being used for a use-case other than the faucet, I highly recommend you do not implement a minting mechanism or if you have to, make sure that:
- You lock down the minting mechanism using the
onlyOwner
modifier in conjunction with theOwnable
contract from OpenZeppelin. - You have a burning mechanism in place to combat inflation.
How do we implement a minting mechanism?
This course assumes you have set up the development environment as outlined in the Smart Contracts Introduction Course. If you haven't done so, it's recommended that you do so before continuing.
Setting up our project
Before we start, we need to create a few files.
In the contracts folder:
- Create a new file called
ERC20Mint.sol
.
In the test folder:
- Create a new file called
ERC20Mint.ts
.
Writing our smart contract
In the ERC20Mint.sol
file, add the following code and read the comments:
1// SPDX-License-Identifier: MIT 2pragma solidity ^0.8.18; 3 4import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5import "@openzeppelin/contracts/access/Ownable.sol"; 6 7contract ERC20TokenMint is ERC20, Ownable { 8 /** 9 * This is our constructor. 10 * @param owner The address of the owner 11 * @param name The name of the token 12 * @param symbol The symbol of the token 13 * @param totalSupply The total supply of the token 14 */ 15 constructor( 16 address owner, 17 string memory name, 18 string memory symbol, 19 uint256 totalSupply 20 ) ERC20(name, symbol) { 21 _mint(owner, totalSupply); 22 } 23 24 /** 25 * This is our minting function that can be called by anyone. 26 * It will send new tokens to the caller's address. 27 * @param amount The amount of tokens to mint. 28 */ 29 function mint(uint256 amount) public { 30 _mint(msg.sender, amount); 31 } 32}
We are inheriting from ERC20
and Ownable
. We are also calling the _mint
function to mint tokens to the owner's address.
In this case, we allow our mint to be called by anyone since the purpose of this contract will be to allow users to get our tokens from a faucet.
Writing our tests
In the ERC20Mint.ts
file, add the following code and read the comments:
1import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2import { expect } from "chai"; 3import { ethers } from "hardhat"; 4 5describe("ERC20-mint", function () { 6 // We deploy the contract and mint 100 tokens to the owner 7 async function deployERC20MintFixture() { 8 const [owner, account_1, account_2] = await ethers.getSigners(); 9 const ERC20Mint = await ethers.getContractFactory("ERC20TokenMint"); 10 const erc20Mint = await ERC20Mint.deploy( 11 owner.address, 12 "Test Token", 13 "TST", 14 "100000000000000000000" 15 ); 16 return { erc20Mint, owner, account_1, account_2 }; 17 } 18 19 describe("Deployment Tests", function () { 20 it("Should set the right owner", async function () { 21 const { erc20Mint, owner } = await loadFixture(deployERC20MintFixture); 22 expect(await erc20Mint.owner()).to.equal(owner.address); 23 }); 24 25 it("Should mint 100 tokens to the owner", async function () { 26 const { erc20Mint, owner } = await loadFixture(deployERC20MintFixture); 27 expect((await erc20Mint.balanceOf(owner.address)).toString()).to.equal( 28 ethers.parseEther("100") 29 ); 30 }); 31 }); 32 33 describe("Test Minting Function", function () { 34 it("Should mint 10 tokens to the owner", async function () { 35 const { erc20Mint, owner } = await loadFixture(deployERC20MintFixture); 36 await erc20Mint.mint(ethers.parseEther("10")); 37 expect((await erc20Mint.balanceOf(owner.address)).toString()).to.equal( 38 ethers.parseEther("110") 39 ); 40 }); 41 42 it("Should mint 10 tokens to account_1 (non-owner)", async function () { 43 const { erc20Mint, account_1 } = await loadFixture( 44 deployERC20MintFixture 45 ); 46 await erc20Mint.connect(account_1).mint(ethers.parseEther("10")); 47 expect( 48 (await erc20Mint.balanceOf(account_1.address)).toString() 49 ).to.equal(ethers.parseEther("10")); 50 expect((await erc20Mint.totalSupply()).toString()).to.equal( 51 ethers.parseEther("110") 52 ); 53 }); 54 }); 55});
Let's have a look at the tests we have written:
-
We created a deployment fixture that will deploy our contract and mint 100 tokens to the owner.
-
We test that the deployment has been done correctly.
-
We created a describe block for our minting function called
Test Minting Function
. This block contains two tests:- We test that the owner can mint tokens to their own address.
- We test that a non-owner can mint tokens to their own address (This is important for our faucet dApp).
In both cases we check that the balance of the address has increased by 10 tokens and that the total supply has increased by 10 tokens.
Compiling and running our smart contracts
To run our tests and make sure everything works, run the following command in your terminal:
1npx hardhat test
Now that we have our tests, we can deploy our ERC20 mintable token to the test network.
Deploying our smart contract
To deploy our smart contract, we need to create a deployment script. In the scripts
folder, create a new file called deploy-erc20mint.ts
and add the following code:
1// We require the Hardhat Runtime Environment explicitly here. This is optional 2// but useful for running the script in a standalone fashion through `node <script>`. 3// 4// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat 5// will compile your contracts, add the Hardhat Runtime Environment's members to the 6// global scope, and execute the script. 7import hre from "hardhat"; 8 9const config = { 10 owner_address: "your address here", 11 token_name: "Test Token", 12 token_symbol: "TT", 13 total_supply: "100000000000000000000", // 100 tokens (18 decimal places) 14} 15 16async function main() { 17 const { owner_address, token_name, token_symbol, total_supply } = config; 18 19 console.log(`Deploying token ${token_name}. Owner: ${owner_address}...`); 20 21 const Token = await hre.ethers.getContractFactory("ERC20TokenMint"); 22 const token = await Token.deploy( 23 owner_address, 24 token_name, 25 token_symbol, 26 total_supply 27 ); 28 29 console.log(`Deployed token. Owner: ${owner_address}`); 30 31 console.log("Waiting 1 minute for Etherscan to index the contract..."); 32 await new Promise((r) => setTimeout(r, 60000)); 33 34 console.log("Verifying contract..."); 35 await hre.run("verify:verify", { 36 address: token.getAddress(), 37 constructorArguments: [ 38 owner_address, 39 token_name, 40 token_symbol, 41 total_supply, 42 ], 43 }); 44 45 console.log("Contract verified! 🎉"); 46 console.log( 47 `Please find the verified contract on Etherscan: https://sepolia.etherscan.io/address/${token.getAddress()}` 48 ); 49} 50 51// We recommend this pattern to be able to use async/await everywhere 52// and properly handle errors. 53main().catch((error) => { 54 console.error(error); 55 process.exitCode = 1; 56});
You have successfully created your ERC20 mintable token!
To deploy it to the test net, you can run the following command:
1npx hardhat run scripts/deploy-erc20mint.ts --network sepolia
Conclusion
In this course we learned you learned how to create a mintable ERC20 token which will be the basis for our faucet dApp.