Gas optimization in Solidity is the discipline of reducing on-chain computation costs without breaking the security guarantees your contract depends on. Sounds simple. It isn't. The Optimism double-spend bug in 2022 came down to a low-level call optimization that bypassed a critical check — and it was found before exploit, but only barely. Most aren't that lucky.
Here's the thing nobody says out loud at developer conferences: the most dangerous optimizations are the ones that actually work. They pass tests. They reduce gas by 30-40%. They ship. And then six months later, someone finds the edge case your optimizer introduced and drains the pool.
This guide is for developers who are about to optimize their Solidity contracts and want to know which patterns are safe, which are traps, and how to tell the difference before it costs someone real money.
What Is Gas Optimization in Solidity?
Gas optimization in Solidity means rewriting smart contract logic to consume fewer gas units per transaction — reducing costs for users and making your protocol more competitive. Every EVM opcode costs gas: SLOAD (storage read) costs 2,100 gas cold, MLOAD (memory read) costs 3 gas. The gap is 700x. That gap is where most optimization happens — and where most optimization mistakes happen too.
The common techniques are well-documented: pack storage variables, use calldata instead of memory for read-only function arguments, cache storage reads in local variables, use custom errors instead of revert strings, avoid loops over unbounded arrays. All valid. All with failure modes that can compromise security when applied carelessly.
Why Does Gas Optimization Create Security Vulnerabilities?
Most developers think of security and gas optimization as separate concerns. Write secure code first, then optimize. Clean separation. That mental model breaks in practice because optimizations change execution order, remove intermediate state, and alter which checks run when.
According to the Rekt News database, access control failures and logic errors — not reentrancy — account for the largest category of DeFi losses, responsible for over $1.8B in protocol drains across documented incidents through 2023. A significant subset of those logic errors trace back to optimization changes that were made post-audit, after the security review had already signed off on a different version of the code.
Think about that. The audit was clean. The deployed contract wasn't — because a developer optimized between review and deployment.
Which Smart Contracts Have Been Exploited Through Optimization Anti-Patterns?
The Hundred Finance hack in April 2023 cost $7.4M and is one of the cleaner examples of an optimization-adjacent exploit. The protocol used a compound-style cToken implementation where the exchange rate calculation was vulnerable to share inflation — a pattern that developers often touch when optimizing vault accounting logic to reduce SLOAD calls. When you cache a value that gets manipulated between reads, the cached version becomes a weapon.
The root: optimization pressure leads developers to read storage once and reuse the value. If an attacker can manipulate the underlying state between when you read and when you act, the cached "optimization" becomes the exact attack surface they need.
This is the ERC-4626 inflation attack in miniature. And it shows up constantly in audit reports.
What Is the Most Dangerous Gas Optimization Pattern in Solidity?
Caching storage variables incorrectly. Everyone knows to do this:
// Solidity 0.8.24 — VULNERABLE PATTERN
// Looks like a clean optimization. It's not.
contract VaultOptimizedBad {
mapping(address => uint256) public shares;
uint256 public totalShares;
uint256 public totalAssets;
// "Optimized" withdraw — reads storage once, caches locally
function withdraw(uint256 sharesToBurn) external {
uint256 _totalShares = totalShares; // cache to save SLOAD
uint256 _totalAssets = totalAssets; // cache to save SLOAD
uint256 assetsOut = (sharesToBurn * _totalAssets) / _totalShares;
// ⚠️ State update happens AFTER external call
// If token.transfer() triggers a callback, attacker re-enters
// with _totalShares and _totalAssets still cached at old values
token.transfer(msg.sender, assetsOut);
shares[msg.sender] -= sharesToBurn;
totalShares -= sharesToBurn;
totalAssets -= assetsOut;
}
}
This pattern fails in two ways simultaneously. First, the cached values _totalShares and _totalAssets are stale the moment a reentrant call updates storage. Second, the state updates happen after the external call — a textbook reentrancy setup that your "optimization" just made worse by ensuring the reentering call sees the pre-update totals.
Here's the corrected version — still gas-efficient, actually secure:
// Solidity 0.8.24 — SECURE PATTERN
// Uses OpenZeppelin 5.x ReentrancyGuard and checks-effects-interactions
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract VaultOptimizedSafe is ReentrancyGuard {
mapping(address => uint256) public shares;
uint256 public totalShares;
uint256 public totalAssets;
IERC20 public token;
function withdraw(uint256 sharesToBurn) external nonReentrant {
// Read storage once — still the optimization
uint256 _totalShares = totalShares;
uint256 _totalAssets = totalAssets;
uint256 userShares = shares[msg.sender];
// CHECKS — validate before anything else
require(sharesToBurn > 0 && sharesToBurn <= userShares, "Invalid amount");
require(_totalShares > 0, "No shares");
uint256 assetsOut = (sharesToBurn * _totalAssets) / _totalShares;
// EFFECTS — update all state BEFORE external interaction
shares[msg.sender] = userShares - sharesToBurn;
totalShares = _totalShares - sharesToBurn;
totalAssets = _totalAssets - assetsOut;
// INTERACTIONS — external call last, after state is finalized
token.transfer(msg.sender, assetsOut);
}
}
The gas savings from caching are preserved. The reentrancy vector is closed. nonReentrant adds roughly 2,300 gas as a safety net — worth it, not negotiable. State updates before the external call mean even if reentrancy somehow bypassed the guard, the math would be wrong for the attacker.
How Can I Optimize Gas Without Introducing Security Risks?
The rule is simpler than most developers make it: optimize storage layout and data types freely. Optimize computation carefully. Never optimize control flow or state update ordering without a full mental model of what external calls can do between your lines.
Safe optimizations that don't touch security:
- Pack struct variables so they fit in fewer 32-byte storage slots — pure layout change, zero execution risk
- Use
uint128oruint64instead ofuint256when values are bounded — pack two into one slot - Replace revert strings with custom errors (
error Unauthorized()saves ~50 gas per revert vs a string) - Mark functions
vieworpurewhen they don't modify state — not just for gas, but for clarity that catches security bugs - Use
calldatafor function parameters that are only read:function process(bytes calldata data)instead ofmemory - Use
unchecked {}blocks for arithmetic that is mathematically guaranteed not to overflow — but only after you've proven the bounds, not just tested them
Risky optimizations that need security review every time:
- Removing intermediate variables to save stack depth — can eliminate checks
- Reordering operations to batch storage writes — can break checks-effects-interactions
- Replacing
SafeERC20with rawtransfer()calls to save gas — fails silently on non-standard tokens - Inlining modifier logic to avoid the function call overhead — easy to skip a condition
- Using assembly for any operation that touches external state
How I'd Catch This Before It Ships
When I'm reviewing a contract that's been through a round of gas optimization, I'm not re-running the full audit. I'm looking for a specific set of changes that optimization almost always introduces.
First thing: git diff between the audited version and the current version. If you don't have that diff, that's already a problem. I want to see every line that changed. Optimizations that look like one-line changes are often two-line changes where the second line is a load-bearing check that got deleted.
Second: I trace every external call in the diff. Any transfer(), call(), delegatecall(), or interface function call — I want to know what state is unwritten at the moment that call executes. Storage caching makes this harder to see visually because the writes look like they're happening right there in the code, when actually they're deferred.
Third: I check every unchecked block added in the optimization pass. According to the Solodit audit database, unchecked arithmetic added during optimization appears in roughly 1 in 8 high-severity findings — developers proving overflow impossibility at test time, not at exploit time.
Fourth: storage slot collision checks on any contract that got struct packing changes. You packed variables into the same slot — did you also change a proxy's storage layout without updating the implementation's expected offsets?
Static tools like Slither catch some of this — the reentrancy flags, obvious unchecked overflow patterns. But they miss economic logic. They can't tell you that your cached exchange rate is manipulable via a flash loan three calls deep. That's where you need something that understands context, not just syntax.
How Does SmartContractAuditor.ai Help With Gas and Security Tradeoffs?
If you're optimizing a contract right now, run it through SmartContractAuditor.ai before you deploy. The tool flags the exact patterns covered here — incorrect checks-effects-interactions ordering, unsafe unchecked blocks, storage caching that creates reentrancy windows — and explains in plain English why each one is dangerous in context. It's not just pattern matching. It understands that a cached value before an external call is a different risk than a cached value used for a pure computation. Paste your contract in. 30 seconds. Know what you're shipping.
5 Specific Actions Before You Ship Optimized Solidity
- Run
slither . --detect reentrancy-eth,reentrancy-no-eth,unchecked-lowlevelon your optimized contract — not your pre-optimization version. Optimization changes break things Slither was clean on. - Audit every
unchecked {}block manually. For each one, write a comment explaining exactly why overflow is impossible — the bounds proof, not just "it can't happen." If you can't write the comment, remove theunchecked. - Check
token.transfer()andtoken.transferFrom()calls. Use OpenZeppelin 5.xSafeERC20.safeTransfer()unless you have a specific, documented reason not to. The gas saved by dropping it is not worth the silent failure on USDT and similar non-standard tokens. - Compare storage layouts if you're using a proxy pattern. After struct packing, run a storage layout check:
forge inspect YourContract storage-layout --pretty. Verify slots match what your proxy expects. - Paste your optimized contract into SmartContractAuditor.ai before deployment. It catches the interaction between optimization patterns and security invariants — the layer that static analysis misses and that costs protocols millions after the fact.
Frequently Asked Questions
What is gas optimization in Solidity?
Gas optimization in Solidity is the practice of rewriting smart contract code to consume fewer EVM computation units (gas) per transaction, reducing costs for users and making protocols more competitive. Common techniques include packing storage variables into fewer slots, using calldata instead of memory, caching storage reads in local variables, and replacing revert strings with custom errors. Each technique has performance benefits and potential security tradeoffs if applied without understanding execution context.
How can gas optimization introduce security vulnerabilities?
Gas optimization often changes execution order, removes intermediate state, or alters when checks run — any of which can break security invariants the original code depended on. The most common failure is caching a storage variable before an external call, then updating storage after: reentrancy can exploit the stale cached value. Another frequent issue is adding unchecked arithmetic blocks without rigorously proving overflow is impossible, leaving arithmetic exploitable under edge-case inputs.
Has gas optimization ever directly contributed to a real DeFi exploit?
Yes. The Hundred Finance hack in April 2023 resulted in $7.4M lost, with the exploit rooted in ERC-4626 share inflation — a vulnerability class that frequently intersects with exchange rate caching patterns developers use to reduce storage reads. More broadly, the Rekt News database shows that logic errors, many introduced during post-audit optimization, represent a major subset of protocol losses. Optimizations applied after the security review has closed are a known high-risk window.
Is it safe to use unchecked blocks in Solidity for gas savings?
unchecked blocks are safe only when you can mathematically prove the arithmetic cannot overflow or underflow under any possible input or state. They should never be applied speculatively or just because tests pass — tests don't cover adversarial inputs. Every unchecked block should have an inline comment documenting the exact invariant that prevents overflow. If you can't write that comment confidently, keep the checked arithmetic and accept the gas cost.
What is the difference between safe and unsafe gas optimization in smart contracts?
Safe optimizations work at the data layer — storage packing, variable type sizing, using calldata, custom errors — and don't touch execution order or external call boundaries. Unsafe optimizations change when state is written, reorder operations around external calls, remove guards to save stack depth, or use unchecked math without proof. The key test: if your optimization changes anything that happens between an external call and a state update, treat it as a security change requiring full re-review, not just a performance tweak.
Further Reading
Related Articles
Continue exploring smart contract security with these related insights
OpenZeppelin vs Custom Smart Contract Implementations: The Security Trade-offs Nobody Talks About
Custom ERC20 implementations have caused hundreds of millions in losses — not because developers are bad, but because rolling your own token logic is a minefield. Here's what actually separates safe contracts from the ones that get drained.
OpenZeppelin's Library Secured $37 Trillion. Are You Actually Using It Right?
OpenZeppelin's library secures an estimated $37 trillion. But the library being secure and your deployment being secure are two different claims — here are the real misuse patterns that slip through.
The Smart Contract Security Checklist Every Developer Needs Before Deploying
$197M drained from Euler Finance in 13 minutes because of a donation function nobody thought was dangerous. If that team had run through a proper security checklist before deploying, the exploit path didn't exist. Here's the checklist that actually matters.
Explore more insights on smart contract security andblockchain vulnerabilities