Gas optimization is a key skill in Solidity development that directly affects contract execution costs and user experience. Here are systematic Gas optimization strategies and techniques.
1. Storage Optimization
Storage is the most expensive resource, and optimizing storage usage is the primary task of Gas optimization.
Using Appropriate Data Types
soliditycontract StorageOptimization { // Not recommended: using uint256 for small values uint256 public smallValue; // Occupies 32 bytes // Recommended: use smaller data types uint128 public value128; // Occupies 16 bytes uint64 public value64; // Occupies 8 bytes uint32 public value32; // Occupies 4 bytes uint8 public value8; // Occupies 1 byte // Best: pack storage variables uint128 public balance; // slot 0: 16 bytes uint32 public timestamp; // slot 0: 4 bytes uint16 public status; // slot 0: 2 bytes address public owner; // slot 0: 20 bytes - needs slot 1 // Note: uint128 + uint32 + uint16 = 22 bytes, address needs new slot }
Storage Variable Packing
soliditycontract PackingExample { // Unoptimized: occupies 3 storage slots struct Unoptimized { uint256 a; // slot 0 uint128 b; // slot 1 uint128 c; // slot 2 } // Optimized: only occupies 2 storage slots struct Optimized { uint128 b; // slot 0 (16 bytes) uint128 c; // slot 0 (16 bytes) - packed together uint256 a; // slot 1 } // Best practice: sort by size struct BestPractice { uint128 a; // 16 bytes uint128 b; // 16 bytes - shares slot with a uint64 c; // 8 bytes uint64 d; // 8 bytes - shares with c uint32 e; // 4 bytes uint32 f; // 4 bytes - shares with e } }
Using Memory Instead of Storage
soliditycontract MemoryVsStorage { uint256[] public data; // Expensive: multiple storage accesses function expensiveSum() public view returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; // Accesses storage each loop iteration } return sum; } // Optimized: load to memory first function optimizedSum() public view returns (uint256) { uint256[] memory localData = data; // Load once uint256 sum = 0; for (uint i = 0; i < localData.length; i++) { sum += localData[i]; // Accesses memory, much cheaper } return sum; } }
2. Variable and Calculation Optimization
Using Constants and Immutables
soliditycontract ConstantOptimization { // Expensive: storage variable uint256 public constantValue = 1000; // Optimized: constant (doesn't occupy storage) uint256 public constant CONSTANT_VALUE = 1000; // Better: immutable (determined at compile time, doesn't occupy storage) uint256 public immutable immutableValue; constructor(uint256 _value) { immutableValue = _value; } // Use constant for calculations function calculate(uint256 amount) public pure returns (uint256) { return amount * CONSTANT_VALUE / 10000; } }
Short-Circuit Evaluation
soliditycontract ShortCircuit { // Optimization: leverage short-circuit evaluation function checkConditions(bool a, bool b, bool c) public pure returns (bool) { // Put conditions most likely to be false first return a && b && c; } // Optimization: || operator function checkOr(bool a, bool b, bool c) public pure returns (bool) { // Put conditions most likely to be true first return a || b || c; } }
3. Loop Optimization
Loop Variable Optimization
soliditycontract LoopOptimization { uint256[] public data; // Unoptimized: accesses storage each loop iteration function unoptimizedLoop() public view returns (uint256) { uint256 sum = 0; for (uint256 i = 0; i < data.length; i++) { sum += data[i]; } return sum; } // Optimization 1: cache array length function optimizedLoop1() public view returns (uint256) { uint256 sum = 0; uint256 len = data.length; // Cache length for (uint256 i = 0; i < len; i++) { sum += data[i]; } return sum; } // Optimization 2: use unchecked and ++i function optimizedLoop2() public view returns (uint256) { uint256 sum = 0; uint256 len = data.length; for (uint256 i = 0; i < len; ) { sum += data[i]; unchecked { ++i; } // Use unchecked and pre-increment } return sum; } // Optimization 3: use memory array function optimizedLoop3() public view returns (uint256) { uint256[] memory localData = data; uint256 sum = 0; uint256 len = localData.length; for (uint256 i = 0; i < len; ) { sum += localData[i]; unchecked { ++i; } } return sum; } }
4. Function Optimization
Using Calldata
soliditycontract CalldataOptimization { // Expensive: using memory function processMemory(uint256[] memory data) external pure returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; } return sum; } // Optimized: use calldata (read-only, no copy) function processCalldata(uint256[] calldata data) external pure returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; } return sum; } }
Function Modifier Optimization
soliditycontract ModifierOptimization { // Expensive: modifier code inlined into each function modifier expensiveCheck() { require(msg.sender == owner, "Not owner"); require(!paused, "Paused"); require(balance > 0, "No balance"); _; } // Optimized: extract checks into internal function modifier optimizedCheck() { _checkAccess(); _; } function _checkAccess() internal view { require(msg.sender == owner, "Not owner"); require(!paused, "Paused"); require(balance > 0, "No balance"); } // Compiler can optimize more effectively this way function action1() external optimizedCheck { } function action2() external optimizedCheck { } function action3() external optimizedCheck { } }
5. Error Handling Optimization
Custom Errors (Solidity 0.8.4+)
solidity// File: ErrorOptimization.sol // License: MIT pragma solidity ^0.8.4; contract ErrorOptimization { address public owner; uint256 public balance; // Define custom errors (more Gas efficient than require strings) error NotOwner(address caller); error InsufficientBalance(uint256 requested, uint256 available); error InvalidAmount(uint256 amount); modifier onlyOwner() { if (msg.sender != owner) { revert NotOwner(msg.sender); } _; } function withdraw(uint256 amount) external onlyOwner { if (amount == 0) { revert InvalidAmount(amount); } if (amount > balance) { revert InsufficientBalance(amount, balance); } balance -= amount; payable(msg.sender).transfer(amount); } }
6. Event and Log Optimization
soliditycontract EventOptimization { // Expensive: storing large amounts of data mapping(uint256 => Transaction) public transactions; struct Transaction { address from; address to; uint256 amount; uint256 timestamp; string metadata; } // Optimized: use events instead of storage event TransactionExecuted( address indexed from, address indexed to, uint256 amount, uint256 timestamp ); function executeTransaction(address to, uint256 amount) external { // Execute business logic... // Use event to record (much cheaper than storage) emit TransactionExecuted(msg.sender, to, amount, block.timestamp); } }
7. Library Usage
solidity// Use libraries to reduce code duplication library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c = a + b; require(c >= a, "Addition overflow"); return c; } } } contract UsingLibrary { using SafeMath for uint256; function calculate(uint256 a, uint256 b) public pure returns (uint256) { return a.add(b); // Use library function } }
8. Bitwise Operation Optimization
soliditycontract BitwiseOptimization { // Use bitwise operations instead of mathematical operations // Multiply/divide by powers of 2 function multiplyBy2(uint256 x) public pure returns (uint256) { return x << 1; // Equivalent to x * 2 } function divideBy2(uint256 x) public pure returns (uint256) { return x >> 1; // Equivalent to x / 2 } // Check if power of 2 function isPowerOf2(uint256 x) public pure returns (bool) { return x > 0 && (x & (x - 1)) == 0; } // Use bit masks to store multiple boolean values uint256 private flags; uint256 constant FLAG_PAUSED = 1 << 0; // 1 uint256 constant FLAG_FINALIZED = 1 << 1; // 2 uint256 constant FLAG_APPROVED = 1 << 2; // 4 function setFlag(uint256 flag) internal { flags |= flag; } function clearFlag(uint256 flag) internal { flags &= ~flag; } function hasFlag(uint256 flag) internal view returns (bool) { return (flags & flag) != 0; } }
9. Compiler Optimization
Using Optimizer
json// hardhat.config.js module.exports = { solidity: { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 // Adjust based on contract call frequency } } } };
10. Gas Optimization Comparison Table
| Optimization Technique | Gas Savings | Applicable Scenarios |
|---|---|---|
| Storage packing | 20,000 Gas/slot | Multiple small variables |
| Use constants | 20,000 Gas | Fixed values |
| Use calldata | 3,800 Gas/32 bytes | External function parameters |
| Custom errors | ~50 Gas | Error handling |
| unchecked | ~80 Gas/operation | Mathematical operations |
| Events replace storage | ~19,000 Gas | Logging |
| Short-circuit evaluation | Variable | Conditional judgments |
11. Optimization Tools
- hardhat-gas-reporter: Reports Gas consumption for each test
- eth-gas-reporter: Detailed Gas analysis reports
- Remix IDE: Built-in Gas estimation
- Tenderly: Gas analysis and optimization suggestions
javascript// hardhat.config.js require("hardhat-gas-reporter"); module.exports = { gasReporter: { enabled: true, currency: "USD", gasPrice: 21 } };
12. Best Practices Summary
- Prioritize storage optimization: Storage operations are the most expensive
- Use appropriate data types: Avoid over-allocation
- Leverage compiler optimization: Enable optimizer and adjust runs parameter
- Batch operations: Reduce function call frequency
- Use events: Replace unnecessary storage writes
- Lazy computation: Compute when needed rather than storing
- Regular audits: Use tools to check Gas consumption
Remember: Over-optimization may reduce code readability. Find a balance between performance and maintainability.