Welcome back... ohh wait this is the first blog .In today's article, we cover in more detail one of the most important ERC in the smart contract development
We will see how the ERC20 the contract works, how to write and deploy our every own ERC20 Token. We will also use some contracts from OpenZeppelin to learn how the ERC20 token implementation work in practice while learning the Solidity code behind these popular contracts along the way.
Introduction
Anatomy of an ERC-20 Token
Deep dive into Openzepplin Implementation
Conclusion
Have ever wondered why every website you have evere visited uses HTTP and HTTPS ? This is because of something called RFCs , or "Requests for Comments," which are the standards that define how the internet operates. RFCs provide the foundational rules that enable different systems to communicate seamlessly, ensuring a consistent experience across the web. Similarly, in the world of blockchain, particularly within the Ethereum ecosystem, we have Ethereum Improvement Proposal (EIPs) and Ethereum Request for Comments (ERCs).
EIPs are proposals that outline new features or standards for Ethereum, guiding its ongoing development and improvement. Among these EIPs, certain proposals evolve into ERCs when they define specific technical standards for smart contracts and tokens, much like how certain RFCs become the backbone of internet protocols.
One of the most prominent and widely adopted ERCs is ERC-20. This standard governs the creation and behavior of fungible tokens on Ethereum, establishing a set of rules that all ERC-20 tokens must follow. Just as HTTP/HTTPS ensures that all web browsers and servers can communicate efficiently, ERC-20 ensures that all tokens within its framework are interoperable across various platforms, wallets, and decentralized applications (DApps). Whether it's trading on decentralized exchanges or integrating with DeFi platforms, ERC-20 provides the necessary standardization that enables the smooth functioning of the Ethereum token ecosystem. In this article, we will delve into the ERC-20 specification, exploring its key functions, events, and how it can be implemented in smart contracts.
Before we start we need to get something right , the difference between EIP20 and ERC20. EIP-20 and ERC-20 refer to the same standard for fungible tokens on Ethereum "EIP-20" is the official Ethereum Improvement Proposal defining the standard, while "ERC-20" (Ethereum Request for Comments 20) is the common term used to describe tokens that follow this standard. Both terms are often used interchangeably.
An ERC-20 token according to the EIP-20 consists of a smart contract that implements the standardized interface, which comprises a set of six mandatory functions:
totalSupply(): Returns the total supply of the token.
balanceOf(address): Provides the balance of tokens held by a specific address.
transfer(address, uint256): Transfers a specified amount of tokens from the sender's address to the specified recipient's address.
transferFrom(address, address, uint256): Enables a third party to transfer tokens on behalf of the token owner, given that the owner has approved the transaction.
approve(address, uint256): Allows the token owner to grant permission to a third party to spend a specified amount of tokens on their behalf.
allowance(address, address): Returns the amount of tokens the token owner has allowed a third party to spend on their behalf.
Additionally, ERC-20 tokens can include optional functions that provide descriptive information about the token:
name(): Returns the name of the token, for example, "WEB3BRIDGE"
symbol(): Provides the token's symbol, like "WEB3BD"
decimals(): Indicates the number of decimal places the token can be divided into, typically 18 for most tokens.
Enough of the boring stuff let's get into the code ): . This of the article assumes you are familiar with Solidity . We will we explaining every block of code in the Openzepplin ERC.sol contract with you can find here and how it relates to the original EIP-20 specification.
In the code there are three types of functions (View, Internal and Public). But before we go over the functions let's check out the state variable and imports
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
import {IERC20} from "./IERC20.sol"; : Imports the ERC20 interface defining the core functions for token transfers, allowances, and balance queries, which all ERC20 tokens must implement.
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol"; : Imports the ERC20Metadata interface that extends the basic ERC20 interface by adding functions for querying token name, symbol, and decimals.
import {Context} from "../../utils/Context.sol"; : Imports the Context contract, which provides information about the current execution context, such as the sender of the transaction, useful for implementing access control.
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol"; : Imports the IERC20Errors interface, which defines common error codes for ERC20 operations, ensuring standardized error handling.
The state variable
// to track individual account and the ammount of token own by them
mapping(address account => uint256) private _balances;
// this is a 2d mapping i will explain soon
mapping(address account => mapping(address spender => uint256))
private _allowances;
// to store the total supply
uint256 private _totalSupply;
// to store the token name
string private _name;
// to store the token symbol
string private _symbol;
The constructor
of a smart contract runs once, during deployment, and allows the deployer to define custom data for the initial contract state. For ERC20.sol
, the constructor takes 2 parameters to determine the name and the symbol of the token .
// the constructor intailize the state variable _name and _symbol
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
Getter functions are public view functions in the contract that allow users to read the state (state variables )of a smart contract. For ERC20.sol
, we can see the following getter functions.
// this function returns the name of erc20 token
function name() public view virtual returns (string memory) {
return _name;
}
// this function returns the number of decimal places used by the ERC20 token.
// Decimal places determine the smallest unit of the token that can be represented and transacted.
// For example, if a token has 18 decimals,
//it can be divided into 10^18 (1,000,000,000,000,000,000) smaller units.
// This is important for precision in financial calculations and
//token transfers, allowing for very fine-grained transactions.
// The standard setting for most ERC20 tokens is 18 decimals,
//which aligns with Ethereum's own precision for Ether (ETH) to
//wei since 1 ETHER is 10^18 wei.
function decimals() public view virtual returns (uint8) {
return 18;
}
// this function returns the symbol of the ERC20 token.
function symbol() public view virtual returns (string memory) {
return _symbol;
}
// this function returns the total supply of the ERC20 token.
// the total supply is the total amount of tokens that exist in circulation.
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
// remember the state variable the mapping of a the balances to owner.
// this function returns the balance of tokens held by a specific account.
// it provides the amount of tokens that the account address has in its balance.
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
// it remain the the allowances view function more about that later
Before going further into the code we need to understand some concept such transfer,approval and allowance
The ERC-20 token contract plays a crucial role in managing user balances and facilitating token transfers. Think of it as a digital ledger that tracks ownership and transactions of tokens.
Token Balances
An ERC-20 contract must keep track of the balance of each user, which can be read by calling the balanceOf(address)
view function. This function takes in an address and returns the amount of tokens owned by that address.
In many ERC-20 implementations, the entire initial supply of tokens is typically assigned to the contract creator when the contract is deployed. And the initial supply of token can be set by a process called mint
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
// will explain this function soon
_update(address(0), account, value);
}
ERC-20 tokens can be transferred between users in two primary ways. The first method involves the sender directly instructing the ERC-20 contract to transfer tokens to a recipient by calling the transfer(address _to, uint256 _amount)
function.
Function: transfer(address _to, uint256 _amount) → bool
Description: Transfers _amount
of tokens from the sender (msg.sender
) to the recipient (_to
). Returns true
if the transfer is successful, otherwise the transaction reverts.
The transfer
function deducts the specified amount from the sender's balance and credits it to the recipient's balance. If the sender does not have enough tokens, the function will revert the transaction. In most ERC-20 implementations, if the transfer is valid, it will always return true
.
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
// this is a helper function to help user check if they are not
//sending to a zero address like my great mentor with it help perform sanity check
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
// This is way the main logic is implemented
// the function to update balances and total supply during token transfers or minting/burning.
// If `from` is the zero address, tokens are being minted, so the total supply is increased by `value`.
// - If `to` is the zero address, tokens are being burned, so the total supply is decreased by `value`.
// Otherwise, tokens are being transferred: the balance of `from` is decreased by `value`
//(reverting if insufficient), and the balance of `to` is increased by `value`.
// The function ensures that overflows are not possible and that total supply remains consistent.
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
The second method for transferring tokens involves a more indirect process where:
The token owner first approves another address (referred to as the "spender") to spend a certain amount of tokens on their behalf.
The approved spender then initiates a transfer from the owner's address to a recipient.
This method is particularly useful in scenarios where smart contracts, such as decentralized exchanges, need to manage tokens on behalf of users. The ERC-20 standard includes the approve
and transferFrom
functions to facilitate this form of transfer. The amount that a spender is authorized to transfer can be checked using the allowance
view function.
Function: allowance(address _owner, address _spender) → uint256
Description: Returns the remaining number of tokens that _spender
is allowed to transfer on behalf of _owner
.
Function: approve(address _spender, uint256 _amount) → bool
Description: Approves _spender
to transfer up to _amount
of tokens from the sender's (msg.sender
) account. Returns true
if the approval is successful.
Function: transferFrom(address _from, address _to, uint256 _amount) → bool
Description: Transfers _amount
of tokens from _from
to _to
, using the allowance mechanism. The caller (msg.sender
) must have prior approval from _from
. Returns true
if the transfer is successful, otherwise the transaction reverts.
The transferFrom
function will decrease the spender's allowance by the amount transferred. If there are insufficient funds or the allowance is too low, the transaction will revert. As with direct transfers, the function generally either returns true
or reverts if it encounters an error.
/**
* This function handles token transfers on behalf of a third party.
* When a user (the "spender") wants to transfer tokens from someone else's account
* (the "from" address), this function is used. First, it ensures the spender is
* allowed to move the specified amount of tokens by checking their allowance.
* If everything checks out, the function proceeds to transfer the tokens
* from the "from" address to the "to" address. Finally, it returns `true`
* to signal that the transfer was successful.
*/
function transferFrom(
address from,
address to,
uint256 value
) public virtual returns (bool) {
address spender = _msgSender(); // Identify who is trying to spend the tokens.
_spendAllowance(from, spender, value); // Make sure the spender has permission to spend this amount.
_transfer(from, to, value); // Perform the actual transfer of tokens.
return true; // Confirm that the transfer was successful.
}
/**
* Before a spender can transfer tokens on someone else’s behalf, we need to make sure
* they have the right to do so. This function checks the current allowance (the maximum
* amount the spender is allowed to transfer) and then reduces it by the amount being transferred.
* If the spender tries to transfer more than their allowance, the function will stop the transaction.
* If the allowance is valid, it gets adjusted downwards to reflect the new balance.
*/
function _spendAllowance(
address owner,
address spender,
uint256 value
) internal virtual {
uint256 currentAllowance = allowance(owner, spender); // Check the current allowance.
if (currentAllowance != type(uint256).max) { // If allowance isn't unlimited, verify and adjust it.
if (currentAllowance < value) { // Stop the transaction if the allowance is too low.
revert ERC20InsufficientAllowance(
spender,
currentAllowance,
value
);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false); // Reduce the allowance by the amount spent.
}
}
}
/**
* The `_approve` function is the workhorse behind setting allowances.
* It updates the amount of tokens that a spender is allowed to use on behalf of the owner.
* If needed, it also triggers an event to notify the network of the new allowance,
* although in some cases (like internal adjustments) the event might not be necessary.
* This function ensures that everything stays in sync and that all transactions are transparent.
*/
function _approve(
address owner,
address spender,
uint256 value,
bool emitEvent
) internal virtual {
_allowances[owner][spender] = value; // Set the new allowance amount.
if (emitEvent) {
emit Approval(owner, spender, value); // Emit an event to log this action, if necessary.
}
}
I think with that we are done with the deep dive hope you are not drowning .
Here’s a simple example of a smart contract for a erc20 token:
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract DragonToken is ERC20 {
constructor(uint256 initialSupply) ERC20("WEB3BRIDGE", "WEB3BD") {
_mint(msg.sender, initialSupply);
}
}
In this article, we've taken a deep dive into the ERC-20 standard, one of the foundational elements of smart contract development on Ethereum. We've explored how ERC-20 contracts work, learned the key functions and mechanics behind them, and even looked at how to create one. By utilizing contracts from OpenZeppelin, we've not only seen how these tokens are implemented in practice but also gained insight into the Solidity code that powers these widely-used contracts. With this knowledge, you're now equipped to leverage the ERC-20 standard in your own projects, ensuring interoperability and efficiency in the ever-expanding world of decentralized finance.
see you next time ...... IAM0TI