Smart Contract Optimization: Reduce Gas Costs Safely

Smart contract optimization is not about shaving two gas units from a loop while ignoring a bad protocol flow. The real savings usually come from fewer storage writes, cleaner data layout, smaller calldata, batching, and choosing the right execution layer. On high volume contracts, those choices can cut gas by 20 to 30 percent in normal cases, and much more when storage-heavy logic is redesigned.
If you build on Ethereum or another EVM chain, gas is part of your product design. Users feel it. Protocol treasuries feel it. Auditors feel it too, especially when developers bolt on risky assembly after the fact. Measure first. Then optimize the expensive path.

Where Gas Costs Come From
The EVM charges different prices for different work. Storage dominates. A write to contract storage can cost far more than a memory read, and a design that updates several storage slots per user action will stay expensive even if the Solidity code looks tidy.
- Storage writes: Usually the biggest cost driver. Reducing state changes is the first serious optimization step.
- Storage reads: Cheaper than writes, but still costly compared with memory. Cache repeated reads inside a function.
- Calldata: Large arrays, long strings, and repeated parameters increase transaction cost.
- Loops: Unbounded loops over dynamic arrays can fail with out-of-gas errors when the data grows.
- External calls: Cross-contract calls can trigger extra reads, checks, and failure paths.
A common beginner mistake is optimizing arithmetic before checking storage layout. That is backwards. I have seen contracts save more gas by moving a uint64, address, and bool into the same 32-byte storage slot than by rewriting five functions.
Start With Measurement, Not Guesswork
Do not optimize from memory. Add gas reports to your test suite and compare every change against a baseline. Hardhat and Foundry both make this straightforward.
Useful tools
- Hardhat Gas Reporter: Good for test-level gas reporting and pull request checks.
- Foundry gas snapshots: Fast, developer-friendly, and useful for spotting regressions.
- Slither: Static analysis that can reveal risky patterns before you chase gas savings.
- Solidity optimizer: A standard deployment setting, but the optimizer runs value should match your usage pattern.
Enabling the Solidity optimizer in Hardhat is usually part of a production setup:
module.exports = {
solidity: {
version: '0.8.24',
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
};
The runs setting is not magic. A low value can favor lower deployment cost. A higher value can favor repeated execution. For high volume DeFi functions, test several values rather than copying 200 blindly.
One practical warning: optimizer changes can push bytecode size over the EIP-170 contract size limit of 24,576 bytes. In Hardhat, this often appears as a deployment failure about contract code size exceeding the maximum. If that happens, split modules, remove dead code, or reconsider inheritance depth before blaming the network.
Storage Optimization: The Biggest Lever
Storage optimization sits at the core of this work because the EVM stores state in 32-byte slots. If you waste slots, users pay for it forever.
Pack variables deliberately
Solidity packs adjacent smaller values into the same slot when it can. Order matters.
contract PackedUser {
struct User {
uint128 balance;
uint64 lastClaim;
bool active;
address owner;
}
}
This is not always the best layout, but it shows the idea. Group small types together where they fit. Use uint256 when it is cheaper for arithmetic or alignment, but reach for smaller integers when packing saves slots. Do not scatter bool values between full-width integers and expect the compiler to rescue the layout.
Use mappings for lookup-heavy data
If you need direct access by user address, a mapping is usually better than searching an array.
- Use
mapping(address => uint256)for balances. - Use arrays when you truly need ordering or enumeration.
- Avoid loops that iterate over all users, all positions, or all NFTs in a core transaction path.
Unbounded iteration is not just expensive. It is a reliability bug waiting for enough users.
Cache storage reads
If a function reads the same storage variable multiple times, read it once into a local variable. In Solidity 0.8.x, the compiler is better than it used to be, but explicit caching still helps in many real contracts, especially with mappings and nested structs.
Reduce Calldata and Revert Costs
Calldata is cheaper than storage, but it is not free. Large input arrays and strings add cost. For read-only external parameters, prefer calldata over memory.
function batchClaim(uint256[] calldata tokenIds) external {
uint256 length = tokenIds.length;
for (uint256 i; i < length; ) {
_claim(tokenIds[i]);
unchecked { ++i; }
}
}
This snippet uses calldata, caches array length, and uses unchecked for the increment. The unchecked block is safe only because the loop condition bounds i. Do not use unchecked arithmetic as a habit. Use it where the proof is simple and visible.
Use custom errors
Custom errors are cheaper than long revert strings and are easier for front ends to decode.
error NotOwner();
function withdraw(address owner) external view {
if (msg.sender != owner) revert NotOwner();
}
Replacing repeated revert strings with custom errors can save meaningful deployment gas and reduce revert cost. It also keeps bytecode smaller.
Function Design Choices That Matter
Small function-level changes still add up, especially on contracts with millions of calls.
- Use external where appropriate: If a function is not called internally,
externalcan avoid unnecessary memory copying for parameters. - Use constant and immutable: Constants are inlined. Immutable values are set at deployment and avoid normal storage reads.
- Prefer bytes32 for fixed identifiers: A hash or fixed ID should not be a
string. - Remove redundant modifiers: Modifiers are readable, but repeated checks can bloat hot paths.
- Keep fallback and receive simple: Expensive fallback logic can surprise integrators and break transfers.
Be careful with public state variables. Solidity generates getter functions for them. That is fine for simple values, but explicit getters give you more control when the read path needs filtering or packed data decoding.
Architecture Beats Gas Golf
To be blunt, most late-stage micro-optimization is less valuable than a better protocol flow. If a user must approve, deposit, stake, claim, and restake across five transactions, no loop tweak will fix the experience or the cost.
Batch operations
Batching combines several actions into one transaction and amortizes fixed costs. This can cut per-action gas sharply for claims, mints, settlements, and admin operations. The trade-off is complexity. Batch functions need clear limits so they cannot hit block gas limits.
Use ERC-20 Permit when it fits
ERC-20 Permit, standardized in EIP-2612, lets users approve token spending with a signature rather than a separate approve transaction. For DeFi flows, that can remove an entire on-chain interaction. It is a strong choice when token support is available. It is the wrong choice if you assume every token implements Permit. Many do not.
Move heavy activity to Layer 2
For games, small payments, and high-frequency trading logic, Layer 2 should be considered early. EIP-4844 introduced blob transactions, which reduce data availability costs for rollups. That changes the optimization question: instead of forcing every state update onto Ethereum mainnet, you can put high volume execution on an L2 and settle or bridge only what matters.
Mainnet still matters for settlement, liquidity, and security assumptions. But if each user action is low value and frequent, L1 is often the wrong execution environment.
When Inline Assembly Is Worth It
Inline assembly can save gas in hot paths, but it raises audit cost and increases the chance of subtle bugs. Use it only after safer changes are measured and exhausted. If the code holds user funds, assembly should come with tests, fuzzing, and a professional review.
A good rule: if you cannot explain the memory layout on a whiteboard, do not write Yul in production.
A Practical Optimization Workflow
- Write the secure version first. Clear code is easier to audit and benchmark.
- Add gas reporting. Track deployment gas and transaction gas in CI.
- Find hot paths. Optimize functions users call most often, not admin functions used twice a year.
- Fix storage layout. Pack variables, reduce writes, and avoid unnecessary persistent state.
- Reduce calldata and errors. Use
calldata, custom errors, and fixed-size types where sensible. - Redesign flows. Add batching, Permit support, pull payments, or off-chain computation where the product allows it.
- Consider L2. For high throughput workloads, this may beat every code-level trick.
- Re-test security. Run unit tests, fuzz tests, static analysis, and audits after optimization.
How Much Can You Save?
Published research and industry case studies show a wide range. Automatic static optimization may save a few percent reliably. Systematic refinement techniques have reported average transaction savings around 21 percent. Storage-heavy refactors and batch-oriented redesigns can reach 40 to 70 percent in the right workload.
That range is normal. A simple ERC-20 transfer function does not have the same optimization room as a complex vault, game, or rewards distributor. The best metric is not a generic percentage. It is gas per user action under realistic volume.
Skills Worth Building Next
If you want to get good at this, learn EVM storage, Solidity 0.8.x behavior, EIP-1559 fee mechanics, and the trade-offs of L2 rollups. Then build benchmarks. Guessing is not engineering.
For structured learning, Blockchain Council readers can explore certifications such as Certified Smart Contract Developer™, Certified Solidity Developer™, and Certified Blockchain Developer™. Pair that study with one practical exercise: take a naive staking or airdrop contract, add gas snapshots, then reduce gas without changing external behavior.
Start with storage. Measure every change. Stop before clever code becomes dangerous code.
Related Articles
View AllSmart Contracts
Gas Fees Explained: How Smart Contract Execution Costs Are Calculated
Gas fees measure the cost of smart contract execution. Learn how opcode usage, storage writes, EIP-1559 pricing, and gas limits shape transaction fees.
Smart Contracts
Smart Contract Certification Guide: How to Validate Your Blockchain Skills
Learn how smart contract certification validates blockchain skills, which developer or auditor path to choose, and how to prove expertise with real projects.
Smart Contracts
Smart Contract Career Guide: Roles, Salaries, and Learning Paths
Explore smart contract career roles, salaries, core skills, and learning paths for developers, auditors, protocol engineers, and Web3 professionals.
Trending Articles
Top 5 DeFi Platforms
Explore the leading decentralized finance platforms and what makes each one unique in the evolving DeFi landscape.
How Blockchain Secures AI Data
Understand how blockchain technology is being applied to protect the integrity and security of AI training data.
Claude AI Tools for Productivity
Discover Claude AI tools for productivity to streamline tasks, manage workflows, and improve efficiency.