Solidity Gas Optimization Guide

repository·main·Indexed 41 days ago

https://github.com/harendra-shakya/solidity-gas-optimization

A collection of EVM gas optimization tricks and resources for Solidity developers. Covers best practices for storage usage, variable packing, data type selection, function call optimization, logical short-circuiting, and low-level Yul/Assembly techniques to write more efficient smart contracts.

Tokens
2.4K
Snippets
4
Records
13
Agent score
2%

What's inside Solidity Gas Optimization

  1. Optimize Inheritance and Variable Packing

    main

    When extending a contract, variables in the child contract can be packed with variables in the parent contract.

    Variables are ordered based on C3 linearization, meaning child variables are placed after parent variables in the storage layout. Designing your inheritance hierarchy with this in mind allows for better variable packing across the hierarchy.

  2. Manage Memory vs Storage efficiently

    main

    Moving data between memory and storage incurs gas costs.

    • Avoid copying arrays: Instead of copying an entire array from storage to memory, use a storage pointer to reference the data.
    • Memory costs: Memory allocation is not linear; you "buy" it in chunks, and the cost increases quadratically as more memory is used.
    • Keyword optimization: Experiment with storage and memory keywords. Because memory costs vary, switching a variable's location might improve or degrade performance depending on the number of copying operations performed.
  3. Claim gas refunds via storage and selfdestruct

    main

    You can reclaim gas through specific operations:

    • Zeroing variables: Freeing a storage slot by zeroing out a variable when it is no longer needed can refund 15,000 gas.
    • Selfdestruct: Using the selfdestruct opcode to remove a contract refunds 24,000 gas.

    Note: A refund must not exceed half of the gas used by the ongoing contract call.

  4. Optimize Function calls and Loops

    main

    Functions

    • Prefer external: external functions receive parameters as calldata, avoiding the gas cost of copying them to memory. This is especially important for dynamic strings or arrays.
    • Function Ordering: Each function position has an extra 22 gas cost. Put frequently called functions earlier in the contract.
    • Minimize Parameters: Large input data increases gas costs due to memory allocation.
    • Payable functions: payable functions can be slightly cheaper than non-payable ones because the compiler skips certain checks.
    • Modifiers vs Functions: Solidity modifiers increase contract code size; sometimes it is more efficient to use a regular function instead.
    • Fallback Functions: Fallback (or sig-less) functions are cheaper than regular function calls because they don't require a function signature.

    Loops

    • Use memory variables: Perform loop operations on memory variables rather than storage variables.
    • Avoid unbounded loops: Unbounded loops can lead to out-of-gas errors and high costs.
    • Prefix increment: Using ++i (prefix) can be slightly cheaper than i++ (postfix) in some contexts.
  5. Optimize Variables and Mappings

    main

    Variable Best Practices

    • Avoid public variables: They automatically generate a getter function which costs gas.
    • Use private for globals: Using global variables with private visibility can save gas.
    • Use Events: Instead of storing every piece of data in state, use events to emit data.
    • Named Return Values: Naming the return values of a function allows you to avoid creating unnecessary local variables.

    Mappings vs Arrays

    • Use Mappings: Mappings are generally cheaper than arrays for lookups.
    • Use Arrays for small sets: If the dataset is very small, an array might be a viable alternative.

    Fixed vs Dynamic

    • Fixed size is cheaper: Fixed-size variables (and fixed-size memory arrays) are always more gas-efficient than dynamic ones.
    • Array growth: If you must use dynamic arrays, structure functions to be additive (extending) rather than subtractive (truncating). Extending an array costs constant gas, while truncating costs linear gas.
  6. Optimize Storage usage and gas costs

    main

    Storage is one of the most expensive areas in Solidity. Understanding the gas costs for different operations helps in writing efficient contracts:

    • Saving a variable in storage: ~20,000 gas.
    • Rewriting a variable: ~5,000 gas.
    • Reading from a slot: ~200 gas.
    • Variable declaration: Costs nothing if no initialization is performed.

    Best Practices

    • Use Memory for calculations: Always save a storage variable into a memory variable within a function if you need to perform multiple operations on it. Calculate everything in memory first, then update the storage variable once.
    • Variable Packing: Organize and pack multiple storage variables (including those within structs) into a single 32-byte slot to reduce the number of slots used.
    • Avoid Zero-Value Initialization: Instead of uint256 index = 0;, use uint256 index;. Since uint256 defaults to 0, you avoid the gas cost of explicit initialization.
    • Use Constants: Make Solidity values constant whenever possible.
    // Instead of this:
    uint256 index = 0;
    
    // Do this:
    uint256 index;
  7. Inspect Solidity Yul output with solc

    main

    To understand how the Solidity compiler translates your code into Yul and to verify if your manual assembly is actually more efficient than the compiler's output, use the following solc compiler options:

    • -yul: View the Yul output.
    • -ir: View the intermediate representation.
    solc --yul --ir your_contract.sol
  8. Optimize data types and variable packing

    main

    How you define data types significantly impacts gas consumption:

    • Use bytes32: This is the most optimized storage type. It is cheaper than using string. If a string is short enough, cast it to bytes32.
    • Minimize byte length: If the length of bytes can be limited, use the smallest possible size (e.g., bytes1 to bytes32).
    • Packing logic: Variable packing only occurs in storage. You will not save gas by trying to pack function arguments or local variables in memory or calldata.
    • Small integers: Storing a single small number in a uint8 is not cheaper than a uint256 because it still occupies a full 32-byte slot. Savings only occur when you pack multiple small variables into a single 32-byte slot.
    contract StringToBytes32 {
        bytes32 public fixedSizeString;
    
        function setFixedSizeData(string memory str) public {
            require(bytes(str).length <= 32, "String exceeds 32 bytes.");
            fixedSizeString = bytes32(bytes(str));
        }
    }
  9. Optimize Logical Operations and Arithmetic

    main

    Short-Circuiting

    Use the rules of logical disjunction (||) and conjunction (&&) to your advantage by ordering functions from cheapest to most expensive:

    • In || (OR): Place the function most likely to be true (and the cheapest) first. If the first is true, the second is never executed.
    • In && (AND): Place the function most likely to be false (and the cheapest) first. If the first is false, the second is never executed.

    Arithmetic

    • Use unchecked: For arithmetic operations where you are certain no overflow or underflow can occur, wrap the code in an unchecked block to skip the default Solidity 0.8.0+ safety checks and save gas.
  10. Optimize gas using Yul and Assembly tricks

    main

    When writing low-level Yul/Assembly code, use these techniques to minimize gas costs:

    • Access Lists: Call eth_createAccessList on a node (like Geth) to generate an access list. Including this list in your transaction blob can save gas, especially when writing to many storage slots.
    • Memory Management: Since Solidity doesn't garbage collect, overwrite unused values in memory/storage with new values to conserve space.
    • Calldata vs Memory: Keep data in calldata whenever possible. If a function parameter is not modified, read it directly from calldata instead of copying it to memory.
    • Vanity Addresses: Use addresses with many leading zeroes (e.g., 0x0000000000f38210) to pack multiple addresses into a single storage slot.
    • Storage Slot Reuse: Writing to an existing storage slot is cheaper than using a new one. Avoid emptying a slot if you plan to refill it later (refer to EIP-2200).
    • Gas Forwarding: When using call() in Yul, use gas() as the gas parameter to forward all available gas instead of manually calculating it.
    • Zero Address Checks: In NFT implementations, many zero-address checks are unnecessary unless the 0 address has unique permissions or can call the contract.
    • Overflow Protection: In Solidity, use unchecked {} blocks for counters or operations that cannot overflow without reaching uint256(-1) to avoid the cost of Safemath-style checks.
  11. Reference: Hash Function Gas Costs

    main

    When choosing a hash function, consider the gas overhead:

    • keccak256: 30 gas + 6 gas per word of input.
    • sha256: 60 gas + 12 gas per word of input.
    • ripemd160: 600 gas + 120 gas per word of input.

    Recommendation: Use keccak256 unless you have a specific requirement for another function.

    keccak256: 30 gas + 6 gas for each word of input data
    sha256: 60 gas + 12 gas for each word of input data
    ripemd160: 600 gas + 120 gas for each word of input data