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.
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.
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:
// 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).
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.
// 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:]
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.
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.
// 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
}
}
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:
DELEGATECALL to forward every call to a separate implementation contract.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.
// 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.
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.
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.