Skip to main content
BlockchainMar 28, 2026

Deep EVM #3: Understanding Gas — Why Your Contract Costs What It Costs

OS
Open Soft Team

Engineering Team

Gas Is Not an Abstraction

Developers often treat gas as a vague “cost” metric. In reality, gas is a precisely defined resource accounting system with exact formulas. Every opcode, every byte of calldata, every storage access has a deterministic gas cost defined in the Yellow Paper and subsequent EIPs.

Understanding these costs transforms gas optimization from guesswork into engineering.

Intrinsic Gas: The Baseline

Before your contract code executes a single opcode, the transaction has already consumed gas. This is the intrinsic gas:

intrinsic_gas = 21000                           // base transaction cost
             + 16 * nonzero_calldata_bytes       // per nonzero byte
             + 4 * zero_calldata_bytes            // per zero byte
             + access_list_cost                   // if EIP-2930 access list
             + 32000 (if contract creation)       // CREATE transactions only

For a simple ERC-20 transfer(address,uint256) call:

  • Base: 21000 gas
  • Function selector (4 bytes, likely nonzero): 4 * 16 = 64 gas
  • Address parameter (32 bytes, ~12 nonzero): 12 * 16 + 20 * 4 = 272 gas
  • Amount parameter (32 bytes, ~8 nonzero): 8 * 16 + 24 * 4 = 224 gas
  • Total intrinsic: ~21,560 gas — before any opcode executes.

This is why “zero-calldata” patterns matter for MEV bots. If your bot can encode information in zero bytes instead of nonzero bytes, you save 12 gas per byte. For a backrun transaction with 200 bytes of calldata, optimizing zero bytes can save 2,400 gas.

EIP-2929: The Access List Revolution

Before EIP-2929 (Berlin, April 2021), SLOAD always cost 800 gas and address access cost 700 gas. EIP-2929 changed everything by introducing per-transaction access sets.

How It Works

  1. At transaction start, only the sender and recipient addresses are in the “warm” set.
  2. The first time you touch an address (BALANCE, EXTCODESIZE, CALL, etc.) or storage slot (SLOAD, SSTORE), it is “cold” — you pay the cold surcharge.
  3. All subsequent accesses to the same address/slot within the transaction are “warm” — you pay the warm price.

Cold vs Warm Gas Costs

OpcodeColdWarmSavings
SLOAD21001002000
SSTORE+2100 cold surchargebase cost only2100
BALANCE26001002500
EXTCODESIZE26001002500
EXTCODECOPY26001002500
EXTCODEHASH26001002500
CALL26001002500
STATICCALL26001002500
DELEGATECALL26001002500

EIP-2930 Access Lists

You can pre-declare which addresses and storage slots your transaction will access by including an access list. This “pre-warms” them, paying the cold cost upfront but at a discount:

access_list_cost = 2400 per address + 1900 per storage key

Compare: cold SLOAD costs 2100 gas, but pre-warming via access list costs 1900. You save 200 gas per slot. For transactions that access many storage slots (like DEX swaps reading multiple pool variables), access lists provide meaningful savings.

// Transaction with access list
{
  "to": "0xUniswapV3Pool",
  "accessList": [
    {
      "address": "0xUniswapV3Pool",
      "storageKeys": [
        "0x0000...0000",  // slot0 (sqrtPriceX96, tick, etc.)
        "0x0000...0004",  // liquidity
        "0x0000...0003"   // feeGrowthGlobal
      ]
    }
  ]
}

SSTORE: The Most Complex Opcode

SSTORE gas cost is governed by EIP-2200 (Istanbul) and modified by EIP-2929 and EIP-3529. The cost depends on three factors: the original value (at transaction start), the current value, and the new value.

SSTORE Gas Table

OriginalCurrentNewGas CostRefund
00nonzero22100 (cold) / 20000 (warm)0
nonzerosamedifferent nonzero5000 (cold) / 2900 (warm)0
nonzerosame05000 (cold) / 2900 (warm)4800
nonzerodifferentoriginal200restore refund*
0nonzero020019900

*Restore refunds vary based on whether the slot was originally zero or nonzero.

The key insight: setting a zero slot to nonzero costs 20000 gas (writing a new state trie leaf), while modifying an existing nonzero slot costs 2900 gas (warm). This 7x difference is why:

  • Initializing mappings for new users is expensive (first transfer to a new address)
  • Using 1 instead of 0 as the “unset” value for reentrancy locks saves 17100 gas
  • Deleting storage (setting to 0) gives a refund, but not enough to make “create then delete” patterns free

Gas Refunds After EIP-3529

EIP-3529 (London, August 2021) dramatically reduced gas refunds:

  • Maximum refund capped at 20% of total gas used (was 50%)
  • SELFDESTRUCT refund removed entirely
  • Only SSTORE nonzero-to-zero refund remains: 4800 gas

This killed gas tokens (CHI, GST2) which exploited the old 50% refund by creating and destroying contracts/storage to “bank” gas refunds. With a 20% cap, the math no longer works.

Optimization Patterns That Actually Matter

1. Cache Storage Reads in Memory

Every SLOAD costs 100-2100 gas. If you read the same slot multiple times, cache it:

// BAD: 3 SLOADs (300+ gas)
function withdraw() external {
    require(balances[msg.sender] > 0);
    uint256 amount = balances[msg.sender];
    balances[msg.sender] = 0;
}

// GOOD: 1 SLOAD + 1 SSTORE (but Solidity often optimizes this)
function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0);
    balances[msg.sender] = 0;
}

2. Pack Storage Variables

Multiple variables in one slot = one SLOAD instead of many:

// 3 slots, 3 SLOADs = 6300 gas (cold)
uint256 price;     // slot 0
uint256 amount;    // slot 1
uint256 timestamp; // slot 2

// 1 slot, 1 SLOAD = 2100 gas (cold)
uint128 price;     // slot 0, lower 128 bits
uint64 amount;     // slot 0, next 64 bits
uint64 timestamp;  // slot 0, upper 64 bits

3. Use Calldata Instead of Memory for External Parameters

// BAD: copies array to memory
function processOrders(Order[] memory orders) external { ... }

// GOOD: reads directly from calldata
function processOrders(Order[] calldata orders) external { ... }

4. Short-Circuit Expensive Operations

// BAD: always reads storage
function isAllowed(address user) public view returns (bool) {
    return allowlist[user] || owner() == user;
}

// GOOD: check cheap comparison first
function isAllowed(address user) public view returns (bool) {
    if (owner() == user) return true;  // CALLER is 2 gas
    return allowlist[user];             // SLOAD only if needed
}

5. Use Unchecked Arithmetic When Safe

// Solidity 0.8+ adds overflow checks: ~40 gas overhead per operation
for (uint256 i = 0; i < length; i++) { ... }

// Unchecked: saves ~40 gas per iteration
for (uint256 i = 0; i < length;) {
    // ... loop body ...
    unchecked { ++i; }
}

6. Use Custom Errors Instead of Require Strings

// BAD: stores string in bytecode, expensive revert data
require(amount > 0, "Amount must be positive");

// GOOD: 4-byte selector, no string storage
error InvalidAmount();
if (amount == 0) revert InvalidAmount();

Custom errors save both deployment gas (less bytecode) and runtime gas (4 bytes vs 64+ bytes of revert data).

7. Minimize Calldata Zeros vs Nonzeros

For MEV bots, this is critical. Structure your calldata to maximize zero bytes:

// 16 gas per nonzero byte vs 4 gas per zero byte
// If you can choose how to encode your data, prefer representations
// that contain more zero bytes.
// Example: encode amounts in wei rather than custom units when
// the wei representation has trailing zeros.

Real-World Gas Breakdown: Uniswap V3 Swap

Let us trace the gas consumption of a typical Uniswap V3 exact-input swap:

ComponentGas
Intrinsic (21000 + calldata)~21,500
Function dispatch~100
Input validation~200
Pool state reads (slot0, liquidity)~4,200 (cold)
Price computation (tick math)~5,000
Balance updates (2 SSTOREs)~10,000
Token transfers (2 external calls)~15,000
Event emission~1,500
Total~57,500

Nearly 60% of the gas goes to storage operations and external calls. The actual math (price computation, tick crossing) is a small fraction.

Conclusion

Gas optimization is not about micro-optimizing arithmetic opcodes — it is about minimizing storage access and external calls. The patterns that save the most gas are architectural: storage packing, caching, calldata usage, and access list optimization. In the next article, we turn to security: how the EVM’s execution model creates — and prevents — vulnerabilities like reentrancy.