The Year of the 20: Creating an ERC20 in 2020

How to use the latest and best tools to create an ERC-20 token contract

You know what an ERC-20 is, you probably have created your own versions of it several times (if not, have a look at: ERC-20). But how would you start in 2020 using the latest tools? Let's create a new ERC-2020 token contract with some basic functionality which focuses on simplicity and latest technology. The new tools include

1. Install the tools

First let's create a new project and initialize it with Buidler.

mkdir erc-2020 && cd erc-2020
npm init
npm install --save-dev @nomiclabs/buidler

Now create the Buidler project and choose the example project for the start. Afterwards, you can install the required dependencies.

npx buidler
npm install --save-dev @nomiclabs/buidler-waffle ethereum-waffle chai @nomiclabs/buidler-ethers ethers

We are almost finished with the setup. Let's change the package.json scripts. Add the npx buidler test command to your package.json scripts and see if everything is working by running npm test.

2. Upgrade to the latest Solidity version

Now we can upgrade to the latest Solidity version 0.6.8. Just change the buidler.config.js and the pragma statement in the contract. Both should be changed to 0.6.8. Let's compile it now with npx buidler compile. You will see some warnings. Either ignore those or add the Spdx comment, a new feature added in 0.6.8. A comment in the top of your contract like // SPDX-License-Identifier: MIT will be sufficient.

3. Using the new OpenZeppelin contracts

Okay, now that we have an example application running, we can start to develop our own token contract. Install the latest Openzeppelin contracts: npm install @openzeppelin/contracts. That's all we need. Let's write our ERC-20 contract.

pragma solidity ^0.6.8;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC2020 is ERC20 {
    constructor() ERC20("Twenty", "TWY") public {}
}

Congratulations, you already have a working ERC-20 contract. So far so good. This is just slightly different syntax than you might be used to. Next up we can make use of some new features.

4. Let's use some new features

We can now add some more features to our contract, adding access control, a manager contract and ultimately we take a look at how the testing is now much easier with the new tools. Let's begin with access control.

pragma solidity ^0.6.8;

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

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

    constructor() ERC20("Twenty", "TWY") public {
        console.log("Setting ", msg.sender, "as admin.");
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public {
        require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        require(hasRole(BURNER_ROLE, msg.sender), "Caller is not a burner");
        _burn(from, amount);
    }
}

Now what's going on here? We make use of the OpenZeppelin AccessControl and we use the Buidler console.log. The latter is great for debugging, you will see more about this later. The access control is good way to give various permissions to other contracts or persons instead of just an owner. In our case we define separate burner and minter roles and we set the deployer as admin role who is allowed to add and revoke roles. Let's make use of those in a manager contract.

pragma solidity ^0.6.8;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@nomiclabs/buidler/console.sol";
import "./ERC2020.sol";

contract Manager2020 {
    using SafeERC20 for IERC20;
    IERC20 public token;

    constructor(IERC20 _token2020) public {
        token = _token2020;
    }

    function mint(uint256 amount) public {
        console.log("About to mint", amount / 1e18, "TWY...");
        ERC2020(address(token)).mint(address(this), amount);
        console.log("Mint was successful");
    }

    function transfer(address to, uint256 amount) public {
        console.log("About to transfer to", to, amount / 1e18, "...");
        token.safeTransfer(to, amount);
        console.log("Transfer was successful");
    }
}

On top you see here that we use the SafeERC20. It's especially a good idea when calling arbitrary token contracts. It ensures to revert the transaction if the transfer or transferFrom returns false. You won't accidentally forget to check the return value.

Testing

Let's write some tests for it. Here you can see the beauty of Buidler and Waffle. Change the sample-test.js file to:

const { expect } = require("chai");
const TWY = value => ethers.utils.parseEther(`${value}`)

describe("ERC2020", function() {
  const [wallet, walletTo] = waffle.provider.getWallets();

  let erc2020;
  let manager2020;

  beforeEach(async () => {
    const ERC2020 = await ethers.getContractFactory("ERC2020");
    const Manager2020 = await ethers.getContractFactory("Manager2020");

    erc2020 = await ERC2020.deploy();
    await erc2020.deployed();
    manager2020 = await Manager2020.deploy(erc2020.address);
    await manager2020.deployed();

    const minterRole = await erc2020.MINTER_ROLE();
    await erc2020.grantRole(minterRole, manager2020.address);
    await manager2020.mint(TWY(4));
  });

  it("sets deployer as admin", async function() {
    const adminRole = await erc2020.DEFAULT_ADMIN_ROLE();
    const isSenderAdmin = await erc2020.hasRole(adminRole, wallet.address);
    
    expect(isSenderAdmin).to.be.true;
  });

  it("sets deployer as admin", async function() {
    await expect(manager2020.transfer(walletTo.address, TWY(5)))
      .to.be.revertedWith('SafeERC20: low-level call failed');

    await manager2020.transfer(walletTo.address, TWY(3));
    
    expect(await erc2020.balanceOf(walletTo.address)).to.equal(TWY(3));
    expect(await erc2020.balanceOf(manager2020.address)).to.equal(TWY(1));
  });
});

Run npm test again. Pay attention to the output. Do you see what's special?

About to transfer to 0xaf..fb 5 TWY... you see this output even though the transaction reverted. The power of console.log over Solidity events. Just make sure to remove those for main-net deployments as it's costing unnecessary gas. But it's great for testing/debugging. And you'll also get a full stack trace from all JavaScript code right into all Solidty code. No more guessing where a revert came from. And Waffle comes also with a host of matchers to help you further. We used the .to.be.revertedWith in the above example to test for the correct transaction revert message.

The future of smart contract developments

That's it, simple and perfect to debug. Have a look at https://github.com/gorgos/ERC2020 for the full example. Of course this setup works for any contract, not just ERC-20. As you can see the years of painful smart contract developments are slowly ending.

Markus Waas

Solidity Developer

More great blog posts from Markus Waas

© 2024 Solidity Dev Studio. All rights reserved.

This website is powered by Scrivito, the next generation React CMS.