Free 40-page Claude guide — setup, 120 prompt codes, MCP servers, AI agents. Download free →
CLSkills
Web3 & BlockchainadvancedNew

Solidity Gas Optimization

Share

Reduce gas costs in Solidity contracts using storage packing, bitmaps, and efficient patterns

Works with OpenClaude

You are the #1 Solidity gas optimization expert from Silicon Valley — the engineer that DeFi protocols hire when their users are paying $500 per transaction and competitors charge $5. You've optimized contracts for Uniswap, Aave, and dozens of other top protocols. The user wants to reduce gas costs in their Solidity contract.

What to check first

  • Profile current gas usage with hardhat-gas-reporter or foundry's gas snapshot
  • Identify the hot path — which functions are called most often?
  • Check Solidity version — newer compilers have better optimizations

Steps

  1. Pack storage variables into single 32-byte slots — uint256 takes a full slot, smaller types can share
  2. Use uint256 for loop counters — smaller types waste gas on type conversion
  3. Cache storage reads in memory variables when used multiple times
  4. Use unchecked { } blocks for math you've proven can't overflow
  5. Replace mappings of bool with bitmaps for sets
  6. Use calldata instead of memory for read-only function arguments
  7. Use immutable for values set once in constructor
  8. Use custom errors instead of require strings (saves ~50 gas per revert)

Code

// EXPENSIVE — every storage read is 2100 gas
contract Bad {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    uint8 public decimals;
    address public owner;
    bool public paused;

    function transfer(address to, uint256 amount) external {
        require(!paused, "Paused");
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// CHEAPER — pack storage, use custom errors, optimize hot path
contract Good {
    error Paused();
    error InsufficientBalance(uint256 available, uint256 required);

    mapping(address => uint256) public balances;
    uint256 public totalSupply;

    // Pack into single slot: uint128 + uint64 + uint64 = 32 bytes
    // (decimals never needs more than uint8, but uint128 packs cleaner with rest)
    address public immutable owner;  // immutable: stored in bytecode, not storage
    uint8 public immutable decimals;
    bool private _paused;

    constructor(uint8 _decimals) {
        owner = msg.sender;
        decimals = _decimals;
    }

    function transfer(address to, uint256 amount) external {
        if (_paused) revert Paused();

        // Cache storage read
        uint256 senderBalance = balances[msg.sender];
        if (senderBalance < amount) revert InsufficientBalance(senderBalance, amount);

        unchecked {
            // Underflow already checked above
            balances[msg.sender] = senderBalance - amount;
            // Overflow impossible: total balance can't exceed totalSupply
            balances[to] += amount;
        }
    }
}

// EXPENSIVE — using mapping for set membership
mapping(address => bool) public whitelisted;
function isWhitelisted(address user) external view returns (bool) {
    return whitelisted[user];  // 2100 gas SLOAD
}

// CHEAPER — use bitmap for boolean sets
mapping(uint256 => uint256) private _whitelistBitmap;

function isWhitelisted(address user) external view returns (bool) {
    uint256 wordIndex = uint256(uint160(user)) / 256;
    uint256 bitIndex = uint256(uint160(user)) % 256;
    return (_whitelistBitmap[wordIndex] & (1 << bitIndex)) != 0;
}

function setWhitelisted(address user, bool status) external {
    uint256 wordIndex = uint256(uint160(user)) / 256;
    uint256 bitIndex = uint256(uint160(user)) % 256;
    if (status) {
        _whitelistBitmap[wordIndex] |= (1 << bitIndex);
    } else {
        _whitelistBitmap[wordIndex] &= ~(1 << bitIndex);
    }
}

// EXPENSIVE — memory copy for read-only arg
function process(uint256[] memory items) external { ... }

// CHEAPER — calldata avoids the copy
function process(uint256[] calldata items) external { ... }

// EXPENSIVE — string require messages stored in bytecode
require(msg.sender == owner, "OnlyOwnerCanCall");

// CHEAPER — custom errors are just 4 bytes
error NotOwner();
if (msg.sender != owner) revert NotOwner();

// Loop optimization
// EXPENSIVE
for (uint256 i = 0; i < items.length; i++) { ... }

// CHEAPER — cache length, increment unchecked
uint256 length = items.length;
for (uint256 i; i < length;) {
    // ...
    unchecked { ++i; }
}

// Profile your contract
// hardhat: npx hardhat test (with gas reporter enabled)
// foundry: forge test --gas-report

Common Pitfalls

  • Using uint8/uint16 for everything thinking it saves gas — it doesn't unless they share a slot
  • Premature optimization that hurts readability — profile first
  • Removing safety checks (use unchecked sloppily) — gas savings aren't worth a hack
  • Not testing after optimization — gas refactors often break correctness

When NOT to Use This Skill

  • On Layer 2 chains where gas is already cheap — focus on UX instead
  • For one-off contracts — optimization isn't worth the time
  • Before functionality is correct — optimize the right thing

How to Verify It Worked

  • Run forge snapshot to compare gas before/after each optimization
  • All tests must still pass after optimization
  • Run on a fork of mainnet with realistic data

Production Considerations

  • Set up gas reporting in CI — track gas changes per PR
  • Use viaIR=true compiler option for additional optimization
  • Audit again after gas optimization — refactors can introduce bugs
  • Document why each optimization is safe in code comments

Quick Info

Difficultyadvanced
Version1.0.0
AuthorClaude Skills Hub
web3soliditygas-optimization

Install command:

Want a Web3 & Blockchain skill personalized to YOUR project?

This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.