A Beginner’s Guide to Understanding ERC-20 Tokens

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:

  1. Transferring tokens from one account to another.

  2. Approving and spending tokens on behalf of another account.

  3. Querying the balance of an account.

  4. 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.

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 by 10^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 and to 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