EVM Gas, Fees & Tokens — A Complete Guide

EVM Gas, Fees & Tokens — A Complete Guide

This article is written for developers and deployers, providing an in-depth explanation of gas fees, unit systems, wei, native tokens, ERC-20 tokens, and their interrelationships on EVM chains. It covers everything from foundational concepts to the latest upgrades as of 2025 (EIP-4844, ERC-4337, Pectra, EIP-7702).
If you have ever confused gas units with wei, or were unsure what unit a gas limit is measured in, this guide will clarify these concepts once and for all.


Table of Contents


1. Native Tokens vs ERC-20 Tokens

In the EVM ecosystem, the word "token" encompasses two fundamentally different things: native tokens and ERC-20 tokens. Understanding the distinction between them is the first step to mastering the gas fee system.

Token Type Hierarchy EVM Blockchain Native Tokens (Protocol Layer) Built into the blockchain protocol, no contract address Used to pay gas fees ETH Ethereum BNB BSC POL/AVAX Polygon/Avalanche ERC-20 Tokens (Contract Layer) Implemented via smart contracts, follows standard interface Requires native tokens to pay interaction gas USDT 6 decimals USDC 6 decimals UNI 18 decimals DAI 18 decimals Wrapped Tokens (Bridging Native & ERC-20) ERC-20 version of native tokens, 1:1 backed Enables native tokens to participate in DeFi protocols WETH Wrapped ETH WBNB Wrapped BNB WAVAX Wrapped AVAX deposit() ERC-20 Interface

1.1 Native Tokens

Every EVM-compatible chain has one and only one native token. It is the chain’s own "currency," existing at the protocol level and not dependent on any smart contract.

Core Characteristics:

  • One per chain: Ethereum’s ETH, BSC’s BNB, Polygon’s MATIC (now renamed to POL), Avalanche’s AVAX, Arbitrum’s ETH, etc.
  • Exists at the protocol layer: Native tokens are not smart contracts; they are built into the blockchain protocol. When nodes validate blocks, the transfer logic for native tokens is handled directly by the protocol code
  • Used to pay gas fees: All transactions (including interactions with ERC-20 tokens, NFTs, DeFi protocols) must use native tokens to pay gas fees. This is a hard requirement of the EVM (before account abstraction, there was no way to substitute other tokens; see Section 8.2)
  • Simple value transfer: Sending native tokens only requires specifying the amount in the transaction’s value field, without calling any contract function
  • Account balance field: Every Ethereum account (EOA or contract account) has a built-in balance field that records the amount of native tokens it holds. This field is maintained directly by the protocol

Example — Sending native tokens (no contract call needed):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Using viem to send 0.1 ETH — specify value directly in the transaction, no data field needed
import { createWalletClient, http, parseEther } from 'viem';
import { mainnet } from 'viem/chains';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

const hash = await client.sendTransaction({
to: '0xRecipientAddress...',
value: parseEther('0.1'), // 0.1 ETH = 100000000000000000 wei
// Note: no data field needed because no contract call is required
});

1.2 ERC-20 Tokens

ERC-20 tokens are smart contract-based tokens that follow the standardized interface defined by the ERC-20 standard. They are the cornerstone of the DeFi ecosystem.

Core Characteristics:

  • Smart contract implementation: Each ERC-20 token is an independent smart contract deployed on-chain with its own unique contract address. For example, USDT on Ethereum mainnet is at 0xdAC17F958D2ee523a2206206994597C13D831ec7
  • Standard interface: All ERC-20 tokens implement the same interface functions: transfer(), transferFrom(), approve(), balanceOf(), totalSupply(), allowance(), etc.
  • Transfer via contract functions: Sending ERC-20 tokens requires calling the token contract’s transfer() or transferFrom() function, which is entirely different from sending native tokens
  • Internal balance mapping: The token contract maintains a mapping(address => uint256) that records how many tokens each address holds. This is contract state, not protocol-level balance
  • Requires native tokens to pay gas: Any interaction with ERC-20 tokens (transfers, approvals, queries) is an on-chain transaction that requires native tokens to pay gas fees. This means even if you hold a large amount of USDT, you cannot transfer it without ETH (on Ethereum)

Common ERC-20 Token Examples:

Token Type Description
USDT Stablecoin USD-pegged stablecoin
USDC Stablecoin USD stablecoin issued by Circle
WETH Wrapped Token ERC-20 version of ETH
WBNB Wrapped Token ERC-20 version of BNB
UNI Governance Token Uniswap governance token
CAKE Governance Token PancakeSwap governance token
LINK Utility Token Chainlink oracle token
DAI Stablecoin Decentralized stablecoin

Example — Sending ERC-20 tokens (contract call required):

1
2
3
4
5
6
7
8
9
10
11
12
// Using viem to send 100 USDT — must call USDT contract's transfer function
import { createWalletClient, http, encodeFunctionData, erc20Abi } from 'viem';

const hash = await client.sendTransaction({
to: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT contract address
value: 0n, // not sending native tokens
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: ['0xRecipientAddress...', 100_000_000n], // 100 USDT (6 decimals)
}),
});

1.3 Wrapped Native Tokens (WETH, WBNB)

Wrapped Native Tokens are ERC-20 versions of native tokens, always backed 1:1. They exist to solve an important interface compatibility problem.

Why are wrapped tokens needed?

Native tokens (such as ETH) do not conform to the ERC-20 standard interface. However, DeFi protocols (DEXs, lending platforms, liquidity pools) are typically designed assuming all tokens follow the ERC-20 interface. To allow native tokens to participate in these protocols, a "wrapped" version that follows the ERC-20 standard is needed.

Wrapped tokens by chain:

Chain Native Token Wrapped Token Wrapper Contract
Ethereum ETH WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
BSC BNB WBNB 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
Polygon MATIC WMATIC 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270
Avalanche AVAX WAVAX 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7
Arbitrum ETH WETH 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1

Wrapping process:

  1. The user sends native ETH to the WETH contract (via the deposit() function or by sending directly)
  2. The contract receives and holds the native ETH
  3. The contract mints an equal amount of WETH (ERC-20 token) to the user
  4. Result: ETH held by the contract = total circulating WETH supply

Unwrapping process:

  1. The user calls the WETH contract’s withdraw() function
  2. The contract burns the user’s WETH
  3. The contract sends an equal amount of native ETH back to the user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WETH contract core logic (simplified)
contract WETH {
mapping(address => uint256) public balanceOf;

// Wrap: deposit ETH, mint WETH
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
}

// Unwrap: burn WETH, withdraw ETH
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}

1.4 Key Differences Summary Table

Aspect Native Token ERC-20 Token
Storage Location Protocol-level balance (balance field of each account) Smart contract internal mapping (mapping(address => uint256))
Transfer Method value field in the transaction Call the contract’s transfer() or transferFrom() function
Gas Payment Yes (only native tokens can pay gas) No (must use native tokens to pay gas)
Contract Address None (built into the chain’s protocol layer) Has its own contract address
Creation Method Chain genesis or protocol consensus Deploy a smart contract
Standard Interface No unified standard ERC-20 standard interface
Approval Mechanism No approval needed (send directly) Requires approve() before third-party usage
Examples ETH, BNB, MATIC, AVAX USDT, USDC, WETH, WBNB, UNI, CAKE

2. Understanding Wei — The Smallest Unit

2.1 Denomination System

Just as the US dollar has "dollars" and "cents" as denominations, Ethereum has its own denomination system. Wei is the smallest denomination unit, like "cents" for the dollar — but the difference is that ETH has 18 decimal places, while the dollar only has 2.

Wei Denomination Ladder Visual scale from wei to ether — each level differs by 10^3 (serpentine ascending path) wei 10^0 kwei 10^3 mwei 10^6 gwei 10^9 = Gas Price Unit x10^3 szabo 10^12 finney 10^15 ether 10^18 = Human-Readable Unit x10^3 x10^3 x10^3 x10^3 x10^3 = Commonly used units (three that developers must remember) = Historical units (rarely used)

Complete Ethereum denomination table:

Unit Wei Value Scientific Notation Practical Use
wei 1 10^0 Base unit for EVM internal operations
kwei (babbage) 1,000 10^3 Rarely used
mwei (lovelace) 1,000,000 10^6 Rarely used
gwei (shannon) 1,000,000,000 10^9 Standard unit for gas price
szabo 1,000,000,000,000 10^12 Historical unit, rarely used
finney 1,000,000,000,000,000 10^15 Historical unit, rarely used
ether 1,000,000,000,000,000,000 10^18 Human-readable standard unit

Key Takeaway: Wei is to ETH what cents are to dollars — except with 18 decimal places instead of 2.
In daily use, you only need to remember three units: wei (EVM internal), gwei (gas price), ether (user interface).

2.2 Why Wei Is Needed

The EVM uses wei as the base unit not by arbitrary choice, but by technical necessity:

  • The EVM does not support floating-point numbers: All arithmetic operations in the Ethereum Virtual Machine are integer operations (uint256). There is no floating-point type, no decimal point. This is to guarantee determinism — floating-point operations on different hardware may produce different results, but integer operations are always consistent
  • 18 decimal places provide sufficient precision: 18 decimal places means you can represent precision down to 0.000000000000000001 ETH, which is crucial for micro-transactions and precise calculations in DeFi protocols
  • All values are stored and computed in wei: The EVM has no concept of "ETH" internally, only wei. When a wallet displays "1 ETH," the EVM actually stores and processes 1,000,000,000,000,000,000 wei (i.e., 1e18)
  • Avoiding precision loss: If floating-point numbers were used, 0.1 + 0.2 might equal 0.30000000000000004. In financial systems, such precision loss is unacceptable. Integer wei arithmetic is completely precise
1
2
3
4
// EVM internal: all integer operations
uint256 balance = 1000000000000000000; // this is "1 ETH"
uint256 half = balance / 2; // 500000000000000000 = 0.5 ETH
// no precision loss

2.3 Common Conversions and Practical Examples

Basic conversion relationships:

1
2
1 ETH  = 1,000,000,000 gwei  = 1,000,000,000,000,000,000 wei
1 gwei = 1,000,000,000 wei

Practical scenario examples:

Scenario ETH Value Gwei Value Wei Value
1 ETH 1 1,000,000,000 1,000,000,000,000,000,000
One-thousandth of an ETH 0.001 1,000,000 1,000,000,000,000,000
Gas price 5 gwei 0.000000005 5 5,000,000,000
Gas price 30 gwei 0.00000003 30 30,000,000,000
Simple transfer fee (5 gwei) 0.000105 105,000 105,000,000,000,000

Conversions in code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { parseEther, formatEther, parseGwei, formatGwei } from 'viem';

// human readable → wei (before sending transaction)
parseEther('1.0') // → 1000000000000000000n (BigInt)
parseEther('0.001') // → 1000000000000000n
parseGwei('5') // → 5000000000n

// wei → human readable (before displaying to user)
formatEther(1000000000000000000n) // → '1.0'
formatEther(1000000000000000n) // → '0.001'
formatGwei(5000000000n) // → '5'

// common mistake: confusing parseEther and parseGwei
parseEther('5') // 5 ETH = 5000000000000000000n ← this is not 5 gwei!
parseGwei('5') // 5 gwei = 5000000000n ← this is 5 gwei

2.4 ERC-20 Token Decimals Are Not Always 18

This is an extremely important point. Although ETH and most native tokens use 18 decimal places, the decimal count for ERC-20 tokens is defined by each token contract individually and varies.

Common tokens and their decimals:

Token Decimals Smallest Unit Value for 1 Token Notes
ETH (Native) 18 1,000,000,000,000,000,000 Standard 18 decimals
BNB (Native) 18 1,000,000,000,000,000,000 Standard 18 decimals
WETH 18 1,000,000,000,000,000,000 Same as ETH
UNI 18 1,000,000,000,000,000,000 Standard 18 decimals
LINK 18 1,000,000,000,000,000,000 Standard 18 decimals
DAI 18 1,000,000,000,000,000,000 Standard 18 decimals
USDT 6 1,000,000 Note: not 18!
USDC 6 1,000,000 Note: not 18!
WBTC 8 100,000,000 Note: not 18!

Why do decimals differ?

  • USDT/USDC use 6 decimals because they are pegged to the US dollar, and dollar precision typically goes to cents (2 decimals), so 6 decimals is more than sufficient
  • WBTC uses 8 decimals because Bitcoin natively uses 8 decimals (1 BTC = 100,000,000 satoshis)

Code must handle different decimal places:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Wrong example: assuming all tokens have 18 decimals
const amount = parseEther('100'); // 100 * 10^18 — for USDT this is an astronomical number!

// Correct approach: use the token's actual decimals
import { parseUnits, formatUnits } from 'viem';

const decimals = await tokenContract.read.decimals(); // USDT returns 6

parseUnits('100', 6); // 100 USDT = 100000000n (10^8)
parseUnits('100', 18); // 100 ETH = 100000000000000000000n (10^20)

formatUnits(100000000n, 6); // '100.0' USDT
formatUnits(100000000000000000000n, 18); // '100.0' ETH

Critical Reminder: When writing code that deals with token amounts, always call the token contract’s decimals() function to get the correct decimal count. Assuming 18 decimals is one of the most common mistakes in DeFi development.


3. Gas — The Unit of Computation

3.1 What Is Gas?

Gas is an abstract unit of measurement in the EVM, used to quantify the computational work required to execute a smart contract or transaction. Understanding this is crucial:

  • Gas is not a token: You cannot "hold" gas, "transfer" gas, or see a gas balance in your wallet
  • Gas is not a currency: Gas has no market price; you cannot buy or sell gas on an exchange
  • Gas is not a denomination of ETH: Gas is not wei, not gwei, and not ether. They are entirely different measurement systems
  • Gas measures computation: Think of it as "CPU cycles" or "computational steps" — it measures how much work the EVM needs to do to execute a given operation

Analogy:

Imagine you are shopping at a supermarket. Each item has a "weight" (in grams), and you are pushing a cart with a weight limit:

  • Item weight (grams) is analogous to Gas units — measuring the "heft" of each operation
  • Price per gram ($/gram) is analogous to Gas price (gwei) — the market-determined unit price
  • Total cost ($) is analogous to Gas fee (ETH/BNB) — the actual amount you pay
  • Cart weight limit is analogous to Gas limit — the maximum load you are willing to bear

Every EVM opcode has a fixed gas cost defined by the Ethereum protocol. These costs reflect the computational complexity and resource consumption of each operation.

3.2 Gas Cost per Operation

EVM Gas Metering Flow How gas is consumed opcode by opcode — ERC-20 transfer example Initial Gas Limit 65,000 gas 1. Base transaction fee -21,000 gas Fixed cost per transaction Remaining: 44,000 2. Calldata encoding (function sig + params) -2,176 gas 68 bytes x (16 or 4) gas/byte Remaining: 41,824 3. SLOAD: Read sender balance (cold access) -2,100 gas First storage slot read Remaining: 39,724 4. SLOAD: Read receiver balance (cold access) -2,100 gas Another storage slot Remaining: 37,624 5. SSTORE x2: Update both balances -10,000 gas 2 x 5,000 (update existing value) Remaining: 27,624 6. LOG3: Emit Transfer event -1,756 gas Event log + indexed params Remaining: 25,868 Actual used: ~39,132 gas + other opcodes (ADD, PUSH, JUMP, etc.) Refunded: ~25,868

Below are the gas costs for common EVM opcodes. These values are defined by the Ethereum Yellow Paper and subsequent EIP upgrades:

Operation Gas Cost Description
ADD / SUB 3 Simple arithmetic (addition, subtraction)
MUL / DIV 5 Multiplication, division
ADDMOD / MULMOD 8 Modular arithmetic
EXP 10 + 50 x byte count Exponentiation, cost increases with exponent size
SHA3 (KECCAK256) 30 + 6 x word count Hashing, used for mapping key computation, etc.
BALANCE 2,600 Query address balance (cold access after EIP-2929)
SLOAD 2,100 Read a 256-bit value from storage (cold access)
SLOAD (warm access) 100 Re-reading the same storage slot in the same transaction
SSTORE (new value) 20,000 Write a value to a previously zero storage slot — most expensive common operation
SSTORE (update) 5,000 Update an existing value in a storage slot
SSTORE (clear to zero) 5,000 + refund Set a storage slot to zero (eligible for gas refund)
LOG0 375 Emit event with no indexed parameters
LOG1 750 Emit event with 1 indexed parameter
LOG2 1,125 Emit event with 2 indexed parameters
CALL 2,600 Call another contract (cold access)
CREATE 32,000 Deploy a new contract (excluding contract code gas)
CREATE2 32,000 Deploy a new contract to a deterministic address
SELFDESTRUCT 5,000 Destroy a contract (deprecated)
PUSH0 2 Added by EIP-3855: Push zero value, saves 1 gas over PUSH1 0x00
TLOAD 100 Added by EIP-1153: Read transient storage (see Section 8.4)
TSTORE 100 Added by EIP-1153: Write transient storage
Base transaction 21,000 Minimum cost for every transaction
Transaction data (zero byte) 4 / byte Zero bytes in transaction calldata
Transaction data (non-zero byte) 16 / byte Non-zero bytes in transaction calldata

Key Observation: Storage operations (SSTORE) are the most expensive. Writing a new storage slot (20,000 gas) costs nearly 7,000 times more than a simple addition (3 gas). This is why the core strategy for Solidity gas optimization is to minimize storage writes. The transient storage (TLOAD/TSTORE) introduced by EIP-1153 offers an alternative for temporary data within a transaction at only 100 gas per operation.

3.3 Gas Limit vs Actual Usage

This is one of the most commonly confused concepts among developers. Every transaction has two gas-related values:

Gas Limit:

  • Set by the user (or dApp) before sending the transaction
  • Represents "the maximum gas I am willing to spend on this transaction"
  • Acts as a safety cap to prevent unexpectedly excessive gas consumption
  • Wallets typically auto-estimate and set a reasonable value

Actual Usage (Gas Used):

  • Can only be determined after the transaction finishes executing
  • Represents the gas actually consumed by the transaction
  • Always less than or equal to the gas limit
Gas Limit vs Actual Usage — Three Scenarios Scenario 1: Normal Execution Actual usage < Gas limit Used: 65,000 Refunded: 35,000 Gas Limit: 100,000 Transaction succeeds Fee = 65,000 x 5 gwei = 0.000325 BNB Scenario 2: Exactly Used Up Actual usage = Gas limit Used: 100,000 (all) Gas Limit: 100,000 Transaction succeeds (but no refund) Fee = 100,000 x 5 gwei = 0.0005 BNB Scenario 3: Out of Gas Actual need > Gas limit All exhausted: 50,000 / needed 65,000 Gas Limit: 50,000 (not enough!) Transaction reverts (Out of Gas) Fee = 50,000 x 5 gwei (no refund!) = 0.00025 BNB wasted Key Takeaways 1. Gas limit is a safety cap, not actual consumption. The excess is refunded (Scenario 1) 2. Insufficient gas causes a transaction revert; the consumed gas fee is NOT refunded (Scenario 3) — the most common gas pitfall 3. Wallet auto-estimates typically add a 20-30% safety buffer on top of the actual requirement

Three scenarios:

Scenario Result Fee
Actual usage < Gas limit Transaction succeeds, unused gas is refunded Only pay for actual usage
Actual usage = Gas limit Transaction succeeds (just barely) Pay for the full gas limit
Actual usage > Gas limit Transaction reverts All gas is consumed, no refund, but state changes are rolled back

Concrete example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scenario: ERC-20 transfer, gas price 5 gwei

Gas limit set: 100,000
Actual gas used: 65,000
Unused gas: 35,000 (refunded)

Actual fee = 65,000 x 5 gwei = 325,000 gwei = 0.000325 ETH
Refund amount = 35,000 x 5 gwei = 175,000 gwei = 0.000175 ETH

If gas limit is set to 50,000 (less than the 65,000 actually needed):
→ Transaction runs out at 50,000 gas and reverts
→ All 50,000 gas is consumed (0.00025 ETH)
→ Transfer does not occur (state unchanged)
→ You lose the gas fee for nothing

Best Practice: Do not set the gas limit too low (the transaction will fail and you lose the gas fee), and do not set it excessively high (although the excess is refunded, some edge cases can cause unexpectedly high consumption). Most wallets’ auto-estimates add a safety buffer on top of the actual requirement (typically 20-30%).

3.4 Why Gas Is Needed

The gas mechanism is not an optional design — it solves fundamental problems facing blockchains:

1. The Halting Problem

The EVM is a Turing-complete virtual machine, meaning it can execute arbitrarily complex programs — including infinite loops. In computation theory, it is impossible to determine in advance whether an arbitrary program will terminate (this is the "Halting Problem").

The gas mechanism elegantly solves this: every operation consumes gas, and the gas limit caps the total execution. No matter how complex a program is, execution must stop when gas runs out.

1
2
3
4
5
6
7
// Without gas mechanism, this contract runs forever
function infiniteLoop() public {
while (true) {
// Never stops...
}
}
// With gas mechanism, execution stops at gas limit and reverts

2. Preventing Resource Abuse

Every validator node in the network must execute the code in transactions. Without an execution cost, attackers could flood the network with computationally intensive transactions to paralyze it (DoS attack). Gas fees make such attacks economically infeasible.

3. Block Space Market

Each block has a gas cap (currently about 36 million gas on Ethereum, increased to 36 million after the Pectra upgrade). When the network is busy, users compete for limited block space. The gas price mechanism creates an efficient market: transactions willing to pay a higher gas price are processed first.

4. Incentivizing Validators

Validators (or miners) consume real computational resources to execute transactions. Gas fees compensate their costs and provide profit incentives, ensuring the network continues to operate.

3.5 Typical Gas Usage by Transaction Type

The following data is based on approximate on-chain statistics to help you estimate the cost of various operations:

Transaction Type Typical Gas Usage At 5 gwei (BSC) At 30 gwei (ETH)
Simple ETH/BNB transfer 21,000 0.000105 BNB 0.00063 ETH
ERC-20 token transfer ~65,000 0.000325 BNB 0.00195 ETH
ERC-20 approve ~46,000 0.00023 BNB 0.00138 ETH
DEX swap (simple path) ~150,000 0.00075 BNB 0.0045 ETH
DEX swap (multi-hop path) ~300,000 0.0015 BNB 0.009 ETH
Add liquidity ~200,000-300,000 0.001-0.0015 BNB 0.006-0.009 ETH
Remove liquidity ~150,000-250,000 0.00075-0.00125 BNB 0.0045-0.0075 ETH
NFT mint (single) ~100,000-150,000 0.0005-0.00075 BNB 0.003-0.0045 ETH
NFT batch mint ~150,000-500,000 0.00075-0.0025 BNB 0.0045-0.015 ETH
ERC-20 contract deployment ~1,000,000-2,000,000 0.005-0.01 BNB 0.03-0.06 ETH
Complex contract deployment ~3,000,000-5,000,000 0.015-0.025 BNB 0.09-0.15 ETH

Note: The fees above are denominated in native tokens. The actual dollar cost depends on the native token’s market price. Transactions on BSC are typically 10-100x cheaper than on Ethereum, which is one reason many projects choose BSC or Layer 2.


4. Transaction Fees — Putting It All Together

Now that we understand gas (computation unit) and wei (token denomination), let’s see how they combine to form actual transaction fees.

4.1 Legacy Fee Model (Pre-EIP-1559)

Before Ethereum’s London Upgrade (EIP-1559) in 2021, and in the model still used by chains like BSC, fee calculation is straightforward:

1
Transaction Fee = Actual Gas Used x Gas Price
  • Actual Gas Used: In gas units (pure number, no denomination)
  • Gas Price: In gwei (set by user or suggested by wallet)
  • Transaction Fee: In native token (ETH, BNB, etc.)

Full calculation example:

1
2
3
4
5
6
7
8
9
10
11
Scenario: A DEX swap on BSC

Actual gas used: 200,000 gas units
Gas price: 5 gwei

Calculation process:
200,000 x 5 gwei = 1,000,000 gwei
1,000,000 gwei = 1,000,000 x 10^9 wei = 1,000,000,000,000,000 wei
1,000,000,000,000,000 wei = 0.001 BNB (because 1 BNB = 10^18 wei)

Final fee: 0.001 BNB

In this model, users set a gas price, and miners prioritize higher-bidding transactions. This led to a problem: during network congestion, users had to keep raising their bids to ensure their transactions were included, causing gas prices to fluctuate wildly and become unpredictable.

4.2 EIP-1559 Fee Model (Post-London Upgrade)

Ethereum introduced EIP-1559 in the August 2021 London Upgrade, fundamentally changing the fee mechanism. This model introduces two core concepts:

1
Transaction Fee = Actual Gas Used x (Base Fee + Priority Fee)
EIP-1559 Fee Structure Complete flow of base fee, priority fee, and burn mechanism User Sets maxFeePerGas Sets maxPriorityFee msg.value Transaction Fee Calculation Effective Gas Price = baseFee + priorityFee Total Fee = gasUsed x Effective Gas Price Excess (maxFee - effective price) x gasUsed refunded to user Refund difference Base Fee (Burned) gasUsed x baseFee Permanently removed from ETH supply Makes ETH deflationary Priority Fee (Validator) gasUsed x priorityFee Paid to block validator Incentivizes tx inclusion 🔥 EIP-1559 Burn Validator Revenue Example: gasUsed=200,000 baseFee=25 gwei priorityFee=2 gwei maxFee=35 gwei Total fee=0.0054 ETH | Burned=0.005 ETH | Validator=0.0004 ETH | Refund=0.00335 ETH

Base Fee:

  • Automatically calculated by the protocol based on the previous block’s utilization rate; users cannot set it
  • If the previous block was more than 50% full, the base fee increases (up to 12.5%); below 50% full, the base fee decreases
  • The base fee is burned: Permanently removed from the ETH total supply, given to no one. This makes ETH potentially deflationary during periods of high usage
  • Makes gas prices more predictable — users can see the current base fee and reasonably estimate the next block’s fee

Priority Fee / Tip:

  • Set by the user
  • Paid to the validator (not burned)
  • Incentivizes validators to include your transaction in a block
  • When the network is not busy, a small priority fee (e.g., 1-2 gwei) suffices

Max Fee (maxFeePerGas):

  • The maximum total fee per gas unit the user is willing to pay
  • Must be greater than or equal to Base Fee + Priority Fee
  • If the max fee > actual cost (base fee + priority fee), the excess is refunded
EIP-1559 Base Fee Adjustment Mechanism How base fee auto-adjusts with block utilization (max 12.5% change per block) Base Fee (gwei) Block Number 10 20 30 40 50 60 Target: 30 gwei 50% 55% 70% 85% 100% 60% 45% 30% 25% 20% Block full, fee spikes Block idle, fee drops Base fee change curve Bottom numbers = block utilization (target 50%) >50%: base fee rises | <50%: base fee falls

Full calculation example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Scenario: A DEX swap on Ethereum

Parameters set:
Gas limit: 250,000
Max fee (maxFeePerGas): 35 gwei
Priority fee (maxPriorityFeePerGas): 2 gwei

During execution:
Base fee (protocol-set): 25 gwei
Actual gas used: 200,000

Actual fee calculation:
Effective gas price = base fee + priority fee = 25 + 2 = 27 gwei
Transaction fee = 200,000 x 27 gwei = 5,400,000 gwei = 0.0054 ETH

Fee distribution:
Burned portion = 200,000 x 25 gwei = 5,000,000 gwei = 0.005 ETH (permanently destroyed)
Validator reward = 200,000 x 2 gwei = 400,000 gwei = 0.0004 ETH

Refund:
Pre locked amount = gas limit x max fee = 250,000 x 35 gwei = 8,750,000 gwei
Actual fee = 5,400,000 gwei
Refund = 8,750,000 - 5,400,000 = 3,350,000 gwei = 0.00335 ETH

4.3 BSC Fee Model

BSC (BNB Smart Chain) uses a simplified fee model close to the legacy model, but with its own characteristics:

  • Extremely low gas price: BSC’s typical gas price is 3-5 gwei, while Ethereum typically runs at 20-50 gwei or higher
  • Short block time: BSC produces a block approximately every 3 seconds (Ethereum ~12 seconds); faster blocks mean more block space supply
  • Basic formula: Fee = Actual Gas Used x Gas Price
  • BNB token price is much lower than ETH: Even with the same gas units, the dollar cost of BNB-denominated fees is lower
1
2
3
4
5
6
7
8
9
10
11
BSC Example: Deploy a token contract

Actual gas used: 2,000,000
Gas price: 5 gwei

Fee = 2,000,000 x 5 gwei = 10,000,000 gwei = 0.01 BNB
At BNB = $600: approximately $6.00

The same contract on Ethereum:
Fee = 2,000,000 x 30 gwei = 60,000,000 gwei = 0.06 ETH
At ETH = $3,000: approximately $180.00

This is why many developers and users choose to deploy and interact on BSC — the cost difference can be 30x or more.

4.4 Cross-Chain Fee Comparison

Cross-Chain Gas Cost Comparison (Simple Transfer) Approximate 2025 reference values — horizontal bar chart (USD denominated) Ethereum BSC Polygon Arbitrum Optimism Base $0.50 - $2.00 $0.01 - $0.02 $0.001 - $0.005 $0.01 - $0.05 $0.001 - $0.01 $0.001 - $0.01 Note: L2 chain fees (Arbitrum, Optimism, Base) include L1 data posting costs. L2 fees dropped significantly after EIP-4844. Logarithmic scale — Ethereum fees are 100-1000x those of L2 chains

The following comparison shows typical fee levels across different EVM chains. Note that gas prices and token prices fluctuate; the values below are approximate references for 2025:

Chain Typical Gas Price Simple Transfer Cost (Native Token) Simple Transfer Cost (USD Approx.) DEX Swap Cost (USD Approx.)
Ethereum 20-50 gwei 0.00042-0.00105 ETH $0.50-$2.00 $5-$30
BSC 3-5 gwei 0.000063-0.000105 BNB $0.01-$0.02 $0.10-$0.30
Polygon 30-50 gwei 0.00063-0.00105 MATIC $0.001-$0.005 $0.01-$0.05
Arbitrum 0.1-0.5 gwei 0.0000021-0.0000105 ETH $0.01-$0.05 $0.10-$0.50
Optimism 0.01-0.1 gwei 0.00000021-0.0000021 ETH $0.001-$0.01 $0.01-$0.10
Base 0.01-0.05 gwei 0.00000021-0.00000105 ETH $0.001-$0.01 $0.01-$0.10
Avalanche C-Chain 25-30 gwei 0.000525-0.00063 AVAX $0.01-$0.03 $0.10-$0.30

Note: Layer 2 chains (Arbitrum, Optimism, Base) have extremely low gas prices, but they also have additional L1 data posting fees (the cost of submitting transaction data to the Ethereum mainnet). The USD approximations in the table above include this component. Since the implementation of EIP-4844 (Dencun Upgrade), L2 data posting fees have dropped by approximately 90%.


5. Three Meanings of "Gas" — A Common Source of Confusion

This is possibly the most important section of this guide. In everyday conversation, documentation, and even code comments, the word "gas" is often used loosely, but it can actually refer to three entirely different meanings. Confusing them is one of the most common sources of errors in EVM development.

5.1 Terminology Disambiguation

Term Actual Meaning Unit Order of Magnitude Example
Gas (units / gas units) Computation budget / workload measurement Abstract dimensionless unit Tens of thousands to millions "This transaction used 200,000 gas"
Gas price Cost per gas unit gwei (= 10^9 wei) Single digits to hundreds "The current gas price is 5 gwei"
Gas fee Actual total cost in native token ETH / BNB / MATIC Decimal values "I paid 0.001 BNB in gas fees"

Mathematical relationship between the three:

1
2
3
4
5
6
Gas fee (ETH/BNB) = Gas units x Gas price (gwei) x 10^-9

Concrete example:
Gas fee = 200,000 x 5 gwei x 10^-9
= 1,000,000 x 10^-9
= 0.001 BNB

Common confusion scenarios:

1
2
3
4
5
6
7
8
9
10
11
12
"I set 200,000 gas"
Correct understanding: I set a computation budget of 200,000 gas units
Wrong understanding: I will pay 200,000 BNB/ETH

"Gas is 5"
→ Could mean: gas price is 5 gwei (most common meaning)
→ Could also mean: some operation consumes 5 gas units
→ Needs context to determine

"Gas is expensive"
→ Means: the gas fee (total cost) is high
→ Usually because the gas price (gwei) is high

5.2 Real-World Analogy

Use the analogy of filling up a car with fuel to understand these three concepts:

Filling Up a Car EVM Gas Relationship
Distance traveled (km) Gas units How long your trip is / how complex the computation is
Fuel price ($/liter) Gas price (gwei) Market-determined unit price
Fuel consumption (liters/100km) Gas cost per operation Fixed consumption rate
Total fuel cost ($) Gas fee (ETH/BNB) The actual amount you pay from your wallet
Fuel tank capacity (liters) Gas limit The maximum consumption you are willing to bear

Extended analogy:

1
2
3
4
5
6
7
8
9
10
11
12
13
"I set a gas limit of 200,000 gas"

= "My fuel tank holds a maximum of 200,000 liters"
→ Does not mean you will use all 200,000 liters
→ Does not mean you will pay 200,000 dollars
→ Only means if the trip needs more than 200,000 liters of fuel, you give up the journey
(transaction reverts, but consumed fuel fee is not refunded)

In reality:
Trip only needs 65,000 liters (actual gas used)
Fuel price 5 dollars/liter (gas price 5 gwei)
Total fuel cost = 65,000 x 5 = 325,000 dollars (= 325,000 gwei = 0.000325 BNB)
Remaining tank capacity 135,000 liters (refunded gas)

Core Takeaway: When you see or set a gas-related value, first confirm which meaning it represents (units? price? fee?). 200,000 gas units, 200,000 gwei, and 200,000 wei are completely different orders of magnitude.


6. LayerZero Cross-Chain Gas — Enforced Options Context

Cross-chain communication adds a new layer of complexity to the gas concept. In the LayerZero protocol, a single cross-chain transaction involves execution on two chains, while the user only pays fees on the source chain. Understanding this mechanism is critical for correctly configuring NativeOFT and cross-chain swaps.

LayerZero Cross-Chain Gas Flow Complete gas flow: Source Chain → LayerZero Network → Destination Chain Source Chain (BSC) 1. User calls send() msg.value = LZ total fee 2. Source chain gas consumption ~300,000 gas x 5 gwei 3. LZ fee breakdown Protocol fee + DVN fee + Executor fee (incl. dest chain gas) User only needs BNB No dest chain native token needed Single-chain payment experience LayerZero Network 4. DVN security verification Message authenticity confirmed 5. Executor preparation Carries prepaid gas funds Enforced gas limit check lzReceive: 200k, lzCompose: 500k Dest Chain (Sepolia) 6. lzReceive execution OFT credit + WETH unwrap 7. lzCompose execution DEX swap 8. Dest chain gas consumption 700,000 gas (paid by executor) 9. User receives tokens Swapped destination tokens Arrive at user's dest chain address Total user payment = Source chain gas fee + LZ cross-chain fee (incl. dest chain gas) approx. 0.01-0.06 BNB

6.1 Why Cross-Chain Requires Gas Limits

When NativeOFT (Native OFT tokens) sends a cross-chain message via LayerZero, the entire flow is as follows:

  1. Source chain: User calls send() or initiateSwap() → consumes source chain gas → emits a LayerZero message
  2. LayerZero network: Security verification layer (DVN) verifies the message
  3. Destination chain: LayerZero Executor calls the destination contract → consumes destination chain gas

The key question: Who pays for the gas on the destination chain?

The answer is: The source chain user pays in advance. The LZ fee paid by the user on the source chain includes the gas cost for destination chain execution. The LayerZero Executor uses these prepaid fees to execute transactions on the destination chain.

The purpose of setEnforcedOptions is to tell the LayerZero protocol: "When executing my contract code on the destination chain, at least allocate X gas units." This ensures sufficient gas on the destination chain to complete the operation, preventing cross-chain transactions from failing due to insufficient gas.

6.2 What the Numbers Mean

Let’s break down the gas limit values in a cross-chain swap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Enforced Options configuration example:

lzReceive gas limit: 200,000 gas units
├─ Purpose: Execute OFT token credit (bookkeeping) + WETH unwrap + native token transfer
├─ This is gas units, not wei, not gwei, not BNB
├─ At BSC destination chain 5 gwei, actual cost:
│ 200,000 x 5 gwei = 1,000,000 gwei = 0.001 BNB
└─ Consumed by LayerZero executor on destination chain

lzCompose gas limit: 500,000 gas units
├─ Purpose: Execute token swap (DEX) on destination chain
├─ DEX swap is much more complex than simple transfer, requires more gas
├─ At BSC destination chain 5 gwei, actual cost:
│ 500,000 x 5 gwei = 2,500,000 gwei = 0.0025 BNB
└─ Consumed by LayerZero executor on destination chain

Total destination chain gas budget:
200,000 + 500,000 = 700,000 gas units
At BSC 5 gwei, approximately 0.0035 BNB

Note: This 0.0035 BNB is not directly paid by the user, but is included in
the total LZ fee paid on the source chain.

Important Distinction: 200,000 and 500,000 are gas units (abstract computation amount), not wei or gwei. They tell the LayerZero Executor "allocate at least this much computation budget for destination chain operations." The actual fee (in destination chain native token) depends on the destination chain’s gas price at the time.

6.3 How LayerZero Fees Work

The msg.value the user pays on the source chain when sending a cross-chain transaction includes multiple components:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
msg.value composition when user pays on source chain:

msg.value = LZ protocol fee + executor fee + DVN fee

Components:

┌─ LZ protocol fee (Treasury Fee)
│ └─ Base fee charged by LayerZero protocol

├─ Executor fee (Executor Fee)
│ ├─ Destination chain lzReceive gas fee (200,000 gas units x destination chain gas price)
│ ├─ Destination chain lzCompose gas fee (500,000 gas units x destination chain gas price)
│ └─ Executor profit margin

└─ DVN fee (Decentralized Verifier Network Fee)
└─ Service fee for the security verification network

Relationship between Enforced Options and quoteSend():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. Contract admin calls setEnforcedOptions()
→ Sets minimum gas limits (e.g., lzReceive: 200,000, lzCompose: 500,000)

2. User (or frontend dApp) calls quoteSend()
→ LayerZero endpoint calculates based on enforced options gas limits + current destination chain gas price
→ Returns the required total fee (nativeFee)

3. User sends transaction, msg.value = fee returned by quoteSend()
→ Source chain contract transfers fee to LayerZero endpoint
→ LayerZero executor executes on destination chain after receiving funds

4. If enforced options gas limit is too low:
→ Insufficient gas during destination chain execution → execution fails
→ Message may get stuck in LayerZero and require manual retry

If gas limit is set too high:
→ User prepays more on source chain (quoteSend returns higher value)
→ Destination chain may not actually use all this gas
→ Excess goes to executor (not refunded to user)

6.4 Practical Example: BSC Testnet to Sepolia Cross-Chain Swap

Below is a full fee breakdown of a cross-chain swap transaction, helping you understand how all the parts come together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Scenario: Cross-chain swap from BSC testnet to Sepolia testnet

User action:
Calls initiateSwap() on BSC testnet
Sends 0.01 BNB for cross-chain swap

=================================================
Fee breakdown:
=================================================

1. Source chain gas fee (BSC testnet)
├─ Operation: Execute initiateSwap() function
├─ Actual gas used: ~300,000 gas units
├─ Gas price: 5 gwei
├─ Fee: 300,000 x 5 gwei = 0.0015 BNB
└─ Payment method: Deducted from user's BNB balance

2. LayerZero cross-chain fee
├─ Pre-calculated via quoteSend()
├─ Includes:
│ ├─ Destination chain lzReceive gas (200,000 units)
│ ├─ Destination chain lzCompose gas (500,000 units)
│ ├─ Executor profit
│ ├─ DVN verification fee
│ └─ Protocol fee
├─ Total: ~0.01-0.05 BNB (depending on network conditions)
└─ Payment method: Sent as msg.value

3. Destination chain gas fee (Sepolia)
├─ Paid by LayerZero executor (from LZ fee deduction)
├─ User does not pay directly
├─ lzReceive execution: ~200,000 gas
└─ lzCompose execution (DEX swap): ~300,000-500,000 gas

=================================================
Total cost actually paid by user:
Source chain gas fee + LayerZero cross-chain fee
Approximately 0.0015 + 0.01~0.05
Approximately 0.01 - 0.06 BNB
=================================================

Note:
- User only needs to hold BNB on source chain (BSC)
- Does not need to hold ETH on destination chain (Sepolia)
- All destination chain fees are included in LZ fee
- This is the core of cross-chain UX optimization: single-chain payment

7. Quick Reference Cheat Sheet

Conversion Formulas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=================================================
Unit conversion:
1 ETH = 10^9 gwei = 10^18 wei
1 gwei = 10^9 wei
1 BNB = 10^9 gwei = 10^18 wei (same denomination system)

Fee calculation:
Gas fee = actual gas used (gas units) x gas price (gwei) x 10^-9

Unit reminders:
Gas price: in gwei (1 gwei = 10^9 wei)
Gas limit: in gas units (not wei, not gwei, not ETH)
Transaction fee: in native token (ETH, BNB, MATIC, etc.)
=================================================

Common Value Reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
=================================================
Typical gas usage by transaction type:
Simple transfer (ETH/BNB): 21,000 gas
ERC-20 token transfer: ~65,000 gas
ERC-20 approval (approve): ~46,000 gas
DEX swap (simple): ~150,000 gas
DEX swap (multi-hop path): ~300,000 gas
Contract deployment: ~1,000,000-5,000,000 gas
NFT minting: ~100,000-200,000 gas

LayerZero cross-chain Enforced Options:
lzReceive: ~200,000 gas (enforced minimum)
lzCompose: ~500,000 gas (DEX swap enforced minimum)

Typical gas prices by chain:
Ethereum: 20-50 gwei
BSC: 3-5 gwei
Polygon: 30-50 gwei
Arbitrum: 0.1-0.5 gwei
Base: 0.01-0.05 gwei
=================================================

ERC-20 Approval Flow

ERC-20 Approval Flow approve() → transferFrom() pattern explained User (Alice) Token holder ERC-20 Contract USDT / USDC etc. DEX Router Uniswap / PancakeSwap Step 1 approve(DEX, 1000) User authorizes DEX to use up to 1000 tokens Costs ~46,000 gas allowance[Alice][DEX] = 1000 Contract internally records approval amount Step 2 transferFrom(Alice, DEX, 500) DEX transfers 500 tokens from Alice's account Costs ~65,000 gas | Remaining approval: 500 Security tip: Avoid approving type(uint256).max (unlimited approval); approve only the needed amount or use Permit2

Code Examples (viem/ethers.js v6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { parseEther, parseGwei, formatEther, formatGwei, parseUnits, formatUnits } from 'viem';

// =================================================
// Basic unit conversion
// =================================================

// ETH / BNB → wei
parseEther('1.0') // 1000000000000000000n (1e18 wei)
parseEther('0.001') // 1000000000000000n (1e15 wei)
parseEther('0.000000005') // 5000000000n (wei value corresponding to 5 gwei)

// gwei → wei
parseGwei('5') // 5000000000n (5e9 wei)
parseGwei('30') // 30000000000n (30e9 wei)

// wei → human readable
formatEther(1000000000000000000n) // '1.0' (ETH/BNB)
formatEther(1000000000000000n) // '0.001'
formatGwei(5000000000n) // '5' (gwei)

// =================================================
// ERC-20 tokens (note different decimals!)
// =================================================

// 18 decimal tokens (WETH, UNI, DAI, etc.)
parseUnits('100', 18) // 100000000000000000000n
formatUnits(100000000000000000000n, 18) // '100.0'

// 6 decimal tokens (USDT, USDC)
parseUnits('100', 6) // 100000000n
formatUnits(100000000n, 6) // '100.0'

// 8 decimal tokens (WBTC)
parseUnits('1', 8) // 100000000n
formatUnits(100000000n, 8) // '1.0'

// =================================================
// Gas fee calculation
// =================================================

// Scenario: Calculate transaction fee
const gasUsed = 200_000n; // gas units (BigInt)
const gasPrice = parseGwei('5'); // 5 gwei = 5000000000n wei
const fee = gasUsed * gasPrice; // 1000000000000000n wei
console.log(formatEther(fee)); // '0.001' ETH/BNB

// Scenario: EIP-1559 fee calculation
const baseFee = parseGwei('25'); // 25 gwei
const priorityFee = parseGwei('2'); // 2 gwei
const effectiveGasPrice = baseFee + priorityFee; // 27 gwei
const totalFee = gasUsed * effectiveGasPrice;
console.log(formatEther(totalFee)); // '0.0054' ETH

// =================================================
// LayerZero gas limits (note: these are gas units, not wei!)
// =================================================

// Gas limits in enforced options
const lzReceiveGas = 200_000; // 200,000 gas units — not wei!
const lzComposeGas = 500_000; // 500,000 gas units — not wei!

// Estimate actual destination chain fee
const targetGasPrice = parseGwei('5');
const lzReceiveFee = BigInt(lzReceiveGas) * targetGasPrice;
const lzComposeFee = BigInt(lzComposeGas) * targetGasPrice;
console.log('lzReceive fee:', formatEther(lzReceiveFee), 'BNB'); // '0.001'
console.log('lzCompose fee:', formatEther(lzComposeFee), 'BNB'); // '0.0025'

8. The Modern Gas Landscape (2025)

The Ethereum ecosystem underwent several major protocol upgrades in 2024-2025 that profoundly changed the gas fee landscape. This chapter covers the latest developments as of 2025.

8.1 EIP-4844: Proto-Danksharding and Blob Transactions

Background

EIP-4844 (also known as Proto-Danksharding) was introduced to the Ethereum mainnet in the March 2024 Dencun Upgrade. It introduced a brand new transaction type — Type 3 (blob transactions), designed specifically for Layer 2 Rollup data publishing scenarios.

Core Mechanism

Before EIP-4844, L2 Rollups (such as Arbitrum, Optimism, Base) needed to publish transaction data as calldata to the Ethereum mainnet, which was extremely expensive because calldata is permanently stored on-chain.

EIP-4844 introduced blobs (Binary Large Objects) — a form of temporary data storage. Blob data is processed by the consensus layer and automatically pruned after approximately 18 days, without permanently occupying on-chain space.

1
2
3
4
5
6
7
8
9
10
11
Before EIP-4844 (L2 data publishing):
Data storage method: calldata (permanent storage)
Fee: priced per calldata gas (16 gas/non-zero byte)
L2 per-transaction cost: $0.10 - $1.00

After EIP-4844 (L2 data publishing):
Data storage method: blob (temporary storage, ~18 days)
Fee: separate blob gas market (priced separately from regular gas)
L2 per-transaction cost: $0.001 - $0.01

Fee reduction: approximately 90-99%

Blob Gas Market

Blobs have their own independent fee market that runs in parallel with the EIP-1559 regular gas market:

  • Each block can contain a maximum of 6 blobs (target of 3)
  • Each blob is 128 KB in size
  • Blob gas has its own independent base fee, with an adjustment mechanism similar to EIP-1559
  • When blob space utilization exceeds the target, the blob gas price rises; otherwise it falls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Using viem to send blob transactions
import { createWalletClient, http, toBlobs, parseGwei } from 'viem';
import { mainnet } from 'viem/chains';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

// Construct blob transaction (usually auto-completed by L2 sequencer)
const hash = await client.sendTransaction({
to: '0xRollupInbox...',
// blob related parameters
blobs: toBlobs({ data: batchData }), // Rollup batch data
maxFeePerBlobGas: parseGwei('10'), // blob gas max fee
// regular gas parameters still needed
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

Impact on Developers

  1. L2 fees dropped dramatically: If your dApp is deployed on L2, user experience improves significantly
  2. L2-first strategy is even more justified: L2’s cost advantage has widened further
  3. New gas estimation dimension: Need to consider both execution gas and blob gas

8.2 Account Abstraction (ERC-4337) and Gas Sponsorship

Gas Pain Points of Traditional EOAs

In the traditional model, only EOAs (Externally Owned Accounts, i.e., regular accounts controlled by private keys) can initiate transactions, and they must hold native tokens to pay gas. This creates severe UX issues:

  • New users need to acquire native tokens before they can do anything
  • Users holding ERC-20 tokens but no ETH cannot transfer
  • Each chain requires independently prepared gas funds

ERC-4337 Account Abstraction

ERC-4337 introduced an account abstraction scheme that requires no protocol-level modifications, changing the gas payment model through the following core components:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ERC-4337 core components:

UserOperation (user action)
├─ Similar to traditional transactions but handled by smart contract wallet
├─ Contains callData, gas limits, etc.
└─ User does not need to directly hold ETH

Bundler (bundler)
├─ Bundles multiple UserOperations into a single on-chain transaction
├─ Bundler pays on-chain gas
└─ Recovers fees from Paymaster or user's smart wallet

EntryPoint (entry contract)
├─ Unified on-chain entry, validates and executes UserOperation
└─ Handles gas metering and fee settlement

Paymaster (payer) ← Key innovation
├─ Third-party contract that sponsors user's gas fees
├─ Scenario 1: dApp sponsors user's gas (gasless experience)
├─ Scenario 2: User pays gas with ERC-20 tokens (e.g., pay gas with USDC)
└─ Scenario 3: Subscription gas (monthly gas service)

Code Example: Using a Paymaster to Sponsor Gas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Construct a UserOperation sponsored by Paymaster
import { createSmartAccountClient } from 'permissionless';
import { toSimpleSmartAccount } from 'permissionless/accounts';
import { createPimlicoClient } from 'permissionless/clients/pimlico';

// Create Paymaster client
const pimlicoClient = createPimlicoClient({
transport: http('https://api.pimlico.io/v2/...'),
});

// Create smart account client
const smartAccountClient = createSmartAccountClient({
account: await toSimpleSmartAccount({ /* config */ }),
chain: mainnet,
bundlerTransport: http('https://bundler.example.com'),
// Paymaster config — user does not need to hold ETH
paymaster: pimlicoClient,
});

// Send transaction — gas sponsored by Paymaster
const hash = await smartAccountClient.sendTransaction({
to: '0xTokenContract...',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amount],
}),
// Note: User does not need to specify value to pay gas
// Paymaster automatically handles gas fees
});

Impact on the Gas Landscape

Aspect Traditional EOA ERC-4337 Smart Account
Gas Payer Must be the transaction initiator Can be a third-party Paymaster
Payment Token Native token only Can be ERC-20 (via Paymaster)
New User Experience Must acquire ETH/BNB first Can onboard with zero gas
Batch Operations Each transaction pays gas separately Can batch-bundle, amortizing gas cost

8.3 Pectra Upgrade and EIP-7702

Pectra Upgrade Overview (2025)

Pectra is Ethereum’s major upgrade planned for 2025, merging improvements from Prague (execution layer) and Electra (consensus layer). Key impacts on gas include:

  • Block gas limit increase: From approximately 30 million to 36 million, adding about 20% more block space
  • Blob count increase: Target blobs per block increased from 3 to 6, maximum from 6 to 9
  • EIP-7702: Introduces temporary smart contract capabilities for EOAs

EIP-7702: Smart EOAs

EIP-7702 allows an EOA to temporarily "delegate" to a smart contract’s code within a single transaction. This means ordinary EOA wallets can gain some benefits of account abstraction without migrating to a smart contract wallet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EIP-7702 how it works:

Traditional EOA:
Private key → Sign transaction → Send transaction → Each executed separately
Limitation: Cannot batch operations, cannot sponsor gas, cannot customize verification logic

EIP-7702 enhanced EOA:
Private key → Sign authorization (designation) → EOA temporarily has contract code
├─ Can execute multiple operations in single transaction (batch)
├─ Can use sponsored gas (sponsored transactions)
├─ After transaction ends, EOA returns to normal account
└─ No need to deploy new smart contract wallet

Gas impact:
├─ Batch operations reduce total gas: 5 transfers → 1 batch transaction
│ Traditional: 5 x 21,000 = 105,000 gas
│ EIP-7702 batch: ~80,000 gas (~24% savings)
├─ Compatible with ERC-4337 Paymaster
└─ Reduces account abstraction migration cost
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// EIP-7702 example: EOA temporarily delegates to batch execution contract
import { createWalletClient, http } from 'viem';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

// Sign EIP-7702 authorization
const authorization = await client.signAuthorization({
contractAddress: '0xBatchExecutor...', // batch executor contract address
});

// Send transaction with authorization — EOA temporarily has contract capabilities
const hash = await client.sendTransaction({
authorizationList: [authorization],
to: client.account.address, // send to self (trigger delegated code)
data: encodeFunctionData({
abi: batchExecutorAbi,
functionName: 'executeBatch',
args: [
// batch execute multiple operations
[
{ target: token1, data: transfer1Data, value: 0n },
{ target: token2, data: transfer2Data, value: 0n },
{ target: dex, data: swapData, value: 0n },
],
],
}),
});

8.4 Modern Gas-Optimized Opcodes

EIP-1153: Transient Storage

The TLOAD and TSTORE opcodes introduced in the Dencun Upgrade provide temporary storage within the same transaction, automatically cleared when the transaction ends.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Traditional approach: use SSTORE for reentrancy lock
// Write cost: 20,000 gas (first time), refund available after clearing
contract TraditionalLock {
uint256 private _locked;

modifier nonReentrant() {
require(_locked == 0, "reentrancy");
_locked = 1; // SSTORE: 20,000 gas
_;
_locked = 0; // SSTORE: 5,000 gas + refund
}
}

// Modern approach: use transient storage
// Read/write cost is 100 gas each, no clearing needed
contract TransientLock {
// Using EIP-1153 transient storage
modifier nonReentrant() {
assembly {
if tload(0) { revert(0, 0) } // TLOAD: 100 gas
tstore(0, 1) // TSTORE: 100 gas
}
_;
assembly {
tstore(0, 0) // TSTORE: 100 gas
// automatically cleared after transaction ends, this line is explicit reset
}
}
// Total cost: approximately 300 gas (traditional approach approximately 25,000 gas)
}

EIP-3855: PUSH0 Opcode

The Shanghai Upgrade introduced PUSH0, specifically for pushing a zero value onto the stack. Previously, PUSH1 0x00 (3 gas) was required; now PUSH0 (2 gas) suffices.

1
2
3
4
5
6
7
8
9
// PUSH0 automatically used by Solidity 0.8.20+
// When you write the following code, the compiler uses PUSH0:
uint256 x = 0; // compiles to PUSH0 instead of PUSH1 0x00
function foo() public returns (uint256) {
return 0; // compiles to PUSH0
}

// Note: If your contract needs to be deployed on chains that don't support PUSH0,
// please use Solidity < 0.8.20 or set evm_version = "paris"

8.5 Flashbots, MEV, and Gas

How MEV (Maximal Extractable Value) Affects Gas

MEV is the ability of validators/searchers to extract additional profit by reordering, inserting, or censoring transactions. MEV directly affects the gas market:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Relationship between MEV and Gas:

1. Arbitrage bots (Arbitrage)
├─ Monitor large transactions in mempool
├─ Insert their own transactions before/after target transaction
├─ Willing to pay extremely high priority fee to ensure ordering
└─ Push up short-term gas price

2. Sandwich Attack (Sandwich Attack)
├─ Detect user's large DEX swap
├─ Buy before user's transaction (front-run) → push up price
├─ User's transaction executes at worse price
├─ Sell after user's transaction (back-run) → profit
└─ Attacker's transaction uses high priority fee

3. Liquidation (Liquidation)
├─ When collateral is insufficient in lending protocols
├─ Liquidators compete to execute liquidation
├─ Use extremely high priority fee to ensure priority execution
└─ Causes instant gas price spike

Flashbots and MEV Protection

Flashbots is one of the most important MEV infrastructure projects in the Ethereum ecosystem. It provides MEV protection and more efficient block space auctions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Send private transaction via Flashbots (avoid sandwich attacks)
// Using viem + Flashbots RPC
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

// Send transaction using Flashbots Protect RPC
const client = createWalletClient({
chain: mainnet,
transport: http('https://rpc.flashbots.net'), // Flashbots private RPC
});

// Transaction does not enter public mempool, sent directly to block builder
// Avoid being discovered and attacked by MEV searchers
const hash = await client.sendTransaction({
to: dexRouter,
data: swapData,
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

// Note: Flashbots Protect transactions are not guaranteed to be included
// If your transaction is skipped, wait or increase priority fee to retry

8.6 Access Lists (EIP-2930)

EIP-2930 introduced Access Lists, allowing transactions to pre-declare the storage slots and contract addresses they will access. This can reduce the gas cost of cold access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Access List gas savings principle:

Without Access List:
First access address: 2,600 gas (cold access CALL)
First read storage: 2,100 gas (cold access SLOAD)

With Access List:
Pre-declared address: 2,400 gas (Access List cost) + 100 gas (warm access CALL)
Pre-declared storage: 1,900 gas (Access List cost) + 100 gas (warm access SLOAD)

Savings (per address): 2,600 - 2,500 = 100 gas
Savings (per storage slot): 2,100 - 2,000 = 100 gas

Applicable scenarios:
├─ Complex transactions with intensive cross-contract calls
├─ Operations that access the same storage slot multiple times
└─ L2 transactions (reducing cold access markers in calldata)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Using viem to create transaction with Access List
import { createPublicClient, http } from 'viem';

const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});

// Auto-generate Access List (via eth_createAccessList RPC)
const accessList = await publicClient.createAccessList({
to: '0xDEXRouter...',
data: swapData,
from: userAddress,
});

// Send transaction with Access List (Type 1 transaction)
const hash = await walletClient.sendTransaction({
to: '0xDEXRouter...',
data: swapData,
accessList: accessList.accessList, // pre-declare addresses and storage slots to access
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

9. Developer Gas Optimization Practices

This chapter provides practical Solidity gas optimization tips, with quantitative gas savings analysis for each technique.

9.1 Storage Optimization

1. Variable Packing (Storage Packing)

EVM storage is organized in 32-byte (256-bit) slots. If multiple small variables can fit into the same slot, only one SSTORE/SLOAD operation is needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Bad: each variable occupies independent storage slot (3 slots = 3 SSTORE operations)
contract BadPacking {
uint256 a; // slot 0 — 32 bytes
uint8 b; // slot 1 — only uses 1 byte, but occupies entire 32-byte slot
uint256 c; // slot 2 — 32 bytes
uint8 d; // slot 3 — same as above
// Writing b and d requires 2 SSTORE = 40,000 gas (first write)
}

// Good: small variables packed together, share storage slots (2 slots)
contract GoodPacking {
uint256 a; // slot 0 — 32 bytes
uint256 c; // slot 1 — 32 bytes
uint8 b; // slot 2 — shares with d
uint8 d; // slot 2 — packed with b in same slot
// Writing b and d only needs 1 SSTORE = 20,000 gas
// Savings: 20,000 gas
}

2. Using immutable and constant

1
2
3
4
5
6
7
8
9
10
11
12
13
// constant: determined at compile time, embedded in bytecode (no storage slot)
uint256 constant MAX_SUPPLY = 1_000_000; // read cost: approximately 3 gas (PUSH)

// immutable: determined at deployment time, embedded in bytecode (no storage slot)
uint256 immutable deployTime;
constructor() {
deployTime = block.timestamp; // after deployment read cost: approximately 3 gas
}

// Comparison: regular state variable
uint256 maxSupply = 1_000_000; // read cost: 2,100 gas (cold SLOAD)

// Savings: approximately 2,097 gas saved per read

3. Using mappings instead of arrays for lookups

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Bad: iterate array to search — O(n) complexity, gas grows with array length
address[] whitelist;
function isWhitelisted(address user) public view returns (bool) {
for (uint i = 0; i < whitelist.length; i++) {
if (whitelist[i] == user) return true; // approximately 200 gas per iteration
}
return false;
// 100 elements = approximately 20,000 gas
}

// Good: mapping lookup — O(1) complexity, gas is constant
mapping(address => bool) whitelist;
function isWhitelisted(address user) public view returns (bool) {
return whitelist[user]; // fixed approximately 2,100 gas (cold) or approximately 100 gas (warm)
}

9.2 Calldata and Memory Optimization

1. Using calldata instead of memory for read-only external function parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Bad: memory copies data to memory (extra gas)
function processData(uint256[] memory data) external {
// data copied from calldata to memory
// copy cost: approximately 3 gas/word + memory expansion cost
for (uint i = 0; i < data.length; i++) {
// process...
}
}

// Good: calldata reads directly from transaction data (no copy)
function processData(uint256[] calldata data) external {
// data read directly from calldata, zero copy cost
for (uint i = 0; i < data.length; i++) {
// process...
}
}
// Save approximately 60 gas per uint256 element

2. Shorter revert messages or custom errors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Bad: long string revert messages consume lots of calldata and deployment bytecode
require(balance >= amount, "InsufficientBalance: the user does not have enough tokens");
// String stored in bytecode, increases deployment cost

// Good: use custom errors (Solidity 0.8.4+)
error InsufficientBalance(uint256 available, uint256 required);

function transfer(address to, uint256 amount) external {
if (balanceOf[msg.sender] < amount) {
revert InsufficientBalance(balanceOf[msg.sender], amount);
}
// ...
}
// Savings: deployment gas reduced (shorter bytecode), less gas on revert too
// Custom errors use 4-byte selector vs dynamic string ABI encoding

9.3 Loops and Batch Operations

1. Cache storage variables locally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Bad: read storage variable every loop iteration
function sumBalances(address[] calldata users) external view returns (uint256) {
uint256 total;
for (uint i = 0; i < users.length; i++) { // users.length read from calldata each time (fine)
total += balances[users[i]]; // balances mapping slot SLOAD each time
}
return total;
}

// Good: cache storage value that doesn't change in loop
uint256 public feeRate; // assume used in loop

function applyFees(uint256[] calldata amounts) external view returns (uint256[] memory) {
uint256[] memory results = new uint256[](amounts.length);
uint256 cachedFee = feeRate; // one SLOAD (2,100 gas), then read from stack (3 gas)
for (uint i = 0; i < amounts.length; i++) {
results[i] = amounts[i] * cachedFee / 10000;
// if not cached, would need SLOAD feeRate every loop iteration
}
return results;
}
// 100 loop iterations savings: 99 x 2,000 = approximately 198,000 gas

2. unchecked blocks for provably safe arithmetic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Solidity 0.8+ has overflow check enabled by default (approximately 30 extra gas per arithmetic operation)
function sum(uint256[] calldata data) external pure returns (uint256) {
uint256 total;
for (uint256 i = 0; i < data.length; i++) {
total += data[i]; // includes overflow check
}
// includes i++ overflow check
return total;
}

// When you are certain there will be no overflow, use unchecked to save gas
function sum(uint256[] calldata data) external pure returns (uint256) {
uint256 total;
uint256 len = data.length;
for (uint256 i; i < len; ) {
total += data[i];
unchecked { ++i; } // i cannot overflow uint256
}
return total;
}
// Save approximately 80 gas per loop iteration (i++ check + possible total check)

9.4 Compiler and Deployment Optimization

1. Solidity Optimizer Settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Optimizer config in hardhat.config.js / foundry.toml
module.exports = {
solidity: {
version: '0.8.24',
settings: {
optimizer: {
enabled: true,
runs: 200, // expected number of times contract will be called
// lower runs: cheaper deployment, slightly more expensive runtime
// higher runs: more expensive deployment, cheaper runtime
// general recommendation: 200 (balanced) or 1000 (frequently called contracts)
},
viaIR: true, // compile via IR (intermediate representation), additional 5-15% optimization
evmVersion: 'cancun', // use latest EVM version to get new opcodes
},
},
};

2. Using proxy patterns to reduce deployment cost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// When need to deploy many contracts with same logic (e.g., factory pattern)
// Use minimal proxy (EIP-1167 Minimal Proxy / Clone)

// Implementation contract deployed only once
contract TokenImplementation {
function initialize(string memory name, uint256 supply) external {
// initialization logic...
}
}

// Factory creates proxies via cloning (deployment cost extremely low)
import "@openzeppelin/contracts/proxy/Clones.sol";

contract TokenFactory {
address immutable implementation;

constructor(address _impl) {
implementation = _impl;
}

function createToken(string memory name, uint256 supply) external returns (address) {
// Clone deployment cost: approximately 40,000 gas (vs full deployment 1,000,000+ gas)
address clone = Clones.clone(implementation);
TokenImplementation(clone).initialize(name, supply);
return clone;
}
}

10. Common Pitfalls and Debugging

10.1 Gas Estimation Failures

Scenario 1: eth_estimateGas returns an error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Problem: gas estimation failure usually means transaction will revert
try {
const gasEstimate = await publicClient.estimateGas({
to: contractAddress,
data: callData,
account: userAddress,
});
} catch (error) {
// Common causes:
// 1. Contract require conditions not met (insufficient balance, unauthorized, etc.)
// 2. Contract is paused
// 3. Caller has no permission
// 4. Invalid parameters (zero address, out of range, etc.)

// Debugging method 1: use eth_call to get detailed error
try {
await publicClient.call({
to: contractAddress,
data: callData,
account: userAddress,
});
} catch (callError) {
// callError usually contains revert reason
console.error('Revert reason:', callError.message);
// e.g.: "ERC20: transfer amount exceeds balance"
// e.g.: "Ownable: caller is not the owner"
}
}

Scenario 2: Gas estimation returns an abnormally high value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Problem: estimation returns 30,000,000 gas (close to block limit)
// This usually means there is a problem in contract logic

const gasEstimate = await publicClient.estimateGas({
to: contractAddress,
data: callData,
account: userAddress,
});

if (gasEstimate > 5_000_000n) {
console.warn('Gas estimate abnormally high:', gasEstimate);
// Possible causes:
// 1. Infinite loop or extremely large loop in contract
// 2. Too many storage operations (batch operations exceed reasonable range)
// 3. Contract calls external contract that reverts, but not handled correctly
// 4. Estimator returned block gas limit (fallback value)
}

10.2 Stuck Transactions and Replacement

Transaction stuck in mempool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Transaction stuck usually because gas price too low or nonce issue

// Method 1: send replacement transaction with same nonce but higher gas price
const stuckTxNonce = 42n; // nonce of stuck transaction

const replacementHash = await walletClient.sendTransaction({
to: originalTo,
data: originalData,
value: originalValue,
nonce: stuckTxNonce, // key: use same nonce
// increase gas price (at least 10%, recommend doubling)
maxFeePerGas: parseGwei('60'), // was 30
maxPriorityFeePerGas: parseGwei('5'), // was 2
});

// Method 2: send empty transaction to cancel (use same nonce)
const cancelHash = await walletClient.sendTransaction({
to: walletClient.account.address, // send to self
value: 0n,
nonce: stuckTxNonce,
maxFeePerGas: parseGwei('60'),
maxPriorityFeePerGas: parseGwei('5'),
});

Nonce gap issue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Problem: nonce 3 transaction fails, but nonce 4, 5 have already been sent
// Result: nonce 4, 5 stuck because nonce 3 is not confirmed

// Check current nonce
const confirmedNonce = await publicClient.getTransactionCount({
address: userAddress,
blockTag: 'latest',
});

const pendingNonce = await publicClient.getTransactionCount({
address: userAddress,
blockTag: 'pending',
});

console.log('Confirmed nonce:', confirmedNonce);
console.log('Pending nonce:', pendingNonce);

// If confirmedNonce < pendingNonce, there are stuck transactions
// Need to resend or cancel transactions in order starting from confirmedNonce

10.3 Contract Interaction Pitfalls

Pitfall 1: Forgetting to check existing allowance before approve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Problem: some tokens (like USDT) don't allow approve from non-zero value to another non-zero value
// Must first approve to 0, then approve new value

// Check existing allowance
const currentAllowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [userAddress, spenderAddress],
});

if (currentAllowance > 0n) {
// First set allowance to 0
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 0n],
});
}

// Then set new allowance
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, newAmount],
});

Pitfall 2: Setting maxFeePerGas too low in EIP-1559 transactions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Problem: baseFee may rise between when you send transaction and when it's included
// If maxFeePerGas < baseFee, transaction cannot be included

// Safe approach: query current baseFee and add buffer
const block = await publicClient.getBlock();
const currentBaseFee = block.baseFeePerGas!;

// Add 2x buffer (baseFee rises maximum 12.5% per block,
// 2x buffer covers maximum rise of approximately 6 blocks)
const safeMaxFee = currentBaseFee * 2n + parseGwei('2'); // 2 gwei priority fee

const hash = await walletClient.sendTransaction({
to: recipient,
value: parseEther('1'),
maxFeePerGas: safeMaxFee,
maxPriorityFeePerGas: parseGwei('2'),
});
// Note: excess maxFee will be refunded, so setting higher only locks more funds,
// actual charge is not more

Pitfall 3: Ignoring L1 data fees on L2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Problem: on Arbitrum/Optimism/Base, transaction fees consist of two parts:
// 1. L2 execution fee (usually very low)
// 2. L1 data publishing fee (may be the main cost)

// Use viem to get complete fee estimate for L2 transactions
import { optimism } from 'viem/chains';

// Optimism specific: estimate L1 data fee
const l1Fee = await publicClient.readContract({
address: '0x420000000000000000000000000000000000000F', // L1Block precompile
abi: gasPriceOracleAbi,
functionName: 'getL1Fee',
args: [serializedTx], // serialized transaction data
});

const l2GasCost = gasUsed * l2GasPrice;
const totalCost = l2GasCost + l1Fee;

console.log('L2 execution fee:', formatEther(l2GasCost));
console.log('L1 data fee:', formatEther(l1Fee));
console.log('Total fee:', formatEther(totalCost));

// Note: After EIP-4844 implementation, L1 data fees dropped significantly
// Because L2 now uses blob instead of calldata to publish data

Transaction Lifecycle

Transaction Lifecycle Complete flow from submission to inclusion 1. Build Transaction Set to, value, data Set maxFeePerGas, gasLimit 2. Sign Private key signs transaction data Generates v, r, s signature fields 3. Broadcast to Network Sent to node via RPC Node validates basics (nonce, balance) 4. Mempool Transaction waits here to be selected by validator/block builder Sorted by effective gas price (priority fee) — higher bid = higher priority Note: Public mempool is visible to everyone — MEV searchers look for arbitrage here Flashbots Private Pool Bypasses public mempool Sent directly to block builders Avoids MEV attacks 5. Block Building & Validation Validator/block builder selects transactions to pack into a block Executes transactions, computes state root, generates receipt (incl. actual gasUsed) 6a. Transaction Succeeds State changes take effect Charged: gasUsed x effectiveGasPrice 6b. Transaction Fails (Revert) State changes rolled back Gas fee still charged! Consumed gas not refunded Subsequent block confirmations → Finality Failed transactions are also recorded on-chain

References

Core Specifications

Developer Resources

Tools