Fungible Token with burning on transfer
In this course we will improve the ERC20 token we created in the Fungible Token Burning course.
I'm not going to go over all the burning mechanism details in this course as we have covered it in quite some detail in the previous course linked above.
Goal
- Improve our ERC20 token to include a burning mechanism on transfer. This means that every time a transfer is made, a percentage of the tokens will be burned.
- Implement an additional burn function the owner can use to manually burn tokens from their wallet. When the owner buys back tokens from the market, they can burn them to increase the value of the token.
This will create a hyper-deflationary token that will increase in value over time according to the tokenomics.
Setting up our project
Before we start, we need to create a few files.
In the contracts folder:
- Create a new file called
ERC20BurnOnTransfer.sol
.
In the test folder:
- Create a new file called
ERC20BurnOnTransfer.ts
.
Writing our smart contract
In the ERC20BurnOnTransfer.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 ERC20TokenBurnOnTransfer is ERC20, Ownable { 8 9 uint8 private _burnPercentage; 10 mapping (address => bool) private _excludedFromBurn; 11 12 /** 13 * Our constructor. 14 * @param owner The owner of the contract 15 * @param name The name of the token 16 * @param symbol The symbol of the token 17 * @param totalSupply The total supply of the token 18 * @param burnPercentage_ The percentage of tokens to burn on transfer (0-100) 19 */ 20 constructor(address owner, string memory name, string memory symbol, uint256 totalSupply, uint8 burnPercentage_) ERC20(name, symbol) { 21 require(burnPercentage_ >= 0 && burnPercentage_ <= 100, "ERC20TokenBurn: constructor: invalid burn percentage"); 22 _mint(owner, totalSupply); 23 _burnPercentage = burnPercentage_; 24 } 25 26 /** 27 * We include a burn function for the owner to have the ability to buy back tokens and burn them for 28 * even more heat!!! 🔥🔥🔥 29 * @param amount The amount of tokens to burn 30 */ 31 function burn(uint256 amount) public onlyOwner { 32 _burn(msg.sender, amount); 33 } 34 35 /** 36 * Here we override the transfer function to include the burn on transfer functionality. 37 * @param recipient The address of the recipient 38 * @param amount The amount of tokens to transfer 39 */ 40 function transfer(address recipient, uint256 amount) public override returns (bool) { 41 return _burnTransfer(msg.sender, recipient, amount); 42 } 43 44 /** 45 * Here we override the transferFrom function to include the burn on transfer functionality. 46 * @param sender The address of the sender 47 * @param recipient The address of the recipient 48 * @param amount The amount of tokens to transfer 49 */ 50 function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { 51 _burnTransfer(sender, recipient, amount); 52 _approve(sender, _msgSender(), allowance(sender, _msgSender()) - amount); 53 return true; 54 } 55 56 /** 57 * This function allows the owner to set the burn percentage. 58 * @param burnPercentage_ The percentage of tokens to burn on transfer (0-100) 59 */ 60 function setBurnPercentage(uint8 burnPercentage_) public onlyOwner { 61 require(burnPercentage_ >= 0 && burnPercentage_ <= 100, "ERC20TokenBurn: setBurnPercentage: invalid burn percentage"); 62 _burnPercentage = burnPercentage_; 63 } 64 65 /** 66 * This function allows the owner to set the excluded from burn addresses. 67 * @dev This is useful for exchanges and other contracts that need to transfer tokens. 68 * @param _address The address to exclude from burning 69 * @param _excluded Whether or not to exclude the address from burning 70 */ 71 function setExcludedFromBurn(address _address, bool _excluded) public onlyOwner { 72 _excludedFromBurn[_address] = _excluded; 73 } 74 75 /** 76 * This is our internal burn transfer function. 77 * We use this function to check if the sender or recipient is excluded from burning. 78 * If they are excluded, we simply transfer the amount without burning. 79 * If they are not excluded, we transfer the amount after burning the percentage of tokens. 80 * 81 * @param sender The address of the sender 82 * @param recipient The address of the recipient 83 * @param amount The amount of tokens to transfer 84 */ 85 function _burnTransfer(address sender, address recipient, uint256 amount) internal returns (bool) { 86 // Exclude addresses from burning 87 if (_excludedFromBurn[sender] || _excludedFromBurn[recipient]) { 88 _transfer(sender, recipient, amount); 89 return true; 90 } 91 92 uint256 burnAmount = (amount * _burnPercentage) / 100; 93 uint256 amountAfterBurn = amount - burnAmount; 94 _burn(sender, burnAmount); // Burn the amount of tokens 95 _transfer(sender, recipient, amountAfterBurn); // Transfer amount after burn 96 return true; 97 } 98}
We are doing a whole lot of things here, some of which we have covered in previous courses, so let's look at the notable things we are doing here:
- We have created two new variables.
_burnPercentage
and_excludedFromBurn
. The_burnPercentage
variable is the percentage of tokens to burn on transfer. The_excludedFromBurn
variable is a mapping of addresses that are excluded from burning.
We HAVE to include an exclusion list as we don't want to burn tokens when we transfer tokens to exchanges or other contracts. This will cause a lot of problems. When we transfer tokens TO exchanges, we want to transfer the full amount of tokens. We don't want to burn any tokens.
-
We created a way for the owner of the project to change the percentage of tokens to burn on transfer.
-
We created a way for the owner of the project to exclude addresses from burning.
-
We also overrided the
transfer
andtransferFrom
functions to include the burning mechanism. -
Finally, we created a new internal function called
_burnTransfer
. This function is used to check if the sender or recipient is excluded from burning. If they are excluded, we simply transfer the amount without burning. If they are not excluded, we transfer the amount after burning the percentage of tokens.
Writing our tests
In the ERC20BurnOnTransfer.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-on-transfer", function () { 6 // We deploy the contract and mint 100 tokens to the owner 7 async function deployERC20BurnOnTransferFixture() { 8 const [owner, account_1, account_2] = await ethers.getSigners(); 9 const ERC20BurnOnTransfer = await ethers.getContractFactory( 10 "ERC20TokenBurnOnTransfer" 11 ); 12 const erc20BurnOnTransfer = await ERC20BurnOnTransfer.deploy( 13 owner.address, 14 "Test Token", 15 "TST", 16 ethers.parseEther("100"), 17 BigInt("2") 18 ); 19 return { erc20BurnOnTransfer, owner, account_1, account_2 }; 20 } 21 22 // We send 10 tokens to account_1 23 async function setupAccount1() { 24 const { erc20BurnOnTransfer, owner, account_1, account_2 } = 25 await loadFixture(deployERC20BurnOnTransferFixture); 26 await erc20BurnOnTransfer.setExcludedFromBurn(owner.address, true); 27 await erc20BurnOnTransfer.transfer( 28 account_1.address, 29 ethers.parseEther("10") 30 ); 31 return { erc20BurnOnTransfer, owner, account_1, account_2 }; 32 } 33 34 describe("Deployment Tests", function () { 35 it("Should set the right owner", async function () { 36 const { erc20BurnOnTransfer, owner } = await loadFixture( 37 deployERC20BurnOnTransferFixture 38 ); 39 expect(await erc20BurnOnTransfer.owner()).to.equal(owner.address); 40 }); 41 42 it("Should mint 100 tokens to the owner", async function () { 43 const { erc20BurnOnTransfer, owner } = await loadFixture( 44 deployERC20BurnOnTransferFixture 45 ); 46 expect( 47 (await erc20BurnOnTransfer.balanceOf(owner.address)).toString() 48 ).to.equal(ethers.parseEther("100")); 49 }); 50 }); 51 52 describe("Test Burn Function", function () { 53 it("Should burn 10 tokens from the owner", async function () { 54 const { erc20BurnOnTransfer, owner } = await loadFixture( 55 deployERC20BurnOnTransferFixture 56 ); 57 await erc20BurnOnTransfer.burn(ethers.parseEther("10")); 58 expect( 59 (await erc20BurnOnTransfer.balanceOf(owner.address)).toString() 60 ).to.equal(ethers.parseEther("90")); 61 }); 62 63 it("Should fail to burn 10 tokens from non-owner", async function () { 64 const { erc20BurnOnTransfer, account_1 } = await loadFixture( 65 setupAccount1 66 ); 67 await expect( 68 erc20BurnOnTransfer.connect(account_1).burn(ethers.parseEther("10")) 69 ).to.be.rejectedWith("Ownable: caller is not the owner"); 70 }); 71 }); 72 73 describe("Test burn on transfer (Default 2%)", function () { 74 it("Should transfer tokens from account_1 to account_2 with burn", async function () { 75 const { erc20BurnOnTransfer, account_1, account_2 } = await loadFixture( 76 setupAccount1 77 ); 78 await erc20BurnOnTransfer 79 .connect(account_1) 80 .transfer(account_2.address, ethers.parseEther("10")); 81 // Burn amount is 2%, so account_2 should receive 9.8 tokens 82 expect( 83 (await erc20BurnOnTransfer.balanceOf(account_2.address)).toString() 84 ).to.equal(ethers.parseEther("9.8")); 85 expect((await erc20BurnOnTransfer.totalSupply()).toString()).to.equal( 86 ethers.parseEther("99.8") 87 ); 88 }); 89 90 it("Should exclude from burn if account_1 is excluded", async function () { 91 const { erc20BurnOnTransfer, account_1, account_2 } = await loadFixture( 92 setupAccount1 93 ); 94 await erc20BurnOnTransfer.setExcludedFromBurn(account_1.address, true); 95 await erc20BurnOnTransfer 96 .connect(account_1) 97 .transfer(account_2.address, ethers.parseEther("10")); 98 // Account 1 is excluded, so account_2 should receive 10 tokens 99 expect( 100 (await erc20BurnOnTransfer.balanceOf(account_2.address)).toString() 101 ).to.equal(ethers.parseEther("10")); 102 expect((await erc20BurnOnTransfer.totalSupply()).toString()).to.equal( 103 ethers.parseEther("100") 104 ); 105 }); 106 107 it("Should burn the correct amount if the percentage is changed", async function () { 108 const { erc20BurnOnTransfer, account_1, account_2 } = await loadFixture( 109 setupAccount1 110 ); 111 await erc20BurnOnTransfer.setBurnPercentage("5"); 112 await erc20BurnOnTransfer 113 .connect(account_1) 114 .transfer(account_2.address, ethers.parseEther("10")); 115 // Burn percentage is 5%, so account_2 should receive 9.5 tokens and 0.5 tokens should be burned 116 expect( 117 (await erc20BurnOnTransfer.balanceOf(account_2.address)).toString() 118 ).to.equal(ethers.parseEther("9.5")); 119 expect((await erc20BurnOnTransfer.totalSupply()).toString()).to.equal( 120 ethers.parseEther("99.5") 121 ); 122 }); 123 }); 124});
Since we implemented quite a few new features, our tests will grow quite a bit. Let's look at what we are doing here:
-
We are testing the deployment of the contract and making sure the owner is set correctly and that the owner has 100 tokens.
-
We are testing the burn function and making sure that the owner can burn tokens, while non-owners can not.
-
We are testing the burn on transfer functionality. We are making sure that when we transfer tokens from one account to another, the correct amount of tokens are burned and the correct amount of tokens are transferred.
-
We are testing the exclusion functionality. We are making sure that when we exclude an address from burning, the correct amount of tokens are transferred and no tokens are burned.
-
Finally, we are testing the burn percentage functionality. We are making sure that when we change the burn percentage, the correct amount of tokens are burned and the correct amount of tokens are transferred.
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
And that is it! We have successfully created a hyper-deflationary ERC20 token with a burning mechanism! BRING THE HEAT! 🔥🔥🔥
Let's deploy our contract 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-erc20BurnOnTransfer.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 burn_percentage: "2", 15} 16 17async function main() { 18 const { 19 owner_address, 20 token_name, 21 token_symbol, 22 total_supply, 23 burn_percentage, 24 } = config; 25 26 console.log(`Deploying token ${token_name}. Owner: ${owner_address}...`); 27 28 const Token = await hre.ethers.getContractFactory("ERC20TokenBurn"); 29 const token = await Token.deploy( 30 owner_address, 31 token_name, 32 token_symbol, 33 total_supply, 34 burn_percentage 35 ); 36 37 console.log(`Deployed token. Owner: ${owner_address}`); 38 39 console.log("Waiting 1 minute for Etherscan to index the contract..."); 40 await new Promise((r) => setTimeout(r, 60000)); 41 42 console.log("Verifying contract..."); 43 await hre.run("verify:verify", { 44 address: token.getAddress(), 45 constructorArguments: [ 46 owner_address, 47 token_name, 48 token_symbol, 49 total_supply, 50 burn_percentage, 51 ], 52 }); 53 54 console.log("Contract verified! 🎉"); 55 console.log( 56 `Please find the verified contract on Etherscan: https://sepolia.etherscan.io/address/${token.getAddress()}` 57 ); 58} 59 60// We recommend this pattern to be able to use async/await everywhere 61// and properly handle errors. 62main().catch((error) => { 63 console.error(error); 64 process.exitCode = 1; 65});
Good job! You have successfully created an ERC20 token with an automatic burning mechanism on transfer and a manual burning mechanism for the owner! 🔥🔥🔥
To deploy it to the test net, you can run the following command:
1npx hardhat run scripts/deploy-erc20BurnOnTransfer.ts --network sepolia
Conclusion
In this course, we learned how to create an ERC20 token with massive deflationary tokenomics!