Hashtag Web3 Logo

Gas Optimization Patterns

8 min
advanced

Why Gas Costs Matter

Gas use affects transaction costs. Measure it for representative inputs and state, and keep correctness tests in place when changing an implementation.

EVM Operation Gas Costs SSTORE (storage write) 20,000 gas SLOAD (storage read) 2,100 gas MLOAD (memory read) 3 gas ← Cache storage reads in memory to save 700x per access Bar width proportional to gas cost

1. Cache Storage Reads

Repeated storage reads may be worth caching. Costs differ for cold and warm accesses, and compiler optimizations can already remove some repeated work.

// BAD: 3 storage reads = 6,300 gas
function bad_getTotal() public view returns (uint256) {
 return balances[msg.sender] + balances[msg.sender] + balances[msg.sender];
}

// GOOD: 1 storage read + 2 memory reads = 2,106 gas
function good_getTotal() public view returns (uint256) {
 uint256 bal = balances[msg.sender]; // cache in memory
 return bal + bal + bal;
}

Inspect the generated code or a gas report before assuming that each loop iteration performs the same storage read at the same cost.

2. Pack Storage Variables

Each storage slot is 32 bytes. Solidity packs adjacent variables into the same slot if they fit.

// BAD: Uses 3 storage slots (3 × 20,000 gas to write)
struct BadUser {
 uint256 id; // slot 0 (32 bytes)
 uint8 level; // slot 1 (1 byte, but takes a full slot)
 uint256 balance; // slot 2 (32 bytes)
}

// GOOD: Uses 2 storage slots (2 × 20,000 gas to write)
struct GoodUser {
 uint256 id; // slot 0 (32 bytes)
 uint256 balance; // slot 1 (32 bytes)
 uint8 level; // slot 1 (packed with balance? No - but...)
}

// BEST: Uses 2 storage slots
struct BestUser {
 uint8 level; // slot 0 (1 byte)
 address wallet; // slot 0 (20 bytes) ← packed together = 21 bytes
 uint256 balance; // slot 1 (32 bytes)
}

Rule: put smaller types next to each other. uint8 + address (1 + 20 = 21 bytes) fit in one slot.

3. Use Custom Errors

Introduced in Solidity 0.8.4. Error strings are stored as bytecode - every character costs gas at deployment and at runtime.

// BAD: String stored in bytecode
require(balance >= amount, "ERC20: transfer amount exceeds balance");

// GOOD: Compiles to 4-byte selector
error InsufficientBalance(uint256 available, uint256 required);

function transfer(address to, uint256 amount) public {
 if (balances[msg.sender] < amount) {
 revert InsufficientBalance(balances[msg.sender], amount);
 }
 // ...
}

Saves ~200 gas per revert on average, plus deployment gas proportional to string length.

4. Use unchecked for Safe Arithmetic

Solidity 0.8+ automatically checks for overflow/underflow on every arithmetic operation. Each check costs ~100 gas. When you know overflow is impossible, wrap the operation in unchecked.

// Common pattern: loop counter can never overflow
for (uint256 i = 0; i < length;) {
 // ... do work ...
 unchecked { ++i; } // saves ~100 gas per iteration
}

Use unchecked only after establishing that the particular operation cannot overflow in its reachable states. Check the loop condition, counter type, and update operation rather than apply it to every loop.

5. Use calldata Instead of memory for Read-Only Arrays

When a function receives an array it doesn't modify, use calldata instead of memory. This avoids copying the array into memory.

// BAD: Copies the entire array into memory
function sum(uint256[] memory values) public pure returns (uint256) { ... }

// GOOD: Reads directly from the transaction data
function sum(uint256[] calldata values) public pure returns (uint256) { ... }

For a 100-element array, this saves ~10,000 gas.

Quick Reference

Technique Gas Saved Difficulty
Cache storage in memory 2,000+ per extra read Easy
Pack struct variables 20,000 per saved slot Easy
Custom errors over strings 200+ per revert Easy
unchecked arithmetic 100 per operation Medium
calldata over memory 100+ per array element Easy
Short-circuit && / || Variable Easy

Key takeaways

  • Storage operations are by far the most expensive EVM operations. Cache storage reads in memory variables.
  • Order struct fields by size to pack them into fewer 32-byte storage slots.
  • Custom errors save both deployment and runtime gas compared to string error messages.
  • Use unchecked arithmetic only when you can prove overflow is impossible.
  • Measure before and after. Use forge test --gas-report or Hardhat's gas reporter to quantify savings.

Quiz: Gas Optimization Patterns

1 / 5

Why is gas optimization important for smart contracts?