Gas Optimization

27.1 Principles

Storage is the dominant cost — minimize SSTORE/SLOAD.

Correctness and security come first; never trade safety for a few gas.

Measure with forge test --gas-report, forge snapshot, hardhat-gas-reporter — don't guess.

27.2 Techniques

Example / note constant / immutable avoid storage reads for fixed values Cache storage in memory/stack

uint256 bal = balances[u]; then use bal small types adjacent; structs ≤ 32 bytes when read together calldata instead of memory for external read-only array/string params instead of revert strings where overflow is impossible (proven bounds) Short-circuit cheap checks first if (amount == 0 || paused) revert... Avoid unbounded loops use pagination / pull patterns Events instead of storage for data only needed off-chain

external over public for large calldata historically cheaper; minor today store 256 booleans in one uint256 (e.g., airdrop claims) mapping over array for lookups reentrancy locks and per-tx flags at 100 gas instead of 2,900+

runs ≈ expected call count; higher = cheaper calls, bigger code often smaller/cheaper code; slower compile Avoid zero→nonzero writes keep a non-zero "unset" value (e.g., reentrancy lock 1/2 instead of 0/1) multicall, batched mints (ERC-721A) Clones / CREATE2 cheap per-user contract deployment

27.3 Before / after

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

contract Expensive {
    uint256[] public values;
    uint256 public total;

    function sumAll() external {
        for (uint256 i = 0; i < values.length; i++) {   // reads values.length from storage each time
            total += values[i];                          // SLOAD + SSTORE every iteration
        }
    }
}

contract Cheaper {
    uint256[] public values;
    uint256 public total;

    function sumAll() external {
        uint256[] memory v = values;       // one copy (still n SLOADs, but no repeated length reads)
        uint256 len = v.length;
        uint256 t = total;                 // cache
        for (uint256 i; i < len; ++i) {    // 0.8.22+: increment is auto-unchecked
            t += v[i];
        }
        total = t;                         // single SSTORE
    }
}

27.4 Bitmap example

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

contract ClaimBitmap {
    mapping(uint256 => uint256) private claimedWords;

    function isClaimed(uint256 index) public view returns (bool) {
        uint256 word = claimedWords[index >> 8];         // index / 256
        uint256 mask = 1 << (index & 0xff);              // index % 256
        return word & mask != 0;
    }

    function _setClaimed(uint256 index) internal {
        claimedWords[index >> 8] |= 1 << (index & 0xff);
    }
}

27.5 Gas-saving myths and caveats

++i vs i++: negligible with modern optimizers.

!= 0 vs > 0 for unsigned ints: no difference with optimizer in recent versions.

Making functions payable to save the callvalue check saves ~20 gas but is a footgun — avoid in user-facing APIs.

Assembly can save gas but removes safety checks; use only in audited, hot paths.