ERC-20 Token Standard Solidity Smart Contract: Deep dive.

Khufray SamuelKhufray Samuel
10 min read

Ethereum Illustration. work email 👉shubhamdhage000@gmail.com

ERC-20 Token Standard:

The ERC-20 Token Standard is as a result of EIP-20. EIP means Ethereum Improvement Proposal while ERC-20 means Ethereum Request for Comment 20. ERCs are derived from their corresponding EIP.
ERC-20 is a technical standard/specification for creating fungible tokens on Ethereum and extends to other EVM blockchains. Fungible token here means a token whose value is the same irrespective of condition. A worthy example is the Dollar, a 1 Dollar bill is the same as any other 1 Dollar bill irrespective of location, time, and other factors. To further simplify, a fungible token is a token that each unit has the same type and value as the others.

Solidity:

Solidity is a statically-typed curly-braces programming language designed for developing smart contracts that run on Ethereum.

Smart Contracts:

A smart contract is a computer program or a transaction protocol that is intended to automatically execute, control or document events and actions according to the terms of a contract or an agreement.

ERC-20 Token Standard Solidity Smart Contract

For a token to be considered an ERC-20 token, it must meet the minimum specification of the ERC-20 token standard. These specifications are functionalities that must be implemented and these are described as interfaces. Therefore the actual implementation is down to the developer. Also, since these are minimum requirement, it means more functionalities can be added.

Here are the specifications written in Solidity language:

METHODS

name - OPTIONAL

Returns the name of the token - e.g. "MyToken".
This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

function name() public view returns (string)

symbol - OPTIONAL

Returns the symbol of the token. E.g. “HIX”.
This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

function symbol() public view returns (string)

decimals - OPTIONAL

Returns the number of decimals the token uses - e.g. 8, means to divide the token amount by 100000000 to get its user representation.
This method can be used to improve usability, but interfaces and other contracts MUST NOT expect these values to be present.

function decimals() public view returns (uint8)

totalSupply - REQUIRED

Returns the total token supply.

function totalSupply() public view returns (uint256)

balanceOf - REQUIRED

Returns the account balance of another account with address _owner.

function balanceOf(address _owner) public view returns (uint256 balance)

transfer - REQUIRED

Transfers _value amount of tokens to address _to, and MUST fire the Transfer event. The function SHOULD throw if the message caller’s account balance does not have enough tokens to spend.
Note: Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event.

function transfer(address _to, uint256 _value) public returns (bool success)

transferFrom - REQUIRED

Transfers _value amount of tokens from address _from to address _to, and MUST fire the Transfer event.

The transferFrom method is used for a withdraw workflow, allowing contracts to transfer tokens on your behalf. This can be used for example to allow a contract to transfer tokens on your behalf and/or to charge fees in sub-currencies. The function SHOULD throw unless the _from account has deliberately authorized the sender of the message via some mechanism.
Note: Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event.

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

approve - REQUIRED

Allows _spender to withdraw from your account multiple times, up to the _value amount. If this function is called again it overwrites the current allowance with _value.
NOTE*:* To prevent attack vectors like the one described here and discussed here, clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to 0 before setting it to another value for the same spender. THOUGH The contract itself shouldn’t enforce it, to allow backwards compatibility with contracts deployed before

function approve(address _spender, uint256 _value) public returns (bool success)

allowance - REQUIRED

Returns the amount which _spender is still allowed to withdraw from _owner.

function allowance(address _owner, address _spender) public view returns (uint256 remaining)

EVENTS

Transfer - REQUIRED

MUST trigger when tokens are transferred, including zero value transfers.
A token contract which creates new tokens SHOULD trigger a Transfer event with the _from address set to 0x0 when tokens are created.

event Transfer(address indexed _from, address indexed _to, uint256 _value)

Approval - REQUIRED

MUST trigger on any successful call to approve(address _spender, uint256 _value).

event Approval(address indexed _owner, address indexed _spender, uint256 _value)

Here's a basic solidity interface with all these coupled together

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/// @title ERC-20 Token Standard Interface
/// @notice This is a standard interface for ERC-20 tokens.
/// @dev This interface is a standard interface for ERC-20 tokens.
interface IERC20 {
    /// @notice This is a getter function for the name of the token.
    /// @return The name of the token.
    function name() external view returns (string memory);

    /// @notice This is a getter function for the symbol of the token.
    /// @return The symbol of the token.
    function symbol() external view returns (string memory);

    /// @notice This is a getter function for the number of decimals of the token.
    /// @return The number of decimals of the token.
    function decimals() external view returns (uint8);

    /// @notice This is a getter function for the total supply of the token.
    /// @return The total supply of the token.
    function totalSupply() external view returns (uint256);

    /// @notice This is a getter function for the balance of the token owner.
    /// @param _owner The address of the token owner.
    /// @return balance Returns the balance of the token owner.
    function balanceOf(address _owner) external view returns (uint256 balance);

    /// @notice This is a function to transfer tokens from the sender to the receiver.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens to transfer.
    /// @return success Returns true if the transfer is successful.
    function transfer(
        address _to,
        uint256 _value
    ) external returns (bool success);

    /// @notice This is a function to transfer tokens from the sender to the receiver.
    /// @param _from The address of the sender.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens to transfer.
    /// @return success Returns true if the transfer is successful.
    function transferFrom(
        address _from,
        address _to,
        uint256 _value
    ) external returns (bool success);

    /// @notice This is a function to approve the spender to spend the specified amount of tokens on behalf of the owner.
    /// @param _spender The address of the spender.
    /// @param _value The amount of tokens to be spent.
    /// @return success Returns true if the approval is successful.
    function approve(
        address _spender,
        uint256 _value
    ) external returns (bool success);

    /// @notice This is a getter function for the allowance of the spender on behalf of the owner.
    /// @param _owner The address of the owner.
    /// @param _spender The address of the spender.
    /// @return remaining Returns the remaining amount of tokens that the spender is allowed to spend on behalf of the owner.
    function allowance(
        address _owner,
        address _spender
    ) external view returns (uint256 remaining);

    /// @notice This is an event that is emitted when the transfer of tokens is successful.
    /// @param _from The address of the sender.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens that are transferred.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /// @notice This is an event that is emitted when the approval of the spender is successful.
    /// @param _owner The address of the owner.
    /// @param _spender The address of the spender.
    /// @param _value The amount of tokens that are approved.
    event Approval(
        address indexed _owner,
        address indexed _spender,
        uint256 _value
    );
}

Here's a basic ERC-20 smart contract implementing this interface which adheres to the ERC-20 token standard specifications.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/// @title Sample Token
/// @notice This is a sample token contract.
/// @dev This is a sample token contract.
contract SampleToken {
    /* State variables */

    string _name; // Name of the token
    string _symbol; // Symbol of the token
    uint8 _decimals; // Number of decimals of the token
    uint256 _totalSupply; // Total supply of the token
    mapping(address => uint256) _balances; // Balances of the token holders
    mapping(address => mapping(address => uint256)) _allowances; // Allowances of the token holders

    /// @notice This is an event that is emitted when the transfer of tokens is successful.
    /// @param _from The address of the sender.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens that are transferred.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /// @notice This is an event that is emitted when the approval of the spender is successful.
    /// @param _owner The address of the owner.
    /// @param _spender The address of the spender.
    /// @param _value The amount of tokens that are approved.
    event Approval(
        address indexed _owner,
        address indexed _spender,
        uint256 _value
    );

    /// @notice This is the constructor function that initializes the sample token contract.
    /// @param name The name of the token.
    /// @param symbol The symbol of the token.
    /// @param decimals The number of decimals of the token.
    /// @param totalSupply The total supply of the token.
    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals,
        uint256 totalSupply
    ) {
        _name = name;
        _symbol = symbol;
        _decimals = decimals;
        _totalSupply = totalSupply;
        _balances[msg.sender] = totalSupply;
    }

    /// @notice This is a getter function for the name of the token.
    /// @return The name of the token.
    function name() external view returns (string memory) {
        return _name;
    }

    /// @notice This is a getter function for the symbol of the token.
    /// @return The symbol of the token.
    function symbol() external view returns (string memory) {
        return _symbol;
    }

    /// @notice This is a getter function for the number of decimals of the token.
    /// @return The number of decimals of the token.
    function decimals() external view returns (uint8) {
        return _decimals;
    }

    /// @notice This is a getter function for the total supply of the token.
    /// @return The total supply of the token.
    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    /// @notice This is a getter function for the balance of the token owner.
    /// @param _owner The address of the token owner.
    /// @return balance Returns the balance of the token owner.
    function balanceOf(address _owner) external view returns (uint256 balance) {
        return _balances[_owner];
    }

    /// @notice This is a function to transfer tokens from the sender to the receiver.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens to transfer.
    /// @return success Returns true if the transfer is successful.
    function transfer(
        address _to,
        uint256 _value
    ) external returns (bool success) {
        if (_balances[msg.sender] < _value) revert("Insufficient balance");

        _balances[msg.sender] -= _value;
        _balances[_to] += _value;

        success = true;

        // Emit the Transfer event
        emit Transfer(msg.sender, _to, _value);
    }

    /// @notice This is a function to transfer tokens from the sender to the receiver.
    /// @param _from The address of the sender.
    /// @param _to The address of the receiver.
    /// @param _value The amount of tokens to transfer.
    /// @return success Returns true if the transfer is successful.
    function transferFrom(
        address _from,
        address _to,
        uint256 _value
    ) external returns (bool success) {
        if (_balances[_from] < _value) {
            revert("Insufficient balance");
        }

        if (_allowances[_from][msg.sender] < _value) {
            revert("Insufficient allowance");
        }

        _balances[_from] -= _value;
        _balances[_to] += _value;
        _allowances[_from][msg.sender] -= _value;

        success = true;

        // Emit the Transfer event
        emit Transfer(_from, _to, _value);
    }

    /// @notice This is a function to approve the spender to spend the specified amount of tokens on behalf of the owner.
    /// @param _spender The address of the spender.
    /// @param _value The amount of tokens to be spent.
    /// @return success Returns true if the approval is successful.
    function approve(
        address _spender,
        uint256 _value
    ) external returns (bool success) {
        _allowances[msg.sender][_spender] = _value;

        success = true;

        // Emit the Approval event
        emit Approval(msg.sender, _spender, _value);
    }

    /// @notice This is a getter function for the allowance of the spender on behalf of the owner.
    /// @param _owner The address of the owner.
    /// @param _spender The address of the spender.
    /// @return remaining Returns the remaining amount of tokens that the spender is allowed to spend on behalf of the owner.
    function allowance(
        address _owner,
        address _spender
    ) external view returns (uint256 remaining) {
        return _allowances[_owner][_spender];
    }
}

This is a basic implementation of the ERC-20 standard. As stated earlier, the standard is extensible as to functionalities that can be added to the stated specifications.

Conclusion

In conclusion, this article attempt to breakdown the ERC-20 token standard and provided a basic implementation. The ERC-20 standard is flexible and allows for additional functionalities to be added to the specified requirements. By following this guide, developers can create their own ERC-20 tokens and customize them to meet their specific needs.

References:

Solidity Language: https://soliditylang.org/
Wikipedia: https://en.wikipedia.org/
Ethereum EIPs: https://eips.ethereum.org/EIPS/
Photo by Shubham Dhage on Unsplash

0
Subscribe to my newsletter

Read articles from Khufray Samuel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Khufray Samuel
Khufray Samuel