Smart Contract Fundamentals

Introduction to smart contracts: what they are, how they work, and why they're important for blockchain development.

Intermediate⏱️ 45 min📚 Prerequisites: 3

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:

  1. You insert money (input)
  2. You select a product (function call)
  3. The machine automatically dispenses the product (execution)
  4. 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.

RUST
struct 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.

RUST
impl 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:

RUST
struct 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

  1. Write Code: Write contract in Rust
  2. Compile: Compile to WASM (WebAssembly)
  3. Deploy: Upload to blockchain
  4. Instantiate: Create contract instance
  5. Interact: Call functions (query/execute)
  6. 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()
RUST
fn 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()
RUST
fn 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

RUST
use 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

  1. Trustless: No need for intermediaries
  2. Transparent: Code is visible to everyone
  3. Automatic: Execute automatically
  4. Immutable: Code cannot be changed (security)
  5. Global: Accessible from anywhere
  6. 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 ContractSmart Contract
Written in legal languageWritten in code
Requires lawyersSelf-executing
Manual enforcementAutomatic execution
Can be disputedDeterministic
ExpensiveLower cost
SlowFast

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:

  1. Learn about WebAssembly (WASM) - how contracts are compiled
  2. Study contract structure - organizing your code
  3. Practice testing - ensuring your contracts work correctly
  4. Explore security - protecting against attacks
  5. 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

RUST
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

RUST
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

RUST
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!

Easy

Starter Code:

RUST
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!

Medium

Starter Code:

RUST
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
}