ERC20 Using Hardhat: An Updated Comprehensive Guide

Aw Kai Shin
13 min readAug 11, 2022

--

The crypto development environment is always evolving at a breakneck speed so what better time for a refresher with the upcoming Ethereum merge. To this end, I have compiled an end-to-end guide on how to create your own ERC20 token using Hardhat. This includes:

  • Hardhat Environment Setup & Configuration
  • Extending OpenZeppelin’s ERC20 Contract
  • Deploying Token to Goerli Testnet (getting test ETH, connecting to node, private key exporting)
  • Interacting Programatically with a Local Contract Instance
  • Sending Created ERC20 Token on Goerli Testnet via Metamask

Although targeted at those starting out in the space, this guide requires some basic familiarity with Node.js and Solidity concepts. All the code used in this guide can be found on github.

Related Guides:

Hardhat Setup

We first need to crate a project directory and install Hardhat:

mkdir ERC20
cd ERC20
npm install --save-dev hardhat

Once the hardhat package has been installed, we can then run npx hardhat which will bring up some options for bootstrapping the project:

For this tutorial, we will select the Create a JavaScript project option. You will be prompted with a series of questions which you can continuously select enter to. The last of which will install the project dependencies.

npm install --save-dev @nomicfoundation/hardhat-toolbox@^1.0.1

Following the npx hardhat command, the project will have 3 folders:

  • contracts/ is where the source files for your contracts should be.
  • test/ is where your tests should go.
  • scripts/ is where simple automation scripts go.

In addition a hardhat.config.js file will have been generated. This file will manage the plugins and dependencies viewable by hardhat. Plugins and dependencies will have to be installed first followed by requiring it in the hardhat.config.js file.

The npx hardhat command will have also created a sample Lock contract which we will not require for this guide so you can go ahead and remove them:

rm contracts/Lock.sol test/Lock.js

Now that Hardhat has been setup, we can begin creating our ERC20 token.

ERC20 Contract

OpenZeppelin, which spearheads the advancement of the ERC20 standard, provides a comprehensive library for secure smart contract development which we will be using to implement our token. To do this, we need to first install the OpenZeppelin contracts package.

npm install @openzeppelin/contracts

We can then import the OpenZeppelin contracts into our own contract by prefixing its path with @openzeppelin/contracts/....

Before starting our first ERC20 token contract, we need to complete the most important step: naming your token! The standard practice is to match the smart contract file name with that of your token. For this guide, I will be naming the token FunToken (fungible token) with a symbol of FUN and hence my smart contract file name will be FunToken.sol. Feel free to choose your own token name but do remember to replace any instance of FunToken with your own specially crafted name.

The new contract will be placed in the contracts/ folder:

touch contracts/FunToken.sol

Open FunToken.sol in your code editor and add the following code. The code will utilise the Solidity’s constructor function to mint FunToken upon contract deployment:

We will be importing OpenZeppelin’s ERC20 contract via the path @openzeppelin/contracts/token/ERC20/ERC20.sol. You can find the path for the contract via OpenZeppelin’s docs or their github.

As part of the initialSupply definition, the specified FunToken supply is multiplied by (10**18) as Solidity only supports integer usage. As a workaround, the ERC20 specification provides a decimal field which defines the token decimals. For this guide, we will be sticking to the default 18 decimals so as to avoid having to override the specification. Do feel free to change the initial supply by selecting an integer other than the 1000000 that was used but do remember to keep the (10**18).

ERC20.sol

Save the FunToken.sol contract and we can now compile the completed contract.

Compiling The Contract

For the Ethereum Virtual Machine (EVM) to run our code, we need to compile our Solidity code into the EVM compatible EVM bytecode. To ensure there are no versioning issues, we can specify a Solidity version in the provided hardhat.config.js file.

To compile the FunToken contract, we can use Hardhat’s handy compile command.

npx hardhat compile

Hardhat will have compiled the contract into a newly created artifacts/ folder. Although we only wrote the code for FunToken.sol, the console shows that there were 5 compiled files. This is because of how Solidity handles the importing of @openzeppelin/contracts and it’s dependencies.

Deploying The Contract Locally

Before deploying the contract to a public network, it is best practice to test the contract on a local blockchain first. Hardhat simplifies the process of setting this up by having an in-built local blockchain which can be easily run through a single line of code:

npx hardhat node

Run the above command in a separate command window.

The Hardhat network will print out the address as well as a list of locally generated accounts. Do take note that this local blockchain only stores the interactions until the console is closed hence the state is not preserved between runs. Additionally, take the time to read through the details of the accounts as it is important that you do not use these sample accounts for sending any real money. With the local blockchain up and running, we can then start writing our deployment scripts. Hardhat currently does not have a native deployment system and hence the need for scripts.

For our deployment script, we will be making use of the hardhat-ethers plugin. You can find the documentation for the plugin here. To use it in the project, we need to first install it:

npm install --save-dev @nomiclabs/hardhat-ethers ethers

Navigating to the scripts/ directory, you will be able to see that a deploy.js file was previously created. We can replace the main() function with the following:

Hardhat will deploy the contract using the first account created when we started up the node above. The _mint() function in FunToken.sol will mint the total supply of FUN to this account address. To access this array of accounts, we can use the getSigners() helper function provided by the hardhat-ethers plugin.

Save the file and we are now ready to deploy our FunToken contract!

npx hardhat run --network localhost scripts/deploy.js

As mentioned, we are deploying the contract to our localhost. Do take note of the deployed contract address as we will need it in the next section for interacting with our contract programatically.

Interacting With The Contract

This section is optional but it is a good introduction to how to interact with smart contracts using the ethers.js package. Through this interaction, you will also be able to also experience for yourself the set of default functions which your ERC20 token has inherited from the OpenZeppelin contract.

Getting the deployed contract

In order to get the instance of our deployed contract, you will need the contract address returned from the deployment command. You can replace the contractAddress in the code below with the one provided by your local machine.

With that, we have set up our funToken object which is an abstraction of a contract deployed on the Ethereum network, in this case a local network. This funToken object enables a simple way to serialize calls and transactions to an on-chain contract and deserialize their result logs. You can refer to the ethers documentation for more info.

The sections below showcases how to call the default public functions which our FunToken contract has inherited from OpenZeppelin’s ERC20 contract. Remember to add each section to the main() function in order to call the contract. In order to run the interact.js script, enter the following command into your console:

npx hardhat run --network localhost scripts/interact.js

name()

name() returns the name of the token used in theFunToken.sol constructor.

symbol()

symbol() returns the token symbol used in theFunToken.sol constructor.

decimals()

decimals() returns the default decimals specified by the ERC20 contract.

totalSupply()

totalSupply() returns the initialSupply which was used in the FunToken.sol constructor.

As mentioned while creating our FunToken.sol contract, Solidity only supports integer usage. In the Ethereum world, numbers are usually represented in “wei” for the purposes of maintaining precision while calculating amounts. Wei is the base unit of ETH and 1 ETH = 10¹⁸ wei.

Consequently, notice that the length of the token amount returned is not ideal for readability. As such, we have used the utils native to the hardhat-ethers plugin in order to make the numbers displayed more readable. This utils will be covered in greater details once we start conducting transfers.

balanceOf(address account)

balanceOf() returns the amount of tokens owned by the inputted address.

Do note that all FUN tokens were initially minted to the address which deployed the contract.

transfer(address to, uint256 amount)

transfer() moves the amount of tokens from the from address to the to address.

Due to EVM’s integer design, one limitation which we run into when interacting with our contract via JavaScript is that JS numbers are represented using the double-precision floating-point format. In layman terms, numbers after 2⁵³ -1 (9,007,199,254,740,991) are not safe to use especially given that we are essentially moving tokens which will (hopefully) have a monetary value. In order to ensure the correct handling of numbers in our JS files, we can use the native BigNumber util provided by ethers. The BigNumber util will also help to convert all final values into string which is critical for avoiding any type errors when interacting with ETH using JS.

approve(address spender, uint256 amount)

approve() sets amount as the allowance of the spender over the caller’s token.

Do note that this function applies to the balances of the transaction caller. As such, we need to create a new instance of the funToken object, signerContract, which is instead connected to the recipient’s account.

Due to possible race conditions when the network is confirming this transaction, it is recommended to use the increaseAllowance() and decreaseAllowance() functions instead.

allowance(address owner, address spender)

allowance() returns the remaining amount of the owner tokens which the spender is allowed to spend.

transferFrom(address from, address to, uint256 amount)

transferFrom() moves the amount of tokens from the from address to the to address.

Do note that this function can be called by anyone as it is meant as a way for contracts to transfer tokens on behalf of approved user accounts. The allowance will be deducted accordingly with the transaction failing if the amount exceeds the available allowance.

increaseAllowance(address spender, uint256 addedValue)

increaseAllowance() atomically increases the allowance granted to spender by the caller.

decreaseAllowance(address spender, uint256 subtractedValue)

decreaseAllowance() atomically decreases the allowance granted to spender by the caller.

Testing The Contract

Given that the tokens will have a monetary value, writing automated tests is crucial to ensure that the contract is performing how we expect it to. We will be making use of the chai and hardhat-chai-matchers package to quickly write up our own tests.

$ npm install --save-dev chai
$ npm install --save-dev @nomicfoundation/hardhat-chai-matchers

Once installed, we can then create the test script file:

touch test/FunToken.test.js

As most of the contract functions have been implemented in the above section, the code below is meant more as a starting point for you to then write a comprehensive set of tests.

Upon saving the above into FunToken.test.js, we can then run the tests using:

npx hardhat test

If all goes well, you should expect to see the results of the test printed to your console:

Do play around with the various test scenarios which are suitable for your token. You can even try implementing negative test cases which were not included in the sample above.

Deploying the Contract Publicly

Now that we are confident that the contract is working as expected, it’s finally time to deploy FunToken onto a public network! This is the exciting part, as anybody connected to the network will be able to interact with your token. For the purpose of this guide, we will be deploying the contract onto the Goerli testnet. The Goerli testnet functions identical to the mainnet minus the dollar value attached to ETH. As such, it is the perfect environment to experiment with your contracts prior to deploying to the mainnet. Do note that we are also deploying to Goerli as many of the other testnets (Ropsten, Rinkeby, Kiln) will be stopped following ETH merge.

In order to deploy to Goerli, we will need to first source some Goerli ETH, hereon referred to just ETH. This is required as transactions on the public network need to be processed by a miner which demands a gas fee to be paid. So far, we have been testing the contract on our local network where all the accounts were locally generated with a set amount of ETH. On public networks, all the new ETH generated are minted to the miners and hence we need to source ETH in order to run our contract code.

The easiest way to procure some ETH is via faucets. Faucets are community funded websites where users can request for ETH to be sent to a privately owned wallet. Do note, it is best to send these funds to a separate dev wallet as the private keys will be needed in plain text later. Faucets are used in the test environment as a way to circulate testnet ETH for developers to use. You can easily find such sites by doing a search but I have linked 2 such sites below:

Once you’ve got some ETH, let’s go ahead and add the network to our hardhat.config.js so that Hardhat knows where to deploy to.

In order for Hardhat to deploy the contract to Goerli, it needs two additional things:

  • A Goerli node which it can connect to in order to send the transaction to the network
  • The wallet which will be used for deploying the contract

For the API to connect to the node, you can generate one by signing up on alchemy.io, creating a new app and copying the API KEY under the “VIEW KEY” on the app dashboard. Do note, you only need to sign up for the free account so you can just skip the payment information.

alchemy.io

Next up, we will need the private key of the account that is going to pay for the deployment of the contract. In order to get the private key for the account which received the faucet ETH, you can export it directly from Metamask under the “Account details”. Copy and paste that private key into the config file.

With that, we are now ready to to deploy the contract by using the following command:

npx hardhat run scripts/deploy.js --network goerli

You would have noticed that, in contrast to our local environment, transaction confirmation was not instant and this is because the transaction had to be processed by the Goerli testnet. This gives you a taste of how time-consuming the dev cycle can be if not for a local environment. Also note that the ETH balances in the specified account has decreased by a small amount due to gas fees.

Once deployed, your console should output the newly deployed contract address:

Remember that the FunToken is minted to the wallet which deployed the contract hence in order to view the token, we will need to add our contract to Metamask. Open Metamask and navigate to the assets tab where you will see an “Import tokens” link. Select the link and enter the contract address, the relevant fields should then be auto-populated.

Once imported, you should now be able to see 1,000,000 FUN tokens in your wallet!

From here, you can send and receive the token. For example, I sent half the tokens to my other dev wallet:

All these transactions are publicly viewable hence you can use tools such as Etherscan to view and analyse transactions. For example, the transaction which I just sent can be found here:

Congrats, you have successfully created your very own ERC20 token! Do remember that the token is only as valuable as what it enables users to do. As such, continue adding to your token functionalities to suit what excites you the most. All the best out there!

Thanks for staying till the end. Would love to hear your thought/comments so do drop a comment. I’m active on twitter @AwKaiShin if you would like to receive more digestible tidbits of crypto-related info or visit my personal website if you would like my services :)

--

--

Aw Kai Shin

Web3, Crypto & Blockchain: Building a More Equitable Web | Technical Writer @FactorDAO | www.awkaishin.com