ERC-20: building a fungible token smart contract

Lucas Vieira
Coinmonks
13 min readJun 14, 2022

--

In our last post we detailed which are and the purpose of the most varied types of ERC protocols. They provide us patterns to create contracts that talk to each other, helping the community in the development and use of blockchain services.

In this article we will get our hands dirty and build a contract using the ERC-20 protocol for fungible tokens, with Solidity as our contract programming language, the development tool Hardhat, some OpenZeppelin libraries, Chai for the tests and CoinMarketCap API to retrieve gas costs estimates with real and updated values.

1. Configuration

The first step is to install (if it is not already installed) Node.js and npm in your computer. Next we will open a terminal window, create our directory, initiate a new npm project and install hardhat package inside of it.

Now it is time to create a new hardhat project with the command

It will be prompted a series of options and, at this moment, let's choose the one of creating a sample project

Following we will confirm the directory where the project will be inserted and choose "yes" on the options to add a .gitignore file and to install project's dependencies.

Opening the directory with a code editor we can see that a bunch of files were created like a contract .sol, a deploy script and a test script.

Let's first rename (or delete it and create a new) the contract file to Token.sol and add the following content inside of it.

For a contract be compatible with ERC-20 protocol we need at least have the functions transfer, balanceOf, totalSupply, transferFrom, approve and allowance. Besides of having the events Transfer and Approval. At this point we could simply import and use the OpenZeppeling ERC20 library. But as this post has educational purposes we will roll up our sleeves and write the code by ourselves. Let's begin defining the needed variables, the events and the contract constructor.

First we have the _totalSupply variable to store the available amount of tokens and the mappings _balances and _allowances to store the amount of tokens owned by each address and how many tokens belonging to an address a third party address can manage. Besides that, we have the events Transfer and Approval which must be emitted always that a token transfer happens or when a owner approves that a third address manage part of his tokens. Last we have our contract constructor. The block of code inside of the constructor is executed only once at the moment of the contract creation. In this case we receive a parameter with the initial amount of tokens, we update the variable that stores this information, trasnfer this amount to the address which executed the contract creation (the msg.sender) and, lastly, we emit an event to show that there was made a trasnfer of that amount of tokens to this address. Next we will create the view functions, which are functions that do not alter the state of data stored on the blockchain, they only return that information

So now we define the functions totalSupply, balanceOf and allowance which returns the available amount of tokens, the amount of tokens an address have and the amount of tokens a third party address can manage from another address. The next step will be defining the internal functions (called only by the contract itself) which executes the operations of trasnfer and approval of tokens. Then we will define the external functions (which can be called by the users) to execute the respective functionalities.

The function _transfer allows the transfer of an amount of tokens from address sender to address recipent and emits a Transfer event showing that the operation was fulfilled. The function _approve allows that an address owner grants permission to an address spender to manage an amount of his tokens. Lastly, we have the external functions transfer, approve and transferFrom which can be executed by the users and uses the internal functions with the correct parameters.

Note: by this point we already have a contract with the minimum required to be compatible with the ERC-20 protocol!

2. Customization

Now it is time to customize this contract and include other important features for our project. Let's begin by include the optional metadata extension for the ERC-20 protocol which allows us attach a name, a symbol the number of decimal places of our token.

What we have done here was to include the variables _decimals, _symbol and _name beyond of defining values to them in the moment of the contract creation. We also include the view functions decimals, symbol and name which return the values stores on those variables. Let's now include functions to allows us to create new tokens withe the mint function and destroy existing tokens with the burn function. These functions are sensitive and we need to include a logic so that only the owner of the contract can execute them. And for that, this time, we will use OpenZeppelin Access Control library Ownable. This library gives us, between other features, the modifier onlyOwner which can be attached to any function. The functions which have this modifier will only be able to be executed by the contract owner. Let's now install the OpenZeppelin package with the following command.

To use the library in the contract file we need to import it and include its heritance on the contract definition like we see bellow.

In the mint and burn functions we need to remember of increasing/decreasing the amount of tokens to be created/destroyed from the _totalSupply and the reference address balance. And also emiting a Transfer event.

Note: you can see that the public function mint and burn have the onlyOwner modifier. This way only the contract creator (the msg.sender which executed the deploy) can execute them. The Ownable library also allows us to change the contract owner with the function transferOwnership.

3. Tests

Now, with our Token contract written, let's go to the tests! Let's use Chai library to help us on this process. First let's rename the file test/sample-test.js and start to rewrite it.

We begin the test making sure that the contract has the correct setup once the deploy was executed. First we use the getSigners function from ethers library which returns us an array with 20 ethereum accounts. Whenever that we don't explicit mention which one of the accounts we are using when we interact whit the library, like in the next lines where we use the contractFactory and make the contract deploy, the library by default undestands that the account on the 0 position of the is who is making the call. So as the 0 account called the deploy, it will be considered the contract owner on our tests.

At the deploy function we pass the following arguments (5000000, “NiceToken”, “NTKN”, 18) which are respectively the tokenTotalSupply, tokenName, tokenSymbol e tokenDecimals, the parameters received in the contract constructor. Next we call the view functions and run the assert function from Chai to make sure that they were defined with the correct values! Besides that we check if the balance of the contract owner received all the created tokens. To run the tests we use the following command:

Let's now include the tests to check if the feature of tokens transfer is correctly working. To leave our tests directory organized let's create a new file for the tests of this feature. We created then the test/tokenTransfers.js file.

We have included than 3 more tests. In the first one we check if the contract onwer, initially with all 5000000 tokens can trasnfer 10000 tokens to a second account, from Lucas. Next we test if the contract returns an error when Lucas tries to trasnfer 15000 tokens (more than his balance) to João. Lastly, if Lucas can successfully transfer 5000 (less than his balance) to Carol. Always confirming if the blanaces were correctly updated. Let's run the tests command again to check if everything went well as it should.

Let's look some important points in this last test file. When we want to execute some function from the contract with a different account from the 0 account (the owner's account) we can use the connect function.

We also use the expect function from Chai library in the following way to check if an error was emited.

Basically checking if the error message returned is the same as the message defined on the require from trasnfer function inside the contract.

Another important point is to check if the test fails if we change some parameter from the assert functions. Let's change the last one of the in the following way and run the tests again.

We can see that the test fails as expected.

The following test group will be for us to check the feature of allowing that an address can manage others tokens. So let's begin by creating a new file, the test/tokenAllowances.js.

In this file we execute more 3 tests. The first one to approve an address to manage an amount of your tokens. The second to check that an error is returned when this address tries to transfer a higher amount of tokens than it is allowed. The last to check if the transfer of an amount less or equal the total allowed is succeessfully trasnfered by the manager address. Now let's write the last two test files to check the tokens mint and burn features. Let's create the test/tokenMint.js and test/tokenBurn.js files.

This files have 2 tests each. One to test that the mint/burn was successfully executed by the contract owner and that the available amount of tokens and the balance of who received/lost their tokens were correctly updated. The second test to check that other accounts (not the owner) cannot execute the functions. Now we have tests for all of our features and we can check that they pass as expected.

Another extremely important point on the tests, even more for projects that will run on production, it is the inclusion of the gas reporter to have estimates of real costs of the contract functions. To do this let's install the package hardhat-gas-reporter through the following command.

The following step is tto include the library import on hardhat configuration file hardhat hardat.config.js which is defined on the project root directory.

Now we can see that when we run the command on the terminal we receive a summary with the average gas costs of each one of the functions executed on the tests.

To make even better, we can use the CoinMarketCap API to return the values and aumatically calculate the gas costs of each function in any currency with its currenct market price. To do that, we need to create an account on this site. After validating the email we will have access to the site and our API Key, which we will copy to use on the project.

With this key in hands, we go back to hardhat configuration file and include the following object on the file export.

Where COINMARKETCAP_API_KEY is the copied key from the site. After that we also choose the currency in which the summary will be made. The final configuration file should look like this.

Now if we run the tests command on the terminal we will have the following summary.

With the average costs on the chosen currency. If we use "BRL" in the currency parameter we have the average costs in brazilian reals.

4. Deploy

Now we will write the script to deploy our contract. The deploy is made so we actually upload our contract to some network like Mainnet, Rinkeby and Goerli from Ethereum or even to Mainnet and Mumbai from Polygon.

Inside the scripts/ directory we have a sample-script.js file which was automatically generated when we start a project with hardhat. Let's delete this file and create another one with the name deploy.js. Initially this file will have the following format.

First we will import hardhat task function from irs configuration library, which allows us to set an action that can be executed with the following command

The parameter deploy of this command is the first argument defined in the task function. So if we set it as "pokemon" there, we should use

to execute it. Next we will include the necessary logi for the contract deployment.

The first point to note here is that we return a deployer variable which will be used in the deploy's next steps. This varable is defined in the hardhat configuration file, as we will se further. The function getContractFactory has as its first parameter the contract name ("Token") defined in contract class inside Token.sol.

Another important point is that before we deploy our contract, we need to compile it. The above mentioned function will check the contract name inside artifacts/ directory which is generated after the contract compilation. We can run the compilation with the following command:

Next, we execute the deployment with the values that we want to pass to the contract constructor and we put a log to identify the address of the deployed contract (remember of copying this value!).

If we execute the deploy command now, we can see that it won't work. This is because we still need to make some modifications on our configuration file hardhat.config.js. First we delete the default task of accounts which comes together with the project sample creation. After we include the import file of the created deploy script.

We also have to add the object networks inside the configuration file export with the information of the network which we want to make the deploy.

This object can have multiple objects internally if we want to deploy our contract to different networks. In this case, we will deploy it to the Rinkeby network. We can see more details of this object in the hardhat documentation. The RINKEBY_URL variable must be filled with the value of the blockchain node provider used. The most famous are Alchemy and Infura. In this tutorial we will use Alchemy. We only have to create an account there, select the Ethereum chain and create a new app on the Rinkeby network.

After the app creation, you just have to copy the API key on the HTTP format and insert it on the url parameter from the configuration file. Look that we have another parameter accounts which is an array with the accounts that are returned by the function getSigners in th deploy script. This variable must have the private key of the wallet used to deploy the contract. Remember that to do it correctly we must have some ETH on your wallet. I recommend Metamask. Copy your private key and paste it in the place of DEV_PRIVATE_KEY. In the end our configuration file must have the following format:

As mentioned before, to deploy the contract we will need ETH from Rinkeby network (fake ether) inside our wallet. We can get some by using one of the available faucets (1, 2, 3). Now we just need to compile our contract and then run the deploy command passing the defined network.

And we can see that our contract has successfully been updated to the Rinkeby network in the following address:

We can confirm this by checking this address on the Etherscan page from Rinkeby network! We just need to paste it on the search box. In the case of this contract, is here.

Now all the transactions that are executed in this contract can be followed on this page.

--

--

Lucas Vieira
Coinmonks

Hi! I'm a brazilian software engineer who love to learn and build new things. My goal here is to share I little about that on the way :) https://bushi.solutions