Fungible Tokens Introduction

Fungible Tokens Introduction

Wednesday Feb 21, 2024

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:

  1. Create a new file called ERC20.sol.

In the test folder:

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

  1. ERC20 is a contract that implements the ERC20 standard. It provides basic functionality for ERC20 tokens, such as transfer, transferFrom, approve, allowance, and totalSupply.

  2. Ownable is a contract that provides basic authorization control functions. It assigns an owner to the contract and provides an onlyOwner 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});

HAPPY

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!