Do you want to create your own ERC20 token on Ethereum? Here we program one and learn more on solidity while we’re at it. Tokens can be used for all kinds of things, not only to fuction as a currency. In future posts we will see an example on what this could look like.

If you want to learn more on ERC20 tokens you can look at the official specifications on het EIP website here. More on the openzeppelin implementation we will use here can be found in the openzepelin docs here.

In this post we will start with an existing smart contract from this previous post on the truffle suite, where you can learn the basics on how to create, test and run a smart contract using truffle. The complete code for this post can be found on github here. If you like to work in an online environment you could also look at this previous post on remix.

The ERC20 standard

ERC20 is a standard for tokens on the ethereum blockchain. The specification describes all the methods a token contract must implement to ensure it behaves like a token (see list here).

For example we should implement a function to check the balance of a given address to see how many of our tokens it holds:

function balanceOf(address _owner) public view returns (uint256 balance)

Or another important method is the ability to transfer tokens to another address:

function transfer(address _to, uint256 _value) public returns (bool success)

The list goes on but luckily there is a quick way to implement all of them as we will see now.

Create an ERC20 token

Starting from the code of our trustfund project (from this previous post on truffle) we first update the dependencies to the latest versions. Then we set the solidity version to a later range in the truffle-config.js file:

compilers: {
  solc: {
    version: ">=0.6.0 <0.8.0",
    optimizer: {
      enabled: true,
      runs: 200
    }
  }
}

Now that everything is up-to-date we can create our token. Create a new file in the contracts directory, named TrustFundToken.sol. We setup the contract like any other and extend the ERC20 contract from openzeppelin:

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;

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

contract TrustFundToken is ERC20 {

    constructor() public payable ERC20("Trustfund token", "TFT") {}
}

Note that we call the constructor of the ERC20 contract from our own constructor with the name and symbol of our new token.

And that is it! Openzeppelin implemented all the methods and your new token is good to go. (For more information on openzeppelin see the docs here) Now lets test the basics to see our token in action. To deploy our new contract we have to add it to the migrations file first:

const TrustFundToken = artifacts.require("./TrustFundToken.sol");
const TrustFund = artifacts.require("./TrustFund.sol");

module.exports = async function(deployer) {
  // Deploy token contract
  await deployer.deploy(TrustFundToken);

  // Deploy trustfund contract
  await deployer.deploy(TrustFund);
};

Here we import the contract from their solidity files and use the deployer to deploy both contracts.

Now start a local blockchain with truffle develop and deploy all contracts. If you have trouble doing this take a look at this previous post on the truffle suite. Now test some of the functions on the token contract.

truffle(develop)> let token = await TrustFundToken.deployed()
undefined

truffle(develop)> token.name()
'Trustfund token'

truffle(develop)> token.symbol()
'TFT'

truffle(develop)> let accounts = web3.eth.getAccounts()
undefined

truffle(develop)> token.balanceOf(accounts[0])
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }

To test the transfer function we need to create some tokens first. This is not part of the ERC20 standard for all projects go about this in different ways. We will use the internal _mint function from openzeppelin.

function mint(address account, uint256 amount) public {
_mint(account, amount);
}

Now we can create (mint) tokens to an arbitrary address.

truffle(develop)> token.mint(accounts[0], 10)
{
  tx: '0x45d541ce5a9cde15be192d9f6b8e0dc405a75e9e5509027eb9f882fffdfb293a',
  receipt: {
    ...
  },
  logs: [
    ...
  ]
}

truffle(develop)> token.balanceOf(accounts[0])
BN { negative: 0, words: [ 10, <1 empty item> ], length: 1, red: null }

Note that I omitted a lot of the output of the mint function. This is because the state of the blockchain changed and this is all of the output. We can now say that we have successfully created an ERC20 token.

Extend the token

There is a lot to say to make this contract better/safer. We start by ensuring that only the creator of the contract can mint more tokens. (Until now anyone could create more token :O)

contract TrustFundToken is ERC20 {
    address public minter;

    constructor() public payable ERC20("Trustfund token", "TFT") {
        minter = msg.sender;
    }

    function mint(address account, uint256 amount) public {
        require(msg.sender == minter, 'Error, sender is not a minter');
        _mint(account, amount);
    }
}

We store the address of the creator of the contract (msg.sender in the constructor) in a variable inside the contract. In the mint method we now require the caller (msg.sender in the mint method) to be equal to the minter. When we try to execute the mint function from another address we see that an error returns.

truffle(develop)> token.mint(accounts[1], 100, { from: accounts[1] })
Uncaught:
Error: Returned error: VM Exception while processing transaction: revert Error, sender is not a minter -- Reason given: Error, sender is not a minter.

Ok, not everyone can create tokens but we still have the creator that has the total control of the supply… Not a great feature of a public token. So we want to be able to pass the minter role to another address, namely the trustfund smart contract, so the creation of new tokens is automated.

Let’s make a test for this new transferMinterRole method to check that only the current minter can transfer his role. (I created a file test/trustfundtoken.js with more tests to ensure the right behaviour.)

it("Non minters cannot transfer minter role", function() {
  return TrustFundToken.deployed().then(function(instance) {
    contractInstance = instance;

    return contractInstance.transferMinterRole(tokenReceiver, {from: tokenReceiver});
  }).catch(function(exception) {
    expect("Error, only minter can transfer minter role").to.be.equal(exception.reason);
  });
});

Now implement this method and add the require statement to make the test succeed.

function transferMinterRole(address newMinter) public returns (bool) {
    require(msg.sender == minter, 'Error, only minter can transfer minter role');
    minter = newMinter;

    return true;
}

We see the tests we made for the trustfund contract and this new test.

truffle test output passing 1024x178 - ERC20 TOKEN

Events

Events are another feature of smart contracts. If we look for example to the contract of the stablecoin USDC on etherscan here, we see in the events tab that the ERC20 functions emit events. Like the transfer function:

events tab on etherscan 1024x245 - ERC20 TOKEN

This can also be found in the ERC20 specification so all these tokens emit these events.

transfer function erc20 specs 1024x157 - ERC20 TOKEN

Other contracts can listen to these events and react on certain changes. We want to communicate when the minter role is transferred for this is an important event. We can update the method as followed:

event MinterChanged(address indexed from, address to);

...

function transferMinterRole(address newMinter) public returns (bool) {
    require(msg.sender == minter, 'Error, only minter can transfer minter role');
    minter = newMinter;

    emit MinterChanged(msg.sender, newMinter);
    return true;
}

Note that we have to define the event MinterChanged with its signature. Then we can use emit to send the event with the given values.

When we capture the result of the call we can see in the logs that MinterChanged event is emitted.

truffle(develop)> let result = await token.transferMinterRole(accounts[1])
undefined

truffle(develop)> result.logs[0]
{
  logIndex: 0,
  transactionIndex: 0,
  transactionHash: '0x88c4048142330e112efd9b6c9662e4d76dec2e4de704bc5b06b1960824bcac94',
  blockHash: '0x48cf3158a22e999d0a3e0993222f9ea959de118e0fc866629e7be08e42cd87d1',
  blockNumber: 48,
  address: '0xE99EABd948EE979585F7fb5e62bbA8e3C6a5c262',
  type: 'mined',
  id: 'log_f7433e3a',
  event: 'MinterChanged',
  args: Result {
    '0': '0x1855b768EB5724641253B12548e0B72009FD0E5d',
    '1': '0xA060590F69D9CAb96c8E07d12B07859AF46f8657',
    __length__: 2,
    from: '0x1855b768EB5724641253B12548e0B72009FD0E5d',
    to: '0xA060590F69D9CAb96c8E07d12B07859AF46f8657'
  }
}

Hopefully you are now able to create your own ERC20 token and extend it to your own needs. Happy tokening!