Solidity – Best Practices for Optimizing Gas Costs in Solidity Smart Contracts

contract-developmentgasgas-pricesolidity

I'm working on a Solidity smart contract and I'm concerned about the gas costs associated with its execution. I want to optimize my contract for gas efficiency to save on transaction fees and improve overall performance.

Here are some specific questions I have:

Are there any best practices for reducing gas costs in Solidity?

What are the common gas-consuming operations or coding patterns to avoid?

Are there any tools or techniques to estimate and analyze gas usage in my contracts?

How can I optimize storage and memory usage in my Solidity code to minimize gas consumption?

Are there any recent updates or changes in the Solidity language that affect gas optimization strategies?
I'd appreciate any insights, tips, or code examples from experienced Solidity developers who have successfully optimized their contracts for gas efficiency. Thank you!

Best Answer

I think my answer, in one way or another, answers all five of your specific questions.

There are a handful of commonly known practices to avoid to keep gas costs low and/or prevent transactions from running out of gas. Here are three examples:

Developers usually try to stray away from using and looping through dynamic arrays since the transaction will fail with out-of-gas if the size of the array becomes too large.

 function dynamicArray(uint input) public {
        uint[] memory bigArray = new uint[](1e6);
        for(uint i; i < bigArray.length; i++) {
            if(bigArray[i] == input) {
                x = 7;
            }
        }
    }

In this example I created an array with 1e6 indices, then I tried to loop through the array to find a value that matched the input. The issue is that this transaction cost quite a bit of gas and with default values will likely always fail.

enter image description here

Ever since solidity version 0.8.4 was released, using custom errors over require statements with hard-coded string error messages is cheaper:

/**@notice showcases gas cost of simple custom error 
      * COLD SUCCESS: gas: 50335 gas, transaction cost: 43769 gas, execution cost: 22565 gas 
      * HOT SUCCESS: gas: 27450 gas, transaction cost: 23869 gas, execution cost: 2665 gas
      * FAIL: gas: 3000000 gas, transaction cost: 21688 gas, execution cost: 484 gas 
    */
    function customError(uint input) public {
        if(input != 7) {
            revert GasOptimization__InputMustBeSeven();
        }
        x = input;
    }

    /**@notice showcases gas cost of simple require statement 
      * COLD SUCCESS: gas: 50360 gas, transaction cost: 43791 gas, execution cost: 22587 gas
      * HOT SUCCESS: gas: 27475 gas, transaction cost: 23891 gas, execution cost: 2687 gas 
      * FAIL: 3000000 gas, transaction cost 21964 gas, execution cost 760 gas 
    */
    function requireStatement(uint input) public {
        require(input == 7, "InputMustBeSeven");
        x = input;
    }

As you can see, using the require is more slightly more expensive than the custom error in every scenario.

Finally, one more example that saves gas is to store a local variable in memory if you need to access its value more than once, since you aren't reading from storage multiple times, it is cheaper. You can learn more about the differences of memory and storage here in this Medium article and here in this YouTube video.

/**@notice showcases gas cost of reading storage variable multiple times 
    gas: 30026 gas transaction cost: 26109 gas execution cost: 5045 gas 
    */
    function teamStorage() public returns(bool) {
       if(x == 7) {
           y = 7;
       }
       if(y == 7) {
           x = 7;
       }
       if(x + y == 14) {
              return true;
          } else {
              return false;
          }
    }


    /**@notice showcases saving storage variable to memory before reading 
    gas: 29800 gas transaction cost: 25913 gas execution cost: 4849 gas
    */
    function teamMemory() public returns(bool){
        uint xlocal = x;
        uint ylocal = y;
          if(xlocal == 7) {
           y = 7;
          }
          if(ylocal == 7) {
           x = 7;
          }
          if(xlocal + ylocal == 14) {
              return true;
          } else {
              return false;
          }
    }

You can ignore the actual logic in the above functions, the only thing worth noting in these two functions is that while teamStorage() reads the values of x and y twice each, teamMemory() only reads the values of x and y from storage once.

There is a massive amount of different optimization techniques you can use, likewise, there are plenty of educational resources on this topic that you can explore further. Here is a quick list:

  1. Alchemy gas optimzation guide
  2. Certik medium article
  3. Uniswap gas optimzation guide
  4. Repo filled with gas optimization resources
  5. Art of Gas Optimization blog post
  6. Yamen Mehri Medium article about gas optimization
  7. Harrison's twitter, no seriously, this guy hates gas

One tool that I've found useful in keeping track of gas costs in Hardhat is Hardhat-gas-reporter.

Foundry comes with gas reporting built in and has really well-written documentation on it here and here.

Related Topic