DeFi Protocols: Lending, AMM, and Staking
Building decentralized finance protocols: lending platforms, automated market makers, and staking systems.
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
RUSTstruct 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 Ay: Amount of token Bk: Constant (invariant)
Implementation
RUSTstruct 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
RUSTstruct 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
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
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!
Starter Code:
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);
}