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
Solidity Developer