Introduction
Gas optimization is critical for smart contract development. Every operation on Ethereum costs gas, and inefficient code can result in contracts that are expensive to deploy and use. A poorly optimized contract might cost $10,000 to deploy, while an optimized version costs $2,000. For users, high gas costs can make transactions prohibitively expensive.
This comprehensive guide covers practical gas optimization techniques that can reduce contract costs by 50-80%, with real-world examples and measurable improvements.
Core Concepts & Terminology
Gas
The unit of computational work on Ethereum. Each operation (addition, storage write, function call) costs a specific amount of gas. Gas price fluctuates based on network demand.
Gas Limit
Maximum gas a transaction can consume. If exceeded, the transaction reverts and gas is consumed.
Gas Price (Gwei)
The price per unit of gas in Gwei (1 Gwei = 10^-9 ETH). Users set this to control transaction speed.
Transaction Cost
Total cost = Gas Used ร Gas Price. Example: 100,000 gas ร 50 Gwei = 0.005 ETH (~$15 at $3000/ETH).
Storage Slot
32-byte storage location on the blockchain. Each storage write costs 20,000 gas (cold) or 5,000 gas (warm).
Memory
Temporary storage during execution. Cheaper than storage but cleared after transaction. Costs scale quadratically.
Calldata
Immutable input data for a transaction. Cheapest storage option: 4 gas per zero byte, 16 gas per non-zero byte.
Bytecode
Compiled smart contract code. Larger bytecode costs more to deploy (200 gas per byte).
SSTORE vs SLOAD
SSTORE: Write to storage (20,000 gas cold, 5,000 gas warm) SLOAD: Read from storage (2,100 gas cold, 100 gas warm)
Warm vs Cold Storage Access
Cold: First access in transaction (expensive) Warm: Subsequent accesses (cheap)
Gas Costs Reference Table
| Operation | Gas Cost | Notes |
|---|---|---|
| ADD, SUB, MUL | 3 | Basic arithmetic |
| DIV, MOD | 5 | Division operations |
| SSTORE (cold) | 20,000 | First storage write |
| SSTORE (warm) | 5,000 | Subsequent writes |
| SLOAD (cold) | 2,100 | First storage read |
| SLOAD (warm) | 100 | Subsequent reads |
| MSTORE | 3 | Memory write |
| MLOAD | 3 | Memory read |
| CALL | 700 | External call |
| DELEGATECALL | 700 | Delegated call |
| CREATE | 32,000 | Deploy contract |
| SELFDESTRUCT | 5,000 | Destroy contract |
| LOG0 | 375 | Emit event (no indexed) |
| LOG1 | 750 | Emit event (1 indexed) |
| LOG2 | 1,125 | Emit event (2 indexed) |
| LOG3 | 1,500 | Emit event (3 indexed) |
| LOG4 | 1,875 | Emit event (4 indexed) |
Storage Optimization Techniques
1. Variable Packing
Store multiple variables in a single 32-byte slot:
// โ INEFFICIENT: 3 storage slots (96 bytes)
contract Inefficient {
uint256 public id; // 32 bytes
uint256 public timestamp; // 32 bytes
uint256 public amount; // 32 bytes
}
// โ
OPTIMIZED: 1 storage slot (32 bytes)
contract Optimized {
uint96 public id; // 12 bytes
uint64 public timestamp; // 8 bytes
uint96 public amount; // 12 bytes
// Total: 32 bytes in one slot
}
Gas Savings: ~40,000 gas per deployment, ~15,000 gas per transaction
2. Struct Packing
Arrange struct fields by size:
// โ INEFFICIENT: 4 storage slots
struct User {
address wallet; // 20 bytes (slot 1)
uint256 balance; // 32 bytes (slot 2)
uint32 nonce; // 4 bytes (slot 3)
bool active; // 1 byte (slot 4)
}
// โ
OPTIMIZED: 2 storage slots
struct User {
address wallet; // 20 bytes
uint32 nonce; // 4 bytes
bool active; // 1 byte
// Total: 25 bytes (slot 1)
uint256 balance; // 32 bytes (slot 2)
}
Gas Savings: ~40,000 gas per deployment
3. Immutable Variables
Use immutable for values set once at deployment:
// โ INEFFICIENT: 5,000 gas per read
contract Inefficient {
address public owner;
constructor(address _owner) {
owner = _owner;
}
}
// โ
OPTIMIZED: 3 gas per read (embedded in bytecode)
contract Optimized {
address public immutable owner;
constructor(address _owner) {
owner = _owner;
}
}
Gas Savings: ~2,000 gas per read (100x improvement)
4. Constant Variables
Use constant for compile-time values:
// โ INEFFICIENT: 2,100 gas per read
contract Inefficient {
uint256 public MAX_SUPPLY = 1_000_000e18;
}
// โ
OPTIMIZED: 3 gas per read (embedded in bytecode)
contract Optimized {
uint256 public constant MAX_SUPPLY = 1_000_000e18;
}
Gas Savings: ~2,000 gas per read
5. Mapping vs Array
Use mappings for sparse data:
// โ INEFFICIENT: O(n) iteration, wasted space
contract Inefficient {
address[] public users;
function getUser(address addr) public view returns (bool) {
for (uint i = 0; i < users.length; i++) {
if (users[i] == addr) return true;
}
return false;
}
}
// โ
OPTIMIZED: O(1) lookup
contract Optimized {
mapping(address => bool) public isUser;
function getUser(address addr) public view returns (bool) {
return isUser[addr];
}
}
Gas Savings: ~20,000+ gas per lookup
Bytecode Optimization
1. Function Selector Optimization
Reorder functions to optimize selector distribution:
// โ
OPTIMIZED: Frequently called functions first
contract Optimized {
// Most called functions (smaller bytecode offset)
function transfer(address to, uint256 amount) external {
// ...
}
function balanceOf(address account) external view returns (uint256) {
// ...
}
// Less frequently called functions
function initialize() external {
// ...
}
}
Gas Savings: ~100-500 gas per transaction (minimal)
2. Inline Assembly for Hot Paths
Use assembly for frequently called functions:
// โ INEFFICIENT: Solidity overhead
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// โ
OPTIMIZED: Assembly (rarely needed for simple ops)
function add(uint256 a, uint256 b) public pure returns (uint256 result) {
assembly {
result := add(a, b)
}
}
Gas Savings: ~5-10 gas per call (minimal for simple operations)
3. Reduce Contract Size
Remove unnecessary code:
// โ INEFFICIENT: Unused imports and functions
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract Token is ERC20, Ownable, Pausable {
// Only uses ERC20 functionality
}
// โ
OPTIMIZED: Only import what you need
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
// ...
}
Gas Savings: ~5,000-20,000 gas deployment cost
Execution Optimization
1. Caching Storage Variables
Read storage once and cache in memory:
// โ INEFFICIENT: 3 storage reads (6,300 gas)
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}
// โ
OPTIMIZED: 1 storage read (2,100 gas)
function transfer(address to, uint256 amount) external {
uint256 senderBalance = balances[msg.sender];
require(senderBalance >= amount);
balances[msg.sender] = senderBalance - amount;
balances[to] += amount;
}
Gas Savings: ~4,200 gas per transaction
2. Short-Circuit Evaluation
Order conditions by likelihood:
// โ INEFFICIENT: Expensive check first
function withdraw(uint256 amount) external {
require(amount <= maxWithdrawal); // Expensive calculation
require(balances[msg.sender] >= amount); // Cheap check
}
// โ
OPTIMIZED: Cheap check first
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount); // Cheap check
require(amount <= maxWithdrawal); // Expensive calculation
}
Gas Savings: ~2,000-5,000 gas on failed transactions
3. Batch Operations
Process multiple items in one transaction:
// โ INEFFICIENT: Multiple transactions
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
}
// โ
OPTIMIZED: Batch transfers
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length);
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
require(balances[msg.sender] >= totalAmount);
balances[msg.sender] -= totalAmount;
for (uint256 i = 0; i < recipients.length; i++) {
balances[recipients[i]] += amounts[i];
}
}
Gas Savings: ~50% per transfer in batch
4. Unchecked Arithmetic
Skip overflow checks when safe:
// โ INEFFICIENT: Overflow check (22 gas)
function increment(uint256 counter) external pure returns (uint256) {
return counter + 1;
}
// โ
OPTIMIZED: No overflow check (3 gas)
function increment(uint256 counter) external pure returns (uint256) {
unchecked {
return counter + 1;
}
}
Gas Savings: ~20 gas per operation
Advanced Optimization Patterns
1. Proxy Pattern for Upgradeable Contracts
Separate logic from storage:
// Storage contract (never changes)
contract StorageLayout {
mapping(address => uint256) public balances;
uint256 public totalSupply;
}
// Logic contract (upgradeable)
contract TokenLogic is StorageLayout {
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Proxy (delegates to logic)
contract TokenProxy {
address public implementation;
fallback() external payable {
address impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
returndatacopy(ptr, 0, returndatasize())
switch result
case 0 { revert(ptr, returndatasize()) }
default { return(ptr, returndatasize()) }
}
}
}
Gas Savings: Reduced deployment cost for upgrades
2. Diamond Pattern (Multi-Facet)
Multiple logic contracts sharing storage:
// Diamond storage pattern
library LibDiamond {
bytes32 constant DIAMOND_STORAGE_POSITION =
keccak256("diamond.standard.diamond.storage");
struct DiamondStorage {
mapping(address => uint256) balances;
uint256 totalSupply;
}
function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}
// Facet 1
contract TransferFacet {
function transfer(address to, uint256 amount) external {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
ds.balances[msg.sender] -= amount;
ds.balances[to] += amount;
}
}
Gas Savings: Modular upgrades without redeployment
3. ERC-1167 Minimal Proxy
Deploy cheap contract clones:
// Factory for minimal proxies
contract MinimalProxyFactory {
function createClone(address implementation) external returns (address instance) {
bytes20 targetBytes = bytes20(implementation);
assembly {
let clone := mload(0x40)
mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(clone, 0x14), targetBytes)
mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, clone, 0x37)
}
}
}
Gas Savings: ~90% cheaper deployment vs full contract
Real-World Optimization Example
Before Optimization
contract Token {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowance;
address public owner;
bool public paused;
function transfer(address to, uint256 amount) external {
require(!paused);
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
}
Deployment Cost: 150,000 gas ($45 at 50 Gwei)
**Transfer Cost**: ~50,000 gas (~$1.50 per transfer)
After Optimization
contract TokenOptimized {
string public name;
string public symbol;
uint8 public decimals;
uint96 public totalSupply;
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowance;
address public immutable owner;
bool public paused;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
owner = msg.sender;
}
function transfer(address to, uint256 amount) external {
require(!paused);
uint256 senderBalance = balances[msg.sender];
require(senderBalance >= amount);
unchecked {
balances[msg.sender] = senderBalance - amount;
balances[to] += amount;
}
emit Transfer(msg.sender, to, amount);
}
}
Deployment Cost: 95,000 gas ($28.50 at 50 Gwei) - **37% reduction**
**Transfer Cost**: ~35,000 gas (~$1.05 per transfer) - 30% reduction
Gas Profiling Tools
1. Hardhat Gas Reporter
// hardhat.config.js
module.exports = {
gasReporter: {
enabled: true,
currency: 'USD',
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
};
2. Foundry Gas Snapshots
forge test --gas-report
forge snapshot
forge snapshot --diff
3. Etherscan Gas Tracker
View real gas usage on mainnet:
- https://etherscan.io/gastracker
- Compare contract deployments
- Analyze transaction costs
Best Practices & Common Pitfalls
Best Practices
- Profile Before Optimizing: Use gas profiling tools to identify bottlenecks
- Prioritize Hot Paths: Optimize frequently called functions first
- Test Thoroughly: Ensure optimizations don’t introduce bugs
- Document Changes: Explain why optimizations were made
- Use Established Patterns: Leverage battle-tested optimization patterns
- Monitor Gas Prices: Adjust optimization strategy based on network conditions
- Balance Readability: Don’t sacrifice code clarity for minor gas savings
Common Pitfalls
- Over-Optimization: Spending hours to save $10 in gas
- Premature Optimization: Optimizing before profiling
- Unsafe Unchecked: Using unchecked without proper bounds checking
- Storage Thrashing: Repeatedly reading/writing same storage slot
- Memory Leaks: Allocating memory without cleanup
- Inefficient Loops: Iterating over large arrays unnecessarily
- Ignoring Calldata: Not using calldata for function parameters
Comparison: Optimization Techniques
| Technique | Gas Savings | Difficulty | Risk |
|---|---|---|---|
| Variable Packing | 40,000 | Low | Low |
| Immutable Variables | 2,000/read | Low | Low |
| Caching Storage | 4,200 | Low | Low |
| Unchecked Arithmetic | 20/op | Medium | Medium |
| Assembly | 5-100 | High | High |
| Proxy Pattern | 50,000 | High | Medium |
| Minimal Proxy | 100,000 | High | Low |
External Resources
Documentation & Guides
Tools & Profilers
Learning Resources
Optimization Libraries
Conclusion
Gas optimization is essential for building efficient, cost-effective smart contracts. By applying storage optimization, bytecode reduction, and execution optimization techniques, developers can reduce contract costs by 50-80%.
The key is to profile first, identify bottlenecks, and apply targeted optimizations. Start with high-impact techniques like variable packing and immutable variables, then move to advanced patterns as needed.
Remember: premature optimization is the root of all evil. Profile, measure, and optimize strategically.
Comments