Skip to content

The ERC20 Token

Do you know where the definition "ERC20" comes from? It has an interesting history!

Maybe you came across RFC in the world of traditional IT. RFC stands for Request for Comment. And ERC was the early version of this for Ethereum: Ethereum Request for Comment.

The number 20 simply refers to the 20th ERC that was posted by someone. That person proposed a general interface for a fungible token.

Nowadays the ERC's are called EIPs: Ethereum Improvement Proposals. Because the majority of newcomers did not understand any difference between EIPs and ERCs they were merged. Pepperidge Farmer remembers that EIPs were more Etheruem Protocol update proposals...

What's a fungible token? That is, where each token doesn't have any sort of unique serial number and they are all worth equally much. Like Euro or Dollar coins. You take out the coins in your pocket and 50 cents are 50 cents, doesn't matter if the coin is old and used or new and shiny.

Same is with ERC20. It doesn't matter if you have token #1 or token #1000000, because internally they are not having any serial number and are indistinguishable from each other.

So, how does that interface look like?

It defines a couple of functions. And for that, let's open up the original proposal: https://eips.ethereum.org/EIPS/eip-20

You see a few functions are optional and some are mandatory. And at the bottom you see a sample implementation from OpenZeppelin and ConsenSys. And both of them are old, using Solidity 0.4.something. At least at the time of writing this.

So, instead we will use this one https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol.

If we walk through this, you see a few things:

  1. The constructor takes a name and a symbol.
  2. The decimals are always 18, that is now more standardized, but wasn't before. Be careful with old contracts, not all of them are placing the decimal point on the 18th digit of the balance.
  3. The functions balanceOf, transfer, transferFrom should be easily readable for you
  4. There is an additional "allowance" functionality that allows someone to spend money in the name of someone else.

OpenZeppelin Wizard

OpenZeppelin has a convenient Contract-Wizard on their Website

I'm going to use this wizard here to generate the token samples I am showing you.

ERC20 Basic contract

If you want to use the token template for your own token, you can do that easily by just importing and extending that ERC20 base contract.

For example, this would be a viable token:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MYT") {}
}

Minting Tokens

The OpenZeppelin implementation always has an internal _mint function. The visibility specifier defines the function as internal, that means, another function must actually call this function upon a certain event.

There are two large groups of implementations for tokens:

  1. Fixed Supply Tokens
  2. Variable Supply Tokens

The difference is mainly in how the mint functionality is used. If you use OpenZeppelin smart contracts, then with fixed supply, the mint function is callable only in the constructor once. That means, once the token is deployed, there is no more access for the internal mint functionality, the supply of tokens remains fixed.

The Variable Supply Tokens implement some sort of functionality, so that its possible to mint more tokens after the contract is deployed.

Burning Tokens

The OpenZeppelin implementation of the ERC20 token also has an internal _burn function. Again, it is internal, that means, another function must actually call this function when using the ERC20 contract as a base contract.

Ownership and Access Control

OpenZeppelin also has ownership covered in two flavors:

  1. The simple Ownable, which we used before
  2. A more elaborate Access Control structure with Roles

I'm not explaining again the simple Ownership functionality, we already did that with mappings. But the Access Control and Roles are something that is actually pretty cool.

You can define roles, such as a MINTER_ROLE or a BURNER_ROLE, which is basically just a keccak256 hash of the role name

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

Then, the internal function grantRole can be called to grant a specific address a specific role. This function, itself, is checking if the person who is calling is the admin allowed to do so, and so on and so forth...

Then use can extend the Access Control Smart Contract and use the modifier onlyRole to make sure only a specific role has access to the function.

Let's try this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("MyToken", "MYT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}

If the burner role should also be included for a specific address to be allowed to burn tokens, then it might look something like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("MyToken", "MYT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
        _grantRole(BURNER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

Deploying a Token

Let's use a sample contract from OpenZeppelin to deploy a token. This token could represent anything - for example a voucher for coffees.

The flow could be:

  1. We can give users tokens for coffees
  2. The user can spend the coffee token in his own name, or give it to someone else
  3. Coffee tokens get burned when the user gets his coffee.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract CoffeeToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    event CoffeePurchased(address indexed receiver, address indexed buyer);

    constructor() ERC20("CoffeeToken", "CFE") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function buyOneCoffee() public {
        _burn(_msgSender(), 1);
        emit CoffeePurchased(_msgSender(), _msgSender());
    }

    function buyOneCoffeeFrom(address account) public {
        _spendAllowance(account, _msgSender(), 1);
        _burn(account, 1);
        emit CoffeePurchased(_msgSender(), account);
    }
}

Let's have a look now in the next lecture how we can implement some sort of token sale here...


Last update: September 3, 2022