Files
gh-tenequm-claude-plugins-f…/skills/skill/references/gas-optimization.md
2025-11-30 09:01:14 +08:00

7.2 KiB

Solidity Gas Optimization Guide

Comprehensive gas optimization techniques for Solidity 0.8.30 with Foundry profiling.

Storage Optimization

Variable Packing

Pack multiple variables into single 32-byte storage slots:

// BAD: 3 storage slots (3 SSTORE operations)
contract Unoptimized {
    uint256 amount;      // slot 0
    uint8 status;        // slot 1
    address owner;       // slot 2
}

// GOOD: 2 storage slots
contract Optimized {
    uint128 amount;      // slot 0 (16 bytes)
    uint96 lockTime;     // slot 0 (12 bytes)
    uint8 status;        // slot 0 (1 byte)
    address owner;       // slot 1 (20 bytes)
}

Savings: 15,000+ gas per packed write

Struct Packing

Pack struct fields in declaration order:

// BAD: 3 slots
struct BadPacking {
    uint256 a;           // slot 0
    uint8 b;             // slot 1
    uint256 c;           // slot 2
}

// GOOD: 2 slots
struct GoodPacking {
    uint256 a;           // slot 0
    uint128 c;           // slot 1 (16 bytes)
    uint8 b;             // slot 1 (1 byte)
}

Storage vs Memory vs Calldata

Gas Costs:

Operation Gas
SLOAD (cold) 2,100
SLOAD (warm) 100
SSTORE (0→non-0) 20,000
SSTORE (non-0→non-0) 5,000
MLOAD/MSTORE 3 per word
Calldata read 3 per byte

Cache storage reads:

// BAD: 3 SLOAD operations
function process() external returns (uint256) {
    return value * 2 + value * 3 + value * 4;
}

// GOOD: 1 SLOAD + memory operations
function process() external returns (uint256) {
    uint256 v = value;
    return v * 2 + v * 3 + v * 4;
}

Use calldata for external function arrays:

// BAD: Copies to memory
function batchProcess(uint256[] memory values) external { }

// GOOD: Reads directly from calldata
function batchProcess(uint256[] calldata values) external {
    for (uint256 i; i < values.length; ) {
        // Process values[i]
        unchecked { i++; }
    }
}

Arithmetic Optimization

Unchecked Blocks

Solidity 0.8+ adds overflow checks by default (~50-100 gas per operation):

// BAD: Overflow check on every increment
for (uint256 i = 0; i < 100; i++) {
    result += i;
}

// GOOD: Unchecked increment (safe when bounded)
for (uint256 i = 0; i < 100; ) {
    result += i;
    unchecked { i++; }
}

Savings: 50-100 gas per iteration

Short-Circuit Logic

Order conditions by likelihood to fail:

// Expensive check last (short-circuits if first fails)
if (amount > 0 && expensiveValidation(amount)) {
    // ...
}

Bitwise Flag Packing

Combine booleans into single uint256:

// BAD: 4 storage slots
bool isActive;
bool isVerified;
bool isPaused;
bool isBlocked;

// GOOD: 1 storage slot, bit manipulation
uint256 flags;

function isActive() view returns (bool) {
    return (flags & (1 << 0)) != 0;
}

function setActive(bool _active) {
    if (_active) flags |= (1 << 0);
    else flags &= ~(1 << 0);
}

Variable Declaration

Immutable vs Constant vs Storage

// Storage variable: 2,100 gas (cold read)
uint256 public maxSupply = 1_000_000;

// Constant: ~3 gas (embedded in bytecode)
uint256 public constant MAX_SUPPLY = 1_000_000;

// Immutable: ~100 gas (set once in constructor)
uint256 public immutable maxSupply;
constructor(uint256 _max) { maxSupply = _max; }

Use constant for compile-time values, immutable for constructor-set values.

Function Visibility

// Public creates getter (extra dispatch)
uint256 public value;

// External is cheaper for functions not called internally
function getValue() external view returns (uint256) {
    return _value;
}

Error Handling

Custom Errors vs Require Strings

// BAD: String stored in bytecode (~50+ gas)
require(msg.sender == owner, "Unauthorized access");

// GOOD: Custom error (~24 gas)
error Unauthorized();
if (msg.sender != owner) revert Unauthorized();

Savings: 20-50 gas per revert

Events vs Storage

Store transient data in events instead of contract storage:

// BAD: 20,000+ gas per write
uint256[] public transfers;
function recordTransfer(uint256 amount) external {
    transfers.push(amount);
}

// GOOD: ~375 gas base + 8 gas per byte
event Transfer(address indexed to, uint256 amount);
function recordTransfer(address to, uint256 amount) external {
    emit Transfer(to, amount);
}

Compiler Settings

# foundry.toml
[profile.default]
optimizer = true
optimizer_runs = 200     # Balance size/runtime

[profile.production]
optimizer = true
optimizer_runs = 1000000 # Optimize for runtime
via_ir = true            # IR pipeline (slower compile, better optimization)

Guidance:

  • 200 runs: Smaller deployment, higher runtime cost
  • 10000+ runs: Larger deployment, lower runtime cost
  • via_ir = true: Best optimization, slowest compilation

EVM Opcode Reference

Operation Gas Notes
ADD/SUB/MUL 3 Basic arithmetic
DIV/MOD 5 Division operations
SLOAD (cold) 2,100 First storage read
SLOAD (warm) 100 Subsequent reads
SSTORE (0→non-0) 20,000 New storage slot
SSTORE (non-0→non-0) 5,000 Update existing
SSTORE (→0) 5,000 +15,000 refund
CALL 700+ External call base
KECCAK256 30 +6 per word
LOG0-LOG4 375-1,875 Events

Foundry Gas Profiling

Gas Reports

# Generate gas report
forge test --gas-report

# Filter by contract
forge test --match-contract MyContract --gas-report

Gas Snapshots

# Create snapshot
forge snapshot

# Compare against previous
forge snapshot --diff

# Check threshold
forge snapshot --check --tolerance 5

Inline Gas Measurement

function testGasUsage() public {
    uint256 gasBefore = gasleft();

    // Code to measure
    contract.doSomething();

    uint256 gasUsed = gasBefore - gasleft();
    console.log("Gas used:", gasUsed);
}

Section Snapshots

function testOptimization() public {
    vm.startSnapshotGas("operation");

    // Code to profile
    value = 1;

    uint256 gasUsed = vm.stopSnapshotGas();
}

Optimization Checklist

Storage:

  • Pack related variables together
  • Use smaller integer types when possible
  • Cache frequently accessed storage variables
  • Use mappings instead of arrays for lookups
  • Use immutable/constant for fixed values

Functions:

  • Mark external (not public) when not called internally
  • Use calldata for array parameters
  • Use unchecked blocks for safe arithmetic
  • Short-circuit expensive conditions
  • Use custom errors instead of require strings

Compilation:

  • Enable optimizer
  • Set appropriate optimizer_runs
  • Consider via_ir for production

Testing:

  • Use forge test --gas-report regularly
  • Create gas snapshots for regression detection
  • Profile critical functions

Quick Wins

Pattern Gas Saved Effort
Custom errors 20-50/revert Low
Unchecked loops 50-100/iter Low
Calldata vs memory 5,000+ Low
Variable packing 15,000/write Low
Immutable vars 2,000/read Low
Bitwise flags 15,000 Medium
Events vs storage 19,000+ Medium