Hashtag Web3 Logo

Gas Optimization Patterns

8 min
advanced

Why Gas Costs Matter

Every smart contract operation costs gas. Users pay for gas in ETH. The difference between a well-optimized and poorly-optimized contract can be $5 vs. $50 per transaction. Across millions of users, this determines whether a protocol succeeds or gets abandoned.

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

The single most impactful optimization. Storage reads (SLOAD) cost 2,100 gas. Memory reads cost 3 gas.

// 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;
}

This matters most inside loops. If you read array.length from storage on every iteration, you pay 2,100 gas per loop.

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
}

Only use unchecked when you have a mathematical proof that overflow cannot occur. For a loop counter bounded by an array length, this is always safe.

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

TechniqueGas SavedDifficulty
Cache storage in memory2,000+ per extra readEasy
Pack struct variables20,000 per saved slotEasy
Custom errors over strings200+ per revertEasy
unchecked arithmetic100 per operationMedium
calldata over memory100+ per array elementEasy
Short-circuit && / `\\`VariableEasy

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?