How to Write Gas-Optimized Smart Contracts in Solidity Advanced Techniques
Every operation in a Solidity smart contract costs gas, and on a network like Ethereum mainnet, unoptimized code translates directly into real money for your users. As dApps scale, poorly optimized contracts can mean the difference between a protocol that’s economically viable and one that prices out its own users during periods of network congestion.
Why Gas Optimization Still Matters in 2026
Even with the rise of L2 rollups and reduced calldata costs post-EIP-4844 (proto-danksharding), gas optimization remains critical because:
- L2 execution costs are still non-trivial for high-frequency contracts (AMMs, lending protocols, gaming).
- Contract deployment costs scale with bytecode size regardless of chain.
- User experience — cheaper transactions directly correlate with higher protocol adoption.
- MEV and arbitrage bots depend on razor-thin gas margins where even small inefficiencies matter.
1. Optimize Storage Layout with Variable Packing
Storage is the most expensive resource in the EVM. Each storage slot is 32 bytes, and the EVM only charges for the number of slots used — not the number of variables.
// Inefficient: uses 3 storage slots
contract Unoptimized {
uint256 a;
uint8 b;
uint256 c;
}
// Optimized: uses 2 storage slots
contract Optimized {
uint256 a;
uint256 c;
uint8 b;
}
Key rule: Group variables smaller than 32 bytes (uint8, uint16, bool, address) together so the compiler can pack them into a single storage slot. Order matters — the Solidity compiler packs sequentially, so interleaving large and small types wastes slots.
2. Use calldata Instead of memory for External Function Parameters
When a function parameter doesn’t need to be modified, declaring it as calldata avoids an unnecessary copy into memory.
// More expensive
function processArray(uint256[] memory data) external { ... }
// Cheaper
function processArray(uint256[] calldata data) external { ... }
This is especially impactful for functions accepting arrays or structs, where the memory-copy cost scales with data size.
3. Cache Storage Variables in Memory Within Loops
Repeated SLOAD operations inside a loop are one of the most common sources of wasted gas.
// Inefficient: reads storage on every iteration
for (uint256 i = 0; i < items.length; i++) {
total += items[i];
}
// Optimized: cache length and use unchecked increment
uint256 length = items.length;
for (uint256 i = 0; i < length;) {
total += items[i];
unchecked { ++i; }
}
Caching items.length avoids repeated storage/calldata reads, and wrapping the increment in unchecked skips Solidity’s automatic overflow checks when overflow is provably impossible.
4. Use unchecked Blocks Strategically
Since Solidity 0.8.x, arithmetic operations include automatic overflow/underflow checks, which add gas overhead. When you can mathematically guarantee an operation won’t overflow (e.g., loop counters bounded by array length), wrapping it in unchecked removes this overhead safely.
unchecked {
for (uint256 i; i < length; ++i) {
// safe operations here
}
}
Caution: Only use unchecked where overflow is provably impossible — this is a common source of vulnerabilities when misapplied.
5. Replace require Strings with Custom Errors
Custom errors (introduced in Solidity 0.8.4) are significantly cheaper than string-based require messages, both in deployment (smaller bytecode) and runtime gas.
// More expensive
require(balance >= amount, "Insufficient balance");
// Cheaper
error InsufficientBalance();
if (balance < amount) revert InsufficientBalance();
Custom errors avoid storing and emitting string data, and can optionally include parameters for debugging without the gas cost of string concatenation.
6. Use immutable and constant for Fixed Values
Variables that never change after deployment should be declared immutable (set once in the constructor) or constant (known at compile time). Both avoid expensive SLOAD operations entirely, since their values are embedded directly into the contract bytecode.
address public immutable owner;
uint256 public constant MAX_SUPPLY = 10_000;
constructor() {
owner = msg.sender;
}
7. Minimize External Calls and Batch Operations
Each external call carries a fixed overhead (~2600 gas for cold address access under EIP-2929) plus the cost of the called function itself. Where possible:
- Batch multiple operations into a single transaction using multicall patterns.
- Avoid redundant calls to the same external contract within a function.
- Use
staticcallfor read-only external calls where applicable to signal intent and enable certain compiler optimizations.
8. Use Bitmaps Instead of Boolean Mappings
Storing individual mapping(uint256 => bool) entries wastes an entire storage slot per boolean. Bitmaps pack 256 boolean flags into a single storage slot.
// Inefficient: one storage slot per entry
mapping(uint256 => bool) public claimed;
// Optimized: 256 flags per storage slot
mapping(uint256 => uint256) private claimedBitMap;
function isClaimed(uint256 index) public view returns (bool) {
uint256 wordIndex = index / 256;
uint256 bitIndex = index % 256;
uint256 word = claimedBitMap[wordIndex];
return (word >> bitIndex) & 1 == 1;
}
This pattern is widely used in gas-efficient airdrop and allowlist contracts (e.g., Merkle-based claim systems).
9. Short-Circuit Conditional Logic
Order conditions in require/if statements so the cheapest and most likely to fail conditions are evaluated first, avoiding unnecessary computation.
// Better: cheap check first
require(amount > 0 && expensiveValidation(amount), "Invalid");
Since Solidity evaluates && and || with short-circuiting, placing lower-cost checks first can skip expensive operations entirely when the cheap check fails.
10. Leverage Assembly (Yul) for Critical Hot Paths
For highly gas-sensitive functions (called frequently or by many users), inline assembly can bypass Solidity’s safety abstractions for direct EVM control.
function efficientTransfer(address to, uint256 amount) external {
assembly {
let success := call(gas(), to, amount, 0, 0, 0, 0)
if iszero(success) {
revert(0, 0)
}
}
}
Important: Assembly should be reserved for well-audited, high-value optimizations. It bypasses Solidity’s built-in safety checks, increasing both bug risk and audit complexity — use only when the gas savings justify the added risk and always pair with thorough testing.
11. Reduce Contract Size to Lower Deployment Costs
Deployment cost scales with bytecode size (200 gas per byte). Techniques to reduce size include:
- Using libraries with
delegatecallto share code across multiple contracts instead of duplicating logic. - Splitting large contracts into smaller ones connected via proxy patterns.
- Removing unused imports and dead code — the compiler doesn’t always eliminate everything automatically.
- Using the Solidity optimizer (
optimizer: trueinhardhat.config.jsorfoundry.toml) with an appropriaterunsvalue tuned to your contract’s expected call frequency.
12. Use Events Instead of Storage for Non-Critical Data
If data doesn’t need to be read on-chain by other contracts, emitting an event is far cheaper than writing to storage, since event data isn’t stored in contract state (though it is stored in transaction logs).
event Purchase(address indexed buyer, uint256 amount, uint256 timestamp);
function buy(uint256 amount) external {
// logic
emit Purchase(msg.sender, amount, block.timestamp);
}
Gas Optimization Checklist
| Technique | Typical Savings | Risk Level |
|---|---|---|
| Storage variable packing | High | Low |
| Custom errors vs require strings | Medium | Low |
calldata over memory |
Medium | Low |
unchecked arithmetic |
Low–Medium | Medium |
| Bitmaps for boolean flags | High | Low |
| Inline assembly | High | High |
| Reducing external calls | Medium | Low |
Tools for Measuring Gas Usage
Optimization without measurement is guesswork. Use these tools to validate improvements:
- Foundry’s
forge test --gas-report— detailed per-function gas breakdowns. - Hardhat Gas Reporter plugin — integrates gas cost tracking into your existing test suite.
- Tenderly — simulates transactions and visualizes gas usage by opcode.
- Solidity optimizer runs tuning — benchmark different
runsvalues against your contract’s actual usage pattern (lowrunsfavors cheaper deployment, highrunsfavors cheaper repeated execution).
Common Gas Optimization Mistakes
- Over-using
uncheckedblocks without verifying overflow is truly impossible. - Packing storage variables in the wrong order, negating potential slot savings.
- Using assembly prematurely before profiling actually identifies a bottleneck.
- Ignoring cold vs. warm storage/access costs under EIP-2929 when designing call patterns.
- Optimizing for gas at the expense of code readability and auditability without clear ROI.
Conclusion
Gas optimization in Solidity is a discipline of trade-offs — between cost savings, code readability, and security risk. The highest-leverage techniques (storage packing, custom errors, calldata usage, bitmaps) offer strong gas savings with minimal added risk and should be applied by default. More aggressive techniques like inline assembly should be reserved for well-tested, high-traffic contract paths where the gas savings clearly outweigh the added complexity and audit burden. Always profile before and after optimization — intuition about gas costs is frequently wrong, and real measurement should drive every optimization decision.
Frequently Asked Questions
What is the easiest way to reduce Solidity gas costs?
Storage variable packing and replacing require strings with custom errors offer the best savings-to-risk ratio and require minimal code restructuring.
Does using unchecked always save gas?
Yes, it removes overflow/underflow checks, but it should only be used where overflow is mathematically impossible — misuse can introduce serious vulnerabilities.
Is inline assembly safe for gas optimization?
It can be, but it bypasses Solidity’s built-in safety checks and increases audit complexity. Reserve it for well-tested, high-frequency code paths after profiling confirms it’s worth the risk.
Does gas optimization matter on Layer 2 networks?
Yes, though less than on Ethereum mainnet. Execution gas costs still apply on L2s, and efficient contracts remain cheaper to deploy and interact with even as calldata costs have dropped post-EIP-4844.