Solidity Tutorial: Create an ERC20 Token

Cyrille
5 min readFeb 14, 2023

This article will show you how to easily create a token that follows the ERC20 Token Standard.

In this article, we will cover the following:

  1. Tooling
  2. ERC20 Standard
  3. OpenZeppelin ERC20 Contract
  4. Test Cases
  5. Our ERC20 Contract
Photo by Clint Patterson on Unsplash

Tooling

Hardhat — an Ethereum Development Environment. If you haven’t worked with Hardhat, I suggest you follow their Getting Started guide.

OpenZeppelin Contracts- A library of modular, reusable, secure smart contracts for the Ethereum network, written in Solidity. We will be leveraging their smart contracts, making our life much easier.

ERC20 Standard

The ERC20 Standard allows for implementing a standard API for tokens within smart contracts. It presents a set of specifications (methods and events) that must be implemented in a smart contract in order to meet the standard.

// Methods
name()
symbol()
decimals()
totalSupply()
balanceOf(owner)
transfer(to, amount)
transferFrom(from, to, amount)
approve(spender, amount)
allowance(owner, spender)

// Events
Transfer(from, to, value)
Approval(owner, spender, value

If we wanted, we could implement every method and event mentioned above. In most cases though, we aren’t looking for any special behavior. No need to reinvent the wheel, say hello to OpenZeppelin Contracts.

OpenZeppelin ERC20 Contract

The OpenZeppelin ERC20 contract already contains all the building blocks we need to create an ERC20 token. All the methods have been implemented, we just need to provide basic values or override methods where we want custom functionality.

So, how does this all come together?

If you haven’t already, create your hardhat project:

mkdir simple-erc20-token
cd simple-erc20-token

yarn add --dev hardhat
npx hardhat

// Follow the CLI instructions, I use the Typescript project

Once that is done, you can simply add the OpenZeppelin Contracts library by running:

yarn add --dev @openzeppelin/contracts

Test Cases

When writing code, I recommend you approach it through Test Driven Development. The Hardhat Environment provides a great set of tools to enable this approach.

We will create a file test/Token.ts that will include our test cases. We will include some test cases around the token properties (name, symbol, totalSupply) and test cases around the actions (transfer, approve, transferFrom) and events (Transfer, Approval).

Hardhat uses mocha and chai as a testing library supplemented by custom matcher and helpers to make our life as developers easier. There is an excellent overview article about testing contracts for those that want to go a bit deeper.

We will write some specifications that describe our ERC20 contract and then code those into test cases.

Specifications

  1. name: OnMyChain
  2. symbol: OMC
  3. total supply: 100,000
  4. deployer owns the total supply at deployment
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"
import { expect } from "chai"
import { Contract } from "ethers";
import { ethers } from "hardhat"

describe("Token", function () {

let token: Contract;

beforeEach(async function () {
token = await loadFixture(deploy)
})

async function deploy() {
const Token = await ethers.getContractFactory("Token")
const token = await Token.deploy()
return token
}

describe("deploy", function () {
it("should be named OnMyChain", async function () {
expect(await token.name()).to.eq("OnMyChain")
})
it("should have OMC symbol", async function () {
expect(await token.symbol()).to.eq("OMC")
})
it("should have a total supply of 100,000", async function () {
expect(await token.totalSupply()).to.eq(
ethers.utils.parseEther("100000")
)
})
it("should mint total supply to deployer", async function () {
const [deployer] = await ethers.getSigners()
expect(await token.balanceOf(deployer.address)).to.eq(
ethers.utils.parseEther("100000")
)
})
})

describe("transfer", function () {
const amount = ethers.utils.parseEther("100")

it("should transfer amount", async function () {
const [from, to] = await ethers.getSigners()
await expect(token.transfer(to.address, amount)).to.changeTokenBalances(token,
[from, to],
[amount.mul(-1), amount]
)
})
it("should transfer amount from a specific account", async function () {
const [deployer, account0, account1] = await ethers.getSigners()
// first we will transfer 100 to account0 (from the deployer)
await token.transfer(account0.address, amount)
// next, we need to connect as account0 and approve
// the approval will allow the deployer to send tokens
// on behalf of account0
await token.connect(account0).approve(deployer.address, amount)
// last, we will use transferFrom to allow the deployer to
// transfer on behalf of account0
await expect(token.transferFrom(account0.address, account1.address, amount)).to.changeTokenBalances(token,
[deployer, account0, account1],
[0, amount.mul(-1), amount]
)
})
})

describe("events", function () {
const amount = ethers.utils.parseEther("100")

it("should emit Transfer event", async function () {
const [from, to] = await ethers.getSigners()
await expect(token.transfer(to.address, amount)).to.emit(token, 'Transfer').withArgs(
from.address, to.address, amount
)
})
it("should emit Approval event", async function () {
const [owner, spender] = await ethers.getSigners()
await expect(token.approve(spender.address, amount)).to.emit(token, 'Approval').withArgs(
owner.address, spender.address, amount
)
})
})
})

You can run the test through npx hardhat test and you will receive the following error: HardhatError: HH700: Artifact for contract “Token” not found.

This is simply because we haven’t written our contract yet.

Our ERC20 Contract

We will go step by step in building this out. Firstly, create a file contracts/Token.sol

The foundation of any smart contract consists of:

  1. The License Identifier
  2. The solidity version
  3. The contract code

We will start by creating an empty contract.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract Token {

}

If you run npx hardhat test again, you will notice the error changes. Hardhat has recognized the contract, but the contract doesn’t hold any of the methods we are trying to test.

Now — the power of OpenZeppelin contracts. Let’s modify our contract a bit.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {

}

If you run npx hardhat test again, you will notice a different error. We are missing the implementation of the ERC20 constructor function.

Our constructor function needs to call the OpenZeppelin ERC20 constructor function. This function takes two arguments:

  1. name
  2. symbol

Let’s improve our code to support this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {

constructor() ERC20("OnMyChain", "OMC") {}

}

Now run npx hardhat test and you should see the following:

Token
deploy
✔ should be named OnMyChain
✔ should have OMC symbol
1) should have a total supply of 100,000
2) should mint total supply to deployer
transfer
3) should transfer amount
4) should transfer amount from a specific account
events
5) should emit Transfer event
6) should emit Approval event


2 passing (334ms)
6 failing

Great start! Our tests for the name and symbol are passing!

If you look through the errors, you will see that the tests are failing due to the token's total supply of 0. That is because we haven’t minted a supply. This can be accomplished in one line of code.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {

constructor() ERC20("OnMyChain", "OMC") {
_mint(msg.sender, 100000 * 10 ** decimals());
}

}

Let’s run npx hardhat test , you should get the following result:

Token
deploy
✔ should be named OnMyChain
✔ should have OMC symbol
✔ should have a total supply of 100,000
✔ should mint total supply to deployer
transfer
✔ should transfer amount (42ms)
✔ should transfer amount from a specific account (87ms)
events
✔ should emit Transfer event
✔ should emit Approval event


8 passing (419ms)

Congratulations! All our tests are passing! We have built our very own token.

Next up:

  • Deploying an ERC20 contract using Hardhat
  • Adding a transfer tax to a ERC20 token

I hope you enjoyed this tutorial. You can view the code on GitHub.

--

--

Cyrille

I build web2 and web3 products and teams. Depending on the project, I operate as a CTO, product manager, developer, advisor, or investor.