Gas Optimization
In January 2025, a simple Uniswap token swap cost $86 in gas fees during peak congestion. Today, February 1, 2026, that same swap costs $0.39.
Ethereum gas fees have dropped 95% following the Dencun upgrade and mass Layer 2 adoption. Average gas prices hover around 0.08–1.17 gwei, down from 4.99 gwei a year ago. On January 17, 2026, Ethereum processed a record 2.6 million transactions without congestion.
So why does gas optimization still matter?
Because when the next DeFi summer hits and gas spikes back to 50+ gwei, your inefficient contract will cost $50 per transaction while optimized competitors charge $5. Users will abandon your dApp faster than they left during the 2021 NFT mania when gas hit 500 gwei.
This is Day 36 of the 60-Day Web3 journey, still in Phase 3: Development. Yesterday we covered the deployment checklist including gas testing. Today we go deeper: understanding why certain patterns cost more and how to architect for efficiency from the start.
Come hang out in Web3ForHumans on Telegram.
Follow me on Medium | Twitter | Future
Gas optimization isn’t just about saving money. It’s about user retention, protocol sustainability, and building for scale.
2026 reality:
But history repeats:
The pattern: Every bull run brings congestion. Every new use case (DeFi, NFTs, inscriptions, AI agents) floods the network. If your contract isn’t optimized, users will pay the price.
Understanding gas costs requires understanding how Ethereum Virtual Machine (EVM) works.
Gas cost hierarchy (from cheap to expensive):
Why storage is expensive:
The golden rule: Minimize storage operations. Everything else is relatively cheap.
Solidity stores variables in 32-byte (256-bit) slots. If you’re smart, you can pack multiple variables into one slot.
Bad (uses 3 storage slots):
contract Inefficient {
uint256 public a = 1; // Slot 0
uint256 public b = 2; // Slot 1
uint256 public c = 3; // Slot 2
}
// Cost: 3 SSTORE operations = 60,000 gas
Good (uses 1 storage slot):
contract Efficient {
uint128 public a = 1; // Slot 0 (first 128 bits)
uint64 public b = 2; // Slot 0 (next 64 bits)
uint64 public c = 3; // Slot 0 (final 64 bits)
}
// Cost: 1 SSTORE operation = 20,000 gas
// Savings: 40,000 gas (67% reduction)
Real-world example from Uniswap V3:
struct Slot0 {
uint160 sqrtPriceX96; // 160 bits
int24 tick; // 24 bits
uint16 observationIndex; // 16 bits
uint16 observationCardinality; // 16 bits
uint16 observationCardinalityNext; // 16 bits
uint8 feeProtocol; // 8 bits
bool unlocked; // 8 bits
}
// All 7 variables packed into ONE 256-bit slot!
Packing rules:
Variables declared as constant or immutable are not stored in storage. They're compiled directly into the bytecode.
Expensive:
contract Expensive {
address public owner; // SLOAD cost: 2,100 gas per read
uint256 public maxSupply; // SLOAD cost: 2,100 gas per read
constructor() {
owner = msg.sender;
maxSupply = 1000000;
}
}
Cheap:
contract Cheap {
address public immutable owner; // No SLOAD, direct bytecode access
uint256 public constant MAX_SUPPLY = 1000000; // No SLOAD
constructor() {
owner = msg.sender;
}
}
// Savings: 2,100 gas per read (100% reduction for that operation)
When to use:
Every SLOAD costs 2,100 gas. If you read the same storage variable multiple times, cache it in memory.
Expensive:
function badTransfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount); // SLOAD #1
balances[msg.sender] -= amount; // SLOAD #2
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// Cost: 2 SLOADs = 4,200 gas just for reading
Cheap:
function goodTransfer(address to, uint256 amount) public {
uint256 senderBalance = balances[msg.sender]; // SLOAD #1 (cache in memory)
require(senderBalance >= amount); // Memory read (3 gas)
balances[msg.sender] = senderBalance - amount; // SSTORE
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// Cost: 1 SLOAD = 2,100 gas
// Savings: 2,100 gas (50% reduction for that operation)
Arrays require iterating (expensive). Mappings are direct lookups (cheap).
Expensive:
contract ExpensiveWhitelist {
address[] public whitelist;
function isWhitelisted(address user) public view returns (bool) {
for (uint i = 0; i < whitelist.length; i++) { // O(n) complexity
if (whitelist[i] == user) return true;
}
return false;
}
}
// Cost with 100 users: ~210,000+ gas (grows linearly)
Cheap:
contract CheapWhitelist {
mapping(address => bool) public whitelist;
function isWhitelisted(address user) public view returns (bool) {
return whitelist[user]; // O(1) complexity
}
}
// Cost: 2,100 gas (constant, regardless of size)
// Savings: 99% reduction for large lists
public functions can be called both internally and externally, requiring more gas. If a function is only called externally, mark it external.
Expensive:
function publicFunction(uint256[] memory data) public {
// Function body
}
// Cost: Copies array from calldata to memory (expensive)
Cheap:
function externalFunction(uint256[] calldata data) external {
// Function body
}
// Cost: Reads directly from calldata (cheap)
// Savings: ~1,000-5,000 gas depending on array size
Functions that don’t modify state should be marked view or pure. They're free when called externally (off-chain).
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
// Cost when called externally: 0 gas (read-only RPC call)
// Cost when called internally: 2,100 gas (SLOAD)
Order your conditions to fail fast.
Expensive:
require(expensiveCheck() && cheapCheck());
// Runs expensiveCheck() first, even if cheapCheck() would fail
Cheap:
require(cheapCheck() && expensiveCheck());
// Fails on cheapCheck() without running expensiveCheck()
// Savings: Variable, but can be significant
Instead of storing multiple bool variables (each takes a full 256-bit slot), use a single uint256 as a bitmap.
Expensive (8 slots):
contract Flags {
bool flag1;
bool flag2;
bool flag3;
bool flag4;
bool flag5;
bool flag6;
bool flag7;
bool flag8;
}
// Cost: 8 slots = 160,000 gas to initialize
Cheap (1 slot):
contract OptimizedFlags {
uint256 private flags; // Can store 256 boolean flags
function setFlag(uint8 position) internal {
flags |= (1 << position);
}
function clearFlag(uint8 position) internal {
flags &= ~(1 << position);
}
function getFlag(uint8 position) internal view returns (bool) {
return (flags & (1 << position)) != 0;
}
}
// Cost: 1 slot = 20,000 gas
// Savings: 140,000 gas (87.5% reduction)
Inline assembly gives you low-level control and can be more gas-efficient for specific operations.
Solidity:
function getHash(uint256 a, uint256 b) public pure returns (bytes32) {
return keccak256(abi.encodePacked(a, b));
}
// Cost: ~300 gas
Assembly:
function getHashOptimized(uint256 a, uint256 b) public pure returns (bytes32 result) {
assembly {
mstore(0x00, a)
mstore(0x20, b)
result := keccak256(0x00, 0x40)
}
}
// Cost: ~200 gas
// Savings: ~33% reduction
Warning: Assembly bypasses Solidity’s safety checks. Only use for hot paths after thorough testing.
Process multiple items in one transaction instead of many separate transactions.
Expensive:
// User calls this 10 times
function mint(address to) public {
_mint(to, nextTokenId++);
}
// Cost: 10 transactions × 21,000 base gas = 210,000+ gas
Cheap:
function batchMint(address[] calldata recipients) public {
for (uint i = 0; i < recipients.length; i++) {
_mint(recipients[i], nextTokenId++);
}
}
// Cost: 1 transaction × 21,000 base gas + loop operations
// Savings: ~180,000 gas (86% reduction)
Use forge snapshot to track gas usage across your test suite.
Step 1: Write gas-focused tests
// test/GasOptimization.t.sol
contract GasOptimizationTest is Test {
function testTransferGas() public {
token.transfer(alice, 100 ether);
}
function testBatchTransferGas() public {
address[] memory recipients = new address[](10);
uint256[] memory amounts = new uint256[](10);
// ... fill arrays
token.batchTransfer(recipients, amounts);
}
}
Step 2: Create baseline snapshot
forge snapshot
Step 3: Optimize your contract
Step 4: Compare gas usage
forge snapshot --diff .gas-snapshot
Output shows exactly how much gas you saved:
testTransferGas() (gas: -2100 (-4.5%))
testBatchTransferGas() (gas: -18000 (-12.3%))
This workflow is how protocols like Aave and Uniswap maintain gas efficiency across updates.
Uniswap V3 is one of the most gas-optimized protocols in DeFi. Here’s what they did:
1. Packed storage (Slot0 struct):
2. Bitmap for tick tracking:
3. Custom math libraries in assembly:
4. Minimized external calls:
Result: Uniswap V3 swaps cost 30–40% less gas than V2, despite being more feature-rich.
Premature optimization is the root of all evil — Donald Knuth
Don’t optimize:
Do optimize:
The trade-off:
// Readable but expensive
function isWhitelisted(address user) public view returns (bool) {
for (uint i = 0; i < whitelist.length; i++) {
if (whitelist[i] == user) return true;
}
return false;
}
// Optimized but requires more storage management
mapping(address => bool) public whitelist;
function isWhitelisted(address user) public view returns (bool) {
return whitelist[user];
}
For admin-only functions called once a month? The readable version is fine. For user-facing checks called thousands of times daily? Optimize it.
Before deploying to mainnet, run through this checklist:
Current state (February 2026):
Sounds cheap, right?
But remember:
The math:
Inefficient contract: 150,000 gas
Optimized contract: 50,000 gas
At 1 gwei (today): $0.30 vs $0.10 (nobody cares)
At 50 gwei (next bull run): $15 vs $5 (users abandon the $15 one)
At 500 gwei (2021 NFT mania): $150 vs $50 (only whales use the $150 one)
The protocols that survive bull runs are the ones optimized during bear markets.
Mistake 1: Optimizing the wrong things
// Saved 50 gas on an admin function called once a month
function setOwner(address newOwner) public onlyOwner {
owner = newOwner; // Already optimized enough
}
// Ignored 5,000 gas waste on user function called 1000x/day
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount); // Multiple SLOADs!
balances[msg.sender] -= amount;
// ...
}
Focus on user-facing, high-frequency functions first.
Mistake 2: Breaking security for gas savings
// DON'T DO THIS
function unsafeTransfer(address to, uint amount) external {
balances[msg.sender] -= amount; // No checks!
balances[to] += amount;
}
Security > Gas savings. Always.
Mistake 3: Not measuring before optimizing
// Spent 2 hours optimizing this
function complexCalculation() internal pure returns (uint) {
// assembly magic that saved 200 gas
}
// But function is only called once during deployment
Use forge snapshot to identify actual bottlenecks.
1. Foundry’s gas profiling
# Detailed gas report
forge test --gas-report
# Snapshot for tracking changes
forge snapshot
# Compare against baseline
forge snapshot --diff .gas-snapshot
2. Hardhat Gas Reporter (if using Hardhat)
// hardhat.config.js
module.exports = {
gasReporter: {
enabled: true,
currency: 'USD',
gasPrice: 21
}
};
3. Solidity Visual Developer (VSCode extension)
4. Tenderly (for deployed contracts)
Gas optimization is about respecting your users’ money.
Today’s $0.39 Uniswap swap is cheap because teams spent years optimizing. When gas was 500 gwei in 2021, unoptimized DEXes died while Uniswap thrived.
The techniques we covered today aren’t theoretical:
You don’t need to optimize on Day 1. But by the time you’re ready for mainnet deployment, gas efficiency should be baked into your architecture.
The 80/20 rule for gas optimization:
Start with the easy wins. The advanced techniques (assembly, bitmaps) come later when you’re squeezing the last bits of efficiency.
Open your favorite DeFi protocol (Uniswap, Aave, Compound) on Etherscan. Look at their contract source code. Count how many times you see:
These aren’t accidents. They’re the result of thousands of hours optimizing for user experience.
Now look at a failed DeFi protocol from 2021. Check their gas costs per transaction. See the difference?
Gas optimization isn’t glamorous. But it’s what separates protocols that scale from protocols that die when gas spikes.
Come hang out in Web3ForHumans on Telegram.
Follow me on Medium | Twitter | Future
Read the previous article: Testnet to Mainnet: The Checklist Every Developer Needs Before Deploying Real Money
Want to try this yourself? Take one of your contracts and run forge snapshot. Identify the most expensive function. Apply storage caching or variable packing. Run forge snapshot --diff and see how much gas you saved. Share your results in the Telegram community!
Gas Optimization Strategies: Why Your Contract Costs More (And How to Fix It) was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

Copy linkX (Twitter)LinkedInFacebookEmail
Why a $58,000 bitcoin is the key number for