Smart Contract Fundamentals
Introduction to smart contracts: what they are, how they work, and why they're important for blockchain development.
Smart Contract Fundamentals
Smart contracts are self-executing programs that run on blockchains. They automatically execute when predefined conditions are met, without requiring intermediaries.
What is a Smart Contract?
A smart contract is:
- Code: Written in a programming language (like Rust)
- Deployed: Stored on the blockchain
- Automatic: Executes automatically when called
- Immutable: Once deployed, code cannot be changed (unless upgradeable)
- Transparent: Code and execution are visible to everyone
- Trustless: No need to trust a third party
Real-World Analogy
Think of a vending machine:
- You insert money (input)
- You select a product (function call)
- The machine automatically dispenses the product (execution)
- No human operator needed (trustless)
Smart contracts work similarly, but on a blockchain!
Key Concepts
1. State
Smart contracts maintain state - data that persists between function calls.
RUSTstruct SimpleContract { owner: String, // State: who owns the contract balance: u64, // State: contract balance counter: u64, // State: a counter value }
2. Functions
Contracts expose functions that can be called to interact with the contract.
RUSTimpl SimpleContract { // Read function (query) - doesn't change state fn get_balance(&self) -> u64 { self.balance } // Write function (execute) - changes state fn increment_counter(&mut self) { self.counter += 1; } }
3. Transactions
Every function call is a transaction that:
- Costs gas (fee)
- Is recorded on the blockchain
- Can modify state
- Returns a result
Your First Smart Contract
Let's build a simple counter contract:
RUSTstruct CounterContract { count: u64, owner: String, } impl CounterContract { // Initialize the contract fn new(owner: String) -> Self { CounterContract { count: 0, owner, } } // Read the count (query) fn get_count(&self) -> u64 { self.count } // Increment the count (execute) fn increment(&mut self, caller: &str) -> Result<(), String> { // Simple access control if caller != self.owner { return Err(String::from("Only owner can increment")); } self.count += 1; Ok(()) } // Reset the count (execute) fn reset(&mut self, caller: &str) -> Result<(), String> { if caller != self.owner { return Err(String::from("Only owner can reset")); } self.count = 0; Ok(()) } }
How Smart Contracts Work
1. Deployment
RUST// Deploy the contract let mut contract = CounterContract::new(String::from("0xAlice"));
2. Query (Read)
RUST// Read state - doesn't cost gas, doesn't change state let current_count = contract.get_count(); println!("Current count: {}", current_count);
3. Execute (Write)
RUST// Execute function - costs gas, changes state match contract.increment("0xAlice") { Ok(_) => println!("Count incremented!"), Err(e) => println!("Error: {}", e), }
Smart Contract Lifecycle
- Write Code: Write contract in Rust
- Compile: Compile to WASM (WebAssembly)
- Deploy: Upload to blockchain
- Instantiate: Create contract instance
- Interact: Call functions (query/execute)
- Monitor: Watch events and state changes
Types of Functions
Query Functions (Read)
- No state change: Only read data
- Free: Usually don't cost gas
- Fast: Execute immediately
- Examples:
get_balance(),get_count(),get_owner()
RUSTfn get_balance(&self, address: &str) -> u64 { self.balances.get(address).copied().unwrap_or(0) }
Execute Functions (Write)
- State change: Modify contract state
- Costs gas: Requires payment
- Recorded: Stored on blockchain
- Examples:
transfer(),mint(),approve()
RUSTfn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> { // Validate let balance = self.balances.get(from).copied().unwrap_or(0); if balance < amount { return Err(String::from("Insufficient balance")); } // Update state *self.balances.entry(from.to_string()).or_insert(0) -= amount; *self.balances.entry(to.to_string()).or_insert(0) += amount; Ok(()) }
Simple Token Contract Example
RUSTuse std::collections::HashMap; struct TokenContract { name: String, symbol: String, total_supply: u64, balances: HashMap<String, u64>, owner: String, } impl TokenContract { // Initialize contract fn new(name: String, symbol: String, initial_supply: u64, owner: String) -> Self { let mut balances = HashMap::new(); balances.insert(owner.clone(), initial_supply); TokenContract { name, symbol, total_supply: initial_supply, balances, owner, } } // Query: Get balance fn balance_of(&self, address: &str) -> u64 { self.balances.get(address).copied().unwrap_or(0) } // Query: Get total supply fn get_total_supply(&self) -> u64 { self.total_supply } // Execute: Transfer tokens fn transfer( &mut self, from: &str, to: &str, amount: u64, ) -> Result<(), String> { // Validate addresses if from.is_empty() || to.is_empty() { return Err(String::from("Invalid address")); } if from == to { return Err(String::from("Cannot transfer to self")); } // Validate amount if amount == 0 { return Err(String::from("Amount must be greater than 0")); } // Check balance let balance = self.balance_of(from); if balance < amount { return Err(String::from("Insufficient balance")); } // Execute transfer *self.balances.entry(from.to_string()).or_insert(0) -= amount; *self.balances.entry(to.to_string()).or_insert(0) += amount; Ok(()) } }
Why Smart Contracts?
Advantages
- Trustless: No need for intermediaries
- Transparent: Code is visible to everyone
- Automatic: Execute automatically
- Immutable: Code cannot be changed (security)
- Global: Accessible from anywhere
- Cost-effective: Reduce middleman fees
Use Cases
- Tokens: Create cryptocurrencies
- DeFi: Lending, trading, staking
- NFTs: Digital collectibles
- DAOs: Decentralized organizations
- Gaming: In-game assets and logic
- Supply Chain: Track products
- Voting: Transparent elections
Smart Contract vs Traditional Contract
| Traditional Contract | Smart Contract |
|---|---|
| Written in legal language | Written in code |
| Requires lawyers | Self-executing |
| Manual enforcement | Automatic execution |
| Can be disputed | Deterministic |
| Expensive | Lower cost |
| Slow | Fast |
Important Concepts
Gas
- Gas is the fee paid to execute smart contracts
- More complex operations = more gas
- Gas price varies by network
- Failed transactions still cost gas
Immutability
- Once deployed, code cannot be changed
- This ensures security and trust
- Some contracts support upgrades (with admin)
Determinism
- Same input always produces same output
- Critical for blockchain consensus
- No random numbers (unless from oracle)
Next Steps
Now that you understand the fundamentals:
- Learn about WebAssembly (WASM) - how contracts are compiled
- Study contract structure - organizing your code
- Practice testing - ensuring your contracts work correctly
- Explore security - protecting against attacks
- Build real projects - apply what you've learned
Key Takeaways
- Smart contracts are self-executing programs on blockchains
- They maintain state and expose functions
- Query functions read data, execute functions modify state
- They're trustless, transparent, and automatic
- Gas is required to execute functions
- Contracts are immutable once deployed
Code Examples
Your First Smart Contract
A simple counter contract to understand the basics
struct CounterContract {
count: u64,
owner: String,
}
impl CounterContract {
fn new(owner: String) -> Self {
CounterContract {
count: 0,
owner,
}
}
// Query function - read state
fn get_count(&self) -> u64 {
self.count
}
// Execute function - modify state
fn increment(&mut self, caller: &str) -> Result<(), String> {
if caller != self.owner {
return Err(String::from("Only owner can increment"));
}
self.count += 1;
Ok(())
}
}
fn main() {
// Deploy contract
let mut contract = CounterContract::new(String::from("0xAlice"));
// Query: Read count
println!("Initial count: {}", contract.get_count());
// Execute: Increment
contract.increment("0xAlice").unwrap();
println!("After increment: {}", contract.get_count());
// Try unauthorized access
match contract.increment("0xBob") {
Ok(_) => println!("Incremented"),
Err(e) => println!("Error: {}", e),
}
}Explanation:
This simple counter contract demonstrates the basic concepts: state (count), query function (get_count), and execute function (increment). It also shows basic access control.
Simple Token Contract
A basic token contract with transfer functionality
use std::collections::HashMap;
struct TokenContract {
name: String,
symbol: String,
total_supply: u64,
balances: HashMap<String, u64>,
}
impl TokenContract {
fn new(name: String, symbol: String, initial_supply: u64, owner: String) -> Self {
let mut balances = HashMap::new();
balances.insert(owner.clone(), initial_supply);
TokenContract {
name,
symbol,
total_supply: initial_supply,
balances,
}
}
// Query: Get balance
fn balance_of(&self, address: &str) -> u64 {
self.balances.get(address).copied().unwrap_or(0)
}
// Execute: Transfer tokens
fn transfer(
&mut self,
from: &str,
to: &str,
amount: u64,
) -> Result<(), String> {
// Validate
if amount == 0 {
return Err(String::from("Amount must be > 0"));
}
let balance = self.balance_of(from);
if balance < amount {
return Err(String::from("Insufficient balance"));
}
// Transfer
*self.balances.entry(from.to_string()).or_insert(0) -= amount;
*self.balances.entry(to.to_string()).or_insert(0) += amount;
Ok(())
}
}
fn main() {
// Deploy token
let mut token = TokenContract::new(
String::from("MyToken"),
String::from("MTK"),
1000000,
String::from("0xAlice"),
);
println!("Token: {} ({})", token.name, token.symbol);
println!("Alice balance: {}", token.balance_of("0xAlice"));
// Transfer tokens
token.transfer("0xAlice", "0xBob", 1000).unwrap();
println!("After transfer:");
println!("Alice balance: {}", token.balance_of("0xAlice"));
println!("Bob balance: {}", token.balance_of("0xBob"));
}Explanation:
This token contract shows how smart contracts manage state (balances) and provide functions to interact with that state. Transfer is an execute function that modifies state.
Query vs Execute
Understanding the difference between query and execute functions
struct SimpleContract {
value: u64,
owner: String,
}
impl SimpleContract {
fn new(owner: String) -> Self {
SimpleContract {
value: 0,
owner,
}
}
// QUERY: Read-only, no state change, free
fn get_value(&self) -> u64 {
self.value
}
// EXECUTE: Modifies state, costs gas
fn set_value(&mut self, caller: &str, new_value: u64) -> Result<(), String> {
if caller != self.owner {
return Err(String::from("Unauthorized"));
}
self.value = new_value;
Ok(())
}
// EXECUTE: Can also return values
fn increment(&mut self, caller: &str) -> Result<u64, String> {
if caller != self.owner {
return Err(String::from("Unauthorized"));
}
self.value += 1;
Ok(self.value)
}
}
fn main() {
let mut contract = SimpleContract::new(String::from("0xAlice"));
// Query - free, instant
println!("Value: {}", contract.get_value());
// Execute - costs gas, changes state
contract.set_value("0xAlice", 42).unwrap();
println!("After set: {}", contract.get_value());
// Execute with return value
let new_value = contract.increment("0xAlice").unwrap();
println!("After increment: {}", new_value);
}Explanation:
Query functions are read-only and free. Execute functions modify state and cost gas. Both are important for smart contract functionality.
Exercises
Create a Simple Contract
Create a simple contract that stores a message and allows the owner to update it!
Starter Code:
struct MessageContract {
message: String,
owner: String,
}
fn main() {
let mut contract = MessageContract::new(String::from("0xAlice"));
println!("Message: {}", contract.get_message());
}Add Transfer Function
Add a transfer function to the token contract!
Starter Code:
use std::collections::HashMap;
struct TokenContract {
balances: HashMap<String, u64>,
}
impl TokenContract {
fn new(initial_supply: u64, owner: String) -> Self {
let mut balances = HashMap::new();
balances.insert(owner, initial_supply);
TokenContract { balances }
}
fn balance_of(&self, address: &str) -> u64 {
self.balances.get(address).copied().unwrap_or(0)
}
// Add transfer function here
}
fn main() {
let mut token = TokenContract::new(1000, String::from("0xAlice"));
// Test transfer here
}