Gas optimization in Solidity - 12 techniques and when they apply
Practical gas-saving techniques for smart contracts: storage packing, calldata, caching, immutable, unchecked and the main rule - never trade away security.
In this article
Every EVM operation costs gas, and gas is money (gas and fees). For a service used by thousands, saving even a few thousand units of gas per operation is a real benefit. But optimisation without care is dangerous: the "cleverer" the code, the easier it is to make a mistake. Correctness and security first, gas second.
Rule one: measure
Do not optimise "by eye". Use forge test --gas-report and forge snapshot (Foundry): they show where gas really goes. Typically 80% of the spend is a couple of storage operations.
Where the cost comes from
| Operation | Gas (order of magnitude) |
|---|---|
| Simple arithmetic | 3-5 |
| Reading memory | 3 |
Reading storage (SLOAD), cold | ~2,100 |
| Reading storage, warm | ~100 |
| Writing to an empty slot | ~20,000 |
| Writing to a non-empty slot | ~2,900 - 5,000 |
| Calling another contract (cold address) | ~2,600+ |
An event (LOG) | ~375 + 375 per topic + 8 per byte |
The takeaway: the most expensive operations are working with storage and external calls (EVM under the hood).
12 techniques
1. Packing storage variables
Several small variables in one slot instead of separate slots. In Coin-Trust's deal struct 17 fields are fitted into 5 slots, saving on the order of 240,000 gas at creation (layout).
2. Caching storage reads
Each access to a state variable is an SLOAD. If you read several times, save it in a local variable:
// expensive: two reads
if (d.status != Status.Active) revert ...;
emit E(d.status);
// cheaper: one read
Status s = d.status;
if (s != Status.Active) revert ...;
emit E(s);The same for structs: Deal storage d = _deals[id]; gives a reference and does not copy the whole object.
3. calldata instead of memory
For parameters of external functions an array or struct in calldata need not be copied to memory:
function createOffer(OfferParams calldata p) external payable { ... }4. constant and immutable
Compile-time constants (constant) and values set in the constructor (immutable) are embedded in the bytecode and not read from storage. In the escrow PLATFORM_FEE_BPS is a constant.
5. Custom errors
revert NotAuthorized() is cheaper than require(cond, "long message") because it stores no string in the bytecode and returns just a 4-byte selector (ABI).
6. unchecked where proven
Since 0.8 arithmetic is checked. If you have proven there is no overflow (say a counter incremented by just 1 after a check), an unchecked block removes the extra checks:
unchecked { ++i; }Apply with care: a mistake here brings back overflow vulnerabilities (integer math).
7. Avoid unbounded loops
A loop over an array of growing size will sooner or later stop fitting into the gas limit and block the function (DoS). Use mappings and "pick it up yourself" (reentrancy and pull).
8. Events instead of storage
If data is needed only by outside observers, write an event, not a slot: an event is an order of magnitude cheaper (events). It does not fit if the contract itself must read the data later.
9. Zeroing and gas refunds
Zeroing a slot refunds part of the gas. Do not abuse it: the refund is capped at a fraction of the transaction.
10. A short call chain
Each external call is overhead, plus a reentrancy risk. Pack related actions into one transaction (including at the wallet level: account abstraction).
11. Fail early
Cheap checks (arguments, rights) go at the start of a function, expensive ones (storage reads, external calls) later. Then a failing transaction reverts cheaply.
12. Optimizer settings
The Solidity compiler can optimise code. The optimizer_runs parameter shows how many calls in the contract's lifetime the code is tuned for: more - a cheaper call but a larger size. The escrow is built with 200 runs (parameters).
What not to do
- Sacrifice readability for trifles. Saving 20 gas is not worth the risk of an error.
- Break security. Do not remove
nonReentrantor checks to save gas (catalogue of vulnerabilities). - Use assembly without need. A powerful tool, but it bypasses the compiler's protections.
- Optimise before tests. Correctness first, then measurements.
Optimisation from a user's viewpoint
A user cannot influence a contract's design, but can reduce costs:
- choose a network with low fees (choosing a network, Layer 2);
- send transactions at a less busy time (the fee market);
- avoid extra operations - for example do not repeat
approveif a suitable allowance already exists; - check the cost in the wallet before confirming.
Look at the components of the service's fees in the article fees by example.
What to read next
Return to the basics and reinforce your path: the glossary. Keep studying security: audits and bug bounties.
Keep reading
- 3 · Under the hood
Storage layout and variable packing in Solidity
How a contract stores data in 32-byte slots, how variables are packed, where mappings and arrays live and how the Coin-Trust deal struct is laid out.
4 min read - 3 · Under the hood
EIP-1559 - how the Ethereum fee market works (base fee, tips and burning)
How fees work after the London upgrade - the base fee, the priority tip, maxFeePerGas, automatic 12.5% adjustment and ETH burning. With calculations and examples.
4 min read
Mentioned in
- Integer math and rounding in smart contracts
- Storage layout and variable packing in Solidity
- CREATE2: how the same contract address appears on four networks
- Oracles and the oracle problem - how a blockchain learns about the outside world
- EIP-1559 - how the Ethereum fee market works (base fee, tips and burning)
- MEV and front-running - who earns from transaction order and how
- Foundry for beginners - compiling, tests, fuzzing and a local network
- Audits and bug bounties - how smart contracts are examined and how to read a report
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

