Skip to main content

Type a word from a question or an article title.

3 · Under the hood

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.

4 min readUpdated:
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

OperationGas (order of magnitude)
Simple arithmetic3-5
Reading memory3
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:

solidity
// 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:

solidity
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:

solidity
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 nonReentrant or 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 approve if 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.

Return to the basics and reinforce your path: the glossary. Keep studying security: audits and bug bounties.

Mentioned in

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.