Gas Optimization
27.1 Principles
Storage is the dominant cost — minimize SSTORE/SLOAD.
Correctness and security come first; never trade safety for a few gas.
Measure with forge test --gas-report, forge snapshot, hardhat-gas-reporter — don't guess.
27.2 Techniques
Example / note constant / immutable avoid storage reads for fixed values Cache storage in memory/stack
uint256 bal = balances[u]; then use bal small types adjacent; structs ≤ 32 bytes when read together calldata instead of memory for external read-only array/string params instead of revert strings where overflow is impossible (proven bounds) Short-circuit cheap checks first if (amount == 0 || paused) revert... Avoid unbounded loops use pagination / pull patterns Events instead of storage for data only needed off-chain
external over public for large calldata historically cheaper; minor today store 256 booleans in one uint256 (e.g., airdrop claims) mapping over array for lookups reentrancy locks and per-tx flags at 100 gas instead of 2,900+
runs ≈ expected call count; higher = cheaper calls, bigger code often smaller/cheaper code; slower compile Avoid zero→nonzero writes keep a non-zero "unset" value (e.g., reentrancy lock 1/2 instead of 0/1) multicall, batched mints (ERC-721A) Clones / CREATE2 cheap per-user contract deployment
27.3 Before / after
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract Expensive {
uint256[] public values;
uint256 public total;
function sumAll() external {
for (uint256 i = 0; i < values.length; i++) { // reads values.length from storage each time
total += values[i]; // SLOAD + SSTORE every iteration
}
}
}
contract Cheaper {
uint256[] public values;
uint256 public total;
function sumAll() external {
uint256[] memory v = values; // one copy (still n SLOADs, but no repeated length reads)
uint256 len = v.length;
uint256 t = total; // cache
for (uint256 i; i < len; ++i) { // 0.8.22+: increment is auto-unchecked
t += v[i];
}
total = t; // single SSTORE
}
}27.4 Bitmap example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ClaimBitmap {
mapping(uint256 => uint256) private claimedWords;
function isClaimed(uint256 index) public view returns (bool) {
uint256 word = claimedWords[index >> 8]; // index / 256
uint256 mask = 1 << (index & 0xff); // index % 256
return word & mask != 0;
}
function _setClaimed(uint256 index) internal {
claimedWords[index >> 8] |= 1 << (index & 0xff);
}
}27.5 Gas-saving myths and caveats
++i vs i++: negligible with modern optimizers.
!= 0 vs > 0 for unsigned ints: no difference with optimizer in recent versions.
Making functions payable to save the callvalue check saves ~20 gas but is a footgun — avoid in user-facing APIs.
Assembly can save gas but removes safety checks; use only in audited, hot paths.