Gas optimization is the discipline of minimizing the Ethereum gas consumption of smart contracts and transactions through deliberate code structure choices, storage management techniques, assembly-level optimizations, architectural patterns, and transaction timing — with the goals of reducing per-transaction fees for end users, lowering smart contract deployment costs for developers, enabling more complex logic within block gas limits, and improving protocol competitiveness in a market where high gas costs routinely deter user activity. Every EVM (Ethereum Virtual Machine) opcode has a defined gas cost, and gas optimization involves selecting lower-cost opcodes, minimizing expensive storage operations, batching transactions, and exploiting calldata pricing differences to achieve the same functional result at lower total gas consumption.
EVM Gas Cost Fundamentals
How Gas Is Calculated
“`
Total Fee = (Base Fee + Priority Fee) × Gas Used
“`
- Base fee: Network-determined, burned (EIP-1559)
- Priority fee: User-set tip to validator
- Gas used: Sum of gas costs of all opcodes executed
Most Expensive EVM Operations
| Operation | Gas Cost | Notes |
|---|---|---|
SSTORE (new slot) |
20,000 | Writing new data to storage — most expensive common op |
SSTORE (update) |
2,900 | Updating existing non-zero storage |
SLOAD |
2,100 | Reading from storage (EIP-2929 cold access) |
CREATE / CREATE2 |
32,000+ | Deploying a new contract |
CALL (external) |
700+ | Calling another contract |
LOG (events) |
375 + 8/byte | Emitting events |
MLOAD / MSTORE |
3 | Memory operations (cheap) |
ADD, MUL, SUB |
3–5 | Arithmetic (cheap) |
SHA3 (keccak256) |
30 + 6/word | Hashing |
Storage Optimization (Biggest Wins)
Avoid Unnecessary SSTOREs
“`solidity
// Bad: 2 separate SSTOREs = 40,000 gas
function update(uint256 a, uint256 b) external {
storedA = a; // 20,000 gas
storedB = b; // 20,000 gas
}
// Better: Read both, compute in memory, write once if changed
“`
Struct Packing
“`solidity
// Bad: 3 separate storage slots (96,000 gas to initialize)
uint256 a; // 32 bytes
uint256 b; // 32 bytes
uint256 c; // 32 bytes
// Good: All fit in 1 storage slot (32,000 gas to initialize)
uint128 a; // 16 bytes
uint64 b; // 8 bytes
uint64 c; // 8 bytes = 32 bytes total = 1 slot
“`
Use uint256 Over Smaller Uints (Counterintuitively)
“`solidity
// In isolation (not in struct): uint256 is cheaper than uint8
uint256 counter; // no masking needed
uint8 counter; // EVM pads to 256-bit, adds masking cost
“`
Constants and Immutables
// Bad: State variable (costs SLOAD every read = 2,100 gas)
uint256 public fee = 100;
// Good: Constant (inlined at compile time, ~3 gas)
uint256 public constant FEE = 100;
// Good: Immutable (set once in constructor, ~3 gas reads)
uint256 public immutable fee;
constructor(uint256 _fee) { fee = _fee; }
“`
Computation Optimization
Short-Circuit Boolean Evaluation
“`solidity
// Put cheapest/most likely to fail check FIRST
require(amount > 0 && token.balanceOf(msg.sender) >= amount);
// If amount == 0, skips the SLOAD for balanceOf
“`
Cache Storage Reads in Memory
// Bad: 3 SLOADs at 2,100 gas each = 6,300 gas
function calculate() external view returns (uint256) {
return storedValue * storedValue + storedValue;
}
// Good: 1 SLOAD + 2 cheap MLOAD = ~2,106 gas
function calculate() external view returns (uint256) {
uint256 val = storedValue; // one SLOAD
return val * val + val;
}
“`
Unchecked Arithmetic (Solidity 0.8+)
“`solidity
// Standard (checked): ~40 gas overhead per operation
for (uint256 i = 0; i < length; i++) { … }
// Unchecked (safe when i < length guaranteed): saves ~30 gas/iteration
for (uint256 i = 0; i < length; ) {
// loop body
unchecked { ++i; } // ++i cheaper than i++ too
}
“`
Calldata and ABI Optimization
Calldata Cost
- Non-zero bytes: 16 gas per byte
Minimizing calldata (especially non-zero bytes) reduces transaction cost:
“`solidity
// Bad: sends full 32-byte address encoded in calldata
function transfer(address to, uint256 amount) external { … }
// Optimization: encode addresses as shorter custom types when possible
// Use minimal-ABI selectors, avoid unnecessary padding
“`
Batch Operations
“`solidity
// Bad: 3 transactions × 21,000 base = 63,000 base gas
approve(token, amount);
deposit(amount);
stake(amount);
// Good: 1 transaction with all logic
depositAndStake(token, amount);
“`
Architecture-Level Optimizations
Events Over Storage for Historical Data
“`solidity
// Bad: Storing historical data on-chain = 20,000 gas/write
mapping(uint256 => uint256) public priceHistory;
// Good: Emit event = ~375 + data bytes gas, readable off-chain
event PriceUpdated(uint256 indexed timestamp, uint256 price);
“`
Proxy Patterns Reduce Deployment Cost
Gas Tokens (Historical)
Transaction Timing Optimization
Gas Price Timing
| Period | Typical Base Fee |
|---|---|
| Weekday 9am–5pm UTC | High (peak activity) |
| Weekday evenings UTC | Medium |
| Weekend late night UTC | Lowest |
| During market crashes | Spike (panic transactions) |
| During bull market NFT drops | Extreme spike |
Tools: GasNow (defunct), Etherscan Gas Tracker, EIP-1559 fee estimators
History
- 2015–2020: Gas optimization as black art — small dev community shares Solidity tricks; few comprehensive resources
- 2020: DeFi Summer drives gas prices to 500+ gwei; optimization becomes economically critical
- 2021 Aug: EIP-1559 London fork — base fee + priority fee model; eliminates gas token strategies
- 2021: Solidity 0.8.x adds automatic overflow checks —
uncheckedblock introduced for opt-out - 2022: Layer 2 rollups (Optimism, Arbitrage) reduce L2 execution costs dramatically; L1 calldata cost becomes dominant L2 expense
- 2024 Mar: EIP-4844 (Dencun upgrade) — blob transactions reduce L2 calldata cost by 10–100× on L1
- 2024–2025: Gas optimization focus shifts from L1 execution to L2 proof costs and blob data efficiency