견고성 : 가스 최적화

목차


  • Introduction
  • On Chain or Off Chain
  • Solidity Optimiser
  • EVM Slots
  • Loop through an array
  • Libraries
  • Events
  • Computed Values
  • Constants
  • Error handling

  • 소개

    When writing smart contract in Solidity, you may not be really concerned about the gas consumption in Remix environment or in a testnet. You only realise how much cost every OPCODES when you deploy in the mainnet on Ethereum (or in L2 but it's quite cheap).
    Today, we will cover some optimisation we can do in Solidity. This list is not exhaustive and feel free to test every option you can.

    The very first rule we keep in mind is : Every state variable we update cost you some fees. For more information, you can see the following documentation : Opcode . 많은 EIP는 EVM에서 가스 운영 비용을 최적화하거나 조정하려고 합니다. (예: EIP 2929, EIP 1559)

    온체인 또는 오프체인 데이터

    As mentioned above, writing state variable cost you some gas. That's why you should be aware that every user's call can cost a lot. You should be asking : "Do I really need to store this information on chain ?"
    Every data related to the "ecosystem" or that doesn't need to be censored should be inside your smart contract. When it comes to metadata, files you can handle them off chain.


    견고성 옵티마이저

    It's a mode that enable to produce a more efficient bytecode by reducing, the code size or the gas needed for the deployment. Keep in mind that the optimiser is not turned on by default because it takes more time to compile the actual solidity code which is not efficient in development mode. Turn on the optimiser when you want to deploy your smart contract


    EVM 슬롯

    You have probably seen or heard someone telling you to order your state variables by type ?
    This is not without any reason. The EVM stores the data of a smart contract into a memory slots of 256 bits. A uint256 will fill one slot. But a uint128 will fill 128 bits over 256. if the next state variable is a uint256 then, the variable will be stored in the next slot. The EVM will perform a computation to force the previous slots to fit the remaining space.
    Take this code :

    uint128 variable_1
    uint128 variable_2
    uint256 variable_3
    

    variable_1 will be stored in slot 0. Then variable_2 can fit inside the slot 0 because it has a remaining space of 128 bits.
    After that, the variable_3 will be stored in slot 1.

    In a not optimised version we can have this code

    address address_1
    uint128 variable_1
    uint256 variable_2
    address address_2
    uint128 variable_3
    

    address_1 is 160 bits long (address is a 20 bytes type). It will be stored in slot 0. variable_1 is 128 bits. But 128 + 160 = 288 bits. It can't fit in the 96 bits remaining space. (256-160). Then, variable_1 will be stored in slot 1. variable_2 is a 256 bits length. It will be stored in slot 2 because it can't fit in the slot 1 (128 bits remaining space). address_2 will be in slot 3 because the slot 2 is full. The variable_3 will be in slot 4 because the slot 3 has already 160 bits reserved (96 remaining).

    To optimise this version, we can adjust the variable as follow :

    uint128 variable_1
    uint128 variable_3
    uint256 variable_2
    address address_1
    address address_2
    

    Here, there is 4 slots taken while the non-optimised version has 5 slots taken. We have saved 32 bytes and prevent unnecessary computation.


    배열을 통한 루프

    Arrays are quite common nowadays and it's pretty straightforward to read values from them with a for-loop.
    If you have guessed where I'm going by saying that, it means you read carefully the introduction.
    If you read an array which is a state variable, looping inside the array without caching it into a memory variable cost you more gas.

    In the following example, you can see that for a 9-length array, we spent 83037 gas in the un-optimised version and only 56475 in the most optimised

    My advice: Avoid looping through an array that has a dynamic length. The gas consumption increases overtime and it will become a very expensive process.

    (Try it on Remix)

    // SPDX-License-Identifier: GPL-3.0
    
    pragma solidity >=0.7.0 <0.9.0;
    
    contract GasTest {
        uint[] public arrayTest;
        uint public sumArray;
    
        constructor(){
            arrayTest = [1,2,3,4,5,6,7,8,9];
        }
    
        /**
        * Keep reading the value of the array from the state variable
        * Keep writing the result in the state variable sumArray
        * Bad idea
        * Gas cost = 83037 
        */
        function calculateSum() external {
            for(uint i = 0; i<arrayTest.length; i++){
                sumArray += arrayTest[i];
            }
        }
    
        /**
        * Keep reading the value of the array from the state variable
        * Caching the sum in the memory. Write only once in the state variable
        * Not an excellent idea
        * Gas cost = 58087 
        */
        function calculateSum2() external {
            uint _sumArray = 0;
            for(uint i = 0; i<arrayTest.length; i++){
                _sumArray += arrayTest[i];
            }
            sumArray = _sumArray;
        }
    
        /**
        * Caching the array in the memory
        * Caching the sum in the memory. Write only once in the state variable
        * Excellent idea
        * Gas cost = 56475 
        */
        function calculateSum3() external {
            uint _sumArray = 0;
            uint[] memory _arrayTest = arrayTest;
            for(uint i = 0; i<_arrayTest.length; i++){
                _sumArray += _arrayTest[i];
            }
            sumArray = _sumArray;
        }
    }
    

    도서관

    If you have many smart contracts using the same functionalities, you should extract all these functionalities to a library and deploy it.
    Now, all the smart contracts can reference the deployed library instead of deploying the same code again and again and increase the bytecode length.
    Because it's a deployed library, make sure that all your functions are external


    이벤트

    If you have data created on a smart contract but does not need to be read after by the smart contract, then events are a good candidate. Events consume less gas than a state variable.


    계산된 값

    Sometimes, you need to store values into the smart contract (hashes for example) and you need to compute this value. My advice is to compute this value off chain and instantiate your smart contract with the computed value. (Remember: The less OPCODE you have, the better optimisation you will get)


    상수

    You can have some constant values in your state variables that won't change. Constant values can be useful for gas optimisation.

    Here an example:

    • Read state variable with const: 21420 gas
    • Read state variable without const: 23541 gas

    (Try it on Remix)

    // SPDX-License-Identifier: GPL-3.0
    
    pragma solidity >=0.7.0 <0.9.0;
    
    contract GasTestConst {
        //Gas cost : 21420 
        address public constant CONST_ADDR = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    }
    
    contract GasTestNoConst {
        //Gas Cost: 23541
        address public addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    }
    

    You can say that immutable works the same as const . The only difference between immutable and const is that you can initialise the value of the immutable variable into the constructor.


    오류 처리

    When you use require , you pass the error reason if the condition is not met. But very long string cost more gas.
    Using a custom error cost less gas. For the example below, I put the same string.
    Remember that with custom error, you should log the value.

    The gas cost is just slightly different but after a while, it saves some money.

    (Try it on Remix)

    // SPDX-License-Identifier: GPL-3.0
    
    pragma solidity >=0.7.0 <0.9.0;
    
    contract GasTestError {
    
        error CustomError(string message);
    
        //Gas cost: 21977 
        function testRequire(uint _val) public pure {
           require(_val <= 2, "Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
           //process
        }
    
        //Gas cost: 21955 
        function testError(uint _val) public pure {
           if(_val > 2){
               revert CustomError("Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
           }
           //process
       }
    }
    
    

    결론



    이 목록은 완전하지 않습니다. 프록시 계약을 위해 EIP 1167을 추가할 수도 있지만 고급 기술을 설명하고 싶지는 않습니다. 이 10가지 팁을 시도하고 스마트 계약을 활용하십시오. 목표는 초보자를 위한 기본 최적화로 약간의 가스를 절약하는 것입니다.
    질문이 있으시면 아래에 댓글을 달아주세요 ;)

    좋은 웹페이지 즐겨찾기