What is an ERC-20 token?
An ERC-20 token is a smart contract on Ethereum that implements the methods and events specified in the ERC-20 standard. It is designed to be used as a fungible token, meaning each instance (or unit) of a token has the same value as another instance of the same token. It provides the following functionalities (ethereum.org):
- transfer tokens from one account to another
- get the current token balance of an account
- get the total supply of the token available on the network
- approve whether an amount of token from an account can be spent by a third-party account
Since it was proposed in November 2015, the popularity of ERC-20 tokens has exploded and, at the time of writing, Token Tracker on etherscan.io claims that it finds nearly half a million ERC-20 tokens. It is not surprising because creating an ERC-20 token is very easy. But first, letās look at the ERC-20 specification.
Overview of a basic ERC-20 token
It is easy to understand the functionality by looking at a concrete example. Brownie has a mix called Token Mix, so letās use it as an example. We can find the smart contract file Token.sol
on the Github repository, which implements the basic requirements for an ERC-20 token. Later in this article, we will install it on the local machine and try some functions on the local blockchain.
balances state variable
The main part of an ERC-20 token is the balances state variable. It has a map data structure (like a Python dictionary) containing addresses and their respective holdings of the ERC-20 token. In other words, it keeps track of who has how much amount of the token.
For example, when Alice has 10 units of the token and Bob has 5 units, the balances variable contains the following entries.
balances
Address | Value |
Alice | 10 |
Bob | 5 |
When Alice sends 5 units to Bob, the balances
variable is updated.
balances
Address | Value |
Alice | 5 |
Bob | 10 |
So, simply put, when we say we āownā a certain amount of an ERC-20 token, it just means our addresses and the corresponding amount are stored in the balances variable in the specific smart contract. Our accounts do not actually āownā anything.
This mechanism is fundamentally different from the ether (ETH). When an Ethereum account has ether, the amount is stored in the balance field of the account. On the other hand, ERC-20 tokens are not stored in the account, and only the address and the record about the holdings are stored in the tokenās smart contract.
transfer() workflow
Another essential thing to note is two workflows that transfer tokens from one account to another. One is when the spender directly transfers the token to another address, and the other is when a third party transfers the token on behalf of the spender.
The first workflow is straightforward. The sender directly calls the function transfer()
with the receiver address and the amount to send as parameters. For example, Alice can execute transfer(Bob, 5)
to send 5 units of her token to Bob. The function updates the balances state variable to reduce Aliceās balance by 5 units and increase Bobās balance by the same amount, as we saw earlier.
transferFrom() workflow
The function transferFrom()
is used in the second workflow. In this case, the address that calls the function is not the same as the sender. To prevent unauthorized transfers, the sender first executes the approve()
function to allow the third party address to send a specific amount of token on its behalf. It updates the allowed
state variable with the information about who approved whom to spend how much.
For example, letās say Alice wants Charlie to send 5 units of the token to Bob on her behalf. Before Charlie can make the transfer, Alice executes approve(Charlie, 5)
first. The function adds the following entry to the allowed
state variable.
allowed
Owner address | Spender address | Value |
Alice | Charlie | 5 |
It shows that Charlie can transfer up to 5 units of the token from Aliceās balance.
Now, Charlie can execute transferFrom(Alice, Bob, 5)
. First, the function reduces Charlieās allowance by 5 units in the allowed
variable.
allowed
Owner address | Spender address | Value |
Alice | Charlie | 0 |
Then, it updates the Aliceās and Bobās values in the balances
variable, as we saw in the transfer()
workflow, which completes the transfer.
The third-party address can be an externally owned account (= human), but more often, it is a smart contract address, such as an exchange, to delegate certain operations to a system.
ERC-20 token with Brownie
This section will create an ERC-20 token using Brownie Token Mix and try its functionality on the Brownie console.
Install Brownie Token Mix
If you havenāt installed Brownie, you can install it by following the tutorial below:
After installing Brownie, go to a new directory and run the brownie bake token
command.
[~/erc20_test]$ brownie bake token Brownie v1.17.1 - Python development framework for Ethereum Downloading from https://github.com/brownie-mix/token-mix/archive/master.zip... 9.13kiB [00:00, 4.57MiB/s] SUCCESS: Brownie mix 'token' has been initiated at /Users/mikio/erc20_test/token
You can find the ERC-20 smart contract file (Token.sol
) in the token/contracts directory.
Set up accounts
In this example, we will use the accounts alice
, bob
, charlie
, which we can create in Brownie as shown below. When prompted, type a password. Remember it, as we need it in the later steps.
[~/erc20_test/token]$ brownie accounts generate alice Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'xxxx xxxx ...' Enter the password to encrypt this account with: SUCCESS: A new account '0xf56B3FEC97cCc891999a1F8D4BfF30455C89594F' has been generated with the id 'alice' [~/erc20_test/token]$ brownie accounts generate bob Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'yyyy yyyy ....' Enter the password to encrypt this account with: SUCCESS: A new account '0x9fecB3A269327fEf1c03636Bca82Ec8B6C875121' has been generated with the id 'bob' [~/erc20_test/token]$ brownie accounts generate charlie Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'zzzz zzzz ....' Enter the password to encrypt this account with: SUCCESS: A new account '0x86Aa12E566Ebecb6C8a4b887A375c1b46f015326' has been generated with the id 'charlie'
Import accounts into MetaMask
We can optionally import the accounts into MetaMask. This step is not necessary to understand ERC-20 tokens, but Iāve included it just for demonstrations purposes.
If you have not installed MetaMask, you can find the tutorial on Finxter below.
Firstly, we need to export accounts into keystore files. On the terminal, run the following commands, which exports the keystone files in the current directory.
[~/erc20_test/token]$ brownie accounts export alice ./alice.json Brownie v1.17.1 - Python development framework for Ethereum SUCCESS: Account with id 'alice' has been exported to keystore '/Users/mikio/erc20_test/token/alice.json' [~/erc20_test/token]$ brownie accounts export bob ./bob.json Brownie v1.17.1 - Python development framework for Ethereum SUCCESS: Account with id 'bob' has been exported to keystore '/Users/mikio/erc20_test/token/bob.json' [~/erc20_test/token]$ brownie accounts export charlie ./charlie.json Brownie v1.17.1 - Python development framework for Ethereum SUCCESS: Account with id 'charlie' has been exported to keystore '/Users/mikio/erc20_test/token/charlie.json'
Log on to MetaMask, click on the account icon in the top-right corner and select āImport Accountā
Select āJSON Fileā from the drop-down menu as Select Type and upload the file exported in the previous step. Type the password and click āImportā.
The account will appear on the list after a few minutes. Click on the three dots next to the account name and select āAccount detailsā.
Then click on the pencil icon next to the account name to change the account name (e.g. Alice)
Repeat the same steps for the other two accounts (Bob and Charlie).
Deploy smart contract
To create an ERC-20 token, we need to decide the following attributes.
- Name: the name of the token
- Symbol: the symbol of the token
- Total supply: the total token supply
- Decimals: the number of decimals the token uses
We will create āMiko Tokenā for this example, as shown below.
Name | Symbol | Total Supply | Decimals |
Mikio Token | MIK | 100000 | 2 |
The attribute Decimals
is set to 2. It means that the amount of token is divided by 100 (=10**2) to get its representation, similar to representing 100 cents as 1 USD. As the total supply equals 100,000, the maximum amount of Mikio Token is 1000.00 MIK (= totalSupply / decimals = 100000 / 10**2). Note that this is just an example, and it can be a different value, such as 18, which is the same value as ether (1 ether = 10**18 wei).
Now we can deploy the smart contract. First, start the Brownie console using the local blockchain.
[~/erc20_test/token]$ brownie console Brownie v1.17.1 - Python development framework for Ethereum Compiling contracts... Solc version: 0.6.12 Optimiser: Enabled Runs: 200 EVM Version: Istanbul Generating build data... - SafeMath - Token TokenProject is the active project. Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'... Brownie environment is ready. >>>
Load the accounts. Type the password for each account when prompted.
>>> alice = accounts.load('alice') Enter password for "alice": >>> bob = accounts.load('bob') Enter password for "bob": >>> charlie = accounts.load('charlie') Enter password for "charlie":
Brownie console automatically loads the Token contract object. We can deploy it to the network by running the deploy()
method with the abovementioned details as the arguments. The deployment account is specified in the from field. Letās use the account alice to deploy the smart contract.
>>> token = Token.deploy('Mikio Token', 'MIK', 2, 100000, {'from': alice}) Transaction sent: 0xd63d04a68f4bc6e16ef58003bf9cca7bfa6b480bab3d7911fabcc7cf33e3d302 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0 Token.constructor confirmed Block: 1 Gas used: 516339 (4.30%) Token deployed at: 0xe5D97d3F9bfDb9B5981DA4b89c72cCcABe25cbd7
The total supply is automatically transferred to the deployment account when the smart contract is created, as we can see in the constructor() function. We can confirm it by checking Aliceās balance.
>>> token.balanceOf(alice) 100000
This value means 100,000 cents or 1,000 MIK, as explained above. We can also confirm that Bob and Charlie donāt have any MIK tokens.
>>> token.balanceOf(bob) 0 >>> token.balanceOf(charlie) 0
The smart contract’s balances state variable would contain the following entry.
balances
Address | Value |
Alice | 100000 |
Import Token into MetaMask
We can see the balances on MetaMask as well. MetaMask knows nothing about āMikio Tokenā yet, so letās import it.
Log on to MetaMask and select Alice’s account. Make sure to choose the network āLocalhost 8545ā. Then, click the link āImport tokensā at the bottom.
Then, copy the token address from the deploy command output on the console in the previous step and paste it to the Token Contract Address field. The other fields (Token Symbol and Token Decimal) will be automatically filled. Click the Add Custom Token button.
On the next screen, click on the Import Tokens button.
Go back to Alice accountās main page, and we can now see that Alice has 1000 MIK.
Repeat the same steps for Bob and Charlie. We can see that they donāt have any MIK tokens yet.
Transfer workflow
Letās say Alice has decided to transfer 100 MIK to Bob. We can simulate it by running the transfer()
command with Alice in the from field.
>>> token.transfer(bob, 10000, {'from': alice}) Transaction sent: 0xe7c4fc8ef260a1a4cca696e0cde73eba4e9b792136f661cf05ec18bc7c24b810 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1 Token.transfer confirmed Block: 2 Gas used: 51608 (0.43%) <Transaction '0xe7c4fc8ef260a1a4cca696e0cde73eba4e9b792136f661cf05ec18bc7c24b810'>
We can check the balance of Alice and Bob as shown below.
>>> token.balanceOf(alice) 90000 >>> token.balanceOf(bob) 10000
On MetaMask, we can also see that Alice has 900 MIK and Bob has 100 MIK.
In the smart contract, the balances
state variable would contain the following entries.
balances
Address | Value |
Alice | 90000 |
Bob | 10000 |
TransferFrom workflow
Alice can delegate transfer operations to Charlie. Charlie can be an externally owned account (human user) or a smart contract address, such as an exchange. Letās see how it works.
First, Alice needs to execute the approve()
function to grant the allowance for Charlie, as shown below.
>>> token.approve(charlie, 10000, {'from': alice}) Transaction sent: 0xb045165c372ba1f53145933f866bd9f67b434c4be166f410d5cdef12c3b2e56b Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 2 Token.approve confirmed Block: 3 Gas used: 43683 (0.36%) <Transaction '0xb045165c372ba1f53145933f866bd9f67b434c4be166f410d5cdef12c3b2e56b'>
The smart contract’s allowed
state variable would now contain the following entry.
allowed
Owner address | Spender address | Value |
Alice | Charlie | 10000 |
Now, Charlie can transfer up to 100 MIK from Aliceās balance, as shown below. Note that the from
field is Charlie because Charlie is now executing the function.
>>> token.transferFrom(alice, bob, 10000, {'from': charlie}) Transaction sent: 0x8eab4ad0560073187a0c9e346f901bc313134195b2577b206d01fb038b3d0f37 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0 Token.transferFrom confirmed Block: 4 Gas used: 29180 (0.24%) <Transaction '0x8eab4ad0560073187a0c9e346f901bc313134195b2577b206d01fb038b3d0f37'>
We can check Aliceās and Bobās balances.
>>> token.balanceOf(alice) 80000 >>> token.balanceOf(bob) 20000
We can also check the balances on MetaMask.
In the smart contract, the state variables would contain the following entries.
balances
Address | Value |
Alice | 80000 |
Bob | 20000 |
allowed
Owner address | Spender address | Value |
Alice | Charlie | 0 |
Run unit test
Brownie Token Mix comes with the following test files in the tests
directory.
They are useful because we can see how the transfer()
, transferFrom()
and approve()
functions should work, especially in edge cases.
To run the tests for Mikio Token, we first need to update the token pytest fixture
in line 15 in the file tests/conftest.py
, as shown below.
@pytest.fixture(scope="module") def token(Token, accounts): return Token.deploy("Mikio Token", "MIK", 2, 100000, {'from': accounts[0]})
Then, we can run the brownie test command from the terminal.
[~/erc20_test/token]$ brownie test Brownie v1.17.1 - Python development framework for Ethereum ========================================================== test session starts =========================================================== platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 rootdir: /Users/mikio/erc20_test/token plugins: eth-brownie-1.17.1, web3-5.24.0, hypothesis-6.24.0, xdist-1.34.0, forked-1.3.0 collected 38 items Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'... tests/test_approve.py ............ [ 31%] tests/test_transfer.py ......... [ 55%] tests/test_transferFrom.py ................. [100%] ========================================================== 38 passed in 10.42s =========================================================== Terminating local RPC client...
Summary
In this article, we looked at the basic functionality of an ERC-20 token by implementing a token using Brownie Token Mix.
An ERC-20 token is a smart contract on Ethereum that implements the methods and events specified in the ERC-20 standard. It is designed to be used as a fungible token, meaning each unit has the same value as another unit of the same token.
The main part of an ERC-20 token is the balances state variable, and it has a map data structure containing addresses and their respective holdings of the ERC-20 token. Unlike ether, ERC-20 token balances are stored not in accounts but this state variable in the smart contract.
There are two workflows to transfer tokens. The spender can execute the transfer()
function to transfer tokens to another address directly. Alternatively, the spender can delegate transfer operations to a third party. In this case, the spender needs to execute the approve()
function to grant the allowance for the third party. Then, the third party can run the transferFrom()
function to make the transfer on behalf of the spender.
I hope this overview has been helpful to understand how ERC-20 tokens work.
You can find more in the following resources.
- EIP-20: Token Standard
- ERC-20 TOKEN STANDARD
- Etherscan Token Tracker
- Brownie Token Mix
- Mastering Ethereum – Tokens
Learn Solidity Course
Solidity is the programming language of the future.
It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.
In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.
NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.
This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.