| name | gas-optimization |
| description | Gas optimization techniques and patterns for efficient Solidity smart contracts. Use when optimizing contract gas costs or reviewing code for efficiency improvements. |
Gas Optimization Skill
This skill provides techniques, patterns, and best practices for optimizing gas costs in Solidity smart contracts.
When to Use
Use this skill when:
- Optimizing contract gas costs
- Reviewing code for efficiency improvements
- Preparing contracts for production deployment
- Reducing transaction costs for users
- Optimizing storage usage
- Improving loop efficiency
Important: Always prioritize security over gas optimization. Only optimize after ensuring correctness and security.
Gas Cost Basics
Operation Costs (Approximate)
| Operation | Gas Cost |
|---|---|
| Addition/Subtraction | 3 |
| Multiplication/Division | 5 |
| SLOAD (storage read) | 2,100 |
| SSTORE (storage write, zero to non-zero) | 20,000 |
| SSTORE (storage write, non-zero to non-zero) | 5,000 |
| SSTORE (storage write, non-zero to zero) | 15,000 refund |
| Memory expansion | Quadratic |
| LOG0-LOG4 | 375 + 375 * topics |
| CALL | 100-9,000 |
| CREATE | 32,000 |
Key Principles
- Storage is expensive - Minimize SLOAD/SSTORE
- Memory is cheap - Use for temporary data
- Calldata is cheapest - Use for read-only function parameters
- Smaller is better - Pack variables, use smaller types
- Batch operations - Combine multiple operations
- Avoid redundancy - Cache values, eliminate duplicate code
Storage Optimization
1. Variable Packing
Pack variables into 32-byte slots:
// ❌ Bad: Uses 3 storage slots (96 bytes)
contract Inefficient {
uint8 a; // slot 0 (wastes 31 bytes)
uint256 b; // slot 1
uint8 c; // slot 2 (wastes 31 bytes)
}
// ✅ Good: Uses 2 storage slots (64 bytes)
contract Efficient {
uint8 a; // slot 0
uint8 c; // slot 0 (packed)
uint256 b; // slot 1
}
// ✅ Better: Pack related variables
contract BetterPacking {
address owner; // 20 bytes - slot 0
uint96 balance; // 12 bytes - slot 0 (packed, total 32 bytes)
uint128 value1; // 16 bytes - slot 1
uint128 value2; // 16 bytes - slot 1 (packed, total 32 bytes)
}
Savings: ~15,000 gas per avoided storage slot
2. Use Smaller Types When Possible
// ❌ Unnecessary uint256 for small values
uint256 public percentage; // 0-100, wastes space
// ✅ Use appropriate size
uint8 public percentage; // 0-255, sufficient for percentages
// Timestamps
uint32 public timestamp; // Valid until year 2106
uint40 public timestamp; // Valid until year 36812
// Counters
uint32 public counter; // Supports 4.29 billion items
Warning: Smaller types don't save gas in function parameters or local variables, only in storage!
3. Caching Storage Variables
// ❌ Bad: Multiple storage reads (6,300 gas)
function bad() public view returns (uint256) {
return storageValue + storageValue + storageValue;
}
// ✅ Good: Cache in memory (2,103 gas)
function good() public view returns (uint256) {
uint256 cached = storageValue; // 1 SLOAD
return cached + cached + cached;
}
Pattern: Cache before loops:
// ❌ Bad: SLOAD in every iteration
function sumBad() public view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < items.length; i++) {
sum += multiplier * items[i]; // SLOAD multiplier each time
}
return sum;
}
// ✅ Good: Cache outside loop
function sumGood() public view returns (uint256) {
uint256 sum = 0;
uint256 _multiplier = multiplier; // Cache once
for (uint256 i = 0; i < items.length; i++) {
sum += _multiplier * items[i];
}
return sum;
}
4. Use Constants and Immutables
// ❌ Bad: Storage variable (2,100 gas per read)
uint256 public maxSupply = 1000000;
// ✅ Good: Constant (no storage, embedded in bytecode)
uint256 public constant MAX_SUPPLY = 1000000;
// ✅ Good: Immutable (set once in constructor, cheap to read)
address public immutable owner;
constructor() {
owner = msg.sender;
}
Savings:
constant: ~2,100 gas per read vs storageimmutable: ~2,000 gas per read vs storage
5. Short-Circuit Storage Updates
// ❌ Always writes (5,000-20,000 gas)
function setBad(uint256 newValue) public {
value = newValue;
}
// ✅ Only write if changed (saves gas when unchanged)
function setGood(uint256 newValue) public {
if (value != newValue) {
value = newValue;
}
}
6. Delete to Get Refunds
// Clear storage to get refund (15,000 gas)
delete balances[user]; // Sets to 0
// Or explicitly
balances[user] = 0;
Note: Refund is capped at 50% of gas used in transaction
Function Optimization
1. Function Visibility
// ❌ public functions cost more
function getData() public view returns (uint256) {
return data;
}
// ✅ external is cheaper (saves ~200 gas per call)
function getData() external view returns (uint256) {
return data;
}
Rule: Use external if function is only called externally
2. Payable Functions
// ❌ Non-payable has additional checks
function transfer(address to, uint256 amount) public {
// Costs extra to check msg.value == 0
}
// ✅ Payable skips check (saves ~24 gas)
function transfer(address to, uint256 amount) public payable {
// Only use if function legitimately accepts ETH
}
Warning: Only use payable if function should accept ETH!
3. Short-Circuit Conditions
// ✅ Put cheap checks first
require(msg.sender == owner && expensiveCheck(), "Failed");
// Expensive check only runs if sender is owner
4. Custom Errors
// ❌ String errors are expensive
require(balance >= amount, "Insufficient balance");
// ✅ Custom errors save gas (~50 gas)
error InsufficientBalance(uint256 balance, uint256 required);
if (balance < amount) {
revert InsufficientBalance(balance, amount);
}
Savings: ~50 gas per revert + deployment cost savings
Loop Optimization
1. Cache Array Length
// ❌ Bad: Reads length every iteration
for (uint256 i = 0; i < array.length; i++) {
// Process
}
// ✅ Good: Cache length
uint256 length = array.length;
for (uint256 i = 0; i < length; i++) {
// Process
}
Savings: ~100 gas per iteration for storage arrays
2. Unchecked Increments
// ❌ Checked increment
for (uint256 i = 0; i < length; i++) {
// Overflow check costs ~30 gas
}
// ✅ Unchecked increment (safe if bounds known)
for (uint256 i = 0; i < length;) {
// Process
unchecked {
++i; // Saves ~30 gas per iteration
}
}
3. ++i vs i++
// ✅ Prefix increment cheaper
for (uint256 i = 0; i < length; ++i) {
// Saves ~5 gas per iteration
}
// ❌ Postfix increment more expensive
for (uint256 i = 0; i < length; i++) {
// Costs extra gas
}
4. Avoid Storage Array Iteration
// ❌ Very expensive: Storage array iteration
uint256[] public items;
function sumBad() public view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < items.length; i++) {
sum += items[i]; // SLOAD every iteration
}
return sum;
}
// ✅ Better: Use mapping with counter
mapping(uint256 => uint256) public items;
uint256 public itemCount;
// ✅ Or load to memory first
function sumGood() public view returns (uint256) {
uint256[] memory _items = items; // Load once
uint256 sum = 0;
for (uint256 i = 0; i < _items.length; i++) {
sum += _items[i]; // Memory access
}
return sum;
}
Memory vs Calldata
1. Use Calldata for Read-Only Parameters
// ❌ memory copies data (expensive)
function processBad(uint256[] memory data) external {
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
sum += data[i];
}
}
// ✅ calldata reads directly (cheap)
function processGood(uint256[] calldata data) external {
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
sum += data[i];
}
}
Savings: Significant for large arrays (~50-100 gas per element)
2. Return Memory Efficiently
// ❌ Creates unnecessary array
function getBad() public view returns (uint256[] memory) {
uint256[] memory result = new uint256[](100);
// Fill array
return result;
}
// ✅ Return only needed data
function getGood(uint256 start, uint256 count) public view returns (uint256[] memory) {
uint256[] memory result = new uint256[](count);
// Fill with requested range
return result;
}
Data Structure Optimization
1. Mappings vs Arrays
// ✅ Mappings: O(1) access, good for sparse data
mapping(uint256 => User) public users;
// ✅ Arrays: Good for iteration, dense data
User[] public userArray;
Trade-offs:
- Mappings: Fast access, can't iterate, can't get length
- Arrays: Iterable, expensive to grow, costly iteration
2. Nested Mappings vs Structs
// ❌ Multiple mappings
mapping(address => uint256) public balances;
mapping(address => uint256) public rewards;
mapping(address => uint256) public stakes;
// ✅ Single mapping with struct
struct UserData {
uint256 balance;
uint256 reward;
uint256 stake;
}
mapping(address => UserData) public userData;
// Access: userData[user].balance (1 SLOAD for struct pointer)
3. Bit Packing for Booleans
// ❌ 8 booleans = 8 storage slots
bool public flag1;
bool public flag2;
// ... 8 total
// ✅ Use single uint256 with bit operations
uint256 public flags; // Can store 256 booleans
function setFlag(uint256 index, bool value) public {
if (value) {
flags |= (1 << index); // Set bit
} else {
flags &= ~(1 << index); // Clear bit
}
}
function getFlag(uint256 index) public view returns (bool) {
return (flags & (1 << index)) != 0;
}
Advanced Techniques
1. Assembly for Storage Access
// ✅ Assembly can be more efficient for storage
function getValueAsm(uint256 slot) public view returns (uint256 value) {
assembly {
value := sload(slot)
}
}
function setValueAsm(uint256 slot, uint256 value) public {
assembly {
sstore(slot, value)
}
}
Warning: Use assembly cautiously - easy to make mistakes!
2. Batch Operations
// ❌ Multiple transactions
function transferMultipleTx(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]);
}
}
// ✅ Single transaction with batch
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, "Length mismatch");
for (uint256 i = 0; i < recipients.length;) {
_transfer(msg.sender, recipients[i], amounts[i]);
unchecked { ++i; }
}
}
3. Events vs Storage
// ❌ Store historical data on-chain
uint256[] public historicalPrices;
// ✅ Emit events instead (much cheaper)
event PriceUpdated(uint256 price, uint256 timestamp);
function updatePrice(uint256 newPrice) external {
currentPrice = newPrice;
emit PriceUpdated(newPrice, block.timestamp);
}
Savings: Events are ~1/10 the cost of storage
4. Lazy Initialization
// ❌ Initialize all at once
constructor() {
for (uint256 i = 0; i < 100; i++) {
data[i] = initialValue;
}
}
// ✅ Initialize on demand
mapping(uint256 => uint256) private data;
bool private initialized;
function getData(uint256 index) public returns (uint256) {
if (!data[index]) {
data[index] = initialValue;
}
return data[index];
}
Compiler Optimization
1. Optimizer Settings
foundry.toml:
[profile.default]
optimizer = true
optimizer_runs = 200 # Balance deployment vs runtime cost
[profile.production]
optimizer_runs = 10000 # Optimize for runtime (contracts called often)
[profile.deployment]
optimizer_runs = 1 # Optimize for deployment (one-time contracts)
hardhat.config.js:
module.exports = {
solidity: {
version: "0.8.30",
settings: {
optimizer: {
enabled: true,
runs: 200
},
viaIR: true // Use IR-based compiler (can reduce gas further)
}
}
};
2. Solidity Version
// ✅ Use latest stable version for best optimizations
pragma solidity ^0.8.30;
Newer versions have better:
- Optimizer improvements
- Gas optimizations
- Built-in overflow checks
Gas Reporting
Foundry
# Gas report in tests
forge test --gas-report
# Gas snapshot
forge snapshot
# Compare snapshots
forge snapshot --diff
Hardhat
// hardhat.config.js
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: process.env.CMC_API_KEY
}
};
Common Anti-Patterns
❌ 1. Redundant Checks
// ❌ Redundant: SafeMath when using Solidity 0.8+
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
using SafeMath for uint256;
// ✅ Built-in overflow protection
uint256 result = a + b; // Reverts on overflow in 0.8+
❌ 2. String Concatenation
// ❌ Very expensive
string memory result = string(abi.encodePacked(str1, str2, str3));
// ✅ Avoid strings in contracts when possible
// Use events or off-chain concatenation
❌ 3. Storing Large Data
// ❌ Don't store large data on-chain
string[] public descriptions; // Each string costs thousands of gas
// ✅ Store hash and keep data off-chain
bytes32[] public descriptionHashes;
❌ 4. Unbounded Loops
// ❌ Can hit gas limit
function processAll() public {
for (uint256 i = 0; i < users.length; i++) {
// Process
}
}
// ✅ Add pagination
function processBatch(uint256 start, uint256 count) public {
uint256 end = start + count;
if (end > users.length) end = users.length;
for (uint256 i = start; i < end; i++) {
// Process
}
}
Optimization Checklist
Storage
- Variables packed into 32-byte slots
- Used smallest appropriate type for storage variables
- Constants used instead of storage where possible
- Immutables used for constructor-set values
- Cached storage variables accessed multiple times
- Unnecessary storage writes removed
- Deletions used to get gas refunds
Functions
- External used instead of public where possible
- Custom errors instead of string errors
- Short-circuit boolean conditions
- Payable used where appropriate (only if accepts ETH)
- Function visibility optimized
Loops
- Array length cached
- Unchecked increment used (where safe)
- ++i used instead of i++
- Storage access minimized in loops
- Unbounded loops avoided
Memory/Calldata
- Calldata used for read-only array parameters
- Memory vs storage trade-offs considered
- Unnecessary memory allocations removed
General
- Batch operations used where possible
- Events used instead of storage for historical data
- Compiler optimizer enabled
- Latest stable Solidity version used
- Gas report reviewed
Measurement and Benchmarking
Before Optimization
# Baseline gas report
forge test --gas-report > gas-before.txt
forge snapshot --snap baseline.snap
After Optimization
# Compare gas usage
forge test --gas-report > gas-after.txt
forge snapshot --diff baseline.snap
Gas Profiling
# Detailed gas profiling
forge test --gas-report -vvv
Trade-offs
Code Readability vs Gas
// More readable
if (condition1 && condition2) {
doSomething();
}
// More gas efficient but less clear
if (condition1) {
if (condition2) {
doSomething();
}
}
Recommendation: Prioritize readability unless gas savings are significant (>5%)
Deployment Cost vs Runtime Cost
// Cheaper deployment, expensive runtime
// Fewer optimizer runs (1-200)
// Expensive deployment, cheap runtime
// More optimizer runs (1000-10000)
Recommendation: Optimize for runtime if contract will be used frequently
Quick Reference
| Technique | Savings | Risk | Effort |
|---|---|---|---|
| Variable packing | High | Low | Low |
| Cache storage reads | High | Low | Low |
| Use constants | High | None | Low |
| External over public | Low | None | Low |
| Custom errors | Medium | None | Low |
| Unchecked increments | Low | Medium | Low |
| ++i over i++ | Low | None | Low |
| Calldata over memory | Medium | None | Low |
| Batch operations | High | Low | Medium |
| Assembly | High | High | High |
Remember: Security and correctness come first. Only optimize after ensuring your code is secure and functions correctly. Profile before optimizing to identify the most impactful changes.