4: Introduction to Smart Contracts
1. What Are Smart Contracts?
The Paradox: Neither Smart Nor Contracts
Despite their name, smart contracts are neither smart nor contracts in the traditional sense. This paradoxical statement helps us understand what they truly are:
- Not "smart": They are simple programs that execute predetermined logic without artificial intelligence or learning capabilities
- Not "contracts": They are not legal documents but rather self-executing code stored on a blockchain
Definition and Core Concept
A smart contract is a user-defined program that runs on top of a blockchain. More formally, smart contracts are:
Autonomous agents that live inside the blockchain execution environment, always executing specific code when triggered by a message or transaction, with direct control over their own cryptocurrency balance and persistent storage.
Think of smart contracts as vending machines: you insert coins (cryptocurrency), select an option (call a function), and receive a predetermined output (tokens, services, or state changes) without requiring a trusted intermediary.
Historical Context: Nick Szabo's Vision (1994)
Nick Szabo conceptualized smart contracts long before blockchain technology existed:
"A smart contract is a computerized transaction protocol that executes the terms of a contract. The general objectives are to satisfy common contractual conditions (such as payment terms, liens, confidentiality, and even enforcement), minimize exceptions both malicious and accidental, and minimize the need for trusted intermediaries."
The key innovation that Satoshi Nakamoto's Bitcoin introduced in 2009 was the underlying blockchain technology as a tool for distributed consensus. While Bitcoin primarily focused on currency, it demonstrated that blockchain could support more complex applications.
3. Understanding Smart Contracts from a Developer's Perspective
The Programming Model
Smart contracts operate on a simple yet powerful model:
Contract Classes and Objects
- Contract Class: Defines the program code and storage variables
- Contract Object: An instance of the class living on the blockchain at a specific address
Core Components
Storage Fields Persistent variables stored in the contract's state. These survive across function calls and transactions.
balances: mapping(address => uint256)
owner: address
totalSupply: uint256
Functions/Methods Code that can be invoked to read or update the contract state.
transfer(address to, uint256 amount)
getBalance(address account) returns (uint256)
Access Control
Use require() statements to enforce authorization rules. Transactions that fail these checks are reverted.
require(msg.sender == owner, "Only owner can call this");
Example: Domain Name Registry
Let's examine a simple domain name registry to understand the contract model:
Storage Structure
domains: mapping(string => address)
Registration Function
function registerDomain(string memory name) public {
require(domains[name] == address(0), "Domain already registered");
domains[name] = msg.sender;
}
Lookup Function
function lookupDomain(string memory name) public view returns (address) {
return domains[name];
}
Account Model in Ethereum
Ethereum uses an account model rather than Bitcoin's UTXO model. There are two types of accounts:
Externally Owned Accounts (EOAs)
- Controlled by private keys
- Have an ether balance
- Can send transactions
- Have no code
Contract Accounts
- Controlled by their contract code
- Have an ether balance
- Have contract code and storage
- Execute code when receiving messages/transactions
Each Ethereum account contains:
- Nonce: Transaction counter (prevents replay attacks)
- Balance: Current ether balance
- Contract Code: For contract accounts only
- Storage: Key-value store for persistent data
4. Ethereum Programming Basics
Introduction to Solidity
Solidity is a high-level, statically-typed programming language designed for writing smart contracts on Ethereum. It compiles to Ethereum Virtual Machine (EVM) bytecode.
Solidity Source Code → Solidity Compiler → EVM Bytecode → Deployed to Blockchain
Data Types
Integers
uint256 totalSupply; // Unsigned 256-bit integer (default)
uint8 decimals; // Unsigned 8-bit integer
int256 balance; // Signed 256-bit integer
Note: Solidity is statically typed like Java, C, or Rust, unlike Python or JavaScript.
Addresses
address owner; // 20-byte Ethereum address
address payable recipient; // Can receive Ether
Mappings (Hash Tables)
mapping(address => uint256) public balances;
mapping(string => address) private domains;
Important:
- Every key initially maps to zero
- No built-in way to query length or iterate over non-zero elements
- Must maintain separate data structures for enumeration
Arrays
uint256[10] fixedArray; // Fixed size
uint256[] dynamicArray; // Dynamic size (more expensive)
address[] public voters; // Storage array (persists)
Strings and Bytes
bytes32 hash; // Fixed size (returned by hash functions)
bytes memory data; // Dynamic byte array
string memory name; // UTF-8 string
Function Structure
function transfer(address to, uint256 amount)
public // Visibility modifier
returns (bool success) // Return type
{
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
Function Components
- Name:
transfer - Arguments:
(address to, uint256 amount) - Visibility:
public,private,internal,external - Mutability:
pure,view,payable - Returns:
returns (bool success)
Visibility Modifiers
For Functions
- public: Callable by anyone, internally or externally
- private: Only callable within the contract
- internal: Callable within the contract and derived contracts
- external: Only callable from outside the contract
For Variables
- public: Automatically creates a getter function
- private: Only accessible within the contract
- internal: Accessible in the contract and derived contracts
Security Note: private variables are not truly secret! All blockchain data is public. The private keyword only restricts access from other contracts.
Mutability Modifiers
// pure: Does not read or modify state
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// view: Reads state but does not modify it
function getBalance(address account) public view returns (uint) {
return balances[account];
}
// payable: Can receive Ether
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// (no modifier): Can read and modify state
function transfer(address to, uint amount) public {
balances[msg.sender] -= amount;
balances[to] += amount;
}
Constructors
Constructors are invoked once when the contract is deployed:
constructor(string memory name, string memory symbol) {
tokenName = name;
tokenSymbol = symbol;
owner = msg.sender;
}
Working with Ether
Ether is the native cryptocurrency of Ethereum. Contracts can hold and transfer Ether:
// Receiving Ether
function deposit() public payable {
// msg.value contains the Ether sent
balance[msg.sender] += msg.value;
}
// Sending Ether
function withdraw(uint amount) public {
require(balance[msg.sender] >= amount);
balance[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// Check contract's Ether balance
uint contractBalance = address(this).balance;
Ether Units:
- 1 Ether = 10^18 Wei
- 1 Gwei = 10^9 Wei (commonly used for gas prices)
- 1 Finney = 10^15 Wei
- 1 Szabo = 10^12 Wei
Blockchain Metadata
Contracts can access current blockchain state:
block.timestamp // Current block timestamp (Unix time)
block.number // Current block number
block.difficulty // Current block difficulty
msg.sender // Address that called the function
msg.value // Amount of Ether sent (in Wei)
tx.origin // Original sender of the transaction
Security Warning: Using block.timestamp for critical logic can be manipulated by miners within ~15 minutes. Never use it as a source of randomness.
Events
Events allow contracts to log information that external applications can listen to:
// Declare the event
event Transfer(address indexed from, address indexed to, uint256 value);
// Emit the event
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
Use Cases:
- Notifying user interfaces of state changes
- Creating searchable transaction logs
- Cheaper than storing data in contract storage
Interacting with Other Contracts
Contracts can call functions of other contracts:
// Define interface
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// Call another contract
function sendTokens(address tokenAddress, address recipient, uint256 amount) public {
IERC20 token = IERC20(tokenAddress);
require(token.transfer(recipient, amount), "Transfer failed");
}
5. Case Study: The Dutch Auction
Understanding Dutch Auctions
A Dutch Auction is a price discovery mechanism where:
- The seller sets a high initial "Buy Now" price
- The price automatically decreases over time
- The first buyer to accept the current price wins
- The auction ends immediately upon purchase
This mechanism was famously used by CryptoKitties, one of the first major NFT projects on Ethereum.
Why Dutch Auctions?
Advantages:
- Efficiency: Auction ends as soon as a buyer is found
- Price Discovery: Market determines fair value
- No Bidding Wars: Buyers don't compete with each other
- Gas Efficiency: Only one transaction needed (compared to English auctions with multiple bids)
Implementation in Solidity
pragma solidity ^0.8.0;
contract DutchAuction {
address payable public seller;
uint256 public startingPrice;
uint256 public priceDecrement;
uint256 public startTime;
uint256 public decrementInterval; // Time between price drops
bool public ended;
constructor(
uint256 _startingPrice,
uint256 _priceDecrement,
uint256 _decrementInterval
) {
seller = payable(msg.sender);
startingPrice = _startingPrice;
priceDecrement = _priceDecrement;
decrementInterval = _decrementInterval;
startTime = block.timestamp;
ended = false;
}
// Calculate current price based on time elapsed
function getCurrentPrice() public view returns (uint256) {
if (ended) return 0;
uint256 timeElapsed = block.timestamp - startTime;
uint256 periods = timeElapsed / decrementInterval;
uint256 discount = periods * priceDecrement;
if (discount >= startingPrice) {
return 0; // Minimum price reached
}
return startingPrice - discount;
}
// Buy at current price
function buy() public payable {
require(!ended, "Auction has ended");
uint256 currentPrice = getCurrentPrice();
require(msg.value >= currentPrice, "Payment insufficient");
// End auction
ended = true;
// Transfer payment to seller
seller.transfer(currentPrice);
// Refund excess payment
if (msg.value > currentPrice) {
payable(msg.sender).transfer(msg.value - currentPrice);
}
// Transfer asset to buyer (in real implementation)
// This would interact with an NFT contract
}
}
Key Design Patterns
Time-Based State Changes
uint256 timeElapsed = block.timestamp - startTime;
uint256 periods = timeElapsed / decrementInterval;
The price automatically decreases based on elapsed time without requiring external updates.
Safe Ether Handling
// Accept payment
msg.value >= currentPrice
// Transfer to seller
seller.transfer(currentPrice);
// Refund excess
payable(msg.sender).transfer(msg.value - currentPrice);
State Management
bool public ended;
require(!ended, "Auction has ended");
ended = true;
Prevents double-spending by tracking auction state.
Potential Improvements
Issues with the Basic Implementation:
- No Asset Transfer: Real auction needs to interact with NFT contract
- No Cancellation: Seller cannot cancel auction
- Frontrunning: Miners can see pending transactions and buy before others
- No Minimum Price: Auction could reach zero
Enhanced Version:
contract EnhancedDutchAuction {
IERC721 public nftContract;
uint256 public tokenId;
uint256 public minimumPrice;
// ... previous code ...
function getCurrentPrice() public view returns (uint256) {
// ... price calculation ...
// Enforce minimum price
if (discount >= startingPrice - minimumPrice) {
return minimumPrice;
}
return startingPrice - discount;
}
function buy() public payable {
// ... previous checks ...
// Transfer NFT to buyer
nftContract.transferFrom(address(this), msg.sender, tokenId);
// ... payment handling ...
}
function cancelAuction() public {
require(msg.sender == seller, "Only seller can cancel");
require(!ended, "Already ended");
ended = true;
nftContract.transferFrom(address(this), seller, tokenId);
}
}
6. Smart Contracts vs Legal Contracts
Traditional Legal Contracts: The Four Elements
Legal contracts require:
- Offer and Acceptance
- One party proposes terms
- Another party accepts those terms
- Consideration
- Something of value exchanged between parties
- "Quid pro quo" - something for something
- Mutual Agreement (Mutuality)
- Both parties understand and agree to terms
- Meeting of the minds
- Legality and Capacity
- Contract purpose must be legal
- Parties must have legal capacity to contract
Smart Contracts Through Legal Lens
Let's analyze a token sale smart contract:
contract TokenSale {
mapping(address => uint256) public tokens;
uint256 public pricePerToken = 0.01 ether;
function buyTokens() public payable {
require(msg.value > 0, "Must send Ether");
uint256 tokenAmount = msg.value / pricePerToken;
tokens[msg.sender] += tokenAmount;
}
}
Mapping to Legal Elements
Offer and Acceptance:
- Offer: Contract deployed with terms (price, availability)
- Acceptance: User signs and submits transaction
- Digital signature proves intent
Consideration:
- Buyer provides: Ether (cryptocurrency)
- Seller provides: Tokens (digital assets)
- Exchange is atomic and automatic
Mutuality:
- Smart contract code is typically published
- Ethereum explorers allow anyone to inspect code
- Users can verify contract logic before interacting
Capacity and Legality:
- Execution automatically carries out transfer
- No trust in counterparty required
- However: code bugs can violate intent
- Legal status varies by jurisdiction
Smart Contracts Through Szabo's Lens
Nick Szabo's original vision emphasized:
Minimizing Need for Trusted Intermediaries
Traditional contracts often require:
- Escrow agents
- Payment processors
- Courts for enforcement
Smart contracts eliminate intermediaries through:
- Code-based enforcement
- Cryptographic proof
- Blockchain immutability
Minimizing Exceptions
Malicious Exceptions: Prevented by:
- Transparent code execution
- Immutable deployment
- Cryptographic security
Accidental Exceptions: Reduced by:
- Deterministic execution
- Formal verification (optional)
- Automated testing
Reducing Transaction Costs
- Fraud Loss: Cryptographic guarantees prevent many fraud types
- Arbitration: Code eliminates disputes about execution
- Enforcement: Automatic and costless
Limitations and Gaps
The Oracle Problem
Smart contracts cannot directly access external data:
// This doesn't exist in reality:
uint price = ethereum.getPrice("ETH/USD");
Solution requires oracles - trusted external data sources, reintroducing trust.
Code vs Intent
- Code executes exactly as written, not as intended
- Bugs can have catastrophic consequences
- No "rollback" button without governance
Famous Example: The DAO hack (2016)
- Bug in DAO smart contract
- Attacker drained 3.6 million ETH
- Required Ethereum hard fork to resolve
Legal Recognition
- Smart contracts may not be legally binding in all jurisdictions
- Jurisdiction for disputes unclear
- Enforcement beyond blockchain difficult
Immutability vs Flexibility
Traditional contracts can be:
- Amended by mutual agreement
- Interpreted by courts
- Enforced selectively
Smart contracts are:
- Immutable once deployed (usually)
- Literal in execution
- Cannot adapt to unforeseen circumstances
Hybrid Approaches
Ricardian Contracts: Combine:
- Human-readable legal prose
- Machine-readable code
- Cryptographic signatures
Legal Wrappers: Some jurisdictions now recognize:
- Smart contracts as legally binding
- Legal agreements that reference smart contract addresses
- Multi-sig contracts for governance
7. Fungible and Non-Fungible Tokens
Understanding Token Standards
Tokens are smart contracts that function as digital assets. Ethereum has standardized interfaces that enable interoperability across the ecosystem.
Fungible vs Non-Fungible
Fungible Tokens (ERC-20):
- Interchangeable units
- Each unit identical to every other
- Can be summed and divided
- Examples: currencies, commodities, shares
Non-Fungible Tokens (ERC-721):
- Unique items with distinct IDs
- Each token has individual attributes
- Cannot be subdivided
- Examples: art, collectibles, real estate deeds
ERC-20: Fungible Token Standard
ERC-20 defines six core functions that every fungible token should implement:
Basic Functionality
// Get total token supply
function totalSupply() public view returns (uint256);
// Get balance of an account
function balanceOf(address account) public view returns (uint256);
// Transfer tokens to another address
function transfer(address to, uint256 amount) public returns (bool);
Approval Mechanism
// Approve another address to spend tokens on your behalf
function approve(address spender, uint256 amount) public returns (bool);
// Check how much a spender is allowed to spend
function allowance(address owner, address spender) public view returns (uint256);
// Transfer tokens from another address (requires approval)
function transferFrom(address from, address to, uint256 amount) public returns (bool);
Complete ERC-20 Implementation
pragma solidity ^0.8.0;
contract ERC20Token {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10**uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Allowance exceeded");
balanceOf[from] -= amount;
balanceOf[to] += amount;
allowance[from][msg.sender] -= amount;
emit Transfer(from, to, amount);
return true;
}
}
The Approval Pattern
The approval mechanism enables powerful composability:
Example: Decentralized Exchange
// Step 1: User approves DEX to spend tokens
token.approve(dexAddress, 1000);
// Step 2: DEX can execute trades on user's behalf
dex.trade(tokenA, tokenB, amount);
This pattern allows:
- Automated trading
- Limit orders
- Complex DeFi protocols
- Without giving full custody
ERC-721: Non-Fungible Token Standard
ERC-721 defines unique tokens with individual identities:
interface IERC721 {
// Get owner of a specific token
function ownerOf(uint256 tokenId) external view returns (address);
// Transfer a specific token
function transferFrom(address from, address to, uint256 tokenId) external;
// Approve someone to transfer a specific token
function approve(address to, uint256 tokenId) external;
// Get approved address for a token
function getApproved(uint256 tokenId) external view returns (address);
// Count of tokens owned by an address
function balanceOf(address owner) external view returns (uint256);
}
Key Differences from ERC-20
| Feature | ERC-20 | ERC-721 |
|---|---|---|
| Identity | Fungible (any token = any other) | Unique (each has tokenId) |
| Transfer | Amount-based | ID-based |
| Approval | Amount for spender | Specific token or all tokens |
| Divisibility | Can be fractional | Indivisible |
Token Metadata
NFTs often include additional metadata:
function tokenURI(uint256 tokenId) public view returns (string memory);
This typically returns a URL to JSON with:
{
"name": "CryptoKitty #1234",
"description": "A cute digital cat",
"image": "ipfs://QmX...",
"attributes": [
{"trait_type": "Color", "value": "Orange"},
{"trait_type": "Generation", "value": 5}
]
}
Why Standardization Matters
Using standard interfaces enables:
- Wallet Support: MetaMask, hardware wallets automatically support any ERC-20/721
- Exchange Listings: DEXs can list any standard token
- DeFi Composability: Protocols can integrate any token without custom code
- User Confidence: Standard interfaces are well-audited
- Network Effects: More tools support standard tokens
8. Gas and Transaction Costs
Understanding Gas
Gas is the unit measuring computational work on Ethereum. Every operation costs gas:
Transaction Fee = Gas Used × Gas Price
- Gas Used: Number of computational steps
- Gas Price: Price per gas unit (in Gwei)
Why Gas Exists
Preventing Denial-of-Service
Without gas:
// Infinite loop that could shut down the network
function attack() public {
while(true) {
// Do nothing forever
}
}
With gas:
- Loop consumes gas for each iteration
- Transaction fails when gas runs out
- Attacker still pays for computation used
Aligning Incentives
- Miners prioritize higher gas price transactions
- Users pay proportionally to resources consumed
- Network remains economically sustainable
Gas Costs by Operation
Different operations cost different amounts of gas:
| Operation | Gas Cost | Reason |
|---|---|---|
| Addition | 3 | Simple arithmetic |
| Multiplication | 5 | More complex |
| Storage write | 20,000 | Permanent storage is expensive |
| Storage read | 200 | Reading state |
| Contract creation | 32,000 | Base cost plus code |
| Transaction | 21,000 | Base fee for any transaction |
| Log (event) | 375 + 375/topic | Cheaper than storage |
Example: Analyzing Gas Usage
function expensiveFunction() public {
uint256 x = 5; // SSTORE: ~20,000 gas
uint256 y = x + 10; // ADD: 3 gas
uint256 z = x * y; // MUL: 5 gas
for(uint i = 0; i < 10; i++) {
storage[i] = i; // 10 × SSTORE: ~200,000 gas
}
}
Total: ~220,000 gas
At 50 Gwei gas price:
- Cost: 220,000 × 50 = 11,000,000 Gwei = 0.011 ETH
- At $2,000/ETH: ~$22 transaction cost
Gas Limit and Gas Price
Every transaction specifies:
Transaction {
gasLimit: 250000, // Maximum gas willing to consume
gasPrice: 50 gwei, // Price per gas unit
...
}
What Happens During Execution?
Scenario 1: Normal Execution
- Gas limit: 250,000
- Gas used: 220,000
- Refund: 30,000 gas
- Cost: 220,000 × 50 Gwei
Scenario 2: Out of Gas
- Gas limit: 200,000
- Gas used: 200,000 (then stopped)
- Changes: Reverted
- Cost: Still pay 200,000 × 50 Gwei
Key Point: You pay for gas consumed even if transaction fails!
Gas Optimization Strategies
1. Use Appropriate Data Types
// Bad: Wastes storage
struct User {
uint256 age; // 256 bits for a number 0-100
uint256 balance;
}
// Good: Pack variables
struct User {
uint8 age; // 8 bits sufficient for age
uint248 balance; // Fits in same storage slot
}
2. Minimize Storage Operations
// Bad: Multiple storage writes
function badIncrement() public {
counter = counter + 1;
counter = counter + 1;
counter = counter + 1;
}
// Good: Single storage write
function goodIncrement() public {
uint256 temp = counter + 3;
counter = temp;
}
3. Use Events Instead of Storage
// Bad: Store entire history
uint256[] public history;
function record(uint256 value) public {
history.push(value); // Expensive!
}
// Good: Emit events
event Recorded(uint256 value);
function record(uint256 value) public {
emit Recorded(value); // Much cheaper!
}
4. Short-Circuit Evaluation
// Put cheaper checks first
require(msg.sender == owner, "Not owner"); // Cheap
require(balanceOf[owner] > 1000 ether, "Low balance"); // Expensive
5. Use view and pure
// Doesn't consume gas when called externally
function getBalance(address account) public view returns (uint256) {
return balances[account];
}
EIP-1559: Modern Gas Pricing
Since London hard fork (August 2021), Ethereum uses EIP-1559:
Transaction Fee = (Base Fee + Priority Fee) × Gas Used
- Base Fee: Algorithmically determined, burned
- Priority Fee: Tip to miners, set by user
- Max Fee: Maximum total fee willing to pay
Benefits:
- More predictable fees
- Burns ETH (deflationary pressure)
- Better user experience
Block Gas Limit
Each block has a maximum gas limit (~30 million gas):
Block Gas Limit ≈ 30,000,000 gas
Average Transaction ≈ 21,000-200,000 gas
Transactions per Block ≈ 150-1,400
This limit:
- Prevents blocks from being too large
- Ensures reasonable validation times
- Adapts to network conditions
9. Security Considerations and Best Practices
Common Vulnerabilities
Reentrancy Attacks
The most famous smart contract vulnerability:
Vulnerable Code:
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
// DANGER: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0;
}
}
Attack:
contract Attacker {
VulnerableBank bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
function attack() external payable {
bank.deposit{value: msg.value}();
bank.withdraw();
}
// Reenters withdraw before balance is zeroed
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
}
Fix: Checks-Effects-Interactions Pattern:
function withdraw() public {
uint256 amount = balances[msg.sender];
// Update state BEFORE external call
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
Or use ReentrancyGuard:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeBank is ReentrancyGuard {
function withdraw() public nonReentrant {
// ... withdrawal logic ...
}
}
Integer Overflow/Underflow
Before Solidity 0.8.0:
uint8 max = 255;
max = max + 1; // Wraps to 0!
uint8 min = 0;
min = min - 1; // Wraps to 255!
Modern Solidity (≥0.8.0): Automatic overflow checking
uint8 max = 255;
max = max + 1; // Reverts with error!
For older versions, use SafeMath:
using SafeMath for uint256;
uint256 result = a.add(b); // Safe addition
Access Control Failures
Vulnerable:
function withdraw() public {
// Anyone can withdraw!
payable(owner).transfer(address(this).balance);
}
Fixed:
function withdraw() public {
require(msg.sender == owner, "Not authorized");
payable(owner).transfer(address(this).balance);
}
Better: Use OpenZeppelin's Ownable:
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
function withdraw() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
Front-Running
Transactions in mempool are visible before execution:
// Vulnerable to front-running
function buyTokens() public payable {
uint256 price = getOraclePrice(); // Attacker can see this
uint256 amount = msg.value / price;
tokens[msg.sender] += amount;
}
Mitigations:
- Commit-reveal schemes
- Batch auctions
- Private mempools (Flashbots)
- Minimum execution delays
Timestamp Manipulation
// BAD: Miners can manipulate timestamp ~15 minutes
function expireAuction() public {
require(block.timestamp > auctionEnd, "Not ended");
// ... finalize auction ...
}
Better:
- Use block numbers instead
- Accept ~15 minute uncertainty
- Use VRF for randomness needs
Best Practices
1. Follow Checks-Effects-Interactions Pattern
function transfer(address to, uint256 amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
require(to != address(0), "Invalid recipient");
// 2. Effects (update state)
balances[msg.sender] -= amount;
balances[to] += amount;
// 3. Interactions (external calls)
emit Transfer(msg.sender, to, amount);
if (isContract(to)) {
// Call external contract last
}
}
2. Use Well-Audited Libraries
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is ERC20, Ownable, Pausable {
// Leverage battle-tested code
}
3. Implement Circuit Breakers
contract SafeContract is Pausable {
function criticalFunction() public whenNotPaused {
// Can be paused in emergency
}
function emergencyPause() public onlyOwner {
_pause();
}
}
4. Rate Limiting
mapping(address => uint256) public lastWithdrawal;
uint256 public constant WITHDRAWAL_DELAY = 1 days;
function withdraw(uint256 amount) public {
require(
block.timestamp >= lastWithdrawal[msg.sender] + WITHDRAWAL_DELAY,
"Withdrawal on cooldown"
);
lastWithdrawal[msg.sender] = block.timestamp;
// ... proceed with withdrawal ...
}
5. Pull Over Push Payments
Bad (Push):
function distribute() public {
for (uint i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amounts[i]); // Can fail!
}
}
Good (Pull):
mapping(address => uint256) public pendingWithdrawals;
function recordPayout(address recipient, uint256 amount) internal {
pendingWithdrawals[recipient] += amount;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
6. Comprehensive Testing
// test/MyContract.test.js
describe("MyContract", function() {
it("Should transfer tokens correctly", async function() {
// Test normal operation
});
it("Should revert on insufficient balance", async function() {
// Test failure cases
});
it("Should handle reentrancy attacks", async function() {
// Test security
});
});
7. Formal Verification
For critical contracts, use formal verification tools:
- Certora: Prove properties mathematically
- K Framework: Specify and verify semantics
- SMTChecker: Built into Solidity compiler
The Security Mindset
Assume Everything Can Fail:
- External calls can revert
- Users can be malicious
- Miners can manipulate transactions
- Code will have bugs
Defense in Depth:
contract SecureVault {
// Multiple layers of security
modifier onlyOwner() { require(msg.sender == owner); _; }
modifier whenNotPaused() { require(!paused); _; }
modifier nonReentrant() { require(!locked); locked = true; _; locked = false; }
modifier validAmount(uint256 amount) { require(amount > 0 && amount <= maxAmount); _; }
function withdraw(uint256 amount)
public
onlyOwner
whenNotPaused
nonReentrant
validAmount(amount)
{
// Multiple checks before executing
}
}
10. Conclusion and Looking Forward
What We've Learned
In this chapter, we've covered the fundamental concepts of smart contracts:
-
Core Concepts: Smart contracts are autonomous programs on blockchains that execute predefined logic without intermediaries
-
Evolution: From Bitcoin's limited scripting to Ethereum's Turing-complete platform
-
Programming Model: Accounts, storage, functions, and composability
-
Solidity Basics: Data types, functions, modifiers, and Ethereum-specific features
-
Practical Applications: Dutch auctions, tokens, and real-world use cases
-
Economics: Gas costs, optimization strategies, and fee mechanisms
-
Security: Common vulnerabilities and best practices for safe contract development
The Smart Contract Revolution
Smart contracts represent a fundamental shift in how we coordinate economic activity:
Traditional Systems:
Agreement → Trust Intermediary → Execute → Dispute → Arbitrate
Smart Contract Systems:
Agreement → Deploy Contract → Automatic Execution → Cryptographic Guarantee
Current Limitations and Future Directions
Scalability Challenges
- Ethereum mainnet: ~15-30 TPS
- Every node processes every transaction
- State growth is unbounded
Solutions in Development:
- Layer 2: Rollups, State Channels
- Sharding: Parallel processing
- Layer 1 Improvements: EIP-4844 (Proto-Danksharding)
Privacy Limitations
- All transaction data is public
- All contract state is visible
- Limited privacy for commercial applications
Emerging Solutions:
- Zero-Knowledge Proofs: Privacy-preserving transactions
- TEEs: Confidential computing
- Private Smart Contracts: Secret Network, Aztec
Oracle Problem
Smart contracts cannot directly access external data
Current Approaches:
- Chainlink: Decentralized oracle networks
- Band Protocol: Data aggregation
- API3: First-party oracles
- UMA: Optimistic oracles
User Experience
- Private key management is difficult
- Gas fees are confusing
- Irreversible transactions are scary
Improvements Underway:
- Account Abstraction: ERC-4337
- Social Recovery: Multi-sig and guardians
- Meta-Transactions: Gasless transactions
- Better Wallets: Improved UX
Real-World Impact
Smart contracts are already transforming:
DeFi (Decentralized Finance):
- $50B+ Total Value Locked
- Lending, borrowing, trading without banks
- Automated market makers
- Yield farming and liquidity provision
NFTs and Digital Ownership:
- $40B+ in sales (2021-2023)
- Digital art, collectibles, gaming
- Provenance and authenticity
- Creator royalties
DAOs (Decentralized Autonomous Organizations):
- Transparent governance
- Community-owned protocols
- Collective investment
- On-chain voting
Supply Chain:
- Tracking provenance
- Automated logistics
- Escrow and payments
- Compliance verification
Identity and Credentials:
- Self-sovereign identity
- Verifiable credentials
- Educational certificates
- Professional licenses
The Path Forward
For Developers
- Learn the Fundamentals: Understand blockchain concepts deeply
- Master Solidity: Practice writing secure, efficient contracts
- Study Security: Learn from past exploits
- Use Standards: Leverage ERC-20, ERC-721, ERC-4626, etc.
- Join the Community: Contribute to open-source projects
For the Ecosystem
- Better Tools: Improved development environments
- Formal Verification: Mathematical proof of correctness
- Auditing Standards: Professional security review processes
- Education: Training the next generation of blockchain developers
- Regulation: Clarity on legal status and compliance
Final Thoughts
Smart contracts are still in their infancy. We're at a similar stage to the early internet in the 1990s—the potential is clear, but the infrastructure is still maturing.
The vision of programmable, trustless, global coordination is becoming reality. Whether it's:
- Financial inclusion for the unbanked
- Censorship-resistant publishing
- Permissionless innovation for entrepreneurs
- Transparent governance for organizations
Smart contracts provide the building blocks for a more open, transparent, and accessible digital economy.
As we move into subsequent chapters, we'll explore how these building blocks combine to create the sophisticated DeFi protocols that are revolutionizing finance: automated market makers, lending protocols, synthetic assets, and more.
The revolution is just beginning.
Additional Resources
Official Documentation
Development Tools
- Remix IDE - Browser-based Solidity IDE
- Hardhat - Ethereum development environment
- Foundry - Fast, portable Ethereum toolkit
- OpenZeppelin - Secure contract libraries
Learning Resources
- CryptoZombies - Interactive Solidity tutorial
- Solidity by Example - Code examples
- Ethereum Stack Exchange - Q&A
- Ethernaut - Security challenges
Security
- Smart Contract Security Best Practices
- SWC Registry - Smart contract weakness classification
- Rekt News - DeFi exploits and post-mortems
Community
- Ethereum Magicians - Technical discussions
- EIP Process - Ethereum improvement proposals
- DeFi Pulse - DeFi analytics
- Dune Analytics - Blockchain data analysis
Exercises
Beginner
- Deploy a simple storage contract that can set and get a value
- Create an ERC-20 token with your own name and symbol
- Implement a simple voting contract
- Write a contract that tracks ownership of digital assets
Intermediate
- Build a Dutch auction contract for NFTs
- Create a multisignature wallet requiring 2-of-3 approval
- Implement a time-locked token vesting schedule
- Write a decentralized raffle/lottery contract
Advanced
- Create a lending pool with interest accrual
- Build an automated market maker (constant product)
- Implement a DAO with proposal and voting mechanisms
- Design a flash loan attack detector
Next Chapter:
In the next chapter, we'll explore how smart contracts enable peer-to-peer trading without centralized intermediaries, introducing concepts like liquidity pools, constant product market makers, and impermanent loss.