Gas optimization is a crucial aspect of Ethereum and other blockchain-based smart contract development. Every operation on the blockchain costs gas, which users pay for using cryptocurrency (usually ETH). Minimizing gas costs leads to more efficient, cost-effective contracts and better scalability for decentralized applications (dApps). Poorly optimized smart contracts can result in higher user fees, failed transactions, or even the inability to deploy contracts due to block gas limits.
In this guide, we will explore the top gas optimization techniques, categorized step-by-step with practical Solidity examples.
1. Use uint256
over Smaller Integers in Storage
While using smaller data types like uint8
or uint16
seems efficient, the EVM (Ethereum Virtual Machine) processes uint256
natively. Using non-256-bit integers in storage can actually increase gas usage due to additional packing and unpacking operations.
// Good
uint256 public value;
// Avoid this (unless for tightly packed struct)
uint8 public smallValue;
2. Pack Structs and Storage Variables Efficiently
Storage is one of the most expensive operations. Variables are stored in 32-byte (256-bit) slots. Packing smaller variables together into a single slot reduces the number of storage slots used.
// Better struct packing
struct Packed {
uint128 a;
uint128 b;
}
// Avoid this (uses more storage slots)
struct Unpacked {
uint256 a;
uint256 b;
}
Place same-sized or smaller variables next to each other to allow Solidity to pack them into a single slot.
3. Use calldata
Instead of memory
for Function Arguments
When dealing with external functions that do not need to modify arguments, use calldata
to reduce copying costs.
// Gas efficient
function processData(uint256[] calldata data) external {
// data is read-only and cheaper
}
// More expensive
function processData(uint256[] memory data) external {
// memory copies the array
}
4. Short-Circuit Conditions and Optimize require
Statements
Place cheaper checks before expensive ones in conditionals to minimize gas in failed transactions.
// Good
require(msg.sender == owner && balance > 0, "Invalid");
// Avoid this
require(balance > 0 && msg.sender == owner, "Invalid"); // Evaluates both
Also, avoid complex require()
messages unless necessary, as longer error strings use more gas.
5. Use immutable
and constant
Where Appropriate
constant
: Used for compile-time constants (e.g., fixed numbers).immutable
: Used for values set once at construction and never changed.
uint256 public constant MAX_SUPPLY = 10000;
address public immutable owner;
constructor() {
owner = msg.sender;
}
Both constant
and immutable
variables save gas compared to regular storage
variables.
6. Minimize Use of storage
Writes
Writing to storage costs significantly more gas than reading from it. Avoid unnecessary writes and use memory
or calldata
when possible.
// Expensive
function update(uint256 newValue) public {
value = newValue; // Always writes
}
// Better
function update(uint256 newValue) public {
if (value != newValue) {
value = newValue; // Only writes if changed
}
}
7. Use unchecked
for Arithmetic Where Safe (Solidity ≥0.8.0)
Solidity 0.8.0+ includes automatic overflow checks. If you’re certain an operation won’t overflow, disable these checks to save gas.
function increment(uint256 x) public pure returns (uint256) {
unchecked {
return x + 1;
}
}
Only use this when you’re 100% sure the operation is safe.
8. Minimize External Contract Calls
External calls cost gas and are also potential security risks (e.g., reentrancy). Minimize them, cache results when possible, and use internal logic when feasible.
// Costly
token.balanceOf(msg.sender);
// Better: cache if needed multiple times
uint256 balance = token.balanceOf(msg.sender);
9. Use for
Loops Carefully
Avoid unbounded loops in functions, especially those triggered by users. These can cause transactions to run out of gas and revert.
// Gas-efficient loop (limited iterations)
function sum(uint256[] calldata nums) external pure returns (uint256) {
uint256 total;
uint256 length = nums.length;
for (uint256 i = 0; i < length; ++i) {
total += nums[i];
}
return total;
}
Always consider the maximum array length in loops.
10. Use Events Instead of Storage Logs
Use emit
to log important data rather than storing it permanently if retrieval is not required by contracts later.
event ActionPerformed(address indexed user, uint256 amount);
function performAction() external {
emit ActionPerformed(msg.sender, 100);
}
Events are stored in transaction logs, which are cheaper than storage writes and useful for external monitoring tools.
11. Avoid Redundant State Variables
If a value can be computed from existing variables, avoid storing it. Instead, calculate it dynamically to save storage costs.
// Avoid redundant storage
uint256 public totalSupply = a + b;
// Better
function totalSupply() public view returns (uint256) {
return a + b;
}
12. Use Libraries for Reusable Logic
OpenZeppelin’s SafeMath
, Counters
, and custom utility libraries can reduce code duplication and contract size.
using Counters for Counters.Counter;
Counters.Counter private _tokenId;
function mint() public {
_tokenId.increment();
uint256 newId = _tokenId.current();
}
13. Delete Storage Variables When No Longer Needed
If temporary storage is no longer needed (like an auction record), explicitly delete it to get a gas refund.
delete auctionData[auctionId];
This refunds some gas based on the storage freed.
14. Return Minimal Data
Returning large arrays or data structures in public view functions increases gas for anyone calling them from a contract (though not for pure web3 reading). Keep outputs small where possible.
15. Avoid selfdestruct
for Gas Refund
In Ethereum’s current roadmap, selfdestruct
will likely be deprecated and no longer provide gas refunds. Avoid relying on it for optimization in future-proof code.