# ERC20
ERC20 (opens new window) is a standard interface for Tokens within Ethereum. It means that there are many different implementations of "Tokens", and, as as long as they follow the standard interface, called ERC20, they all can seamlessly be plug-and-play to the smart contracts built for ERC20. This encourages not only openness and composability of the Ethereum network but also enables reuse of the code and infrastructure.
What do we mean by "Token" (opens new window)? Simply put, a Token represents a quantity of virtually anything: a lottery ticket, one USD, a share of a public company, etc (ref). It is often called "fungible" as each Token is considered identical, like two interchangeable dimes in a physical world. As long as a contract implements the ERC20 APIs (opens new window), it is considered a Token.
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
Before we walk through these APIs (and implement them!), let's talk about some popular uses (opens new window) of this token standard. Two common uses of them are (1) to have own coins like "stars" at Startbucks or "rewards" in Amazon. They mostly represent some quantifiable value in each system, which are guaranteed by them. For example, USDT (opens new window) and wBTC (opens new window) are used to present underlying assets, namely, dollar and Bitcoin. And LINK (opens new window) is used to pay node operators of Chainlink. It is worth noting that Tokens in Ethereum are relatively easy to reuse across platforms unlike "coins" in physical worlds which are hard to interchange without exchanging to fiat currency: we cannot use starbucks "stars" to purchase goods on Amazon.
Another common use case is (2) to represent quantifiable ownership for governance, like shares of a publicly traded company. For example, BLUNT (opens new window) is used to keep track of the ownership of marijuana businesses, and UNI (opens new window) to present the ownership of their liquidity pool. Sometimes, one coin is used to present both the ownership and the value, like ZRX (opens new window) as a medium for the trading fee and for governance of their own smart contract.
Let's see ERC20 (opens new window) is indeed expressive enough to accomplish whichever uses we are seeing in the wild. We will use USDT (opens new window) as a running example and you can interact with it via Etherscan (opens new window).
// Tether USD
function name() public view returns (string)
// USDT
function symbol() public view returns (string)
// 6 (divide the value by 10^6 to get its user representation)
function decimals() public view returns (uint8)
// 32294334767299762 (#tokens issued so far)
function totalSupply() public view returns (uint256)
So far they are self-explanatory! You can check the balance (i.e., how many tokens an account hold) via,
// e.g., how many USDT Binance owns?
//
// balanceOf(0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503) = 1250000115495224
//
// e.g., or how many tokens accidently? or intentionally sent to "zero" address?
//
// balanceOf(0x0000000000000000000000000000000000000000) = 23059172608
//
function balanceOf(address _owner) public view returns (uint256 balance)
You can also transfer your own tokens to any account:
// e.g.,
// transfer(to, 252,310.012236*10^6); $253,066.94!
// https://etherscan.io/tx/0xd7357b4e7fdc46620f83802f12a5261a645d72cbe121d7e9551769da97301b85
function transfer(address _to, uint256 _value) public returns (bool success)
Note, we allow a transfer with zero value as it can be used to check the API before actually sending the real tokens, which are irreversible.
Task 1
It's time to write a simple ERC20 token that implements below APIs:
pragma solidity ^0.8.0;
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
contract ERC20 is ERC20 {
...
}
Please submit your ERC20!
> lab.submitERC20Simple(erc20.address)
function submitERC20Simple(IERC20 token) public {
address me = address(this);
uint256 initialSupply = 1000;
address you = msg.sender;
bool success = false;
bool result = false;
require(token.totalSupply() == initialSupply,
"total supply: returns the total amount of tokens");
require(token.balanceOf(me) == initialSupply - 1,
"I'd like to hold all the tokens - 1?");
require(token.balanceOf(you) == 1,
"one token for you!");
(success, result) = transfer(token, you, initialSupply*2);
require(!success, "I shouldn't be able to send more than what I am holding");
(success, result) = transfer(token, you, 0);
require(success && result, "sending zero");
(success, result) = transfer(token, you, 1);
require(success && result, "sending one");
(success, result) = transfer(token, you, 2);
require(success && result, "sending two");
require(token.balanceOf(you) == 4, "four tokens for you!");
require(token.balanceOf(me) == initialSupply - 4, "-4 tokens for me!");
completed1 = true;
}
Here comes some tricky parts,
but it really unleashes the power of ERC20:
approve()
, allowance()
and transferFrom()
.
It allows others to use your tokens:
by invoking approve(_spender, _value)
, you give
a permission to use _value_
by _spender_
.
With allowance(_owner, _spender)
,
one can check how much value __spender
is allowed to use on behalf of _owner
.
// Sets `amount` as the allowance of `spender` over the caller's tokens.
function approve(address _spender, uint256 _value) public returns (bool success)
// Returns the remaining number of tokens that `spender` will be
// allowed to spend on behalf of `owner` through {transferFrom}. This is
// zero by default.
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
Task 2
Let's implement allowance()
and approve()
. Read carefully the testing code provided below!
pragma solidity ^0.8.0;
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
/* new */
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}
contract ERC20 is ERC20 {
...
}
Please submit your ERC20!
> lab.submitERC20Allowance(erc20.address)
function submitERC20Allowance(IERC20 token) public {
address me = address(this);
uint256 initialSupply = 1000;
address you = msg.sender;
bool success = false;
bool result = false;
require(token.allowance(you, me) == initialSupply,
"let me use all of your tokens!");
require(token.allowance(me, you) == 0,
"no allowance to you!");
token.approve(you, 0);
require(token.allowance(me, you) == 0,
"no allowance to you yet!");
token.approve(you, 10);
require(token.allowance(me, you) == 10,
"I only let you to use 10");
token.approve(you, 11);
require(token.allowance(me, you) == 11,
"I only let you to use 11");
completed2 = true;
}
Lastly, once approved for spending,
you can invoke transferFrom()
to actually transfer the tokens.
This allows a withdraw workflow
that allows either EOA or smart contracts to transfer tokens
on behalf you!
For example, instead of sending the tokens to a smart contract,
you first approve the smart contract to spend
and let it spend the allowance
following the code/policy written in the smart contract.
// If you (i.e., `msg.sender`) is allowed to spend tokens of `_from_`,
// this transaction will transfer `_value_` to `_to_`
// and decreases the allowance of `msg.sender` to `_from_`.
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
Task 3
It's time to complete the implementation of a ERC20 token!
pragma solidity ^0.8.0;
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
/* new */
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
}
contract ERC20 is ERC20 {
...
}
Please submit your ERC20!
> lab.submitERC20(erc20.address)
function submitERC20(IERC20 token) public {
uint initialSupply = 100000000;
address initialHolder = msg.sender;
bool success = false;
bool result = false;
address me = address(this);
require(token.totalSupply() == initialSupply,
"total supply: returns the total amount of tokens");
require(token.balanceOf(me) == 0,
"when the requested account has no tokens, returns zero");
require(token.balanceOf(initialHolder) != 0,
"when the requested account has some tokens, returns the total amount of tokens");
require(token.allowance(initialHolder, me) == initialSupply,
"please allow me to spend all the coins for testing");
require(token.allowance(initialHolder, initialHolder) == 0,
"no allowance from initialSupply to initialSupply?");
require(token.allowance(me, me) == 0,
"no allowance by myself");
(success, result) = transferFrom(token, initialHolder, address(0), initialSupply);
require(!success,
"should not be possible to send to address(0)");
(success, result) = transferFrom(token, initialHolder, address(0), initialSupply + 1);
require(!success,
"should not be possible to send more than what is hold");
(success, result) = transferFrom(token, initialHolder, me, 0);
require(success && result,
"failed to transfer zero token to me");
require(token.transferFrom(initialHolder, me, initialSupply) == true,
"failed to transfer all the supply to me");
require(token.balanceOf(initialHolder) == 0,
"initialHolder still holds some");
require(token.balanceOf(me) == initialSupply,
"initialSupply is not transferred");
(success, result) = transfer(token, me, initialSupply);
require(success,
"should be able to transfer a token to myself? no point though");
(success, result) = transfer(token, initialHolder, initialSupply + 1);
require(!success,
"not allowed to transfer more than what I have");
(success, result) = transfer(token, address(0), 1);
require(!success,
"should not allow to transfer a token to address(0)");
require(token.transfer(initialHolder, 1) == true,
"failed to transfer to initialHolder");
require(token.balanceOf(initialHolder) == 1,
"initialHolder doesn't have 1 token");
require(token.balanceOf(me) == initialSupply - 1,
"my balance is not deducted");
token.approve(initialHolder, initialSupply + 1);
require(token.allowance(me, initialHolder)== initialSupply + 1,
"should be able to approve more than what I have");
token.approve(initialHolder, 0);
require(token.allowance(me, initialHolder)== 0,
"can't change the allowance (should not be accumulating)");
token.approve(initialHolder, MAX_UINT);
require(token.allowance(me, initialHolder)== MAX_UINT,
"can't change the allowance (should not be accumulating)");
uint256 balance = token.balanceOf(me);
token.approve(me, balance);
require(token.allowance(me, me) == balance,
"allow myself to use all");
token.transfer(me, balance);
require(token.balanceOf(me) == balance,
"should not be changed!");
completed3 = true;
}
Once you are done, please submit your instance!
TIP
ERC20 looks very simple! yet when it comes to security,
it has two known attack vectors. The first one
is a potential race condition in approve()
and
transferFrom()
as noted in detail
here (opens new window).
Simply put, one can abuse two approve()
calls
in a way to get the permission of using the sum of two approves
rather than their max allowance.
The second attack is called
"Short Address Attack" (opens new window),
which is caused by a combination of the incorrect encoding of a short address
and the default behavior of filling the zero values on memory loads in EVM.
It was indeed a serious concern as raised by
TKN (opens new window),
and you can see some of Tokens addressing this attack
with the onlyPayloadSize(uint size)
modifier
like in USDT (opens new window).
It simply prohibited the smaller-than-expected encoding of the expected arguments,
and luckily, the latest solidity is handling this issue in the language level.
← Motorbike Token Whale →