EVM & Gas

Difficulty: Beginner

The Ethereum Virtual Machine (EVM) is the computation engine that executes smart contracts on the Ethereum network. It is a stack-based, quasi-Turing-complete virtual machine that runs bytecode compiled from high-level languages like Solidity or Vyper. Every full node in the Ethereum network runs an instance of the EVM, and all nodes execute the same transactions in the same order, arriving at the same state. This deterministic execution across thousands of nodes is what makes Ethereum a trustless, decentralized computer.

The EVM operates on a set of approximately 140 opcodes - low-level instructions similar to assembly language. These opcodes perform operations like arithmetic (ADD, MUL, SUB), comparison (LT, GT, EQ), bitwise operations (AND, OR, XOR), stack manipulation (PUSH, POP, DUP, SWAP), memory operations (MLOAD, MSTORE), storage operations (SLOAD, SSTORE), and control flow (JUMP, JUMPI). When you compile a Solidity contract, it is translated into a sequence of these opcodes. The EVM processes them one by one, maintaining a stack, memory (volatile, cleared after each call), and storage (persistent, stored on-chain).

Gas is the unit that measures the computational effort required to execute operations on the EVM. Every opcode has a fixed gas cost - simple operations like ADD cost 3 gas, while expensive operations like SSTORE (writing to persistent storage) cost 20,000 gas for a new slot. Gas serves two critical purposes: it prevents infinite loops (since the EVM is Turing-complete, a program could theoretically run forever, but gas ensures it eventually runs out of fuel), and it compensates validators for the computational resources they expend to process transactions.

When a user submits a transaction, they specify a gas limit (the maximum amount of gas they are willing to consume) and a gas price (how much ETH they are willing to pay per unit of gas, denominated in gwei - 1 gwei = 10^-9 ETH). The total transaction fee is gas used multiplied by gas price. Since EIP-1559 (August 2021), Ethereum uses a dual fee structure: a base fee (algorithmically determined and burned) plus an optional priority fee (tip to the validator). If a transaction runs out of gas before completion, all state changes are reverted, but the gas fee is still consumed - the work was done even though it failed.

Optimizing gas consumption is a critical skill for Solidity developers. Storage operations are by far the most expensive, so minimizing storage writes, using memory for intermediate calculations, packing multiple values into single storage slots (slot packing), and using events for data that does not need to be read on-chain are all standard optimization techniques. Tools like Foundry's gas reports and Hardhat's gas reporter help developers measure and reduce gas costs during development.

Code examples

Gas Estimation with ethers.js

import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');

async function estimateGasCosts() {
  // Get current gas price info (EIP-1559)
  const feeData = await provider.getFeeData();
  console.log('Current Fee Data:');
  console.log('  Gas Price:', ethers.formatUnits(feeData.gasPrice, 'gwei'), 'gwei');
  console.log('  Max Fee Per Gas:', ethers.formatUnits(feeData.maxFeePerGas, 'gwei'), 'gwei');
  console.log('  Max Priority Fee:', ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei'), 'gwei');

  // Estimate gas for a simple ETH transfer
  const transferGas = await provider.estimateGas({
    from: '0x1234567890123456789012345678901234567890',
    to: '0x0987654321098765432109876543210987654321',
    value: ethers.parseEther('1.0'),
  });
  console.log('\nSimple ETH transfer:');
  console.log('  Gas required:', transferGas.toString());
  console.log('  Cost:', ethers.formatEther(transferGas * feeData.gasPrice), 'ETH');

  // Estimate gas for a contract interaction (ERC-20 transfer)
  const erc20Abi = ['function transfer(address to, uint256 amount) returns (bool)'];
  const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
  const usdcContract = new ethers.Contract(usdcAddress, erc20Abi, provider);

  try {
    const contractGas = await usdcContract.transfer.estimateGas(
      '0x0987654321098765432109876543210987654321',
      1000000 // 1 USDC (6 decimals)
    );
    console.log('\nERC-20 transfer (USDC):');
    console.log('  Gas required:', contractGas.toString());
    console.log('  Cost:', ethers.formatEther(contractGas * feeData.gasPrice), 'ETH');
  } catch (e) {
    console.log('\nERC-20 estimation failed (need valid from address with balance)');
  }

  // Common gas costs for reference
  console.log('\nCommon Gas Costs (reference):');
  console.log('  ETH transfer: 21,000 gas');
  console.log('  ERC-20 transfer: ~46,000-65,000 gas');
  console.log('  ERC-20 approve: ~46,000 gas');
  console.log('  Uniswap swap: ~150,000-300,000 gas');
  console.log('  NFT mint: ~50,000-150,000 gas');
  console.log('  Contract deployment: ~500,000-5,000,000+ gas');
}

estimateGasCosts();

This example demonstrates how to query current gas prices and estimate gas costs for transactions. The EIP-1559 fee structure includes a base fee (burned) and priority fee (tip). A simple ETH transfer always costs exactly 21,000 gas, while contract interactions vary based on the complexity of the operations performed.

Key points

Concepts covered

EVM, Gas, Gas Limit, Opcodes