Gas & Fees
Gas is what makes Ethereum work: every operation costs ETH, paid by the transaction sender. Optimising gas reduces user friction and storage cost; understanding how it’s computed (base fee + priority fee + units) is the difference between “works on testnet” and “ships to mainnet”.
Gas units, EIP-1559, optimisation, traps
EXAMPLE
// 1) Gas — a unit of computational work
// Each EVM opcode costs a fixed number of gas units.
// ADD 3
// MUL 5
// SSTORE 5,000 (new), 20,000 (slot 0 → non-zero)
// SLOAD 100 (warm), 2,100 (cold) post-EIP-2929
// BALANCE 100 (warm), 2,600 (cold)
// CALL 700+ depending on target
//
// Total gas = sum of opcode costs + 21,000 base transaction
// 2) Gas PRICE — what the user pays per unit
// Pre EIP-1559: single 'gasPrice' set by user
// EIP-1559 (London hard fork): split into:
// • baseFee — set by protocol, burnt; rises if previous block > 50% full, falls otherwise
// • maxPriorityFee (tip) — paid to validator; user-set
// • maxFee — cap on (baseFee + tip) user is willing to pay
//
// Wallet UX: 'slow / normal / fast' = different tip amounts
// 3) Reading gas in the wallet
import { ethers } from 'ethers';
const provider = new ethers.BrowserProvider(window.ethereum);
const feeData = await provider.getFeeData();
feeData.maxFeePerGas; // upper bound
feeData.maxPriorityFeePerGas; // tip
feeData.gasPrice; // legacy fallback
// 4) Estimating gas for a transaction
const estimate = await contract.transfer.estimateGas(recipient, amount);
const tx = await contract.transfer(recipient, amount, {
gasLimit: estimate * 12n / 10n, // +20% safety margin
});
// Always estimate + buffer; reverts at execution if gasLimit too low.
// 5) Cost in real terms
// cost (ETH) = gasUsed × effectiveGasPrice
// cost (USD) = cost (ETH) × ETH/USD
//
// Example: 21,000 gas × 30 gwei = 630,000 gwei = 0.00063 ETH
// At $3000 ETH → $1.89 for a plain transfer
//
// Complex operations:
// • ERC-20 transfer ~50,000 gas (~$4.50)
// • Uniswap swap 150,000 gas (~$13)
// • NFT mint 80-200k gas
// • Heavy DeFi 500,000+
// 6) Storage is the most expensive
// • SSTORE 0 → non-zero 20,000 gas
// • SSTORE non-zero → non-zero 5,000 gas
// • SSTORE → 0 (clear) refund up to 4,800 gas (capped)
//
// Pack multiple values into one slot (256 bits) to save gas:
struct Position {
uint128 amount; // 128 bits
uint64 deadline; // 64 bits
address user; // 160 bits — doesn't fit; gets own slot
bool active; // 8 bits — packs with user
}
// Reorder for tight packing:
struct PositionPacked {
uint128 amount;
uint64 deadline;
bool active;
address user; // moved last — fits with bool in one slot
}
// 7) Common gas optimisations
//
// • Use 'calldata' over 'memory' for function arguments
// function foo(uint[] calldata ids) → cheaper than 'memory'
// • Mark functions 'external' instead of 'public' when only called externally
// • Use 'unchecked' for arithmetic you know can't overflow (Solidity 0.8+)
// for (uint i; i < n;) { ...; unchecked { ++i; } }
// • Use 'immutable' for set-once-in-constructor (no SLOAD; baked in)
// • Use 'constant' for compile-time constants
// • Cache storage reads in memory inside loops
// • Custom errors instead of revert strings (Solidity 0.8.4+)
//
// error Unauthorized(address caller);
// if (msg.sender != owner) revert Unauthorized(msg.sender);
//
// • Boolean storage 0/1 uses fresh-slot pricing every flip; use a uint32 mask if many flags
// • Avoid storage writes in revert paths
// 8) Avoid these gas traps
//
// • Unbounded loops — gas grows with input size; user pays
// • Calldata-driven storage growth — bills the user; cap input size
// • External calls in loops — each one costs 700-2600 gas + return
// • String storage — store keccak256 hash + emit string in event instead
// • Large constants in code (bytecode increases deploy cost)
// • Repeating SLOADs — cache to memory
// 9) Events vs storage
// Events are CHEAPER than storage (375 gas + 8 per byte).
// Use them for off-chain consumers / indexers (The Graph, Alchemy):
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(msg.sender, to, amount);
// 10) Batch operations
// Doing multiple operations in one tx amortises the 21,000 base cost.
// • Multicall pattern: takes array of calls; executes
// • OpenZeppelin Multicall.sol or Uniswap's Multicall3 (off-chain SDK)
// 11) Layer 2 — drastic cost reduction
// • Optimistic rollups (Arbitrum, Optimism): 10-100x cheaper than L1
// • zk-rollups (zkSync, StarkNet, Scroll, Linea, Base): similar reductions
// • Most user actions run on L2; L1 reserved for settlement / governance
//
// Code change: usually minimal — same Solidity. Watch for L2-specific opcodes.
// 12) Inspecting gas in tests (Foundry)
// forge test --gas-report
// Shows gas used per function call across the suite.
// forge snapshot — track gas changes between commits
// forge inspect MyContract storageLayout — see slot packing
// 13) Tools
// • Tenderly — debug + gas profiler
// • Etherscan gas tracker — current network rates
// • OpenChain — decode tx + gas breakdown
// • Slither --print human-summary — gas-heavy functions surfaced
// 14) Real-world checklist before mainnet
// • Storage layout reviewed for packing
// • All external functions estimate within reasonable limits
// • No unbounded loops
// • Custom errors instead of revert strings
// • Multicall / batch pattern for users who'd otherwise pay N txs
// • Gas snapshot in CI to catch regressions
// • Public quote API or simulation to show estimated cost in UI
// 15) Common bugs
// • Setting gasLimit too low → 'out of gas' revert; estimate + buffer
// • Forgetting to use 'external' on big functions → public copies arrays to memory
// • SLOAD inside a for loop → big SSTORE-vs-cache savings missed
// • Storing strings/bytes when only a hash needed
// • Using 'now' instead of block.timestamp (alias deprecated)
// • String comparison via keccak256 vs ==; use keccak256
// • Trying to optimise 100 gas off when L2 is the answer to high cost
// • Hardcoding gas prices in tests → break on network changes
// • Multi-step transactions without single-tx Multicall → users pay every time
Why it matters
Gas is the bill for computation; the user pays. Estimate with a 20% buffer, pack storage slots, prefer calldata/external/immutable, replace revert strings with custom errors, emit events instead of writing storage, and reach for Layer 2 (Arbitrum, Optimism, zkSync) when L1 costs make UX painful. Snapshot gas in CI so regressions get caught.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Every op costs gas. tx pays gasUsed × gasPrice (or base + tip on EIP-1559). // Save gas: pack storage slots, use events instead of storing logs, prefer calldata over memory.Try it Yourself »
Discussion
Loading…