Ethereum Smart Contracts
What They Are, Why They Exist, and How They Run

Why Smart Contracts?

In the previous posts we saw how a wallet signs a transaction and how that transaction travels through the network until it reaches finality. But so far, the transactions we described were simple value transfers: Alice sends 1 ETH to Bob. Ethereum would not be very different from Bitcoin if that were all it could do. The real power of Ethereum comes from the ability to deploy and execute arbitrary programs on the blockchain. These programs are called smart contracts.

A smart contract is a piece of code that lives at a specific address on the Ethereum blockchain. Once deployed, its code cannot be modified. It has its own storage, its own balance, and it executes automatically whenever someone sends it a transaction with the right instructions. There is no middleman, no server, no company that can decide to shut it down. The code runs exactly as written, enforced by every node in the network.

Two Types of Accounts

Ethereum has two kinds of accounts, and understanding the difference is essential. An Externally Owned Account (EOA) is what you get when you create a wallet. It is controlled by a private key, it can initiate transactions, and it holds an ETH balance. That is all it does. A Contract Account, on the other hand, has no private key. It cannot initiate transactions on its own. What it has instead is code (compiled bytecode that runs on the EVM) and persistent storage (a key-value store that survives between transactions). A contract also holds an ETH balance, and it can emit events that external applications can listen to.

The key rule: only an EOA can start a transaction. A contract can only execute in response to a transaction (or a message call from another contract). This means every on-chain action ultimately traces back to a human (or a bot) with a private key signing a transaction.

EOA Externally Owned Account 0x71C7656E... ✓ Has a private key ✓ Can initiate transactions ✓ Only holds a balance ✗ No code, no storage { } Contract Smart Contract Account 0xdAC17F958D... ✗ No private key ✗ Cannot initiate transactions ✓ Has code (bytecode) ✓ Has persistent storage vs

How a Contract Call Works

When you want to interact with a smart contract, you send a transaction with two important fields. The to field is set to the contract's address, and the data field contains the function selector (the first 4 bytes of the Keccak-256 hash of the function signature) followed by the ABI-encoded arguments. ABI stands for Application Binary Interface. It is the standard encoding format that the EVM uses to serialize function calls and their parameters into raw bytes. Every argument is padded to 32 bytes, and the function selector tells the contract which function to execute. For example, calling transfer(address,uint256) on an ERC-20 token contract would look like this:

ABI Encoding
// Function signature
transfer(address,uint256)

// Keccak-256 hash of the signature
keccak256("transfer(address,uint256)") = 0xa9059cbb...

// The "data" field in the transaction
0xa9059cbb                                       // function selector (4 bytes)
000000000000000000000000recipientAddress         // arg 1: address (32 bytes)
0000000000000000000000000000000000000000000000000000000000000064  // arg 2: amount (32 bytes)

The Ethereum node receives this transaction, loads the contract's bytecode from state, and feeds it to the Ethereum Virtual Machine (EVM). The EVM is a stack-based execution engine that every node runs identically. It reads the bytecode instruction by instruction, consumes gas for each operation, and can read/write to the contract's storage, transfer ETH, call other contracts, or emit event logs. If the transaction runs out of gas or hits an invalid operation, the entire execution reverts and the state changes are discarded (but the gas is still consumed).

You EOA sends a transaction to: contract address data: 0xa9059cbb... (selector + args) Smart Contract Bytecode (EVM) compiled from Solidity / Vyper Storage balances[addr] = 100 owner = 0x71C7... persistent state Balance 3.5 ETH can hold funds Events emit Transfer(...) { } can call other contracts

How a Contract Gets Deployed

Deploying a smart contract is itself a transaction, but with a twist: the to field is left empty. This tells the EVM that the data field contains bytecode that should be executed as initialization code. The init code typically runs the constructor logic (setting the owner, initial supply, etc.) and then returns the runtime bytecode, which is the actual code that will be stored on-chain at the new contract address. The address is deterministically computed from the deployer's address and their nonce, so you can predict it before the transaction is even mined.

Pseudocode
// Contract address is deterministic
contractAddress = keccak256(rlp([senderAddress, nonce]))[12:]

// Or with CREATE2 (salt-based, even more predictable)
contractAddress = keccak256(0xff + senderAddress + salt + keccak256(bytecode))[12:]

Can a Contract Be Changed or Removed?

Once a contract is deployed, its bytecode is immutable. No one can edit a single instruction, not even the original deployer. This is by design: users interact with a contract because they can read and verify its code, and immutability guarantees that the rules will not change underneath them.

What About SELFDESTRUCT?

Ethereum originally had a SELFDESTRUCT opcode that allowed a contract to delete its own code and storage from the chain, sending any remaining ETH to a specified address. This was the only way to "remove" a contract. However, since the Dencun upgrade (March 2024, EIP-6780), SELFDESTRUCT no longer deletes code or storage. It only transfers the contract's ETH balance. The bytecode and state remain on-chain. The only exception is if SELFDESTRUCT is called within the same transaction that created the contract. In practice, this means deployed contracts now live on-chain permanently.

Solidity
// Post EIP-6780 (Dencun, March 2024)
// SELFDESTRUCT only removes code if called in the SAME transaction as creation

contract Factory {
    function createAndDestroy() external {
        // Deploy and destroy in one transaction — code IS removed
        Temporary temp = new Temporary();
        temp.destroy(payable(msg.sender));
    }
}

contract Temporary {
    function destroy(address payable recipient) external {
        selfdestruct(recipient);
        // Works: same tx as creation → code and storage deleted
    }
}

contract Permanent {
    function tryDestroy(address payable recipient) external {
        selfdestruct(recipient);
        // Called in a later transaction → only sends ETH
        // Code and storage remain on-chain permanently
    }
}

Upgradeable Contracts (Proxy Pattern)

If contract code is immutable, how do projects ship bug fixes or new features? The answer is the proxy pattern. Instead of deploying one contract, you deploy two:

When the team wants to upgrade, they deploy a new implementation contract and update the proxy to point to it. The proxy's address, storage, and balance all stay the same, so users and other contracts do not need to change anything. But the logic that runs when they call it is now different.

The Proxy Pattern User (EOA) tx Proxy Contract fixed address — never changes Storage (lives here) balances[addr] = 100 owner = 0x71C7... totalSupply = 1000000 DELEGATECALL DELEGATECALL Impl V1 (old) Impl V2 (new) updated logic new! Admin / Multisig controls upgrade key upgrade(newImpl) same address, same storage different logic
Solidity
// Simplified proxy mechanism (DELEGATECALL)

// User calls proxy at 0xProxy
// Proxy does:
fallback() external payable {
    address impl = getImplementation();  // reads from storage
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}

It is important to understand that no bytecode is being modified. Each contract's code remains immutable. The proxy simply changes which implementation it delegates to. This is a trade-off: upgradeability gives developers flexibility, but it also means users must trust whoever controls the proxy's admin key. Many projects mitigate this by placing the admin key behind a multisig wallet or a timelock that gives users time to react before an upgrade takes effect.

Why This Matters

Smart contracts are what make Ethereum a programmable blockchain. Without them, you could only send ETH from one address to another. With them, you can build:

The critical insight is composability. Because every contract lives at a public address and exposes a known interface, any contract can call any other contract. This means developers can build on top of existing protocols without asking permission, creating a permissionless ecosystem where financial primitives snap together like building blocks.

Composability — Contracts Calling Contracts { } Your Contract 0xYour... Uniswap (DEX) swap(tokenA, tokenB, amt) Aave (Lending) borrow(usdc, amount) Chainlink (Oracle) getLatestPrice() WETH Token DAI Token USDC Token ETH/USD Feed no permission needed just call the address

To see how transactions reach these contracts in the first place, check out Sending Transactions on Ethereum. To understand how the transaction gets signed before it is sent, see Ethereum Wallets & Transaction Signing.