Fungible Token: Burning

Fungible Token: Burning

Wednesday Feb 21, 2024

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:

  1. Create a new file called ERC20Burn.sol.

In the test folder:

  1. 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:

  1. 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.

  2. We created another fixture called setupAccount1. This fixture calls the deployERC20BurnFixture fixture and then transfers 10 tokens to the first account.

  3. 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.

  4. 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});

HAPPY

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.