Smart Contracts: Code That Holds Money
A smart contract is a program stored on the blockchain that automatically executes when predefined conditions are met. No bank. No lawyer. No middleman. The code is the agreement — and it holds real money.
The Vending Machine Analogy
The best mental model for a smart contract is a vending machine:
Vending Machine:
You insert $2.00
You press B4 (Chips)
Machine automatically dispenses chips
No cashier needed
Rules are mechanical — cannot be bribed
Smart Contract:
You send 0.5 ETH
You call buyToken(tokenId)
Contract automatically transfers token to you
No intermediary needed
Rules are in code — cannot be bribed or overriddenThe critical difference: the vending machine lives in one physical location. The smart contract lives on thousands of computers simultaneously and is enforced by cryptographic consensus — not by any single party.
How Smart Contracts Differ from Web APIs
| Feature | Traditional Web API | Smart Contract |
|---|---|---|
| Code is visible to everyone? | No (private server-side code) | Yes (verified on blockchain) |
| Can hold money? | No (payments go to bank) | Yes (ETH/tokens stored in contract) |
| Can be updated? | Yes (redeploy anytime) | No (immutable unless proxied) |
| Who controls execution? | The company's servers | The Ethereum network (no one) |
| Can be censored? | Yes (company decides) | No (anyone can call it) |
| Bug discovered? | Patch and redeploy | Fork, upgrade proxy, or accept loss |
| Source of trust | Trust the company | Trust the verified code |
| Who audits it? | Company's QA team | External security auditors (public code) |
Contract Deployment — What Actually Happens
Step 1: Write Solidity source code
You write: MyToken.sol
Step 2: Compile to bytecode
solc MyToken.sol --bin --abi
→ MyToken.bin (bytecode for EVM execution)
→ MyToken.abi (interface definition for callers)
Step 3: Send a deployment transaction
from: 0xYou
to: null (no recipient = contract deployment)
data: 0x6080604052... (your bytecode + constructor args)
value: 0 (or initial ETH if constructor is payable)
Step 4: EVM creates the contract
Contract address = keccak256(rlp(deployerAddress, nonce))[12:]
→ 0xContractAddress is deterministic from deployer + nonce
Bytecode is stored permanently at that address
Step 5: Constructor runs ONCE
Any initialization code runs
Constructor is NOT stored — only the runtime bytecode remains
Step 6: Contract is live
Anyone can call it using its ABI
Address never changes
Code never changes (unless proxy pattern used)The EVM — Stack Machine Execution Model
The Ethereum Virtual Machine is a stack-based machine. Unlike register-based CPUs, the EVM operates on a stack of 256-bit words. Understanding this helps when reading exploit traces and decompiled bytecode.
Solidity: a + b * c
Compiles to EVM opcodes:
PUSH1 0x05 // Push 'c' (value 5) onto stack
PUSH1 0x03 // Push 'b' (value 3) onto stack
MUL // Pop b and c, push (b * c = 15)
PUSH1 0x02 // Push 'a' (value 2) onto stack
ADD // Pop a and (b*c), push (a + b*c = 17)
Stack state visualized:
After PUSH c: [5]
After PUSH b: [3, 5]
After MUL: [15] (3*5=15, both popped, 15 pushed)
After PUSH a: [2, 15]
After ADD: [17] (2+15=17, both popped, 17 pushed)
Key properties:
Stack depth: max 1,024 items (stack too deep = revert)
Word size: 256 bits (32 bytes) — everything is uint256 internally
Memory: byte-addressable, temporary (cleared after tx)
Storage: persistent key-value store (survives transactions)State Variables vs Local Variables — Storage Locations
contract StorageExample {
// STATE VARIABLES — stored in CONTRACT STORAGE (persistent, expensive)
uint256 public totalSupply; // Lives at storage slot 0
address public owner; // Lives at storage slot 1
mapping(address => uint256) balances; // keccak256(key, slot) for each entry
function example(uint256 input) external {
// LOCAL VARIABLE — stored in MEMORY (temporary, cheap)
uint256 localVar = input * 2; // Gone when function returns
// CALLDATA — input data, read-only, cheapest
// function params marked 'calldata' stay in the tx input buffer
// Reading storage is expensive! Cache it:
uint256 cachedSupply = totalSupply; // One SLOAD (2,100 gas)
for (uint i = 0; i < 10; i++) {
// Use cachedSupply (memory, 3 gas) not totalSupply (storage, 2,100 gas)
doSomething(cachedSupply);
}
}
}A Complete Real Example — The Simple Bank
Here is a complete contract that demonstrates all the core concepts. We will walk through it line by line:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleBank {
// ═══ STATE VARIABLES (live in contract storage) ═══
// Tracks how much ETH each address has deposited
mapping(address => uint256) private balances;
// Who controls admin functions
address public owner;
// ═══ EVENTS (cheap storage in logs, not queryable by contracts) ═══
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
// ═══ CONSTRUCTOR (runs exactly once at deployment) ═══
constructor() {
owner = msg.sender; // Deployer becomes owner
}
// ═══ DEPOSIT: Accepts ETH and records the balance ═══
function deposit() external payable {
// msg.value: ETH sent with this transaction (in wei)
require(msg.value > 0, "Must deposit something");
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
// ═══ WITHDRAW: Sends ETH back to the caller ═══
function withdraw(uint256 amount) external {
// CHECKS: Verify preconditions
require(balances[msg.sender] >= amount, "Insufficient balance");
// EFFECTS: Update state BEFORE the external call (prevents reentrancy)
balances[msg.sender] -= amount;
// INTERACTIONS: External call last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawn(msg.sender, amount);
}
// ═══ VIEW: Read-only, no state change, no gas when called externally ═══
function getBalance(address user) external view returns (uint256) {
return balances[user];
}
// ═══ FALLBACK: Catches ETH sent directly to contract address ═══
receive() external payable {
// Called when ETH is sent with no data
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
}Contract Interactions: How Contracts Call Each Other
1. EOA → Contract (standard transaction)
Alice sends tx → calls myContract.doSomething()
msg.sender = Alice, msg.value = tx.value
2. Contract → Contract (external call)
MyContract.doSomething() calls OtherContract.helper()
Inside OtherContract: msg.sender = MyContract address
3. CALL: execute other contract's code in OTHER contract's context
otherContract.call(data)
→ OtherContract's storage is modified
→ msg.sender = MyContract
4. DELEGATECALL: execute other contract's code in YOUR context
otherContract.delegatecall(data)
→ YOUR storage is modified (not OtherContract's!)
→ msg.sender = original caller (preserved!)
→ This is HOW PROXY CONTRACTS WORK
→ Also how STORAGE COLLISION vulnerabilities happendelegatecall executes external code in your contract's own storage context. If the called contract has a storage layout that differs from yours, it can overwrite critical variables. The Parity Multisig wallet hack ($150M frozen) exploited delegatecall with an unprotected initialization function. Always audit the storage layout compatibility when using delegatecall.
Upgradability Patterns — Proxies and Their Risks
Standard (non-upgradeable):
User → MyContract (logic + storage in same contract)
Cannot change logic without redeploying to new address
Proxy Pattern:
User → ProxyContract → ImplementationContract
(stores data) (has the logic)
ProxyContract: stores all state, delegates calls via delegatecall
ImplementationContract: has all the logic, no state
To "upgrade": admin changes which ImplementationContract proxy points to
User address never changes — seamless for users
Risks introduced by proxy pattern:
1. Storage collision: Proxy and Implementation must agree on storage layout
2. Initialization: Implementation's constructor doesn't run — must call init()
3. Centralization: Who controls the upgrade? Can they steal funds?
4. Selfdestruct: If implementation selfdestructs, proxy becomes useless
5. Function clashes: Proxy's functions (upgradeTo) vs Implementation's functionsSolidity Contract Structure Overview
// 1. License identifier (required)
// SPDX-License-Identifier: MIT
// 2. Compiler version
pragma solidity ^0.8.20;
// 3. Imports
import "@openzeppelin/contracts/access/Ownable.sol";
// 4. Interfaces (define external contract ABIs)
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
}
// 5. Libraries
// 6. Errors (custom errors, cheaper than strings)
error InsufficientBalance(uint256 requested, uint256 available);
// 7. Main contract
contract MyContract is Ownable {
// 7a. Type declarations
enum Status { Active, Paused, Ended }
// 7b. State variables
uint256 public constant MAX_SUPPLY = 1_000_000;
uint256 public immutable deployedAt;
Status public status;
// 7c. Events
event StatusChanged(Status newStatus);
// 7d. Modifiers
modifier whenActive() {
require(status == Status.Active);
_;
}
// 7e. Constructor
constructor() Ownable(msg.sender) {
deployedAt = block.timestamp;
status = Status.Active;
}
// 7f. Functions (external, public, internal, private)
receive() external payable {}
fallback() external payable {}
}Common Beginner Mistakes
If you want your function to receive ETH, mark it payable. Without it, any ETH sent with the call will cause the transaction to revert. Many tokens have been lost this way — users trying to send ETH to a non-payable contract address (not a function, but the contract directly) will have their transactions fail.
Functions default to public in older Solidity (pre-0.5). In 0.8.x you must specify visibility — but beginner errors still happen. An admin function accidentally left public instead of onlyOwner means anyone can call it. The $34M Nomad bridge hack had a related root cause.
Functions that change ownership, pause the contract, upgrade the implementation, or withdraw funds MUST be access-controlled. The most common audit finding: a function that should be onlyOwner but has no modifier at all.
Traditional software security cares about data leaks and service outages. Smart contract security also cares about direct financial theft. Every line of Solidity code that touches ETH or ERC-20 tokens is potentially an attack surface. A security-conscious developer asks of every function: "What is the worst thing an adversarial caller could do here?" Before you write production smart contracts, have them audited by professionals.
Summary / Key Takeaways
| Concept | Key Fact | Security Implication |
|---|---|---|
| Smart Contract | Immutable code on blockchain | Bugs are permanent — audit before deploy |
| Deployment | One-time bytecode storage at address | Address fixed — cannot patch code |
| Storage | Persistent key-value per contract | Expensive to write, public to read |
| constructor() | Runs once at deploy, not stored | Init bugs are a common attack vector |
| delegatecall | Runs code in caller's storage context | Storage collision can wipe state |
| Proxy pattern | Upgradeable contracts via delegatecall | Adds centralization and storage risks |
| payable | Required to receive ETH | Missing = ETH send reverts |
| receive() | Handles plain ETH sends | Can be exploited for unexpected ETH |