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:
- Transfer - transfer tokens from one address to another
- Mint - create new tokens to increase the supply
- Burn - remove the tokens from supply (sending them to
address(0)
) - Approve - allow an address to take control of owner's tokens
- 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.