Implementing a Custom ERC-20 Token Smart Contract with Solidity

The ERC20 standard is a technical standard that defines a common set of rules that allows us create tokens that are fungible (meaning that each token in the set is not different from the other).

In this article we will be examining a simple smart contract based on that allows us implement a custom ERC-20.

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

contract Token {
    string tokenName;
    string tokenSymbol;
    uint256 tokenTotalSupply;

    mapping(address => uint256) private balances; 

    mapping(address => mapping(address => uint256)) amountAllowed; 

    constructor(
        string memory _name,
        string memory _symbol,
        uint256 _totalSupply
    ) {
        tokenName = _name;
        tokenSymbol = _symbol;
        _mint(msg.sender, _totalSupply);
    }

    event Transfer(
        address indexed owner,
        address indexed receiver,
        uint256 amount
    );
    event Allow(
        address indexed owner,
        address indexed _withdrawer,
        uint256 amount
    );
    event TransferFrom(
        address indexed owner,
        address indexed _withdrawer,
        uint256 amount
    );

    //The code written below is an implementation of the six mandatory functions of an ERC-20 token

    function name() public view returns (string memory) {
        return tokenName;
    }

    function symbol() public view returns (string memory) {
        return tokenSymbol;
    }

    function decimal() public pure returns (uint8) {
        return 18;
    }

    function totalSupply() public view returns (uint256) {
        return tokenTotalSupply;
    }

    function balanceOf(address _userAccount) public view returns (uint256) {
        return balances[_userAccount];
    }

    function transfer(
        address _receiver,
        uint256 _amount
    ) public returns (bool) {
        require(
            _amount <= balances[msg.sender],
            "You cannot transfer more than the current supply"
        );
        require(_receiver != address(0), "Wrong address");

        uint256 transferFee = (_amount * 10) / 100;

        balances[msg.sender] = balances[msg.sender] - (_amount + transferFee);
        balances[_receiver] = balances[_receiver] + _amount;
        _burn(msg.sender, (transferFee));

        emit Transfer(msg.sender, _receiver, _amount);

        return true;
    }

    function approve(
        address _owner,
        address _withdrawer,
        uint256 _amount
    ) public returns (bool) {
        _owner = msg.sender;
        amountAllowed[_owner][_withdrawer] = _amount;
        emit Allow(_owner, _withdrawer, _amount);
        return true;
    }

    function allowance(
        address _owner,
        address _withdrawer
    ) public view returns (uint256) {
        return amountAllowed[_owner][_withdrawer];
    }

    function transferFrom(
        address _owner,
        address _receiver,
        uint256 _amount
    ) public returns (bool) {
        require(
            _amount <= balances[msg.sender],
            "You cannot transfer more than the current supply"
        );
        require(_receiver != address(0), "Wrong address");

        uint256 transferFee = (_amount * 10) / 100;

        balances[_owner] = balances[_owner] - (_amount + transferFee);

        amountAllowed[_owner][msg.sender] =
            amountAllowed[_owner][msg.sender] -
            _amount;

        balances[_receiver] = balances[_receiver] + _amount;

        _burn(_owner, (transferFee));

        emit TransferFrom(msg.sender, _receiver, _amount);

        return true;
    }

    function _burn(address _owner, uint256 _amount) internal {
        tokenTotalSupply = tokenTotalSupply - _amount;

        emit Transfer(_owner, address(0), _amount);
    }

    function _mint(address _owner, uint256 _amount) internal {
        require(_owner != address(0), "Wrong address");
        tokenTotalSupply = _amount * (10 ** 18);
        balances[msg.sender] = _amount * (10 ** 18);

        emit Transfer(address(0), _owner, _amount * (10 ** 18));
    }
}

Here we create a contract with the name Token , we then declare state variables that hold the token name, token symbol and the total supply of the token.

 mapping(address => uint256) private balances;

Here we create a map that relates each token holder's address to their balance of the token.

constructor(
        string memory _name,
        string memory _symbol,
        uint256 _totalSupply
    ) {
        tokenName = _name;
        tokenSymbol = _symbol;
        _mint(msg.sender, _totalSupply);
    }

In the above bock of code, we make use of the constructor to set the token's name, symbol and total supply. We also call the mint function to assign the initial supply to the contract creator's address.

function name() public view returns (string memory) {
        return tokenName;
    }

    function symbol() public view returns (string memory) {
        return tokenSymbol;
    }

    function decimal() public pure returns (uint8) {
        return 18;
    }

    function totalSupply() public view returns (uint256) {
        return tokenTotalSupply;
    }

The functions above, are getter functions that return the name, symbol, decimal and totalSupply of the token respectively.

 function balanceOf(address _userAccount) public view returns (uint256) {
        return balances[_userAccount];
    }

This balanceOf() function above returns the balance of a particular token holder, by passing in the token holder's address.

function transfer(
        address _receiver,
        uint256 _amount
    ) public returns (bool) {
        require(
            _amount <= balances[msg.sender],
            "You cannot transfer more than the current supply"
        );
        require(_receiver != address(0), "Wrong address");

        uint256 transferFee = (_amount * 10) / 100;

        balances[msg.sender] = balances[msg.sender] - (_amount + transferFee);
        balances[_receiver] = balances[_receiver] + _amount;
        _burn(msg.sender, (transferFee));

        emit Transfer(msg.sender, _receiver, _amount);

        return true;
    }

This transfer() function allows a user to send tokens to another address. It checks whether the sender has enough balance and whether the recipient address is valid. If the transfer is successful, it deducts the amount from the sender's balance and adds it to the receiver's balance, emitting a Transfer event. In this particular ERC20 implementation, a 10% transfer fee is applied to the sender and subsequently burnt each time the transfer function is invoked.

function approve(
        address _owner,
        address _withdrawer,
        uint256 _amount
    ) public returns (bool) {
        _owner = msg.sender;
        amountAllowed[_owner][_withdrawer] = _amount;
        emit Allow(_owner, _withdrawer, _amount);
        return true;
    }

The approve() function above, enables one address (the owner) to approve another address (the withdrawer) to withdraw or spend a certain amount of tokens on its behalf.

 function allowance(
        address _owner,
        address _withdrawer
    ) public view returns (uint256) {
        return amountAllowed[_owner][_withdrawer];
    }

The allowance() function returns the remaining number of tokens that the withdrawer is allowed to withdraw/spend on behalf of the owner. This is neccessary because as the withdrawer spends/withdraws, the allocated amount reduces.

function transferFrom(
        address _owner,
        address _receiver,
        uint256 _amount
    ) public returns (bool) {
        require(
            _amount <= balances[msg.sender],
            "You cannot transfer more than the current supply"
        );
        require(_receiver != address(0), "Wrong address");

        uint256 transferFee = (_amount * 10) / 100;

        balances[_owner] = balances[_owner] - (_amount + transferFee);

        amountAllowed[_owner][msg.sender] =
            amountAllowed[_owner][msg.sender] -
            _amount;

        balances[_receiver] = balances[_receiver] + _amount;

        _burn(_owner, (transferFee));

        emit TransferFrom(msg.sender, _receiver, _amount);

        return true;
    }

The transferFrom() function enables a spender/withdrawer to transfer tokens from one address to another within the approved limit. Similar to the transfer function, a 10% transfer fee is deducted from the sender and completely removed from the total token supply.

   function _burn(address _owner, uint256 _amount) internal {
        tokenTotalSupply = tokenTotalSupply - _amount;

        emit Transfer(_owner, address(0), _amount);
    }

The _burn() function destroys tokens from an address's balance, removing them from circulation. It decreases the total supply and the balance of the address passed in . A Transfer event is emitted from the owner's address to the zero address to reflect the burning of tokens

function _mint(address _owner, uint256 _amount) internal {
        require(_owner != address(0), "Wrong address");
        tokenTotalSupply = _amount * (10 ** 18);
        balances[msg.sender] = _amount * (10 ** 18);

        emit Transfer(address(0), _owner, _amount * (10 ** 18));
    }

The _mint() function is an internal function that creates new tokens and assigns them to an address. This function was called in the constructor to to assign the initial supply to the contract creator's address.

Conclusion

In conclusion, implementing a custom ERC-20 token smart contract with Solidity offers a powerful means of creating a versatile and secure digital asset. By adhering to the ERC-20 standard and incorporating custom functionalities such as transfer fees and supply deductions, developers can tailor tokens to specific use cases while ensuring compatibility with a wide range of decentralized applications and exchanges. Solidity's robustness and flexibility make it a valuable tool for constructing ERC-20 tokens that meet diverse requirements, paving the way for the seamless integration of custom tokens into the ever growing blockchain ecosystem.