A Beginner’s Guide to Understanding ERC-20 Tokens


What is an ERC-20 Token?
ERC20 stands for Ethereum Request for Comment 20, which is a technical standard used for creating and issuing smart contracts on the Ethereum blockchain. It defines a set of rules that all Ethereum-based tokens must follow, ensuring compatibility across the ecosystem. These rules include:
Transferring tokens from one account to another.
Approving and spending tokens on behalf of another account.
Querying the balance of an account.
Querying the total supply of the token.
By adhering to this standard, tokens can be easily integrated into wallets, exchanges, and other smart contracts.
Use Cases of ERC-20 Tokens
ERC-20 tokens power many applications in the blockchain ecosystem, including:
Cryptocurrencies: Stablecoins like USDT, USDC, and DAI are ERC-20 tokens.
DeFi (Decentralized Finance): Lending, staking, and yield farming platforms use ERC-20 tokens.
Gaming and NFTs: Some games use ERC-20 for in-game assets, while NFTs often rely on ERC-721 or ERC-1155.
DAOs (Decentralized Autonomous Organizations): Governance tokens allow voting in DAOs.
Popular dApps Using ERC-20 Tokens
Some well-known decentralized applications that use ERC-20 tokens include:
Uniswap (UNI): A decentralized exchange (DEX) for token swaps.
Aave (AAVE): A DeFi lending platform.
Chainlink (LINK): A decentralized oracle network.
Writing an ERC-20 Token in Solidity
Let's break down the code for an ERC-20 token contract.
Full Code for an ERC-20 Token
Here’s the MarToken smart contract written in Solidity:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract MarToken {
// state variables
string public name = "MarToken";
string public symbol = "MAR";
uint8 public decimals = 18;
uint256 public totalSupply;
// mapping for balances and allowances
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
// Custom errors (gas efficient)
error InsufficientBalance();
error AllowanceExceeded();
error InvalidAddress();
// events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// initialize total supply (constructor)
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * (10 ** uint256(decimals));
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
// Transfer function
function transfer(address _to, uint256 _value) public returns (bool success) {
if (_to == address(0)) revert InvalidAddress();
if (balanceOf[msg.sender] < _value) revert InsufficientBalance();
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
// Approve function (grants spending permission)
function approve(address _spender, uint256 _value) public returns (bool success) {
if (_spender == address(0)) revert InvalidAddress();
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
// TransferFrom function (allows spending on behalf of owner)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
if (_from == address(0) || _to == address(0)) revert InvalidAddress();
if (balanceOf[_from] < _value) revert InsufficientBalance();
if (allowance[_from][msg.sender] < _value) revert AllowanceExceeded();
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}
Code Explanation
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract MarToken {
// state variables
string public name = "MarToken";
string public symbol = "MAR";
uint8 public decimals = 18;
uint256 public totalSupply;
SPDX-License-Identifier: This specifies the license under which the contract is released. In this case, it’s unlicensed.
pragma solidity ^0.8.28: This indicates the Solidity compiler version. The ^ symbol means it will work with any version from 0.8.28 up to (but not including) 0.9.0.
State Variables:
name
: The name of the token (MarToken).symbol
: The ticker symbol for the token (MAR).decimals
: The number of decimal places the token can be divided into (18 is standard for ERC20 tokens).totalSupply
: The total number of tokens in circulation.
2. Mappings for Balances and Allowances
// mapping for balances and allowances
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
balanceOf: A mapping that stores the balance of each address.
allowance: A nested mapping that stores the amount of tokens an address is allowed to spend on behalf of another address.
3. Custom Errors (Gas Efficient)
// Custom errors (gas efficient)
error InsufficientBalance();
error AllowanceExceeded();
error InvalidAddress();
Custom Errors: These are gas-efficient ways to handle errors in Solidity.
InsufficientBalance
: Thrown when a user tries to transfer more tokens than they have.AllowanceExceeded
: Thrown when a spender tries to transfer more tokens than they are allowed.InvalidAddress
: Thrown when a function is called with a zero address.
4. Events for Logging Transactions
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
Events: These are used to log important actions on the blockchain.
Transfer
: Emitted when tokens are transferred from one address to another.Approval
: Emitted when an address approves another address to spend tokens on its behalf.
5. Constructor: Minting Tokens on Deployment
// initialize total supply (constructor)
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * (10 ** uint256(decimals));
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
Constructor: This function is called when the contract is deployed.
_initialSupply
: The initial number of tokens to be created.totalSupply
: The total supply is calculated by multiplying the initial supply by10^decimals
(to account for the decimal places).balanceOf[msg.sender]
: The deployer’s address receives the entire initial supply.Transfer
: An event is emitted to log the creation of the tokens.
6. Transfer Function
function transfer(address _to, uint256 _value) public returns (bool success) {
if (_to == address(0)) revert InvalidAddress();
if (balanceOf[msg.sender] < _value) revert InsufficientBalance();
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
transfer: Allows a user to transfer tokens to another address.
Checks if the recipient address is valid (not zero).
Checks if the sender has enough balance.
Updates the balances of the sender and recipient.
Emits a
Transfer
event.Returns
true
if the transfer is successful.
7. Approving a Spender
// Approve function (grants spending permission)
function approve(address _spender, uint256 _value) public returns (bool success) {
if (_spender == address(0)) revert InvalidAddress();
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
approve: Allows an owner to approve another address to spend tokens on their behalf.
Checks if the spender address is valid.
Updates the
allowance
mapping.Emits an
Approval
event.Returns
true
if the approval is successful.
8. transferFrom (Spending an Approved Amount)
// TransferFrom function (allows spending on behalf of owner)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
if (_from == address(0) || _to == address(0)) revert InvalidAddress();
if (balanceOf[_from] < _value) revert InsufficientBalance();
if (allowance[_from][msg.sender] < _value) revert AllowanceExceeded();
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
transferFrom: Allows a spender to transfer tokens on behalf of the owner.
Checks if the
from
andto
addresses are valid.Checks if the
from
address has enough balance.Checks if the spender has enough allowance.
Updates the balances and allowance.
Emits a
Transfer
event.Returns
true
if the transfer is successful.
Conclusion
The MarToken contract is a simple yet complete implementation of an ERC20 token. It includes all the essential functions required by the ERC20 standard, such as transfer
, approve
, and transferFrom
. By understanding this contract, you can build your own tokens or interact with existing ones in the Ethereum ecosystem.
Whether you’re building a new cryptocurrency, a governance token, or an in-game asset, the ERC20 standard provides a robust foundation for your project. Happy coding!
If you found this useful, don’t forget to like and share 🚀
References
Subscribe to my newsletter
Read articles from mosamorphing directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
