DeFi Protocols: Lending, AMM, and Staking

Building decentralized finance protocols: lending platforms, automated market makers, and staking systems.

Advanced⏱️ 60 min📚 Prerequisites: 2

DeFi Protocols: Lending, AMM, and Staking

Decentralized Finance (DeFi) protocols enable financial services without intermediaries.

DeFi Building Blocks

1. Lending Protocols

Lending protocols allow users to lend and borrow assets with interest.

Key Components

  • Liquidity Pools: Users deposit assets
  • Interest Rates: Algorithmic based on supply/demand
  • Collateralization: Borrowers must over-collateralize
  • Liquidation: Under-collateralized positions are liquidated

Example: Compound/Aave Model

RUST
struct LendingPool {
    assets: HashMap<String, AssetPool>,
    interest_rate_model: InterestRateModel,
}

struct AssetPool {
    total_supply: u64,
    total_borrowed: u64,
    reserve_factor: u8, // Percentage kept as reserve
    liquidity_index: f64, // Tracks interest accrual
}

impl LendingPool {
    fn supply(&mut self, asset: &str, amount: u64) -> Result<(), String> {
        // Add to pool, mint interest-bearing tokens
    }
    
    fn borrow(&mut self, asset: &str, amount: u64, collateral: &str) -> Result<(), String> {
        // Check collateralization ratio
        // Transfer borrowed asset
        // Track debt with interest
    }
    
    fn calculate_interest(&mut self, asset: &str) {
        // Update liquidity index based on utilization
    }
}

2. Automated Market Makers (AMM)

AMMs enable token swaps without order books using liquidity pools.

Constant Product Formula (Uniswap)

x * y = k

  • x: Amount of token A
  • y: Amount of token B
  • k: Constant (invariant)

Implementation

RUST
struct AMMPool {
    token_a: String,
    token_b: String,
    reserve_a: u64,
    reserve_b: u64,
    total_supply: u64, // LP tokens
}

impl AMMPool {
    fn swap_a_for_b(&mut self, amount_a: u64) -> Result<u64, String> {
        let k = self.reserve_a * self.reserve_b;
        
        let new_reserve_a = self.reserve_a + amount_a;
        let new_reserve_b = k / new_reserve_a;
        
        let amount_b = self.reserve_b - new_reserve_b;
        
        if amount_b == 0 {
            return Err(String::from("Insufficient liquidity"));
        }
        
        self.reserve_a = new_reserve_a;
        self.reserve_b = new_reserve_b;
        
        Ok(amount_b)
    }
    
    fn add_liquidity(&mut self, amount_a: u64, amount_b: u64) -> u64 {
        // Calculate LP tokens to mint
        let lp_tokens = if self.total_supply == 0 {
            (amount_a * amount_b).sqrt() // Geometric mean
        } else {
            let share_a = (amount_a * self.total_supply) / self.reserve_a;
            let share_b = (amount_b * self.total_supply) / self.reserve_b;
            share_a.min(share_b)
        };
        
        self.reserve_a += amount_a;
        self.reserve_b += amount_b;
        self.total_supply += lp_tokens;
        
        lp_tokens
    }
}

3. Staking Protocols

Staking allows users to lock tokens to earn rewards.

Features

  • Lock Periods: Optional time locks for higher rewards
  • Reward Distribution: Based on stake amount and duration
  • Slashing: Penalties for misbehavior
  • Delegation: Stake through validators
RUST
struct StakingPool {
    total_staked: u64,
    stakers: HashMap<String, StakeInfo>,
    reward_rate: u64, // Rewards per block
    lock_periods: HashMap<u64, u64>, // Duration -> multiplier
}

struct StakeInfo {
    amount: u64,
    lock_until: Option<u64>,
    rewards_earned: u64,
}

impl StakingPool {
    fn stake(&mut self, staker: &str, amount: u64, lock_duration: Option<u64>) {
        let lock_until = lock_duration.map(|d| current_block() + d);
        
        let mut info = self.stakers.entry(staker.to_string()).or_insert(StakeInfo {
            amount: 0,
            lock_until: None,
            rewards_earned: 0,
        });
        
        info.amount += amount;
        info.lock_until = lock_until;
        self.total_staked += amount;
    }
    
    fn calculate_rewards(&mut self, staker: &str) -> u64 {
        let info = self.stakers.get(staker).unwrap();
        let multiplier = if let Some(lock) = info.lock_until {
            if current_block() < lock { 2 } else { 1 } // 2x for locked
        } else {
            1
        };
        
        let share = (info.amount * self.reward_rate) / self.total_staked;
        share * multiplier
    }
}

Security Considerations

Reentrancy

  • Use checks-effects-interactions pattern
  • Reentrancy guards

Oracle Attacks

  • Use multiple price oracles
  • Time-weighted average prices

Flash Loan Attacks

  • Validate state before and after operations
  • Limit flash loan usage

Integer Overflow

  • Use checked arithmetic
  • Safe math libraries

Real-World Examples

  • Lending: Compound, Aave, MakerDAO
  • AMM: Uniswap, SushiSwap, Curve
  • Staking: Ethereum 2.0, Cosmos, Polkadot

Gas Optimization

  • Batch operations
  • Storage optimization
  • Efficient data structures
  • Minimal external calls

Code Examples

AMM Pool

Automated Market Maker with constant product formula

RUST
use std::collections::HashMap;

struct AMMPool {
    token_a: String,
    token_b: String,
    reserve_a: u64,
    reserve_b: u64,
    lp_tokens: HashMap<String, u64>,
    total_lp_supply: u64,
}

impl AMMPool {
    fn new(token_a: String, token_b: String, initial_a: u64, initial_b: u64) -> Self {
        AMMPool {
            token_a,
            token_b,
            reserve_a: initial_a,
            reserve_b: initial_b,
            lp_tokens: HashMap::new(),
            total_lp_supply: 0,
        }
    }
    
    fn swap_a_for_b(&mut self, amount_a: u64) -> Result<u64, String> {
        if amount_a == 0 {
            return Err(String::from("Amount must be greater than 0"));
        }
        
        // Constant product: x * y = k
        let k = self.reserve_a * self.reserve_b;
        let new_reserve_a = self.reserve_a + amount_a;
        
        if new_reserve_a == 0 {
            return Err(String::from("Division by zero"));
        }
        
        let new_reserve_b = k / new_reserve_a;
        
        if new_reserve_b >= self.reserve_b {
            return Err(String::from("Insufficient liquidity"));
        }
        
        let amount_b = self.reserve_b - new_reserve_b;
        
        // Update reserves
        self.reserve_a = new_reserve_a;
        self.reserve_b = new_reserve_b;
        
        Ok(amount_b)
    }
    
    fn add_liquidity(&mut self, provider: &str, amount_a: u64, amount_b: u64) -> Result<u64, String> {
        // Calculate LP tokens to mint
        let lp_tokens = if self.total_lp_supply == 0 {
            // First liquidity: sqrt(amount_a * amount_b)
            ((amount_a as f64 * amount_b as f64).sqrt()) as u64
        } else {
            // Proportional to existing reserves
            let share_a = (amount_a * self.total_lp_supply) / self.reserve_a;
            let share_b = (amount_b * self.total_lp_supply) / self.reserve_b;
            share_a.min(share_b)
        };
        
        if lp_tokens == 0 {
            return Err(String::from("Insufficient liquidity"));
        }
        
        // Update reserves
        self.reserve_a += amount_a;
        self.reserve_b += amount_b;
        
        // Mint LP tokens
        *self.lp_tokens.entry(provider.to_string()).or_insert(0) += lp_tokens;
        self.total_lp_supply += lp_tokens;
        
        Ok(lp_tokens)
    }
    
    fn get_price(&self) -> f64 {
        if self.reserve_b == 0 {
            return 0.0;
        }
        self.reserve_a as f64 / self.reserve_b as f64
    }
}

fn main() {
    let mut pool = AMMPool::new(
        String::from("ETH"),
        String::from("USDC"),
        100, // 100 ETH
        200000, // 200,000 USDC
    );
    
    println!("Initial price: {} USDC per ETH", pool.get_price());
    
    // Add liquidity
    let lp_tokens = pool.add_liquidity("alice", 50, 100000).unwrap();
    println!("Alice received {} LP tokens", lp_tokens);
    
    // Swap ETH for USDC
    match pool.swap_a_for_b(10) {
        Ok(usdc_out) => {
            println!("Swapped 10 ETH for {} USDC", usdc_out);
            println!("New price: {} USDC per ETH", pool.get_price());
        }
        Err(e) => println!("Swap failed: {}", e),
    }
}

Explanation:

AMMs use the constant product formula (x * y = k) to determine swap prices. Adding liquidity mints LP tokens proportional to the contribution. Swaps maintain the constant product, automatically adjusting prices based on supply and demand.

Lending Pool

Simple lending protocol with interest

RUST
use std::collections::HashMap;

struct LendingPool {
    total_supplied: u64,
    total_borrowed: u64,
    supplies: HashMap<String, u64>,
    borrows: HashMap<String, u64>,
    interest_rate: u64, // Basis points (100 = 1%)
}

impl LendingPool {
    fn new() -> Self {
        LendingPool {
            total_supplied: 0,
            total_borrowed: 0,
            supplies: HashMap::new(),
            borrows: HashMap::new(),
            interest_rate: 500, // 5%
        }
    }
    
    fn supply(&mut self, user: &str, amount: u64) {
        *self.supplies.entry(user.to_string()).or_insert(0) += amount;
        self.total_supplied += amount;
        self.update_interest_rate();
    }
    
    fn borrow(&mut self, user: &str, amount: u64) -> Result<(), String> {
        // Check collateral (simplified: need 150% collateralization)
        let collateral = self.supplies.get(user).copied().unwrap_or(0);
        let existing_debt = self.borrows.get(user).copied().unwrap_or(0);
        
        if collateral * 100 < (existing_debt + amount) * 150 {
            return Err(String::from("Insufficient collateral"));
        }
        
        if amount > self.total_supplied - self.total_borrowed {
            return Err(String::from("Insufficient liquidity"));
        }
        
        *self.borrows.entry(user.to_string()).or_insert(0) += amount;
        self.total_borrowed += amount;
        self.update_interest_rate();
        
        Ok(())
    }
    
    fn update_interest_rate(&mut self) {
        // Simple model: higher utilization = higher rate
        let utilization = if self.total_supplied > 0 {
            (self.total_borrowed * 10000) / self.total_supplied
        } else {
            0
        };
        
        // Base rate + utilization component
        self.interest_rate = 300 + (utilization / 10); // 3% base + utilization
    }
    
    fn calculate_interest(&self, principal: u64, blocks: u64) -> u64 {
        (principal * self.interest_rate * blocks) / (10000 * 100) // Simplified
    }
}

fn main() {
    let mut pool = LendingPool::new();
    
    // Users supply assets
    pool.supply("alice", 10000);
    pool.supply("bob", 5000);
    
    println!("Total supplied: {}", pool.total_supplied);
    println!("Interest rate: {} basis points", pool.interest_rate);
    
    // User borrows
    match pool.borrow("alice", 3000) {
        Ok(_) => {
            println!("Alice borrowed 3000");
            println!("Total borrowed: {}", pool.total_borrowed);
            println!("New interest rate: {} basis points", pool.interest_rate);
            
            let interest = pool.calculate_interest(3000, 100);
            println!("Interest after 100 blocks: {}", interest);
        }
        Err(e) => println!("Borrow failed: {}", e),
    }
}

Explanation:

Lending pools allow users to supply assets (earning interest) and borrow assets (paying interest). Interest rates adjust based on utilization. Borrowers must maintain sufficient collateral to prevent defaults. This enables decentralized lending without banks.

Exercises

Simple AMM

Create a simple AMM swap function!

Hard

Starter Code:

RUST
fn swap(reserve_a: u64, reserve_b: u64, amount_a: u64) -> u64 {
    // Implement constant product formula
    // x * y = k
    0
}

fn main() {
    let result = swap(100, 200, 10);
    println!("Swapped 10 tokens A for {} tokens B", result);
}