Implementing ERC-20 in Solidity from scratch

Introduction
Today, the world of DeFi (Decentralized Finance) relies heavily on the ERC-20 (Ethereum Request for Comments 20) standard. This standard defines the essential rules for the creation and interaction of tokens on the Ethereum blockchain, making their use fluid and interoperable.
In this article, we'll explain in detail what the ERC-20 standard is, why it's crucial in the Ethereum ecosystem, and implement its key methods to better understand how it works.
What are tokens and DeFi ?
Before discussing the ERC-20 standard, it's essential to understand two fundamental concepts: tokens and DeFi.
A token, on the Ethereum blockchain, is simply a representation of a digital asset. It can be a currency, an asset or a digital right. Among them are fungible tokens, which follow the ERC-20 standard and function as interchangeable units, just like traditional currencies.
DeFi (Decentralized Finance), meanwhile, is a financial ecosystem that follows the principles of traditional banking systems, but with one major difference: it is decentralized and based on blockchains such as Ethereum. In this environment, transactions are carried out without intermediaries, using cryptocurrencies and tokens for various financial operations (loans, exchanges, investments, etc.).
What is the ERC-20?
As mentioned above, ERC-20 is an Ethereum blockchain standard, introduced in 2015 to solve a major problem: interoperability between tokens. Today, it has become a central element of the Ethereum ecosystem, enabling the creation of fungible tokens used in DeFi, ICOs and other blockchain applications. Structure of the ERC-20 standard
ERC-20 is first and foremost a smart contract that defines a set of methods and state variables essential to the proper functioning of tokens.
Main methods :
transfer
: Transfers tokens from one user to another.transferFrom
: Transfers tokens from one account to another via an intermediary.mint
: Generates new tokens and adds them to the total offer.approve
: Authorizes an address to spend a specific quantity of tokens on behalf of the owner.allowance
: Checks how many tokens a user can still spend on behalf of another.balanceOf
: Returns an address's token balance.
Stats variables :
name
: Token name (e.g. WassToken).symbol
: Token symbol (e.g. WTK, ETH, USDT).totalSupply
: Total number of tokens in circulation.decimals
: Number of decimals used by the token.balances
: Stores user account balances.allowances
: Manages spending authorizations between users.owner
: Address of the owner of the contract or token.
To implement the ERC20, we need to use a programming language that the EVM (Ethereum Virtual Machine) can understand.
Implementing the ERC-20 with Solidity
Variables declaration :
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract ERC20 {
string public name;
string public symbol;
uint256 public totalSupply;
uint8 public decimals;
address public owner;
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// block of code ............... ////////
}
In the code below, we've just declared the variables we'll need. Note that the variables name, symbol, decimals and owner are defined when the contract is deployed and can't be modified, but totalSupply can be updated via functions like mint() and burn().
Variables definition in the constructor:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract ERC20 {
// variable declaration //
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _initialSupply
) {
owner = msg.sender;
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _initialSupply * 10** uint256(decimals);
balances[owner] += totalSupply;
}
// function implementation //
}
Code explanation
pragma solidity ^0.8.28;
Specifies that the contract uses Solidity 0.8.28, which includes protections against integer overflow.
contract ERC20 {
// code block
}
Declare an ERC-20 contract that will define a token compliant with this standard.
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _initialSupply
)
The constructor is a special function that is executed only once when the contract is deployed. It takes four parameters:
name
: Token name (e.g. “MyToken”)symbol
: token symbol (e.g. “MTK”)decimals
: number of decimal places (e.g. 18)initialSupply
: Initial token offer.
// Assigns the address deploying the contract (msg.sender) as token owner.
owner = msg.sender;
// Stores the values supplied in the contract variables.
name = _name;
symbol = _symbol;
decimals = _decimals;
// The total quantity of tokens is multiplied by 10^decimals.
// This ensures that the token can be used with the correct precision (e.g. 18 decimals as ETH).
totalSupply = _initialSupply * 10** uint256(decimals);
// Allocation of the Total Offer to the Owner
balances[owner] += totalSupply;
//Adds the total offer to the owner's balance.
Main methods Implementation:
1- mint function:
function mint(address _account, uint256 _amount) external onlyOwner {
if (_account == address(0)) revert InvalidAddress();
balances[_account] += _amount;
totalSupply += _amount;
emit Transfer(owner, _account, _amount);
}
Eplanation:
The mint function allows us to create tokens to give to a given address
the function takes 2 parameters and is external view that mains it can only be call outside of the contract.
_account
: the address which we want to create token for_amount
: the amount of token we want to create for _accountonlyOwner
: is a modifier that allows us to make sure that the function is calling by the owner of the token.if (_account == address(0)) revert InvalidAddress()
: the sanity check, to make sure that we’re not creating tokens to anaddress(0)
balances[_account] += _amount
: this line increase the balance of_account
by_amount
totalSupply += _amount
: this increase the totalSupply by _amount
2- transfer function:
function transfer(
address _to,
uint256 _amount
) external returns (bool success) {
if (_to == address(0)) revert InvalidAddress();
if (balances[msg.sender] < _amount) revert InsufficentAmount();
balances[_to] += _amount;
balances[msg.sender] -= _amount;
emit Transfer(msg.sender, _to, _amount);
success = true;
}
Explanation:
Check to: Prevents tokens being sent to 0x0 (invalid address).
Balance check: Cancels transaction if sender has insufficient tokens.
Balance update: Deducts amount from msg.sender and adds it to _to.
Send Transfer event. Returns true to indicate success.
3- approve function
function approve(
address _spender,
uint256 _amount
) external returns (bool success) {
if (_spender == address(0)) revert InvalidAddress();
if (balances[msg.sender] < _amount) revert InsufficentBalance();
allowances[msg.sender][_spender] += _amount;
emit Approval(msg.sender, _spender, _amount);
success = true;
}
Explanation:
The approve function allows a user to authorize another address (_spender) to spend a certain amount of tokens on his behalf.
The function takes 2 parameters and is external, meaning it can only be called from outside the contract. spender : the address authorized to spend the tokens. amount: the amount of tokens _spender is authorized to spend.
if (_spender == address(0)) revert InvalidAddress(): Checks that spender is not a null address (0x0).
if (balances[msg.sender] < amount) revert InsufficentBalance(): Checks that the user has enough tokens before approving the allocation.
allowances[msg.sender][_spender] += amount: Add amount to the already existing allocation for spender.
emit Approval(msg.sender, spender, _amount): Emits an Approval event, allowing external applications and contracts to track this approval.
success = true: Indicates that the transaction was successful.
4- balanceOf function
function balanceOf(
address _account
) external view returns (uint256 balance) {
if (_account == address(0)) revert InvalidAddress();
balance = balances[_account];
}
Explanation:
The balanceOf function is used to obtain the balance of a given account.
The function takes one parameter and is external view, meaning it can only be called from outside the contract and does not modify the contract state. _account: the address whose balance you wish to know.
if (_account == address(0)) revert InvalidAddress(): Checks that account is not a null address (0x0) to avoid invalid requests.
balance = balances[account]: Retrieves and returns the balance of _account from the balances mapping.
5- transferFrom function
function transferFrom(
address _from,
address _to,
uint256 _amount
) external returns (bool success) {
if (_from == address(0)) revert InvalidAddress();
if (_to == address(0)) revert InvalidAddress();
if (_amount <= allowances[_from][msg.sender]) {
balances[_to] += _amount;
balances[_from] -= _amount;
allowances[_from][msg.sender] -= _amount;
emit Transfer(_from, _to, _amount);
success = true;
} else {
revert InsufficentAllowance();
}
}
Explanation: The transferFrom function allows a third party to send tokens from a from account to another to account, provided that it has received prior authorization via approve.
The function takes three parameters and is external, meaning it can only be called from outside the contract.
from : the address of the token holder.
to : the recipient's address.
_amount : quantity of tokens to be transferred.
Implementation details :
if (_from == address(0)) revert InvalidAddress(): Checks that the sender's address is not null (0x0).
if (_to == address(0)) revert InvalidAddress(): Checks that the recipient address is not null (0x0).
if (_amount <= allowances[_from][msg.sender]): Checks that the sender (msg.sender) has sufficient allowance to transfer amount from from.
balances[_to] += amount: Adds amount to the recipient's balances to.
balances[from] -= amount: Deducts amount from the balance of from.
allowances[from][msg.sender] -= amount: Reduces the allowance allowed to msg.sender by from.
emit Transfer(_from, to, amount): Emits a Transfer event to notify the transfer.
success = true: Returns true if the transfer was successful. else { revert InsufficentAllowance(); } If the allocation is insufficient, the transaction fails with an InsufficentAllowance error.
6- allowance function
function allowance(
address _owner,
address _spender
) public view returns (uint256 remaining) {
if (_owner == address(0)) revert InvalidAddress();
if (_spender == address(0)) revert InvalidAddress();
remaining = allowances[_owner][_spender];
}
Explanation:
The allowance function allows us to check the remaining tokens that an owner has authorized a spender to use.
The function takes 2 parameters and is external view, meaning it can only be called outside of the contract and doesn't modify the state.
owner: the address of the token owner who authorizes another address (the spender) to use a portion of their tokens.
spender: the address that is allowed to spend tokens on behalf of the owner.
The allowance function includes sanity checks:
if (_owner == address(0)) revert InvalidAddress(); ensures the owner address is not the null address.
if (_spender == address(0)) revert InvalidAddress(); ensures the spender address is not the null address.
Finally, the line remaining = allowances[_owner][_spender]; returns the amount of tokens that the owner has allowed the spender to use.
Conclusion
Thanks for reading. You can find the full code here: https://github.com/WassCodeur/assigmnt-ERC20
Subscribe to my newsletter
Read articles from Wasiu Ibrahim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Wasiu Ibrahim
Wasiu Ibrahim
I'm a passionate software developer(backend), technical writer and community facilitator, committed to strengthening the links between developers and emerging technologies. My background has also led me to actively contribute to open source projects, where I find great satisfaction in collaborating with other technology enthusiasts to create solutions accessible to all. Join me on this adventure where we can not only develop innovative software, but also nurture a dynamic community and participate in the evolution of open source for a more inclusive and collaborative digital future