When an optimization becomes a bug
Gas optimization is a real engineering constraint. A contract that costs hundreds of dollars in gas to deploy when it could cost fewer is a contract that has shipped wrong. A contract that uses two hundred thousand gas per call when it could use eighty thousand is a contract that has priced its users out.
The trade-off appears when an optimization removes a safety property the developer was depending on. Most of the entries below are real patterns from production audits.
The cost of a missed bug is paid in user funds. The cost of a missed optimization is paid in operating cost. The two costs are not interchangeable.
This paper focuses on the EVM ecosystem (Solidity). Gas semantics differ across runtimes; ecosystem-specific cost models for Solana (compute units), Move, Cairo (steps), CosmWasm, and TON (per-contract gas) are covered in their own papers.
The pattern catalog
Variable packing and storage layout. Solidity packs same-slot variables when their sizes allow. A uint128 followed by another uint128 shares a slot; a uint128 followed by a uint256 does not. Packing saves an SSTORE on writes. The risk is that the layout becomes the contract's API for upgrades; a packed slot in v1 cannot be changed in v2 without storage collision.
The audit question: every storage struct, what is the layout, and what does the upgrade plan do with it?
Unchecked arithmetic. Solidity 0.8+ checks arithmetic. unchecked { x += 1 } skips the check. The use cases are loop counters and provably-safe arithmetic. The risk is that the developer's "provably safe" is provably safe under assumptions that the audit needs to verify.
The audit question: every unchecked block, what is the invariant that guarantees no overflow?
Custom errors vs revert strings. Custom errors are cheaper than revert strings and carry structured data. Reverts with strings show up in stack traces and tool output; custom errors require ABI-decoding to interpret. The trade-off is operational visibility against gas cost.
Most modern audits accept custom errors as standard; the question is whether the off-chain tooling understands them.
Assembly shortcuts. Inline assembly lets a developer write specific EVM opcodes. Common use cases: efficient cloning (EIP-1167 minimal proxies), tight loops, function-pointer manipulation, low-level calls.
Risks compound. Assembly bypasses the Solidity compiler's safety guarantees. A memory pointer that is correct in v1 may be wrong in v2 when the compiler changes layout. A function-pointer call that worked under one compiler version may misbehave under another.
The audit question: every assembly block, what guarantees does it depend on that Solidity provides outside the block, and how do we know those guarantees hold?
Events as cold storage. A contract that emits events instead of storing data on-chain saves significant gas on the write. The data is queryable through the log index. The trade-off: on-chain contracts cannot read past events. If a future contract needs to know the historical value, it has lost access.
The audit question: which data was supposed to be queryable on-chain and is now only available off-chain?
Function visibility downgrade. Marking a function external instead of public saves gas on the calling convention; the function cannot be called internally from the same contract. The risk is that a future internal caller fails silently or fails compilation.
The audit question: every visibility annotation, what assumption does it encode about who calls this function?
Gas-optimized standard libraries. Vectorized's Solady library is a gas-optimized alternative to OpenZeppelin's standard contracts; widely used in gas-conscious deployments. Solady's optimizations are aggressive (assembly-heavy, low-level memory manipulation); the audit work shifts from "is this re-implementation of a known pattern correct" to "do the optimizations break invariants that the equivalent OpenZeppelin implementation maintained."
The audit question: every Solady (or equivalent) primitive in use, does the optimization preserve the safety property the audit needs?
Loop unrolling and bounded iteration. A function that loops over an unbounded array can run out of gas. The optimization is to bound the iteration; the risk is that a malicious actor can populate the array up to the bound and DoS the function. Pull-over-push is the canonical mitigation.
The audit question: every loop over user-supplied data, what is the upper bound, and what is the worst case at that bound?
Bit-packing for batch operations. Some contracts pack multiple boolean flags into a single uint256 to amortize storage cost across an operation that touches all of them. The optimization works; the risk is that the bit-level semantics become the contract's authoritative reference, and a developer reading the code may miss what each bit means.
The audit question: every bit-packing scheme, is the encoding documented and tested?
Compiler-pipeline choices. The via_ir compilation pipeline (Solidity 0.8.x+) changes gas costs and stack-allocation semantics; some optimizations apply only with via_ir enabled. The optimizer runs parameter tunes for fewer-deployments-many-calls vs many-deployments-few-calls; setting it wrong shifts optimization in security-relevant ways.
The audit question: was the contract under review compiled with the same flags the audit assumes?
Transient storage (EIP-1153, Cancun). A new EVM primitive for storage that resets at transaction end. Changes reentrancy-guard implementation patterns and the cost model around them. The audit question on any contract deployed post-Cancun: do the guards use transient storage, and does the gas-and-security trade-off still hold?
Memory expansion as a DoS vector. Functions that allocate dynamic memory based on user input (large array parameters, unbounded byte concatenation) hit superlinear gas costs as memory expansion fees compound. An attacker can call the function with input sized just past a target threshold to push the call beyond the block gas limit.
The audit question: every function with dynamic memory allocation, what is the worst-case memory size at the inputs the contract accepts?
Patterns that have broken in production
The case literature is the audit reference.
Storage layout migration failures. Multiple audited protocols have shipped upgrades where a slot reservation gap closed or a variable was reordered. The result is either silent data corruption or non-functional state. The fix is mechanical (slot reservations); the failures are real.
Assembly cloning bugs. Minimal-proxy patterns (EIP-1167) use assembly to copy the proxy bytecode. Implementations that copy the wrong number of bytes, or that initialize in the wrong order, ship a proxy that does not work correctly. We have seen this in production code that was used to deploy many contracts.
Unchecked rounding that compounds. A unchecked division-then-multiplication that the developer reasoned about under "amounts are small" turned out to be exploitable when the contract was used at scale; small per-share rounding losses compounded into a real attack.
We do not publish the protocols. The patterns are real.
What we audit for
On every engagement that includes gas-optimization scope, we audit:
- every storage layout against the upgrade plan
- every
uncheckedblock against its stated invariant - every assembly block against its compiler-version assumptions
- every events-as-storage decision against on-chain query needs
- every loop over user data against the worst-case bound
- every bit-packing scheme against its documentation
- compiler flags (
via_ir, optimizerruns) against the audit assumptions - transient-storage usage against post-Cancun semantics
- memory allocation against worst-case input size
- third-party gas-optimization libraries (Solady and equivalents) against the safety properties they replace
The methodology is in our auditing methodology paper. Gas-optimization audits are usually a sub-scope of a full audit, not a separate engagement.
What clean means
A clean gas-optimization audit means the optimizations the protocol has applied do not introduce security trade-offs the protocol cannot defend. It does not mean the contract is fully optimized; that is a different goal with a different cost model. We do not make optimization claims about contracts we are not paid to fully refactor.