Everything you need to know about the ERC20 token standard

Everything you need to know about the ERC20 token standard

A guide to understanding every detail of the ERC-20 contract.

What is a token?

A token is a type of digital asset that can be used for numerous purposes like tickets to social events, payment systems, etc. They can basically represent anything and everything. Tokens may be used as proof of ownership for digital collectibles or even physical world items. They are also used to raise funds from the community.

Tokens are to crypto, what shares are to the stock market💸

Tokens are cryptocurrencies that are built on top of another blockchain (they are not native assets of the blockchain like ETH is to Ethereum and SOL is to Solana)

While every blockchain follows different rules and regulations to create and manage tokens, ERC20 is a standard used by Ethereum-based blockchains to fulfill this purpose.

The ERC-20 standard

ERC stands for Ethereum Request for Comment. The Ethereum-based tokens created using this standard are fungible tokens meaning that each token when replaced with another of its kind holds the same value in nature as the previous one. For example, if you replace a $1 note with another $1, it still values at $1. The same applies to cryptocurrencies like Bitcoin and Ethereum. (1 BTC will always be equal to 1 BTC) In simple words, ERC-20 tokens are interchangeable.

A token contract should have these 5 basic functionalities:

  1. Transfer - transfer tokens from one address to another
  2. Mint - create new tokens to increase the supply
  3. Burn - remove the tokens from supply (sending them to address(0))
  4. Approve - allow an address to take control of owner's tokens
  5. Transferfrom - transfer tokens from some address to another, only an address approved to do so by the owner of tokens can perform this operation

Popularly, ERC-20 tokens are created using Openzeppelin's ERC-20 contract which can be found here.

It takes only 5 lines of code to actually create ERC20 tokens but the logic behind the same is often unknown to many Solidity developers (which btw is a very big no-no).

So moving on to understanding the code of the ERC-20 contract which we inherit for our custom token contract. LFG.🚀

A contract binds together variables and functions.

State variables and mappings:

The statement uint public _totalSupply; indicates a variable name _totalSupply with data type uint(unsigned integer) is created to maintain a count of the total tokens that are created and are currently in supply

mapping(address => uint) public _balances; maps address to a uint number which keeps a track of the total tokens a particular address holds

mapping(address => mapping(address => uint)) public _allowances; is a nested mapping and is used to approve a particular address to spend some amount of tokens on behalf of the sender (delegate control) here the first address is the one whose control of tokens is delegated to the second address. uint holds the number of tokens that a particular address is allowed to spend

string private _name; stores the name of the token you will create like Aptos

string private _symbol; stores the symbol for the token like APT

the constructor of the contract will assign these values - _name and _symbol

 constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

Functions

1.transfer

Function transfer is a virtual function that returns a boolean and takes two arguments

a. to - the recipient to whom the tokens are to be transferred

b. amount - how many tokens are to be transferred to the recipient

function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

transfer function then calls another internal function _transfer (with an additional argument of owner that stores the sender address) which executes the main logic of transferring tokens from one address to another

function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

Here, it is checked if both addresses are valid with the require conditions then an optional function _beforeTokenTransfer(from, to, amount); can be called if there are some steps to be executed before the token transfer

uint256 fromBalance = _balances[from]; gets the sender's balance from the mapping _balances

require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); is used to check if the sender has enough balance for the specified amount to be transferred if the condition fromBalance >= amount is not satisfied then the transaction reverts with message 'ERC20: transfer amount exceeds balance'

If everything goes well, we deduct the balance of sender and increase the balance of the receiver with _balances[from] = fromBalance - amount; & _balances[to] += amount;

Finally an event emit Transfer(from, to, amount); is emitted indicating successful transfer of function, and an optional function _afterTokenTransfer(from, to, amount); can be executed if required.

2._mint

This function takes an address account to whom the tokens will be transferred after minting and amount i.e. the number of tokens to be minted

function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

Here too, the address is checked to be a valid one before any other transaction is carried out.

_totalSupply += amount; increases the total supply of tokens by amount

_balances[account] += amount; increases the balance of the account whose address was given with the _mint function by the number of tokens minted and finally emits a Transfer event

3._burn

This function has almost the same steps as getting the balance of the account in the _mint function except, instead of creating new tokens it destroys the already created ones and removes them from circulation decreasing the total supply.

 function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

uint256 accountBalance = _balances[account]; get balance of account who is willing to destroy their tokens

require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); checks if there is enough balance

_balances[account] = accountBalance - amount; subtracts the balance by the amount of tokens to be burnt and then reduces the total supply with _totalSupply -= amount;

Finally, a Transfer event is emitted. Note that the account that receives the token is address(0) which means they are sent to an address that cannot be operated.

4.approve

This function approves a particular address spender to spend amount tokens on behalf of the owner - sender of this message

function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

It calls another internal function that is _approve(owner, spender, amount);

function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

The changes are made in the _allowances mapping with _allowances[owner][spender] = amount;

This basically means 'approve the spender to spend amount number of tokens on behalf of the owner' and typically the mapping structure is somewhat like this owner ---> (spender ---> amount)

An event is emitted with emit Approval(owner, spender, amount); which indicates successful approval

5.transferFrom

This function has 3 arguments, from - owner address (owner may not be the message's sender) to - receiver address amount - number of tokens to be transferred

function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

It eventually calls another internal function _spendAllowance(from, spender, amount); which has the main logic to check how much amount of tokens can the approved addresses transfer

_transfer(from, to, amount); uses the logic as explained above to transfer tokens from one address to another

function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

function allowance(owner, spender)

function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

calculates how many tokens owned by the owner are allowed to be controlled by the spender and assigns it to currentAllowance

The_spendAllowance function then checks if currentAllowance that is the number of tokens approved to be controlled by the spender is greater than or equal to the amount of tokens to be transferred

Then _approve(owner, spender, currentAllowance - amount) is called to set the new approval limit and executes as explained above

Here new approval limit will be calculated by subtracting the tokens that will be transferred now(amount) from the total number of tokens allowed(currentAllowance)

After the _spendAllowance function in the transferFrom function is rightfully executed a call to _transfer(from, to, amount); is made which executes the transfer of tokens from one address to another.

Additions

The functions increaseAllowance and decreaseAllowance increase or decrease the number of tokens that can be controlled by the spender on behalf of the owner.

Finally, let us write a contract to deploy our own token on the blockchain

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

contract BlogToken is ERC20{
    constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol){
        _mint(msg.sender, 10000 * 10**18);
    }
}

Note: This is a small contract to show a demo of how our own 10000 tokens can be deployed to the blockchain using _mint and the contract to professionally deploy a token will have a lot more functionality like approve, transferFrom, and others.