What Are Smart Contracts?
A smart contract is a program stored on a blockchain that automatically executes when predetermined conditions are met. Think of it as a self-enforcing digital agreement — the code defines the rules, and the blockchain ensures those rules are followed without any intermediary. Once deployed, a smart contract's code is immutable and its execution is deterministic: given the same inputs, every node in the network produces the same outputs.
Why Smart Contracts Matter
- Trustless Execution: No need to trust a third party — the blockchain guarantees the contract runs as written
- Transparency: Contract code and all interactions are publicly verifiable on the blockchain
- Automation: Business logic executes automatically without manual intervention or approval
- Immutability: Once deployed, the contract code cannot be altered, preventing tampering
- Composability: Contracts can call other contracts, enabling complex systems built from simple building blocks
Smart Contract Lifecycle
The lifecycle of a smart contract follows distinct phases: writing the code in a high-level language (Solidity, Vyper), compiling it to EVM bytecode, deploying it to the blockchain via a transaction, interacting with it through transactions and calls, and potentially upgrading it through proxy patterns if designed to be upgradeable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title SimpleEscrow
/// @notice A basic escrow contract demonstrating smart contract concepts
contract SimpleEscrow {
// State variables
address public buyer;
address public seller;
address public arbiter;
uint256 public amount;
bool public isComplete;
// Events — emitted for off-chain tracking
event Deposited(address indexed buyer, uint256 amount);
event Released(address indexed seller, uint256 amount);
event Refunded(address indexed buyer, uint256 amount);
// Modifiers — reusable access control
modifier onlyArbiter() {
require(msg.sender == arbiter, "Only arbiter can call this");
_;
}
modifier notComplete() {
require(!isComplete, "Escrow already completed");
_;
}
// Constructor — runs once at deployment
constructor(address _seller, address _arbiter) payable {
buyer = msg.sender;
seller = _seller;
arbiter = _arbiter;
amount = msg.value;
emit Deposited(msg.sender, msg.value);
}
// Release funds to seller
function release() external onlyArbiter notComplete {
isComplete = true;
(bool success, ) = seller.call{value: amount}("");
require(success, "Transfer failed");
emit Released(seller, amount);
}
// Refund funds to buyer
function refund() external onlyArbiter notComplete {
isComplete = true;
(bool success, ) = buyer.call{value: amount}("");
require(success, "Transfer failed");
emit Refunded(buyer, amount);
}
// View the contract balance
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
How Smart Contracts Execute
When you send a transaction to a smart contract, every validator node on the network executes the contract code independently. They all process the same transaction against the same state and must arrive at the same result. This is how the network reaches consensus on state changes. The EVM processes the bytecode instruction by instruction, consuming gas for each operation.
Execution Flow
- User sends a transaction with calldata specifying the function and arguments
- The transaction is broadcast to the network and picked up by a validator
- The EVM loads the contract bytecode and starts execution
- Opcodes are executed sequentially, consuming gas for each operation
- State changes (storage writes) are recorded in a temporary cache
- If execution succeeds and enough gas was provided, state changes are committed
- If execution reverts (require fails, out-of-gas), all state changes are rolled back
- A transaction receipt is generated with logs (events), gas used, and status
Contract Storage and Memory
The EVM has distinct data locations that you must understand when writing smart contracts:
- Storage — persistent data stored on the blockchain forever. The most expensive to read and write. Each contract has its own storage space organized as a key-value store with 2^256 slots of 32 bytes each.
- Memory — temporary data that exists only during function execution. Much cheaper than storage. Cleared between external function calls.
- Calldata — immutable, read-only area containing function arguments for external calls. The cheapest data location because it is not copied to memory.
- Stack — the EVM's working area for small local variables and intermediate computations. Limited to 1024 items of 32 bytes each.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataLocations {
// Storage variables — persist between transactions
uint256[] public storedNumbers;
mapping(address => uint256) public balances;
// Using calldata for read-only input (cheapest)
function processData(uint256[] calldata data) external pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
}
// Using memory for temporary arrays
function createArray(uint256 size) external pure returns (uint256[] memory) {
uint256[] memory result = new uint256[](size);
for (uint256 i = 0; i < size; i++) {
result[i] = i * 2;
}
return result;
}
// Writing to storage (most expensive)
function storeNumber(uint256 num) external {
storedNumbers.push(num); // SSTORE opcode — 20,000 gas for new slot
}
}
Events and Logging
Events are a mechanism for smart contracts to communicate with the outside world. When a contract emits an event, the data is stored in the transaction's log — a special data structure that is part of the blockchain but not accessible to smart contracts themselves. Off-chain applications (like your dApp frontend) can listen for these events to react to on-chain changes in real time.
import { ethers } from 'ethers';
// Listening for contract events from a frontend
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
// Listen for Deposited events
contract.on('Deposited', (buyer, amount, event) => {
console.log(`Deposit from ${buyer}: ${ethers.formatEther(amount)} ETH`);
console.log('Block:', event.log.blockNumber);
});
// Query past events
const filter = contract.filters.Released();
const events = await contract.queryFilter(filter, -1000); // Last 1000 blocks
events.forEach((event) => {
console.log('Released:', event.args);
});
Real-World Use Cases
Smart Contracts Power
- DeFi: Lending protocols (Aave), decentralized exchanges (Uniswap), stablecoins (DAI)
- NFTs: Digital art ownership, gaming assets, membership tokens
- DAOs: Decentralized governance where token holders vote on proposals
- Supply Chain: Tracking goods from manufacturer to consumer with verifiable provenance
- Insurance: Parametric insurance that pays out automatically when conditions are met (e.g., flight delay)
- Identity: Self-sovereign identity systems where users control their own credentials
Limitations and Considerations
Smart contracts are powerful but come with important limitations. They cannot access external data directly (they need oracles). They cannot be modified after deployment (unless you use proxy patterns). Every operation costs gas, so complex logic is expensive. And because the code is public, anyone can analyze it for vulnerabilities — making security audits essential before deploying contracts that handle real value.