← Insights
chain

Gas Optimization for EVM Contracts: What Actually Moves the Number

Your contract works. It also burns more gas per transaction than it should, and at scale that overcharge is real money your users pay.

The contract passes every test. It also costs every user more than it has to, on every transaction, forever. That overcharge feels invisible because each transaction is a few cents over budget. Multiply by your user count, multiply by your transaction frequency, and the rounding error becomes a line item your CFO can see.

Gas is the one performance metric in this space that your users pay directly. A slow API costs you servers. An expensive contract costs them money, every single interaction, and they feel it. So when teams ask whether gas optimization is "premature," the answer depends entirely on volume. At a hundred transactions, ignore it. At ten million, the difference between a tight contract and a sloppy one is a budget.

Let's tear down where the gas actually goes, and where chasing it is a waste.

Storage is the whole game

If you optimize one thing, optimize storage. On the EVM, writing a fresh storage slot costs 20,000 gas. Updating an existing nonzero slot costs 5,000. A cold read costs 2,100. Everything else — arithmetic, comparisons, memory — is rounding error next to a storage write. A single SSTORE can cost more than several hundred arithmetic operations.

This reframes the entire problem. Gas optimization is not "make the code clever." It's "touch storage less." Every design conversation should start by asking how many slots this writes and whether it has to.

Pack your structs, but pack them right

The EVM stores state in 32-byte slots. Variables that fit together in 32 bytes can share a slot, and the compiler packs adjacent storage variables automatically — if you let it. A uint128 next to a uint128 shares one slot. A uint256 followed by a uint128 followed by another uint256 wastes the middle slot entirely because the packing got interrupted by alignment.

So order matters. Group small types together. A uint96 timestamp, a uint96 amount, and an address (20 bytes) fit in a single 32-byte slot if you declare them adjacent. Split them across the struct and you've turned one SSTORE into three. At write-heavy volume, that's a 2x gas difference on the most expensive operation in the contract, achieved by reordering three lines.

The trap: packing only helps when the packed fields are written together. If you write the timestamp on one transaction and the amount on another, you read-modify-write the whole slot twice and the packing bought you nothing — sometimes worse, because of the masking overhead. Pack fields that change together.

Calldata is cheaper than storage, and cheaper than you think

Reading from calldata is dramatically cheaper than reading from storage. Mark function parameters calldata instead of memory when you don't mutate them — for arrays and structs this skips a full copy. For external functions taking large arrays, this alone can save thousands of gas per call.

Calldata itself is priced by byte: nonzero bytes cost more than zero bytes. This is why tightly encoded calldata beats verbose ABI encoding for high-frequency functions, and why some protocols ship custom calldata layouts for their hot paths. That's an advanced move — reach for it only when the function is genuinely hot and you've measured it.

Batching is where the real savings live

Every transaction pays a 21,000 gas base cost just to exist, before your code runs. If your users do five related actions, that's 105,000 gas in base cost alone, plus five rounds of the per-call overhead. A single batched function that does all five pays the base cost once.

Batching is usually a bigger win than any micro-optimization inside a function. A multicall pattern, or a purpose-built batch function, collapses N transactions into one and amortizes the base cost, the cold-access costs, and often shared storage reads. If your product has users doing sequences of actions — claims, transfers, updates — batching is the first optimization to reach for, and it changes the cost curve, not just the constant.

The patterns that move the number

In rough order of impact:

// high impact
SSTORE avoidance        → don't write storage you can derive or omit
struct packing          → fields that change together share a slot
batching                → amortize the 21k base cost across actions
calldata over memory    → skip the copy on read-only params
caching storage reads   → read a slot once into memory in a loop

// low impact, usually noise
uint8 vs uint256        → smaller types in memory often cost MORE
unchecked arithmetic    → real but small, and a security tradeoff
custom errors           → saves deploy + revert gas, do it, but minor

That last block matters. Using uint8 for a memory variable to "save space" frequently costs more gas, because the EVM operates on 32-byte words and the compiler inserts masking to enforce the smaller type. Small types are for storage packing, not for memory. Teams who sprinkle uint8 everywhere thinking it's frugal are often making the contract slower.

Where micro-optimization is a waste

Shaving 50 gas off a function that runs once at deployment is theatre. So is hand-unrolling loops the compiler already handles, or rewriting clean Solidity into inline assembly to save a few hundred gas on a function called twice a day. The gas you save has to be worth the readability and audit cost you spend.

The honest accounting: optimize the functions on your hot path, at your real volume, measured. Leave the cold paths readable. A contract that's been assembly-golfed end to end is a contract no auditor wants to read and no engineer wants to maintain, and the savings on the cold 90% of it rounded to zero.

The security tradeoff nobody mentions

Aggressive gas optimization fights safety. unchecked blocks skip overflow protection to save gas — fine when you've mathematically proven the value can't overflow, a live exploit when you're wrong. Inline assembly bypasses Solidity's type and bounds checks entirely, which is the point and also the danger. Packed storage with manual bit manipulation is a class of bug all its own.

The rule: every gas optimization that removes a safety check must come with a written proof of why the check was redundant. "It saves gas" is not a reason to remove an overflow guard. The most expensive gas optimization in history is the one that opened a hole and drained the contract. Cheap transactions on a drained contract are not a win.

What fixed looks like

A fixed contract has been profiled at projected peak volume, with a gas report showing cost per function. The hot path is optimized — storage minimized, structs packed for co-written fields, batching available where users act in sequences, calldata used for read-only params. The cold paths are left readable. Every unchecked block and every assembly section carries a comment proving the safety check it skipped was redundant. And the optimization stopped at the point where further savings cost more in audit and maintenance than they return in gas.

The user sees a transaction that costs what it should, not what a sloppy first draft happened to cost. At scale, that's the difference between a product people use and one they abandon at checkout.

This is for you if

You're running a real-money EVM system at volume — many users, frequent transactions — where gas cost is a competitive and financial concern, not a curiosity. You've shipped, you've seen the per-transaction cost, and you want it audited and brought down without trading away safety. Gas-optimization-and-audit engagements here start at $50k+; a full security-plus-performance pass on a live protocol runs higher.

This is not for you if you're deploying a memecoin once and never touching it again — deploy gas is a one-time cost and not worth optimizing. It's not for NFT drops where the mint happens once and the contract goes quiet. We work on systems that run hot, hold value, and where the gas bill compounds. If your contract gets called a thousand times total, leave it readable and spend your money elsewhere.