Fungible Token with burning mechanism
In this course we will extend the ERC20 token we created in the Fungible Tokens Introduction course to include a burning mechanism. This will allow us to burn tokens from our wallet.
Why would we want to burn tokens?
There are many reasons why we would want to burn tokens. One of the most common reasons is to reduce the total supply of tokens. This can be useful if we want to increase the value of our tokens. It doesn't immediately increase the value, but the potential for increased value is there based on tokenomics (the economics of tokens).
You can learn about it in our Tokenomics course.
What is a burning mechanism?
A burning mechanism is a function that allows us to remove (or burn) tokens from our total supply. This is done by sending tokens to a special address that is not owned by anyone. This address is called the "burn address" or "zero address".
The zero address is literally an address that has all zeros in it like this: 0x0000000000000000000000000000000000000000. At the time of writing this, there is over $150M worth of tokens in this address.
How do we implement a burning 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
ERC20Burn.sol
.
In the test folder:
- Create a new file called
ERC20Burn.ts
.
Writing our smart contract
In the ERC20Burn.sol
file, add the following code and read the comments:
1// SPDX-License-Identifier: MIT 2pragma solidity ^0.8.18; 3 4import "@openzeppelin/contracts/access/Ownable.sol"; 5import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; 6 7contract ERC20TokenBurn is ERC20Burnable, 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
We are inheriting from ERC20Burnable
and Ownable
. We are also calling the _mint
function to mint tokens to the owner's address.
Let's have a look at what we are inheriting:
1/** 2 * This function allows the called to burn tokens from their wallet. 3 * It calls the `_burn` function which is in the `ERC20` contract. 4 * @param amount The amount of tokens to burn. 5 */ 6function burn(uint256 amount) public virtual { 7 _burn(_msgSender(), amount); 8} 9 10/** 11 * This function allows someone that has an allowance to burn tokens from another account. 12 * It calls the `_spenderAllowance` function which is in the `ERC20` contract. 13 * It calls the `_burn` function which is in the `ERC20` contract. 14 * @param account The account to burn tokens from. 15 */ 16function burnFrom(address account, uint256 amount) public virtual { 17 _spendAllowance(account, _msgSender(), amount); 18 _burn(account, amount); 19} 20
Now you might think this is cool and all, but how can we make it more... interesting?
Implementing burn on transfer
So let's think about the tokenomics for a second. What if we want to ensure that we can always have some sort of constant "upward pressure" on the value of our token without having to manually burn tokens?
I know what you're thinking. "What the heck is upward pressure?"
As we discuss in our Tokenomics course, upward pressure is the idea that the value of a token will increase over time. We can do this in several ways, but one of the ways we are going to do it is by burning tokens.
So how do we implement this? Well you might have guessed it. We are going to implement a burn function that will be called every time a transfer is made. We'll discuss this in more detail in the next course, ERC20 Burn on Transfer.
Writing our tests
In the ERC20Burn.ts
file, add the following code:
1import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2import { expect } from "chai"; 3import { ethers } from "hardhat"; 4 5describe("ERC20-burn", function () { 6 // We deploy the contract and mint 100 tokens to the owner 7 async function deployERC20BurnFixture() { 8 const [owner, account_1, account_2] = await ethers.getSigners(); 9 const ERC20Burn = await ethers.getContractFactory("ERC20TokenBurn"); 10 const erc20Burn = await ERC20Burn.deploy( 11 owner.address, 12 "Test Token", 13 "TST", 14 "100000000000000000000" 15 ); 16 return { erc20Burn, owner, account_1, account_2 }; 17 } 18 19 // We send 10 tokens to the other account 20 async function setupAccount1() { 21 const { erc20Burn, owner, account_1, account_2 } = await loadFixture( 22 deployERC20BurnFixture 23 ); 24 await erc20Burn.transfer(account_1.address, ethers.parseEther("10")); 25 return { erc20Burn, owner, account_1, account_2 }; 26 } 27 28 describe("Deployment Tests", function () { 29 it("Should set the right owner", async function () { 30 const { erc20Burn, owner } = await loadFixture(deployERC20BurnFixture); 31 expect(await erc20Burn.owner()).to.equal(owner.address); 32 }); 33 34 it("Should mint 100 tokens to the owner", async function () { 35 const { erc20Burn, owner } = await loadFixture(deployERC20BurnFixture); 36 expect((await erc20Burn.balanceOf(owner.address)).toString()).to.equal( 37 ethers.parseEther("100") 38 ); 39 }); 40 }); 41 42 describe("Test Burn Function", function () { 43 it("Should burn 10 tokens from the owner", async function () { 44 const { erc20Burn, owner } = await loadFixture(deployERC20BurnFixture); 45 await erc20Burn.burn(ethers.parseEther("10")); 46 expect((await erc20Burn.balanceOf(owner.address)).toString()).to.equal( 47 ethers.parseEther("90") 48 ); 49 }); 50 51 it("Should burn 10 tokens from non-owner", async function () { 52 const { erc20Burn, owner, account_1 } = await loadFixture(setupAccount1); 53 await erc20Burn.connect(account_1).burn(ethers.parseEther("10")); 54 expect( 55 (await erc20Burn.balanceOf(account_1.address)).toString() 56 ).to.equal(ethers.parseEther("0")); 57 }); 58 }); 59});
So let's go over what we did here:
-
We created a fixture function called
deployERC20BurnFixture
as we did in our other courses. The fixture sets up our contract and mints 100 tokens to the owner. It also returns the contract, owner and two other accounts. -
We created another fixture called
setupAccount1
. This fixture calls thedeployERC20BurnFixture
fixture and then transfers 10 tokens to the first account. -
We then created a describe block called
Deployment Tests
. This block contains two tests that check if the owner is set correctly and if the owner has 100 tokens. -
Lastly, we created another describe block called
Test Burn Function
. This block contains two tests. The first test checks if the owner can burn 10 tokens. The second test checks if a non-owner can burn 10 tokens.
And that's it, we created our tests for our ERC20 token with a burning mechanism!
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 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-erc20Burn.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("ERC20TokenBurn"); 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});
Good job! You have successfully created an ERC20 token with a burning mechanism!
To deploy it to the test net, you can run the following command:
1npx hardhat run scripts/deploy-erc20Burn.ts --network sepolia
Conclusion
In this course, we learned how to create an ERC20 token with a burning mechanism. In future, we will look at how to create a full-fledged ecosystem around our token that implements minting and burning mechanisms.