7.2 KiB
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:
200runs: Smaller deployment, higher runtime cost10000+runs: Larger deployment, lower runtime costvia_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-reportregularly - 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 |