GAS OPTIMIZATION

 

GAS OPTIMIZATION

 

1. EIP-1559 & Gas Fee Mechanism

Base Fee

        Set by the Ethereum protocol — you cannot control it

        This amount is ALWAYS burned (nobody receives it)

        If last block was FULL → base fee goes UP by ~12%

        If last block was EMPTY → base fee goes DOWN by ~12%

        Accessible in Solidity (v0.8.7+) via block.basefee

 

Max Fee Per Gas

        Maximum gwei per gas you are willing to pay in total

        Must be HIGHER than the current base fee or tx won't go through

        If max fee > base fee → difference is REFUNDED to you

        Gas price per gwei is always ≤ max fee

 

Max Priority Fee Per Gas (Tip)

        Tip to the miner to prioritize your transaction

        Miner receives whichever is SMALLER: priority fee  OR  (max fee − base fee)

 

Example

Variable

Value

Max Fee

50 gwei

Max Priority Fee

2 gwei

Protocol Base Fee

36 gwei

Burned

36 gwei 🔥

Miner receives

2 gwei

Refunded to you

12 gwei 💸

 

💡 The Yellow Paper is outdated. Always refer to EIP-3529 for current refund rules.

 

2. Memory

Memory Layout

        Indexed like an array — in 32-byte (0x20) increments

        Only exists during transaction execution (like RAM)

        Every 32-byte slot allocated costs 3 gas

        Charged for ALL slots up to the highest one written — even if skipping earlier ones

 

Solidity's Reserved Memory (Always Allocated)

Bytes

Purpose

0x00 – 0x3F  (slots 0–1)

Scratch space for hashing methods

0x40 – 0x5F  (slot 2)

Free memory pointer (stores 0x80 = 128)

0x60 – 0x7F  (slot 3)

Zero slot (reserved, never written to)

 

Solidity always executes: PUSH 0x80 → PUSH 0x40 → MSTORE at function start

This stores 128 (0x80) at slot 0x40, indicating: 'free memory starts at byte 128'

 

Memory Explosion Warning

Array Size

Gas Cost

uint256[10]

342 gas

uint256[20]

403 gas

uint256[30]

464 gas

uint256[10,000]

276,374 gas

uint256[20,000]

992,221 gas

uint256[30,000]

OUT OF GAS 💥

 

⚠️  Block gas limit is 31 million. Never allocate very large in-memory arrays — they will consume enormous gas or cause out-of-gas errors.

 

3. Storage

Storage is the MOST expensive operation in Ethereum — an order of magnitude more costly than almost everything else. All nodes must store it permanently.

 

Storage Write Costs

Operation

Gas Cost

Why

Zero → Non-Zero

20,000 gas

New data indexed on all nodes forever

Non-Zero → Non-Zero

5,000 gas

Slot already tracked, just updating

Non-Zero → Zero

REFUND

Freeing storage; reward for cleanup

Write same value (no change)

100 gas

Warm access only, no real change

 

Cold vs Warm Access

        Cold Access (2,100 gas): First time you touch a storage variable in a transaction

        Warm Access (100 gas): Any subsequent access to the SAME variable in the same transaction

 

💡 Cache storage variables into local variables if read multiple times in one function — pay cold access once, then use the cheap local copy.

 

Read + Write Cost Insight

Scenario

Calculation

Total

Read THEN Write (0→nonzero)

2,100 (cold read) + 20,100 (warm write)

22,200 gas

Write only (0→nonzero)

2,100 (cold) + 20,000 (write)

22,100 gas

Conclusion

Cost(Read + Write) ≈ Cost(Write only)

~same!

 

Arrays in Storage

        Arrays use TWO storage slots:

        Slot 0: stores the LENGTH of the array

        Slot 1+: stores the actual VALUES

 

Example — Adding one element to empty array [5]:

Cost

Reason

21,000 gas

Base transaction

22,100 gas

Store VALUE (0→nonzero + cold access)

22,100 gas

Store LENGTH (0→nonzero + cold access)

~65,000 gas total

 

 

⚠️  Use .push() instead of rewriting the whole array — avoids re-touching existing slots.

⚠️  Deleting long arrays is expensive: each element costs 5,000 gas with only partial refund. A 1,000-item array delete could cost millions of gas.

 

Storing 1KB on Ethereum (Practical Calculation)

        1 KB = 1,024 bytes

        Each slot = 32 bytes → 1024 ÷ 32 = 32 slots

        Cost per slot (0→nonzero) = 22,100 gas

        Total: 32 × 22,100 = 707,200 gas

        At 50 gwei gas price: 50 × 707,200 = 35,360,000 gwei = ~$106 USD

 

Gas Refund Mechanics

Berlin Rules (Old — Yellow Paper)

        Refund up to 1/2 of total gas spent

        Max refund: 15,000 gas per cleared variable

        Self-destruct also gave refund

 

London Rules (Current — EIP-3529)

        Refund up to 1/5 of total gas spent

        Max refund: 4,800 gas per cleared variable

        Self-destruct NO LONGER gives refund

 

Rule

Berlin (Old)

London (Current)

Max refund cap

½ of gas spent

⅕ of gas spent

Max per variable

15,000 gas

4,800 gas

Self-destruct refund

Yes

No ❌

 

💡 Each variable you clear: pay 5,000 gas, get back max 4,800 → net cost of 200 gas per extra variable cleared. Refunds only fully work when paired with other expensive operations in the same tx.

 

Count DOWN, Not Up

If tracking a number (e.g. NFTs minted per address):

        Counting UP:  0→1→2→3 = pay 22,100 + 5,000 + 5,000 = 32,100 gas total

        Counting DOWN: 3→2→1→0 = cheaper! Final step to zero earns refund

💡 Design counters to decrement to zero — the refund on the final step offsets earlier costs.

 

4. The Solidity Optimizer

What It Does

The optimizer has one job with two modes — a tradeoff between deploy cost and execution cost.

Runs Setting

Optimizes For

Contract Size

Execution Cost

Low (e.g. 1)

Deployment cost

Smaller ✅

More expensive per call ❌

High (e.g. 1,000,000)

Execution cost

Larger ❌

Cheaper per call ✅

Default (200)

Balanced (deploy)

Moderate

Not optimal for users

 

Real Example — ERC-20 Contract

Optimizer Setting

Contract Size

Transfer Cost

Off

6,160 bytes

47,605 gas

200 runs (default)

2,919 bytes

46,814 gas

10,000 runs

3,587 bytes

~46,700 gas

 

Best Practice

        Keep increasing runs and measure your key function's gas cost

        Stop when you no longer see improvement

        Only use low runs if you are very sensitive to deployment cost

        Uniswap V3 uses 1,000,000 runs — correctly predicting millions of calls

⚠️  Default of 200 in Hardhat/Remix prioritizes the developer's one-time deploy cost over every user's ongoing transaction cost. For production DeFi, increase this significantly.

 

5. Function-Level Optimizations

payable Functions Are Cheaper

Function Type

Gas Cost

Reason

Non-payable

21,186 gas

Auto-generates ETH check: if(msg.value != 0) revert

payable

21,162 gas

No ETH check needed — skips those opcodes

Saving

24 gas

Fewer opcodes executed

 

💡 The payable keyword removes an auto-generated ETH check from compiled bytecode → fewer opcodes → less gas.

 

Function Name Affects Gas (Selector Order)

        Function selector = first 4 bytes of keccak256(functionSignature)

        Solidity sorts selector checks in ASCENDING hexadecimal order

        Functions with lower hex selectors are checked FIRST → cost less gas

        Each additional check costs exactly 22 gas (3+3+3+10+3 from opcodes: PUSH+EQ+PUSH+JUMPI+DUP1)

 

Function

Selector

Position

Extra Gas

red

0x29...

1st checked

+0 gas

blue

0xE1...

2nd checked

+22 gas

green

0xF2...

3rd checked

+44 gas

 

⚠️  When benchmarking a function, NEVER change its name during testing — you won't know if gas changes come from your code edits or from the selector position shifting.

 

calldata vs memory for Function Parameters

Keyword

Gas

Can Modify?

Use When

calldata

21,865

❌ Read-only

Don't need to modify input

memory

22,011

✅ Yes

Need to modify the input

memory (direct)

22,419

✅ Yes

Cheaper than calldata+copy when mutation needed

 

💡 If you need to mutate the input: declare it as 'memory' directly instead of 'calldata + local copy'. This saves ~23 gas.

 

Short-Circuit Evaluation (&&, ||)

        OR  ||  → if first condition is TRUE, second is SKIPPED

        AND && → if first condition is FALSE, second is SKIPPED

        Put the CHEAPER condition first to maximize skipping the expensive one

 

Example — Token Sale:

Check

Cost

Order

block.timestamp > saleStart

2 gas

Put FIRST ✅

isOnAllowList[msg.sender]

2,100 gas (storage)

Put SECOND

 

💡 Real rule: Expected Gas = Cost(A) + P(B is reached) × Cost(B). Multiply probability × cost, not just cost alone.

 

6. Variable Packing

Each storage slot = 32 bytes. Packing means fitting multiple smaller variables into one slot.

 

Without Packing

With Packing

uint256 a  → slot 0 (32 bytes)

uint128 a 

uint256 b  → slot 1 (32 bytes)

uint128 b  ┘ → slot 0 (32 bytes combined)

 

How Solidity Reads Packed Variables

        Each variable has a slot (storage location) AND an offset (position within that slot)

        uint128 a → slot 0, offset 0;   uint128 b → slot 0, offset 16

        To read 'a': load full slot → AND with bitmask to zero out 'b' portion

        This masking costs extra computation compared to plain uint256

 

When to Pack (and When NOT to)

Situation

Use Packing?

Reason

Variables written TOGETHER in same tx

✅ Yes

Saves one full SSTORE (22,100 gas)

Variables written SEPARATELY

❌ No

Masking overhead costs more than saved

Maximum computation speed needed

❌ No

uint256 has no masking overhead

Reducing total storage slots used

✅ Yes

Fewer slots = lower deployment cost

 

💡 Example: Staking contract storing 'amount' (uint128) + 'timestamp' (uint128) — always written together → packs perfectly into one slot → saves ~22,100 gas vs two uint256 slots.

 

7. Loop Optimizations

Cache Array Length

Every time myArray.length is evaluated in a loop condition, it does a WARM SLOAD (100 gas).

 

Version

Gas Cost

Notes

Without cache (dynamic array)

49,119 gas

10 × 100 = 1,000 gas wasted on length reads

With cached length

48,124 gas

Length read only once before loop

Saving

~995 gas

Minus 5 gas for 2 extra opcodes (DUP+POP)

 

Correct pattern:

uint256 length = myArray.length;  // ONE storage read

for (uint256 i = 0; i < length; i++) { ... }

 

⚠️  This trick only works for DYNAMIC arrays. Fixed-size arrays don't store length in storage — the compiler knows it at compile time.

 

8. Bit Shifting Instead of Multiply/Divide

Multiplying or dividing by powers of 2 can be replaced with cheaper bit shift operations.

 

Operation

Traditional Opcode (5 gas)

Bit Shift (3 gas)

Gas Saved

× 2

x * 2

x << 1

2 gas

× 4

x * 4

x << 2

2 gas

× 8

x * 8

x << 3

2 gas

÷ 2

x / 2

x >> 1

5 gas

÷ 4

x / 4

x >> 2

5 gas

 

⚠️  Left shift (<<) can OVERFLOW if ones shift off the left edge. You lose Solidity's built-in overflow protection. Right shift (>>) is safe — worst case is zero.

 

9. Avoiding Same-Value Storage Writes

Writing a value to storage that is ALREADY that value still costs 100 gas (warm access fee).

 

Optimization: use an if-check to skip the write if value won't change.

 

Approach

Gas Cost

Always write (even if same value)

5,000 or 100 gas for no-op

if (_cached != newValue) → write

Saves 35 gas vs always writing

 

💡 This trick saves gas ONLY IF: (a) you already read the variable before writing it, OR (b) most of the time the value is NOT changing. If the value almost always changes, the if-check overhead cancels out the savings.

 

10. Constant Expression Optimization

When Solidity Optimizes Automatically ✅

        Simple arithmetic: 3 * 7 → compiles to PUSH 0x15 (no MUL opcode in bytecode)

        Simple division: 22 / 2 → compiles to PUSH 0xB (no DIV opcode)

        keccak256 of string literal → pre-computed hash in bytecode

 

When Solidity FAILS to Optimize ❌

        Math spread across multiple variables: uint256 a = 22; uint256 b = 2; a / b — DIV opcode appears!

        keccak256 with type conversions — hash computed at runtime

        Exponentiation: 2 ** 10 — EXP opcode appears even though answer is constant

 

⚠️  EXP opcode has VARIABLE gas cost depending on input size. Pre-calculate exponents manually: write 1024 instead of 2**10.

 

How to Check

        Compile in Remix → copy bytecode assembly

        Search for opcodes: MUL, DIV, EXP, SHA3

        If found in a section that should be constant → pre-calculate it yourself

💡 The Solidity compiler is not as mature as GCC/Clang. Never assume it will catch every constant optimization — verify the bytecode when gas matters.

 

11. Summary — 5 Places to Save Gas

#

Area

Key Strategy

1

Deployment Size

Smaller contract = less bytecode stored. Fewer opcodes → cheaper to deploy.

2

Computation

Use fewer or cheaper opcodes. Shift instead of multiply. Use payable. Avoid unnecessary checks.

3

Transaction Data

Fewer bytes sent. Zero bytes cost 4 gas; non-zero cost 16 gas. Minimize calldata.

4

Memory

Avoid allocating more memory than needed. No garbage collector — allocated memory can't be freed.

5

Storage

Most impactful area. Avoid 0→nonzero writes, pack variables, cache reads, count down not up.

 

 


Comments

Popular posts from this blog

Frontend-to-Blockchain Flow

Top-to-bottom map of how an Ethereum node is structured

Arbitrum vs Optimism Rollups