How Smart Contracts Work

Smart contracts are programs stored on a blockchain that execute automatically when predetermined conditions are met. Unlike traditional software running on a company's server, a smart contract runs on every validator node in the network simultaneously, producing deterministic results that no single party can tamper with. The concept was first described by computer scientist Nick Szabo in 1994, but it took two decades and the launch of Ethereum in 2015 to turn the idea into a practical, programmable platform. Today smart contracts power decentralized finance (DeFi), NFT marketplaces, governance systems, and billions of dollars in autonomous on-chain activity.

At their core, smart contracts solve a specific problem: how do two parties who don't trust each other execute an agreement without a middleman? The answer is to encode the agreement as code, deploy it to a blockchain where it becomes immutable, and let the consensus mechanism guarantee that every node executes the same code with the same result. Once deployed, a smart contract's code cannot be changed (barring specific upgrade patterns discussed later), and its execution is entirely transparent.

The Ethereum Virtual Machine

Ethereum's smart contracts run inside the Ethereum Virtual Machine (EVM), a sandboxed, quasi-Turing-complete execution environment embedded in every Ethereum node. The EVM is a stack-based machine with a 256-bit word size, chosen to accommodate the Keccak-256 hashes and large-integer cryptography common in blockchain operations. It has no registers -- all computation happens by pushing values onto and popping them from a last-in-first-out stack with a maximum depth of 1024 items.

EVM Architecture Stack 0xFF..A3 (top) 0x00..01 0x00..20 ... max 1024 items Memory byte-addressable volatile (per call) linear cost to expand MLOAD / MSTORE Storage 256-bit key-value persistent on-chain most expensive ops SLOAD / SSTORE Bytecode (ROM) immutable code sequential opcodes PC (program counter) JUMP / JUMPI Gas Meter gas remaining: 65,000 gas used: 35,000 out of gas = revert Context msg.sender msg.value tx.origin block.number block.timestamp address(this) tx.gasprice calldata

The EVM instruction set contains roughly 140 opcodes, each one byte. Arithmetic operations like ADD, MUL, and SUB pop operands from the stack, compute a result, and push it back. Memory operations (MLOAD, MSTORE) access a volatile byte array that exists only for the duration of the current call. Storage operations (SLOAD, SSTORE) read and write to a persistent 256-bit key-value store that lives on-chain forever -- this is where contract state actually resides between transactions.

Gas: The Computation Meter

Every EVM opcode has a gas cost. Simple arithmetic like ADD costs 3 gas. A storage write (SSTORE) to a previously-zero slot costs 20,000 gas. This gas metering serves two purposes: it prevents infinite loops (a contract that runs out of gas reverts all its changes), and it creates a fee market where users pay validators proportional to the computational resources consumed. When you send a transaction, you specify a gas limit (maximum gas you're willing to spend) and a gas price (how much ETH you'll pay per unit of gas). The total transaction fee is gas_used * gas_price. After Ethereum's EIP-1559, the fee model became a base fee (burned) plus an optional priority fee (paid to validators), but the gas metering itself remains unchanged.

Solidity: The Dominant Smart Contract Language

Solidity is a statically-typed, curly-brace language designed specifically for the EVM. It compiles to EVM bytecode and is used by the vast majority of Ethereum smart contracts. Its syntax resembles JavaScript and C++, but its semantics are heavily shaped by the constraints of blockchain execution: storage is expensive, computation is metered, and all state changes must be deterministic.

State Variables and Storage Layout

State variables in Solidity are variables declared at the contract level. They are stored persistently in the contract's storage -- the key-value store backed by SSTORE/SLOAD. The Solidity compiler assigns each state variable a storage slot (a 256-bit key) sequentially starting from slot 0. Mappings and dynamic arrays use hash-based slot computation to avoid collisions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleToken {
    // Slot 0: owner address (160 bits, packed)
    address public owner;
    // Slot 1: total supply
    uint256 public totalSupply;
    // Slot 2+: mapping uses keccak256(key, slot)
    mapping(address => uint256) public balanceOf;

    // Events: logged, not stored in contract state
    event Transfer(address indexed from, address indexed to, uint256 value);

    // Modifier: reusable access control
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _; // placeholder for the function body
    }

    constructor(uint256 _initialSupply) {
        owner = msg.sender;
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = _initialSupply;
    }

    function transfer(address to, uint256 amount) external {
        require(balanceOf[msg.sender] >= amount, "insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
    }

    function mint(uint256 amount) external onlyOwner {
        totalSupply += amount;
        balanceOf[owner] += amount;
        emit Transfer(address(0), owner, amount);
    }
}

Functions and Visibility

Solidity functions have four visibility levels. public functions can be called externally and internally, and the compiler auto-generates a getter for public state variables. external functions can only be called from outside the contract -- they are more gas-efficient for large calldata since they can read directly from calldata rather than copying to memory. internal functions are accessible within the contract and derived contracts. private functions are accessible only within the defining contract. Functions are also marked with state mutability: view (reads but doesn't modify state), pure (neither reads nor modifies state), or payable (can receive ETH).

Events and Logs

Events are the EVM's logging mechanism. When a contract emits an event, the data is stored in the transaction's log -- a separate data structure from contract storage that is much cheaper to write to (roughly 375 gas for the first topic plus 8 gas per byte of data, compared to 20,000 gas for a new storage slot). Logs are not accessible from within smart contracts; they exist for off-chain consumers. DApp frontends, indexers like The Graph, and analytics tools subscribe to events to track on-chain activity. The indexed keyword on event parameters creates "topics" that can be efficiently filtered by Ethereum nodes.

Modifiers

Modifiers are reusable code fragments that wrap function logic. The most common pattern is access control -- the onlyOwner modifier above checks a condition before executing the function body (represented by _;). Modifiers can also execute code after the function body, or even wrap it in a try-catch. They compile to inline code, not function calls, so they don't incur the gas overhead of a CALL opcode.

Contract Deployment: Creation vs. Runtime Bytecode

Deploying a smart contract is a two-phase process that often confuses newcomers. When you deploy a contract, you send a transaction with no to address and include the creation bytecode in the data field. This creation bytecode is itself an EVM program that runs exactly once. It executes the constructor logic, then returns the runtime bytecode -- the code that will live at the contract's address permanently.

Contract Deployment Flow Deploy TX to: null data: creation bytecode EVM Executes runs constructor initializes storage returns runtime code Contract Created address: 0xAb3..F7 code = runtime bytecode stored Users call functions The creation bytecode runs once. The runtime bytecode persists forever. Contract address = keccak256(deployer_address, nonce)[12:] CREATE2: address = keccak256(0xff, deployer, salt, init_code_hash)[12:]

The contract address is deterministically derived from the deployer's address and nonce (for CREATE) or from a salt and the init code hash (for CREATE2). This means you can predict a contract's address before deploying it, which is essential for patterns like counterfactual instantiation used in state channels and account abstraction.

Understanding this two-phase process matters for security. The creation bytecode is what gets verified on block explorers like Etherscan. If you verify a contract's source code, what you're really verifying is that compiling that source with a specific compiler version produces the exact same creation bytecode, which in turn produces the runtime bytecode stored at that address.

ABI Encoding: The Contract Interface

The Application Binary Interface (ABI) defines how data is encoded for EVM function calls. When you call a smart contract function, the calldata sent in the transaction is not human-readable -- it is a tightly packed binary encoding defined by the ABI specification.

The first 4 bytes of calldata are the function selector: the first 4 bytes of the Keccak-256 hash of the function signature. For example, transfer(address,uint256) produces the selector 0xa9059cbb. After the selector, arguments are encoded as 32-byte (256-bit) ABI-encoded values. Addresses are left-padded to 32 bytes. Dynamic types like bytes and string use an offset-length encoding.

// Calling: transfer(0xAbC...123, 1000)
// Calldata breakdown:
//
// 0xa9059cbb                                                          -- function selector
// 000000000000000000000000AbCdEf0123456789AbCdEf0123456789AbCdEf01     -- address (padded to 32 bytes)
// 00000000000000000000000000000000000000000000000000000000000003e8     -- uint256 (1000 in hex)

The ABI is crucial for interoperability. Wallets, frontends, and other contracts all use the ABI to encode function calls and decode return values. The ABI JSON file generated by the Solidity compiler describes every function, event, and error in the contract, enabling tools to construct valid calldata without understanding the underlying bytecode. This is what makes it possible for a JavaScript frontend using ethers.js or viem to call arbitrary smart contract functions in a type-safe way.

Common Smart Contract Patterns

ERC-20: Fungible Tokens

ERC-20 is the standard interface for fungible tokens on Ethereum. Proposed by Fabian Vogelsteller and Vitalik Buterin in 2015, it defines six functions (totalSupply, balanceOf, transfer, transferFrom, approve, allowance) and two events (Transfer, Approval) that all compliant tokens must implement. This standardization is what allows any ERC-20 token to be traded on any decentralized exchange, held in any wallet, and tracked by any block explorer without custom integration.

The approve/transferFrom pattern deserves attention because it introduces a concept alien to traditional finance: explicit on-chain spending authorization. Because smart contracts cannot initiate transactions (only externally owned accounts can), a contract like Uniswap needs the user to first approve the Uniswap router contract to spend tokens on the user's behalf, and then the router calls transferFrom during the swap. This two-step pattern is the source of frequent UX complaints and has spawned alternatives like ERC-2612 (permit with off-chain signatures).

ERC-721: Non-Fungible Tokens (NFTs)

ERC-721 defines the standard interface for non-fungible tokens -- tokens where each unit is unique and has a distinct tokenId. The core difference from ERC-20 is that transfers specify which token is being moved, not just an amount. ERC-721 also introduced the concept of "safe transfers" via safeTransferFrom, which checks whether the receiving address is a contract and, if so, whether it implements the onERC721Received callback. This prevents tokens from being permanently locked in contracts that don't know how to handle them.

ERC-1155 later unified the fungible and non-fungible token models into a single contract, allowing batch transfers and reducing deployment costs for projects that need both types of tokens.

Proxy Contracts and Upgradeability

Smart contracts are immutable by design -- once deployed, their bytecode cannot change. But real-world software needs bug fixes and feature upgrades. The proxy pattern solves this by splitting a contract into two parts: a proxy contract that holds the storage and receives all calls, and an implementation contract that contains the actual logic. The proxy uses the EVM's DELEGATECALL opcode to execute the implementation's code in the proxy's storage context.

Proxy / Upgradeable Contract Pattern User sends tx Proxy Contract fixed address holds all storage holds user balances fallback() => DELEGATECALL delegatecall Impl V1 logic only no storage (replaceable) Impl V2 (after upgrade) upgrade DELEGATECALL: execute implementation code, but use proxy's storage and msg.sender Users always interact with the same proxy address. Admin swaps the implementation pointer.

The most common upgrade patterns are the Transparent Proxy (EIP-1967, used by OpenZeppelin) and the UUPS (Universal Upgradeable Proxy Standard) (EIP-1822). In the transparent pattern, the proxy contract itself contains the upgrade logic and an admin address. In UUPS, the upgrade logic lives in the implementation contract, making the proxy cheaper to deploy. Both use a well-known storage slot (e.g., 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc for the implementation address, chosen as bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)) to store the implementation address, preventing storage collisions with the implementation's variables.

Upgradeability introduces trust assumptions. If the admin can swap the implementation to arbitrary code, they can drain all funds. This is why many DeFi protocols place the admin behind a multisig wallet or a timelock contract that introduces a delay before upgrades take effect, giving users time to exit.

Security Vulnerabilities

Smart contracts handle real money in adversarial environments with no undo button. A bug in a smart contract can be exploited within seconds, and the resulting losses are typically permanent. The immutability that makes smart contracts trustworthy also makes them unforgiving.

Reentrancy

Reentrancy is the most infamous smart contract vulnerability. It occurs when a contract makes an external call to another contract before updating its own state, allowing the called contract to "re-enter" the calling contract and exploit the stale state. The classic pattern is a withdrawal function that sends ETH before decrementing the user's balance:

// VULNERABLE: sends ETH before updating balance
function withdraw() external {
    uint256 amount = balances[msg.sender];
    // This external call transfers control to msg.sender
    // If msg.sender is a contract, its receive() function executes
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    // Attacker re-enters withdraw() here, balance is still > 0
    balances[msg.sender] = 0;
}

// SAFE: checks-effects-interactions pattern
function withdraw() external {
    uint256 amount = balances[msg.sender];
    balances[msg.sender] = 0;        // effect: update state FIRST
    (bool ok, ) = msg.sender.call{value: amount}("");  // interaction: external call LAST
    require(ok);
}

The fix is the checks-effects-interactions pattern: validate inputs (checks), update state (effects), then make external calls (interactions). An alternative defense is a reentrancy guard -- a mutex flag stored in contract storage that reverts if a function is called while already executing. OpenZeppelin's ReentrancyGuard implements this with a nonReentrant modifier.

Integer Overflow and Underflow

Before Solidity 0.8.0, arithmetic operations silently wrapped on overflow and underflow. A uint256 at its maximum value (2^256 - 1) would wrap to 0 on increment, and a zero value would wrap to 2^256 - 1 on decrement. This led to catastrophic exploits where attackers could mint tokens out of thin air or bypass balance checks. The SafeMath library by OpenZeppelin became ubiquitous as a defensive measure, wrapping every arithmetic operation in overflow checks.

Solidity 0.8.0 made checked arithmetic the default -- overflow and underflow now revert automatically. However, the unchecked block allows disabling these checks for performance-critical code where the developer can prove overflow is impossible, so the vulnerability hasn't entirely disappeared for contracts that use it carelessly.

Front-Running and MEV

Front-running exploits the transparent nature of the Ethereum mempool. When a user submits a transaction, it sits in a public pending pool before being included in a block. Miners (pre-merge) and validators/block builders (post-merge) can observe these pending transactions and insert their own transactions before, after, or between them. This is called Maximal Extractable Value (MEV).

Common MEV strategies include sandwich attacks (placing a buy before and a sell after a large DEX trade to profit from the price impact), liquidation sniping (racing to liquidate undercollateralized positions in lending protocols), and backrunning (placing a transaction immediately after a large trade to arbitrage the price displacement). The Flashbots project and MEV-Boost infrastructure have partially mitigated the worst effects by creating an orderly auction for transaction ordering, but MEV remains a fundamental challenge for blockchain applications.

Access Control Failures

Missing or incorrect access control is deceptively simple but devastatingly common. Functions that should be restricted to the contract owner or specific roles are accidentally left public, or the authorization check has a logic error. The Parity multisig wallet hack in 2017 exploited an uninitialized initWallet function that allowed anyone to take ownership of the library contract and then self-destruct it, permanently freezing over $150 million in ETH across all wallets that depended on it.

Oracle Manipulation

Many DeFi protocols depend on external price feeds (oracles) to value collateral, trigger liquidations, or calculate swap rates. If a protocol uses a spot price from a single DEX pool as its oracle, an attacker can manipulate that price within a single transaction using a flash loan -- borrowing millions of dollars, distorting the pool's price, exploiting the protocol at the manipulated price, then repaying the flash loan. Chainlink's decentralized oracle network and Uniswap V3's time-weighted average price (TWAP) oracle are defensive measures against this, but oracle manipulation remains one of the most exploited vulnerability classes in DeFi.

The DAO Hack: A Case Study

The DAO (Decentralized Autonomous Organization) was a venture capital fund governed entirely by smart contracts on Ethereum, launched in April 2016. It raised over 12 million ETH ($150 million at the time) from thousands of contributors -- the largest crowdfunding event in history at that point. Less than three months later, an attacker exploited a reentrancy vulnerability to drain 3.6 million ETH (roughly $60 million).

The DAO Reentrancy Attack The DAO Contract splitDAO() 1. check balance > 0 2. send ETH to recipient // control transfers here! 3. update balance = 0 // too late: attacker // already re-entered // at step 2 Attacker Contract receive() / fallback() triggered by ETH receipt if (dao.balance > threshold) dao.splitDAO() // re-enter! // each re-entry withdraws // same balance again because // state hasn't been updated ETH sent re-enter splitDAO() Result: 3.6M ETH drained ($60M) -- balance never decremented between recursive calls Ethereum hard-forked to reverse the theft, creating Ethereum (ETH) and Ethereum Classic (ETC)

The vulnerability was in The DAO's splitDAO function, which allowed investors to withdraw their ETH into a "child DAO." The function sent ETH to the caller before zeroing their balance -- the exact anti-pattern described above. The attacker deployed a contract whose fallback function called splitDAO again upon receiving ETH, creating a recursive withdrawal loop that drained the same balance repeatedly.

The aftermath split the Ethereum community. One faction argued for a hard fork to reverse the theft and return the funds -- "code is law, but we can change the law." The other argued that a hard fork to reverse a valid (if unintended) contract execution would undermine Ethereum's credibility as an immutable platform. The hard fork went ahead, creating two chains: Ethereum (ETH), which reversed the hack, and Ethereum Classic (ETC), which preserved the original unaltered history. The DAO hack remains the foundational case study in smart contract security and directly catalyzed the development of formal verification tools, audit practices, and defensive coding patterns used across the industry today.

Beyond the EVM: Solana Programs

While Ethereum and its EVM dominate smart contract development, Solana takes a fundamentally different architectural approach. On Solana, smart contracts are called programs, and they run in a custom virtual machine based on Berkeley Packet Filter (BPF) -- specifically, a variant called Solana BPF (SBF) that has been further evolved into sBPF. Unlike the EVM's stack machine, BPF is a register-based machine with 64-bit registers, making it significantly closer to native CPU architecture and enabling ahead-of-time (AOT) compilation for near-native execution speed.

The Accounts Model

The biggest conceptual difference from Ethereum is Solana's separation of code and state. On Ethereum, a contract's code and storage live together at the same address. On Solana, programs are stateless -- they contain only executable code. All state is stored in separate accounts, which are just data buffers owned by programs. When a user calls a program, the transaction must explicitly list every account the program will read or write. This is radically different from the EVM, where a contract can access any storage slot without declaring it in advance.

Ethereum vs. Solana: Code and State Ethereum (EVM) Contract (0xAb3..F7) Code Storage bytecode key-value slots code + state at same address Solana (SBF) Program code only (stateless) Account A Account B Account C code and state are separate tx must list all accounts upfront

This explicit account listing enables Solana's parallel transaction execution. Because the runtime knows exactly which accounts each transaction touches, it can run non-overlapping transactions in parallel across multiple CPU cores -- a fundamental advantage over the EVM, which executes transactions sequentially because any contract can access any storage slot at any time. Solana programs are most commonly written in Rust, using the Anchor framework that provides macros for account validation, deserialization, and access control.

The accounts model also means that token balances on Solana work differently. Rather than a mapping inside a token contract (as in ERC-20), each user's token balance is stored in a separate "associated token account" -- a program-derived address (PDA) computed from the user's wallet address and the token's mint address. The SPL Token program manages all fungible tokens on Solana, functioning as a shared program rather than deploying a separate contract per token like Ethereum's ERC-20 model.

Formal Verification

Given the irreversible financial consequences of smart contract bugs, the industry has increasingly turned to formal verification -- mathematically proving that a contract's code satisfies a given specification. Unlike testing, which checks specific inputs, formal verification exhaustively examines all possible execution paths and input combinations.

Several approaches exist. Symbolic execution tools like Mythril and Manticore explore all reachable code paths by treating inputs as symbolic variables rather than concrete values, searching for states that violate safety properties (like "no function can reduce the total token supply"). SMT-based verification encodes the contract's semantics as satisfiability modulo theories (SMT) constraints and uses solvers like Z3 to prove properties or find counterexamples. The Solidity compiler itself includes an experimental SMT checker that can verify assertions and detect overflow at compile time.

Model checking with tools like Certora Prover allows developers to write specifications in a dedicated language (CVL -- Certora Verification Language) that describes invariants and rules the contract must satisfy. For example, you can specify that "the sum of all balances must equal totalSupply" and the prover will exhaustively verify this holds across all possible transaction sequences. Runtime verification tools like Forta and OpenZeppelin Defender monitor deployed contracts in real-time, detecting anomalous behavior and triggering alerts or automated responses.

Formal verification is not a silver bullet. It can only prove that a contract satisfies its specification -- if the specification itself is incomplete or wrong, the contract may still be vulnerable. Writing complete formal specifications is at least as difficult as writing correct code, and verification tools can produce false positives or fail to terminate on complex contracts. In practice, most projects combine formal verification with traditional audits, fuzzing (using tools like Foundry's fuzz testing or Echidna), and bug bounty programs.

Smart Contracts and Network Infrastructure

Smart contract execution depends entirely on the underlying network infrastructure. Every function call, state change, and event emission travels as a transaction through the peer-to-peer network, is propagated to validators, included in a block, and replicated across every full node. The Ethereum network uses a gossip protocol over TCP/UDP connections between nodes, each identified by an enode URL containing the node's public key and IP address.

At the network layer, this traffic flows over the same BGP-routed internet infrastructure as everything else. Ethereum nodes run on cloud infrastructure (AWS, Google Cloud, Hetzner), on home servers, and on dedicated hardware. Their IP addresses are announced in BGP prefixes by the autonomous systems that host them. A BGP hijack targeting prefixes that host a significant number of Ethereum nodes could theoretically intercept or delay block propagation -- making the physical network layer a real (if rarely exploited) attack surface for blockchain networks.

The growing concentration of Ethereum nodes in a few cloud providers and geographic regions has raised concerns about infrastructure-level centralization. If a single hosting provider accounts for a large fraction of validators, a targeted outage or censorship action at the network layer could impact the chain's liveness. This is why client diversity, geographic distribution, and monitoring of the physical network topology all matter for blockchain resilience -- and why tools that provide visibility into network infrastructure, like a BGP looking glass, are relevant even in the smart contract context.

Try exploring the network infrastructure behind major blockchain services:

See BGP routing data in real time

Open Looking Glass
More Articles
How Blockchain Domains Work: ENS, Unstoppable Domains, and Web3 Naming
How IPFS Works: Content-Addressed Storage
How ENS (Ethereum Name Service) Works
How the Bitcoin Network Works
How Blockchain Consensus Mechanisms Work
How the Lightning Network Works