Fungible Tokens
In this course, we'll be looking at exactly what an ERC20 token is, what it's used for, what it could be used for, and how to create one.
What is an ERC20 token?
ERC20 is a standard interface for fungible tokens on Ethereum. It provides a set of rules that all ERC20 tokens must follow. This allows for interoperability between ERC20 tokens and other applications on Ethereum.
What is an ERC20 token used for?
ERC20 tokens are used to represent fungible assets on Ethereum. Fungible assets are assets that are interchangeable. For example, if you have 1 ETH and I have 1 ETH, we can exchange our ETH and we will both have 1 ETH. This is because 1 ETH is equal to 1 ETH. This is not the case with non-fungible assets (NFTs). For example, if you have a car and I have a car, we cannot exchange our cars and expect to have the same car. This is because your car is not equal to my car.
What could an ERC20 token be used for?
ERC20 tokens can be used to represent any fungible asset on Ethereum. This includes things like currencies, stocks, bonds, and more. Popular ERC20 tokens are:
- DAI
- USDC
- USDT
- UNI
- LINK
- ...and many more
How do you create an ERC20 token?
This course assumes you have set up the development environment as outlined in the Smart Contracts Beginner course. If you haven't, 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
ERC20.sol
.
In the test folder:
- Create a new file called
ERC20.ts
.
Writing our smart contract
In the ERC20.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 ERC20Token is ERC20, Ownable { 8 9 /** 10 * This is our constructor. 11 * @param owner The address of the owner 12 * @param name The name of the token 13 * @param symbol The symbol of the token 14 * @param totalSupply The total supply of the token 15 */ 16 constructor(address owner, string memory name, string memory symbol, uint256 totalSupply) ERC20(name, symbol) { 17 _mint(owner, totalSupply); 18 } 19}
Aside from the comments, we have to look at the contracts we are inheriting, namely ERC20
and Ownable
.
-
ERC20
is a contract that implements the ERC20 standard. It provides basic functionality for ERC20 tokens, such astransfer
,transferFrom
,approve
,allowance
, andtotalSupply
. -
Ownable
is a contract that provides basic authorization control functions. It assigns anowner
to the contract and provides anonlyOwner
modifier that can be applied to functions to restrict their use to the owner.
Modifiers are functions that will be run before the function it is assigned to. It's typical to create modifier functions to make sure some specific conditions are met before running the function it is assigned to. For example, the
onlyOwner
modifier will check if the caller of the function is the owner of the contract. If it is, the function will run. If it isn't, the function will not run.
Writing our tests
In the ERC20.ts
file, add the following code:
1import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2import { expect } from "chai"; 3import { ethers } from "hardhat"; 4 5describe("ERC20", function () { 6 // We deploy the contract and mint 100 tokens to the owner 7 async function deployERC20Fixture() { 8 const [owner, account_1, account_2] = await ethers.getSigners(); 9 const ERC20 = await ethers.getContractFactory("ERC20Token"); 10 const erc20 = await ERC20.deploy( 11 owner.address, 12 "Test Token", 13 "TST", 14 "100000000000000000000" 15 ); 16 return { erc20, owner, account_1, account_2 }; 17 } 18 19 describe("Deployment Tests", function () { 20 it("Should set the right owner", async function () { 21 const { erc20, owner } = await loadFixture(deployERC20Fixture); 22 expect(await erc20.owner()).to.equal(owner.address); 23 }); 24 25 it("Should mint 100 tokens to the owner", async function () { 26 const { erc20, owner } = await loadFixture(deployERC20Fixture); 27 expect((await erc20.balanceOf(owner.address)).toString()).to.equal( 28 ethers.parseEther("100") 29 ); 30 }); 31 }); 32});
So let's go over what we did here step by step.
We first created a fixture function called deployERC20Fixture
. This function will deploy our ERC20 contract and mint 100 tokens to the owner. We then return the contract and the owner.
Fixtures are functions that return a snapshot of the state of a contract. We can control what the state is when returned. In the code above, we are making sure that the contract is deployed with the correct parameters. We are also returning the owner.
We then created a describe block called Deployment Tests
. This describe block will contain all the tests related to the deployment of the contract. We then created two tests inside the describe block.
The first test checks if the owner of the contract is set correctly. The second test checks if the owner of the contract has 100 tokens.
And with that, you have successfully written your first test! Yay!
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 with that, we have successfully created our ERC20 smart contract and written our tests!
All that is left is to deploy our ERC20 token to the test network.
Deploying our smart contract
To deploy our smart contract, we need to first create a deployment script. In the scripts
folder, create a new file called deploy-erc20.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("ERC20Token"); 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});
And with that, you have successfully created your ERC20 token!
To deploy it to the test net, you can run the following command:
1npx hardhat run scripts/deploy-erc20.ts --network sepolia
Conclusion
In this course, we looked at what an ERC20 token is, what it's used for, what it could be used for, and how to create one. Next up we'll be looking at how to create special versions of ERC20 tokens, like mintable tokens, burnable tokens, taxable tokens, and more!