Lightest ERC20 Token Contract in Full Yul (Solidity Assembly) Language with Suspension Function

Lightest ERC20 Token Contract in Full Yul (Solidity Assembly) Language with Suspension Function

The ERC20 token standard has established itself as the cornerstone of the decentralised finance (DeFi) ecosystem, underpinning a myriad of decentralised applications and protocols. In this article, we delve into the intricacies of token contract development, spotlighting the creation of the lightest ERC20 token contract crafted entirely in the Yul language, a specialised form of Solidity Assembly. Our focus extends beyond mere gas optimization to include a unique suspension function, enriching the token's functionality and versatility.

Our exploration begins with an examination of the evolution of ERC20 contracts and the various versions that have emerged, with a particular emphasis on those implemented in the Yul language. Yul, being a low-level language, enables fine-grained control over the Ethereum Virtual Machine (EVM), facilitating superior gas optimisation.

The featured ERC20 contract distinguishes itself by standing out as the epitome of gas efficiency. Through meticulous coding practices and leveraging the capabilities of Yul, this contract achieves unparalleled optimization, ensuring minimal gas consumption. Such optimisation becomes increasingly crucial in the context of transaction fees and network congestion, making the contract an ideal choice for resource-conscious developers.

Embedded within the lightweight ERC20 contract are two pivotal functions - "mintable" and "burnable." These functionalities empower token issuers with the ability to create new tokens and eliminate existing ones, enhancing the contract's adaptability for diverse use cases. This dual feature not only aligns with industry standards but also reinforces the contract's utility in various decentralised applications.

A distinctive feature of this ERC20 contract is its suspension function, a mechanism designed to halt specific operations under predefined circumstances. Intriguingly, the execution of this suspension function relies on the mem() function from the Proxy.sol contract. This intricate interplay between contracts adds a layer of sophistication, allowing developers to tailor suspension conditions according to specific requirements. To delve deeper into the technical nuances of this mechanism, readers are encouraged to explore the detailed explanation provided at The Lightest and Streamlined ERC897 Proxy Contract with Memory Slot Manipulation .

Below is the full working code.

// SPDX-License-Identifier:None
pragma solidity 0.8.0;

import {Ownable} from "https://github.com/aloycwl/Util.sol/blob/main/Access/Ownable.sol";
import {Check} from "https://github.com/aloycwl/Util.sol/blob/main/Security/Check.sol";
import {Mintable} from "https://github.com/aloycwl/Token.sol/blob/main/ERC20/Mintable.sol";
import {Burnable} from "https://github.com/aloycwl/Token.sol/blob/main/ERC20/Burnable.sol";

contract ERC20 is Ownable, Check, Mintable, Burnable {

    event Transfer(address indexed, address indexed, uint);
    event Approval(address indexed, address indexed, uint);

    function name () external pure returns (string memory) { 
        assembly {
            mstore(0x80, 0x20)
            mstore(0xa0, 0x0b)
            mstore(0xc0, "ERC20 Token")
            return(0x80, 0x60)
        }
    }

    function symbol () external pure returns (string memory) { 
        assembly {
            mstore(0x80, 0x20)
            mstore(0xa0, 0x03)
            mstore(0xc0, "TKN")
            return(0x80, 0x60)
        }
    }

    function decimals () external pure returns (uint val) { 
        assembly { val := 0x12 }
    }
    
    function totalSupply () external view returns (uint amt) { 
        assembly { amt := sload(INF) }
    }

    function allowance (address adr, address ad2) external view returns (uint amt) { 
        assembly {
            mstore(0x00, adr)
            mstore(0x20, ad2)
            amt := sload(keccak256(0x00, 0x40))
        }
    }

    function balanceOf (address adr) external view returns (uint amt) { 
        assembly {
            mstore(0x00, adr)
            amt := sload(keccak256(0x00, 0x20))
        }
    }

    function approve (address adr, uint amt) external returns (bool bol) { 
        assembly {
            mstore(0x00, caller())
            mstore(0x20, adr)
            sstore(keccak256(0x00, 0x40), amt)
            mstore(0x00, amt)
            log3(0x00, 0x20, EAP, caller(), adr)
            bol := 0x01
        }
    }

    function transfer (address adr, uint amt) external returns (bool bol) { 
        assembly {
            mstore(0x00, caller())
            let tmp := keccak256(0x00, 0x20)
            let bal := sload(tmp)
            if gt(amt, bal) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER2)
                revert(0x80, 0x64)
            }
            sstore(tmp, sub(bal, amt))
            mstore(0x00, adr)
            tmp := keccak256(0x00, 0x20)
            sstore(tmp, add(sload(tmp), amt))

            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, caller(), adr)
            bol := 0x01
        }
        isSuspended(msg.sender, adr);
    }
    
    function transferFrom (address adr, address ad2, uint amt) public returns (bool bol) { 
        assembly {
            mstore(0x00, adr)
            let tmp := keccak256(0x00, 0x20)
            let bal := sload(tmp)
            mstore(0x00, adr)
            mstore(0x20, ad2)

            let ptr := keccak256(0x00, 0x40)
            let alw := sload(ptr)
            if or(gt(amt, bal), gt(amt, alw)) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER2)
                revert(0x80, 0x64)
            }

            sstore(ptr, sub(alw, amt))
            sstore(tmp, sub(bal, amt))
            mstore(0x00, ad2)
            tmp := keccak256(0x00, 0x20)
            sstore(tmp, add(sload(tmp), amt))

            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, caller(), adr) 
            bol := 0x01
        }
        isSuspended(adr, ad2);
    }

}        

Below are the functions by functions explanation.

    function name () external pure returns (string memory) { 
        assembly {
            mstore(0x80, 0x20)
            mstore(0xa0, 0x0b)
            mstore(0xc0, "ERC20 Token")
            return(0x80, 0x60)
        }
    }        

Instead of wasting storage space, the name is hard-coded into the contract as the likelihood of the name to be changed is minimal. However, since this contract is to be used with a proxy, the name can be updated with another implementation. To change the name, it is important to change the length of the string too, otherwise it will be truncated or appended with null characters.

The name of the token (32 characters long) is stored at the 0xc0 position. You may change to any name within its character limit, once you got the name, count the number of characters and replace the 0xa0 position. For this case, "ERC20 Token" is 11 characters long so I put the hexadecimal number 0x0b as the length. Change according to your own character length.

    function symbol () external pure returns (string memory) { 
        assembly {
            mstore(0x80, 0x20)
            mstore(0xa0, 0x03)
            mstore(0xc0, "TKN")
            return(0x80, 0x60)
        }
    }        

Similarly to the name, the symbol is the ticker ID of the token. As the symbol will usually be 3 characters, the memory position at 0xa0 is 0x03. Change it too if your symbol have a different length.

    function decimals () external pure returns (uint val) { 
        assembly { val := 0x12 }
    }        

The decimal is fixed at 18 position, meaning all arithmetic operations are down to the precision of 18 decimal.

    function totalSupply () external view returns (uint amt) { 
        assembly { amt := sload(INF) }
    }        

As the total supply of the token is dynamic, meaning it will change at every mint or burn, it requires a storage slot. I am using the INF position from Hashes.sol which is 0x80ac58cd00000000000000000000000000000000000000000000000000000000. This is a reuse of a function call as there are no other circumstances that this storage position will be called. You may change to any slot you want as long as it is not conflicting.

    function allowance (address adr, address ad2) external view returns (uint amt) { 
        assembly {
            mstore(0x00, adr)
            mstore(0x20, ad2)
            amt := sload(keccak256(0x00, 0x40))
        }
    }

    function approve (address adr, uint amt) external returns (bool bol) { 
        assembly {
            mstore(0x00, caller())
            mstore(0x20, adr)
            sstore(keccak256(0x00, 0x40), amt)
            mstore(0x00, amt)
            log3(0x00, 0x20, EAP, caller(), adr)
            bol := 0x01
        }
    }        

For the allowance field, I am using the hash of the spender’s address and recipient’s address. There are no other functions in the ERC20 that require this and therefore a straightforward Keccak256 is sufficient.

Once the approve is set, it will emit the approval event with the EAP hash (0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925).

    function transfer (address adr, uint amt) external returns (bool bol) { 
        assembly {
            mstore(0x00, caller())
            let tmp := keccak256(0x00, 0x20)
            let bal := sload(tmp)
            if gt(amt, bal) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER2)
                revert(0x80, 0x64)
            }
            sstore(tmp, sub(bal, amt))
            mstore(0x00, adr)
            tmp := keccak256(0x00, 0x20)
            sstore(tmp, add(sload(tmp), amt))

            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, caller(), adr)
            bol := 0x01
        }
        isSuspended(msg.sender, adr);
    }        

Programatically, the main difference between transfer and transferForm is that transferFrom will deduct the allowance. Transfer only transfers tokens from the message sender and does not deduct allowance even if there is any.

In this case, the sender’s balance is deducted, the recipient address is added and the transfer event is emitted.

(This operation requires suspension checking).

     function transferFrom (address adr, address ad2, uint amt) public returns (bool bol) { 
        assembly {
            mstore(0x00, adr)
            let tmp := keccak256(0x00, 0x20)
            let bal := sload(tmp)
            mstore(0x00, adr)
            mstore(0x20, ad2)

            let ptr := keccak256(0x00, 0x40)
            let alw := sload(ptr)
            if or(gt(amt, bal), gt(amt, alw)) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER2)
                revert(0x80, 0x64)
            }

            sstore(ptr, sub(alw, amt))
            sstore(tmp, sub(bal, amt))
            mstore(0x00, ad2)
            tmp := keccak256(0x00, 0x20)
            sstore(tmp, add(sload(tmp), amt))

            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, caller(), adr) 
            bol := 0x01
        }
        isSuspended(adr, ad2);
    }        

This is the same as the transfer() function except it can be called from a 3rd-party. Also, the allowance is deducted accordingly.

(This operation requires suspension checking).

    function mint (address adr, uint amt) external onlyOwner {
        assembly {
            mstore(0x00, adr)
            let tmp := keccak256(0x00, 0x20)
            sstore(tmp, add(sload(tmp), amt))
            sstore(INF, add(sload(INF), amt))
            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, 0x00, adr) 
        }
        isSuspended(adr);
    }        

This is a typical mint function but written in Yul. It will add the balance for the user and also the total balance for the contract. A transfer event will also be emitted to broadcast new token is minted.

(This operation requires suspension checking).

    function burn (uint amt) external {
        assembly {
            mstore(0x00, caller())
            let tmp := keccak256(0x00, 0x20)
            sstore(tmp, sub(sload(tmp), amt))
            sstore(INF, sub(sload(INF), amt))
            mstore(0x00, amt)
            log3(0x00, 0x20, ETF, caller(), 0x00) 
        }
        isSuspended(msg.sender);
    }        

Exactly similar to mint except all subtraction instead of addition is being called. It will also check if the burner have sufficient tokens.

(This operation requires suspension checking).

    function isSuspended(address adr) internal view {
        assembly {
            if or(gt(sload(address()), 0x00), gt(sload(adr), 0x00)) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER3)
                revert(0x80, 0x64)
            }
        }
    }

    function isSuspended(address adr, address ad2) internal view {
        assembly {
            if or(or(gt(sload(address()), 0x00), gt(sload(adr), 0x00)), gt(sload(ad2), 0x00)) {
                mstore(0x80, ERR) 
                mstore(0xa0, STR)
                mstore(0xc0, ER3)
                revert(0x80, 0x64)
            }
        }
    }        

This is the pinnacle of the suspension function. It has a single or double parameter to check if the sender and sender or recipient is suspended. Both functions will check the contract address for suspension as default.

To suspend the contract or an address

bytes32 b32;
assembly {
  b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  // Replace this with the contract or user's address
}
Proxy(payable(address(this))).mem(b32, bytes32(uint(0x01)));        

To unsuspend the contract or an address

bytes32 b32;
assembly {
  b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  // Replace this with the contract or user's address
}
Proxy(payable(address(this))).mem(b32, bytes32(uint(0x00)));        

The full repo can be found here: Aloycwl ERC20 Github

要查看或添加评论,请登录

社区洞察

其他会员也浏览了