Your own ERC-20 Token: A Step-by-Step Guide using Foundry

Created by:

About this lesson

If you're eager to learn how to create your first ERC-20 token, you're in the right place. There are a multitude of uses for the ERC-20, and we'll introduce you to some of them. We'll be building a smart contract with a range of developer tools, including the use of Foundry. If you are new to coding, we suggest that you first complete our Getting Started with Smart Contract Development project, to get to grips with the basics of Solidity you'll need in this lesson. And then move on to Build a Basic NFT where you can get familiar with using an IDE, wallet, RPC, testnet and Etherscan, all of which you'll be using here to some degree. We also have a series of follow-up projects 'in the works' to follow on from this tutorial, but let's get our focus on this one first.

We'll have checkpoint questions at stages for testing your previous knowledge, predicting next steps in the lesson, and helping you see how you're absorbing the new content. You can also expect a final quiz, so make sure you're checking out any additional material here for a deeper dive of the new concepts. To complete the lesson, expect somewhere between one and three hours, depending on your previous experience and the need to learn new ideas. Let's make learning enjoyable by taking care of our well-being. Make use of regular breaks, and please do go outside and "touch grass." 😊 Nature is the source of much of our well-being. 🌱

What are we creating?

By the end of this lesson you will have created your own ERC-20 token, and you will be able to change its properties in any way you need. But before we go any further, let's check-in and see what building blocks you have already.

We hope that was a little eye-opener for what's to come!

Tokens, crypto, coins, ERC-20. Is it all the same?

Let's begin with some context, so we all speak the same language. The Ethereum blockchain, and most EVM-compatible blockchains, have a native token that is used to pay for transaction costs, and also to reward validators in Proof of Stake (previously miners in Proof of Work). In Ethereum that native token is Ether, or ETH, in Polygon its POL, and so on. But let's focus on the Ethereum blockchain for the sake of clarity.

Every other token or 'crypto' you see, use or interact with on Ethereum is basically a smart contract that lets you send these tokens, receive them and check your balance. Stablecoins like DAI, Tether (USDT), USD Coin (USDC), or tokens from projects like Uniswap (UNI), MakerDAO (MKR), Basic Attention Token (BAT) are all smart contracts that follow a standard. The ERC-20 standard.

Why would we want to create yet another Token?

A lot of reasons, but here are some. The most fundamental one is that we are learning, and want to know how the ecosystem works. Or we have a project, or DAO that needs some means of governance, or a community that wants take some ownership, be part of, or simply support our project. Maybe we need a token with extra functionality that doesn't even exist yet. The possibilities are endless!

What do we need to do with our Tokens?

Some basic functionality that our tokens need to provide are:

  • Transfer tokens from one account to another
  • Get the current token balance for an account
  • Get the total supply of tokens available in the network
  • Approve an amount of tokens from an account to be spent by a third-party account

Has someone created a battle-tested library for the standard? Yes. OpenZeppelin, among others, has created an implementation we can inherit to create our own tokens easily. Here is a quick overview of what a contract needs in order to be called an ERC-20 Token contract:

Let's code, but first...

Before we start hacking away without any structure to follow, what is our actual project's structure, and what do we want to achieve? Since we are BUIDLING, I'd like to think of our project, or smart contract, roughly the same way as we would build a house:

  • 1 - Foundations
  • 2 - Structure: Framing, Walls and Roof
  • 3 - Doors and Windows
  • 4 - Security
  • 5 - Final touches

Before we build, we need a stable, levelled surface to place our foundations so we can build on top. The tools for coding and the project's file/folder structure will be our foundations.

Setting up Foundations

Before coding our basic contract, we need to create a project and set up our IDE to code. Let's go into the folder where we store all our Academy projects:

## (OPTIONAL) create a folder for our D_D Academy projects
mkdir d_d_academy
cd d_d_academy

You can use whatever tools you feel confident with, or accustomed to. For this example we will be creating a Foundry project, but feel free to use Hardhat, or Truffle if that's your favourite flavor. The main focus of Foundry is that you don't need to use Javascript at all, if you don't want to. All the tools are CLI commands. For tests and scripts, you only need Solidity!

foundry-banner.png

These instructions are for Linux and MacOS. If you use Windows, we strongly recommend using WSL (Windows Subsystem for Linux), or else you'll need to build the tools from source. Using WSL is an investment in the long term anyway, since most development and Open Source work is done inside Unix environments. We encourage you to try it. There is also a Docker container if you prefer it. All options are detailed in the Foundry Book

To download Foundry, we need to run this command on the console:

curl -L https://foundry.paradigm.xyz | bash

This will download and install the foundryup tool. It will also update the $PATH environment variable inside your shell's config file. Please read the output from this command to see if it detected your shell properly e.g. Bash, zsh, fish... and follow the instructions to refresh your environment, or just close the terminal and open a new one to be sure the environment has been updated.

Now let's run this tool to install the Foundry toolkit:

foundryup

If everything goes well, you'll have four tools to run in the console: forge, cast, anvil & chisel. Basic overview of each tool:

  • forge tests, builds and deploys your smart contracts
  • cast can make smart contract calls, send transactions, or retrieve any type of chain data
  • anvil is a local testnet node shipped with Foundry. You can use it for testing your contracts from frontends or for interacting over RPC
  • chisel provides an interactive environment for writing and executing Solidity code, as well as a set of built-in commands for working with and debugging your code, which makes it a useful tool for quickly testing and experimenting with Solidity code without having to spin up a full foundry project

Now we can create our project with:

forge init my_token

Let's see what our project looks like:

cd my_token
tree . -d -L 1

This command outputs something like this:

.
β”œβ”€β”€ lib
β”œβ”€β”€ script
β”œβ”€β”€ src
└── test

4 directories

The folders in our project are used like this:

  • lib dependencies: forge uses Git Submodules instead of Node Packages. This means it works with any GitHub repository that contains smart contracts.
  • script scripts: automate transactions or deployment of our smart contracts. Scripts are written in solidity!
  • src source: where our smart contract code will live
  • test tests: write tests for our smart contracts also in Solidity!

The default project includes a basic smart contract. We can build it to see how the forge commands work:

forge build

My output:

[β ’] Compiling...
[β ”] Compiling 22 files with 0.8.20
[β ƒ] Solc 0.8.20 finished in 4.35s
Compiler run successful!

And now we can test it:

forge test

My output:

[β †] Compiling...
No files changed, compilation skipped

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, ΞΌ: 27631, ~: 28409)
[PASS] test_Increment() (gas: 28379)
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 27.32ms

Ran 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)

As we mentioned earlier, we are going to use OpenZeppelin ERC-20 implementation. To use it, we need to install OpenZeppelin as a dependency in our project. To install dependencies in Foundry, we use:

forge install OpenZeppelin/openzeppelin-contracts

This looks for the repo called openzeppelin-contracts from the GitHub user OpenZeppelin and downloads it as a Git Submodule.

This is the output I got:

Installing openzeppelin-contracts in /home/myuser/d_d_academy/my_token/lib/openzeppelin-contracts (url: Some("https://github.com/OpenZeppelin/openzeppelin-contracts"), tag: None)
	...
	Installed openzeppelin-contracts v5.0.0

This lets us import OpenZeppelin contracts in our smart contracts. We need to create a remappings.txt file in the root of our project:

forge remappings > remappings.txt
echo "@openzeppelin/=lib/openzeppelin-contracts/" >> remappings.txt

This tells forge, β€œHey, anytime you hit @openzepplin/, look in lib/openzeppelin-contracts/ instead.”

Now that we have an overview of the tools and the OpenZeppelin's libraries installed, we can clean up the files we won't use and start afresh:

rm script/*
rm src/*
rm test/*

Go touch grass 😊 🌱

Everything's ready, so let's go ahead and start coding Solidity.

Create the Framing and Walls

We need our house to have a structure and a floor plan with walls dividing the rooms. Let's think of that setting as the 9 methods and 2 events that we saw earlier for our code to be ERC-20 compliant.

It's a great exercise to try and implement them from scratch, and we ecourage you to do that once you start diving deeper into Solidity. It is also a great challenge to do it, and later compare your code to battle-tested code like OpenZeppelin's implemetation.

In this lesson we'll start by inheriting OZ's, and then choose the specifics that our token needs.

Let's start by creating a MyToken.sol file inside the src/ folder with the basics:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract MyToken {
}

Once we have that, we can import and inherit OpenZeppelin ERC-20:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

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

contract MyToken is ERC20 {
}

Now that we have the full implementation inherited, we still have to specify a few more things before compiling, because the ERC-20 constructor needs to receive 2 parameters, i.e. the token's name and symbol, or it will raise an error. In order to do that, we define our constructor and call our inherited contract's constructor:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

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

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {}
}

With those few lines, we inherit all the functions and events we need.

One final consideration is adding a roof above ourselves, to be able to live in and finish the structure around us. We have all the rooms and functions we needed, but no tokens exist yet. What good is a house with no roof, or a token with no supply? We can think of the initial allocation of tokens as the way to put a roof on the house and finish the structure. If you choose not to have an initial allocation, that's also valid and can skip this, but for our example we'll allocate an arbitrary amount of tokens to the deployer of the contract on our constructor:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

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

contract MyToken is ERC20 {
    constructor(uint256 initialAllocation) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialAllocation);
    }
}

The internal function _mint takes care of creating the tokens and assigning them to the msg.sender's balance.

Opening Doors to our house

We now have a full structure, with a roof to shelter us from the weather. The only problem is that we didn't create any openings for anyone to enter or leave. We can think of entering the house as minting new tokens or increasing the total supply, and leaving the house as burning tokens or decreasing the total supply. But these doors don't exist in our contract just yet.

πŸ’‘

If you are planning on leaving a fixed total supply like the initialSupply we used recently, that's also a valid approach and you don't need to create a mint or burn function.

We need to create public facing functions to achieve this:


// ... pragma + import lines

contract MyToken is ERC20 {
    // ... constructor lines

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

    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }

    // In case we have allowance from other address to transfer/burn their tokens:
    function burnFrom(address from, uint256 amount) public {
        _burn(from, amount);
    }
}

When we define our mint function, we decide who will be the receiver of said new tokens. For the burn function on the other hand, we are only letting holders burn their own tokens by using msg.sender as the address to call the internal _burn function in the OpenZeppelin ERC-20 implementation.

In a special case where someone approves operating on an amount of their tokens, we create a burnFrom function to be able to burn within that approved allowance from the other address. If we try to burn, or transfer, more than the approved allowance, the ERC-20 implementation from OZ will revert the transaction because that's required by the ERC-20 standard.

We have now created doors to our house, but we haven't put locks in them!

Lock the Door!

In our present state, our functions for minting and burning can be called by any address. That's ok for burn since people can only burn their own tokens, or their approved allowances. But we don't want just anyone to mint tokens and increase the total supply of our new token.

To be able to limit the access to minting tokens, we are going to use an OpenZeppelin pattern called AccessControl to give granular access to functionality within our code. For that we will inherit yet another great contract from the OZ library. It basically lets us define roles and assign or un-assign them to any address we need. In our case, we will only create a MINTER_ROLE to allow access to our mint function:


// ... pragma + import lines

import "@openzeppelin/contracts/access/AccessControl.sol";

contract MyToken is ERC20, AccessControl {

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(uint256 initialAllocation) ERC20("MyToken", "MTK") {
        // Allow msg.sender to grant roles
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

        // Grant MINTER_ROLE to msg.sender
        _grantRole(MINTER_ROLE, msg.sender);

        _mint(msg.sender, initialAllocation);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    // ... burn functions

}

Let's break down our changes in the code:

  • Added an import before our contract definition
  • Inherited AccessControl in our contract definition
  • Created a bytes32 public constant variable for the minter role. The irreversible keccak256 hash ensures security and prevents unintended changes to the role. As a fixed-size of 32 bytes, it is efficiently storable, and will incur minimal gas consumption.
  • Inside our constructor:
    • Granted the DEFAULT_ADMIN_ROLE to the contract deployer to be able to grant roles to others. Our contract inherits this by default from Access Control as the starting point for all admin roles.
    • Granted the MINTER_ROLE to contract deployer to be able to mint tokens
  • Added the modifier onlyRole(MINTER_ROLE) to the mint function to restrict its access

So, we basically imported AccessControl and inherited it to gain all its functionality. We gave 'minter' and 'admin' roles to the address that will deploy the contract, and then restricted access to our mint function.

Now any address that doesn't have the MINTER_ROLE granted, will revert on any transaction that calls our mint function. Hooray! Our door is now locked, but only for intruders!

Final Touches and details

Congratulations! We have built a fully functioning token!

Given that we have our house completed, you might want to add some personalized detail to it, like special furniture, wall paint, or make it a smart home.

With this in mind, we can add functionality to our token, in any non-standard fashion. We are not going to cover any modifications to our code here, but we strongly encourage you to be bold, creative and experiment with different aspects for the token.

A few examples could be:

  • A token that accrues or drips more tokens, depending on how long have you been hodling. Tip: you can override functions like balanceOf and _beforeTokenTransfer / _afterTokenTransfer from the inherited ERC-20
  • A token that can only be transferred or received if you hold another specific token, i.e. an NFT from a specific collection
  • A token that loses the balance if you don't transact or transfer it regularly

The possibilities are endless, and we want you to go wild with your imagination.

House Inspection

Before we go to the Real Estate Register, we need to make sure everything is working according to your local rules, so let's call an inspector to see if we got everything right.

In our analogy, the inspector will be the Solidity compiler. Later on, when we have more dexterity in this new realm we'll be adding tests to make sure everything is spot on, but that's for another lesson.

Since we layed out our project using Foundry, we only need to step into our project's folder and run one command:

forge build

Here's what I got in the console:

[β ’] Compiling...
[β ’] Compiling 10 files with 0.8.20
[β ‘] Solc 0.8.20 finished in 252.16ms
Compiler run successful!

Everything compiled successfully and is ready to deploy.

If you have any problems here, since the compiler might have found some typos or errors in your code, pay attention to the messages in the console and see if you can find and fix them. If you can't fix them, you can go into our forum and try to ask our community for help by explaining your error, the console output and your configuration (OS, version, Foundry and Solidity version, etc.)

Go touch grass 😊 🌱


Register your house in the Real Estate Register

Or... Let's deploy our contract!

Now that we have our house built, we need to register it so as to guarantee the property rights. We can think of this as deploying our contract to a blockchain, where our token will be fully functioning for every address that wants to interact with it.

We are going to need a small amount of Sepolia ETH to be able to pay for the transaction to deploy our contract. If you already have some Test ETH in Sepolia, you can continue, if you don't you can always ask for some in the many faucets that are online. Some options are: Alchemy, Sepolia Faucet or Quicknode, but you can find more if you search for Sepolia Faucet in any search engine.

We are going to create an .env file, where we will store our secrets. And when we say secrets, we are specially referring to the private key to your wallet. Never, ever share this with anyone, not even us. Anyone who gets hold of your keys, can drain your wallet. This is why we strongly advise using a separate browser profile, and most definitely a separate wallet for your development activities, that should not hold anything with real value in it. It wouldn't be the first time, even for experienced developers, to accidentally upload the .env file to GitHub, and once it's on the internet we can consider it to be, yes, public.

So, with that important notice out of the way, we create a new file in our project's root folder called .env and fill it with this:

RAW_PRIVATE_KEY=REPLACE_THIS_VALUE_WITH_YOUR_PRIVATE_KEY
ETH_RPC_URL=https://rpc.ankr.com/eth_sepolia
CHAIN=sepolia
ETHERSCAN_API_KEY=REPLACE_WITH_ETHERSCAN_API_KEY

Remember to replace the REPLACE_THIS_VALUE_WITH_YOUR_PRIVATE_KEY with your development wallet's private. In the spirit of OSS (Open Source Software) we recommend you to also replace the REPLACE_WITH_ETHERSCAN_API_KEY with your Etherscan API KEY so you can verify your contract.

Overview of our values:

  • RAW_PRIVATE_KEY: Here we shall paste our development wallet's private key after the = sign
  • ETH_RPC_URL: This is the token from the RPC provider. We are using Ankr's public one here, but you could also choose to use a POKT Network Gateway such as Grove, or Nodies, or another provider that you prefer.
  • CHAIN: The blockchain where we will be deploying out contract. We are using the Sepolia testnet since this is not intended to be on mainnet.
  • ETHERSCAN_API_KEY: Here we will use our Etherscan API KEY so we can verify our contract for everyone to see the deployed code and foster transparency in the web3 space.

Once we have these values inside the file, we can source it to have the values as enviroment variables before using Foundry's deploy tools.

There are many ways to load these values into environment variables, depending on which shell you use e.g. Bash, zsh, fish... The next command works with most of bash-compatible shells:

export $(grep -v '^#' .env | xargs)

If you are using Fish or another shell, you can search on how to create a function that does the same. Tip: envsource fish function.

Once we have those values in our environment, our Foundry tool to deploy will know which wallet to deploy from, node or RPC in the blockchain to connect to, and chain we want to deploy to.

πŸ’‘

Remember that if you close your terminal session, all these variables with your private keys gets wiped from memory, so you need to run this command again if you want to continue the lesson at a later date.

We also need to pass our constructor 1 argument, our token's initial allocation. All the Foundry tools expect values to be passed in wei, so we are going to use cast to find out how many wei are, for example, 1000 ETH, or MTK in our case:

cast --to-wei 1000

My output:

1000000000000000000000

Now we can use this value when we need to deploy. To deploy our contract we will use the forge create tool.

πŸ’‘

We don't have to copy our private key in the command! That's one of the reasons to use the .env file - we know exactly where our private key is and we are responsible to not publish this file anywhere.

We can even delete the private key after we deploy the contract. If you write your private key in a command on the terminal, it gets stored in the terminal's history, which could be in a publicly accesible file in your computer. That's why we are using an .env file.

Notice aside, this is the command we can use to finally deploy our contract:

forge create src/MyToken.sol:MyToken \
    --constructor-args 1000000000000000000000 \
    --verify \
    --private-key $RAW_PRIVATE_KEY
πŸ’‘

Remember that if you used a different name for your token and your .sol file, you'll need to adjust this command.

Do you see how we used the value that cast gave us as a parameter after the --constructor-args modifier? We are passing down arguments to our Solidity constructor right from our command-line! How cool is that?

We also used --verify to let the tool know we want to verify our contract in Etherscan. If you didn't fill your Etherscan API KEY, don't use this modifier, or the tool will return an error when trying to verify.

This is the output I got:

[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xf4bAFA90241e808461653C17dB0f8669Fa4342a1
Deployed to: 0xec870005029ED5595F146f6AAAe699b442065b72
Transaction hash: 0x436120f627b1b56ac92ca1df408f1ec591cd578c4168aad4d89bc73c4141aa78
Starting contract verification...
Waiting for etherscan to detect contract deployment...
Start verifying contract `0xec870005029ED5595F146f6AAAe699b442065b72` deployed on sepolia

Submitting verification for [src/MyToken.sol:MyToken] "0xec870005029ED5595F146f6AAAe699b442065b72".
Submitted contract for verification:
        Response: `OK`
        GUID: `hzmpq14eeasu92sd1hgzgdhttdu7iscirjlbs9eb1vsezkn8vc`
        URL:
        https://sepolia.etherscan.io/address/0xec870005029ed5595f146f6aaae699b442065b72
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified

If by chance you get a message saying Etherscan hasn't detected or verified the deployment, try running the command again.

By filtering through the information, we can pay attention to the second line, where the tool tells us the address of the deployed contract: 0xec870005029ED5595F146f6AAAe699b442065b72.

Querying the Block Explorer

You can go into Sepolia Etherscan block explorer and search for the address that the command returned to check out your very own newly deployed smart contract!

ERC20-etherscan

If you got to verify it, you will also get a link to see your contract on the block explorer. In our case, it's the last line of the Submitted contract for verification section of the terminal output Sepolia Etherscan. And you get to see the code for the contract and all its libraries right there in the tab named Contract with the green checkmark.

If you scroll down, you'll see our code. It was the first file of the set for me:

ERC20-verified

Etherscan also gives you a front-end to easily interact with the contract. Inside our contract tab, you get 3 tabs: Code, Read Contract and Write Contract.

We can go and check for our balance by clicking in the Read Contract tab, and then searching for the balanceOf function:

ERC20-balanceOf

Copy and paste your wallet's address - the one you used to deploy the contract, and click Query. You should have as many wei as you passed to the constructor when deploying. In my case it was 1000000000000000000000 wei and since we left our decimals in 18, that would be exactly 1000 MTK.

ERC20-1000MTK

Invite your frens!

That's it! We deployed our contract on the Sepolia testnet, and our token should be live and kicking. Now that we have built our house and it's has been accepted in the real estate register, we can invite our friends by transferring them some MTK, or maybe minting them some tokens, even if that affects your tokenomics πŸ˜‰

Consolidation

Let's take one more moment to consolidate the steps you've just mastered:

  • You tested some of Foundry's amazing capabilities.
  • Set up your foundations and created a project using Foundry tools.
  • Built your own customisable contract utilising the power of forge for effortless compilation.
  • Integrated OpenZeppelin, leveraging their ERC-20 and AccessControl implementations for enhanced functionality and security.
  • Successfully configured your contract for deployment on the Sepolia testnet.

All in all, a very significant accomplishment!


Connect your wallet and Sign in to start the quiz


Next steps?

So where do you go from here? In the future, we'll be adding to this ERC-20 series of lessons. But for now, why not explore A Developer's Guide to Ethereum three part track throught the lens of Python? Or complete the NFT track, all the way from the back-end to creating your own front-end, building your own Test Driven Development suite along the way. And of course, we have lots of other goodies in the pipeline to get your developer juices flowing.

And make sure to check into the Developer_DAO community for our newsletter, workshops, DevNtells, hackathons, VIBES and many more initiatives that are going to enhance your developer journey in creating a better planet Earth with us πŸŒŽπŸŒ€οΈπŸ’¦πŸŒˆπŸŒŠ