How Does the ERC-20 Token Work?

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

AddressValue
Alice10
Bob5

When Alice sends 5 units to Bob, the balances variable is updated.

balances

AddressValue
Alice5
Bob10

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 addressSpender addressValue
AliceCharlie5

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 addressSpender addressValue
AliceCharlie0

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.

NameSymbolTotal SupplyDecimals
Mikio TokenMIK1000002

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

AddressValue
Alice100000

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

AddressValue
Alice90000
Bob10000

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 addressSpender addressValue
AliceCharlie10000

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

AddressValue
Alice80000
Bob20000

allowed

Owner addressSpender addressValue
AliceCharlie0

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.

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.