From the archive9 sections · 2 min readBy Dezső Mező · Published .mdBlockchainSolidityGasOptimizationAudit
We audited 11 Solidity codebases in 2025. Every single one had 15-40% avoidable gas cost, mostly in the same patterns. Here they are in order of impact.
011 · Pack storage slots
A uint256 and a bool in separate slots cost 20k gas per write. Pack them into one slot (uint128 + bool + address) and the second write is a few hundred gas. Look for structs with mixed-size fields.
022 · Custom errors over require strings
require(x, "NotOwner") deploys the string to bytecode. error NotOwner(); revert NotOwner(); is ~50 bytes smaller and costs less at runtime.
// before
require(msg.sender == owner, "NotOwner");
// after
error NotOwner();
if (msg.sender != owner) revert NotOwner();033 · `calldata` over `memory` for read-only params
External function arrays/structs: use calldata unless you mutate. Saves the copy + a chunk of stack.
044 · `unchecked` on safe arithmetic
Solidity 0.8+ checks every math op. Where you've already proven no overflow (post-condition from a prior require, or a bounded loop counter), wrap in unchecked { ... } for significant gas savings.
055 · `immutable` > `constant` for computed-once values
immutable is cheaper to read than constant when the value is computed at construction (like owner = msg.sender). constant wins only for literals.
066 · Short-circuit loops
for (uint i; i < n; ++i) beats i++ (pre-increment saves a few gas per iter). Cache n if it's a storage read. If the work can stop early, break · don't run the full loop.
077 · Events for historical state
Every SSTORE is ~20k gas. An emit is ~1.5k. If data doesn't need on-chain reads - historical logs, UI feeds, analytics - use events, not storage.
Run forge snapshot before and after each change. Measure, don't guess · a 'clever' optimization can cost gas via bytecode bloat.
088-16 · The shortlist
- Use
>=instead of>when semantically equivalent (saves 3 gas). ++costs less than+= 1(same reason).- Cache storage reads in a local variable for loops.
- Use
assembly { mstore(0x40, ...) }for hot paths only · readability cost is real. - Prefer
type(uint256).maxover hardcoded max values. transfer/sendare gas-stipended ·call{value: ...}is cheaper and more flexible.- Batch similar ops into one function to share the function-selector jump cost.
externalfunctions are cheaper thanpublicwhen not called internally.- Avoid double-SSTORE on initialization · use
immutablewhere possible.
09The anti-pattern
Don't optimize gas before you've got correctness + audit. A gas-optimized contract that reverts under edge conditions is a catastrophe, not a win. Correctness first, then gas · in that order.
What to take away
- The 16 gas-optimization patterns we apply on every audit, ranked by real-world impact. 15-40% reductions typical on contracts that haven't been gas-reviewed.
More from the lab
Browse all entries- The 2026 smart contract security checklist before you ship
- Foundry invariant testing · patterns we use on every audit
- Smart contract audit checklist · the one we actually use
- Smart contract audit pricing in 2026: €4k vs €15k vs €60k
Want this looked at on your own system?Start a conversation
