Gas Optimization Patterns
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.
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
| 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
uncheckedarithmetic only when you can prove overflow is impossible. - Measure before and after. Use
forge test --gas-reportor Hardhat's gas reporter to quantify savings.
Quiz: Gas Optimization Patterns
1 / 5Why is gas optimization important for smart contracts?