334 lines
7.2 KiB
Markdown
334 lines
7.2 KiB
Markdown
# 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:
|
|
|
|
```solidity
|
|
// 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:
|
|
|
|
```solidity
|
|
// 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:**
|
|
|
|
```solidity
|
|
// 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:**
|
|
|
|
```solidity
|
|
// 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):
|
|
|
|
```solidity
|
|
// 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:
|
|
|
|
```solidity
|
|
// Expensive check last (short-circuits if first fails)
|
|
if (amount > 0 && expensiveValidation(amount)) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Bitwise Flag Packing
|
|
|
|
Combine booleans into single uint256:
|
|
|
|
```solidity
|
|
// 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
|
|
|
|
```solidity
|
|
// 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
|
|
|
|
```solidity
|
|
// 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
|
|
|
|
```solidity
|
|
// 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:
|
|
|
|
```solidity
|
|
// 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
|
|
|
|
```toml
|
|
# 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
|
|
|
|
```bash
|
|
# Generate gas report
|
|
forge test --gas-report
|
|
|
|
# Filter by contract
|
|
forge test --match-contract MyContract --gas-report
|
|
```
|
|
|
|
### Gas Snapshots
|
|
|
|
```bash
|
|
# Create snapshot
|
|
forge snapshot
|
|
|
|
# Compare against previous
|
|
forge snapshot --diff
|
|
|
|
# Check threshold
|
|
forge snapshot --check --tolerance 5
|
|
```
|
|
|
|
### Inline Gas Measurement
|
|
|
|
```solidity
|
|
function testGasUsage() public {
|
|
uint256 gasBefore = gasleft();
|
|
|
|
// Code to measure
|
|
contract.doSomething();
|
|
|
|
uint256 gasUsed = gasBefore - gasleft();
|
|
console.log("Gas used:", gasUsed);
|
|
}
|
|
```
|
|
|
|
### Section Snapshots
|
|
|
|
```solidity
|
|
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 |
|