Smart Contract Security Patterns
Critical security patterns and best practices for secure smart contract development in Rust.
Smart Contract Security Patterns
Security is paramount in smart contracts. Bugs can lead to loss of funds. Rust's type system helps, but you must follow security best practices.
Common Vulnerabilities
1. Reentrancy Attacks
Problem: External calls can re-enter the contract before state updates.
RUST// VULNERABLE CODE struct Bank { balances: HashMap<String, u64>, } impl Bank { fn withdraw(&mut self, user: &str, amount: u64) -> Result<(), String> { let balance = self.balances.get(user).copied().unwrap_or(0); if balance < amount { return Err(String::from("Insufficient balance")); } // VULNERABLE: External call before state update external_transfer(user, amount)?; // State update happens after external call *self.balances.get_mut(user).unwrap() -= amount; Ok(()) } }
Solution: Checks-Effects-Interactions Pattern
RUST// SECURE CODE impl Bank { fn withdraw(&mut self, user: &str, amount: u64) -> Result<(), String> { // CHECK: Validate inputs let balance = self.balances.get(user).copied().unwrap_or(0); if balance < amount { return Err(String::from("Insufficient balance")); } // EFFECTS: Update state FIRST *self.balances.get_mut(user).unwrap() -= amount; // INTERACTIONS: External call LAST external_transfer(user, amount)?; Ok(()) } }
2. Integer Overflow/Underflow
Problem: Arithmetic operations can overflow.
RUST// VULNERABLE fn add_balance(balance: u64, amount: u64) -> u64 { balance + amount // Can overflow! }
Solution: Use Checked Arithmetic
RUST// SECURE fn add_balance(balance: u64, amount: u64) -> Result<u64, String> { balance.checked_add(amount) .ok_or_else(|| String::from("Overflow")) } fn subtract_balance(balance: u64, amount: u64) -> Result<u64, String> { balance.checked_sub(amount) .ok_or_else(|| String::from("Underflow")) }
3. Access Control
Problem: Missing permission checks.
RUST// VULNERABLE struct Contract { owner: String, funds: u64, } impl Contract { fn withdraw(&mut self, amount: u64) -> Result<(), String> { // Missing owner check! self.funds -= amount; Ok(()) } }
Solution: Always Check Permissions
RUST// SECURE impl Contract { fn withdraw(&mut self, caller: &str, amount: u64) -> Result<(), String> { // Check access if caller != self.owner { return Err(String::from("Unauthorized")); } // Validate amount if amount > self.funds { return Err(String::from("Insufficient funds")); } self.funds -= amount; Ok(()) } }
4. Input Validation
Problem: Unvalidated inputs can cause issues.
RUST// VULNERABLE fn transfer(&mut self, to: String, amount: u64) { // No validation! self.balances.entry(to).or_insert(0) += amount; }
Solution: Validate All Inputs
RUST// SECURE 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 > 0")); } // Validate balance let balance = self.balances.get(from).copied().unwrap_or(0); 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(()) }
Security Patterns
1. Reentrancy Guard
RUSTstruct ReentrancyGuard { locked: bool, } impl ReentrancyGuard { fn new() -> Self { ReentrancyGuard { locked: false } } fn lock(&mut self) -> Result<(), String> { if self.locked { return Err(String::from("Reentrancy detected")); } self.locked = true; Ok(()) } fn unlock(&mut self) { self.locked = false; } } // Usage fn withdraw(&mut self, guard: &mut ReentrancyGuard, amount: u64) -> Result<(), String> { guard.lock()?; // Safe operations self.balance -= amount; guard.unlock(); Ok(()) }
2. Access Control Modifier
RUSTtrait AccessControl { fn require_owner(&self, caller: &str) -> Result<(), String>; fn require_admin(&self, caller: &str) -> Result<(), String>; } struct Contract { owner: String, admins: HashSet<String>, } impl AccessControl for Contract { fn require_owner(&self, caller: &str) -> Result<(), String> { if caller != self.owner { return Err(String::from("Not owner")); } Ok(()) } fn require_admin(&self, caller: &str) -> Result<(), String> { if !self.admins.contains(caller) && caller != self.owner { return Err(String::from("Not admin")); } Ok(()) } }
3. Safe Math Library
RUSTpub mod safe_math { pub fn checked_add(a: u64, b: u64) -> Result<u64, String> { a.checked_add(b).ok_or_else(|| String::from("Addition overflow")) } pub fn checked_sub(a: u64, b: u64) -> Result<u64, String> { a.checked_sub(b).ok_or_else(|| String::from("Subtraction underflow")) } pub fn checked_mul(a: u64, b: u64) -> Result<u64, String> { a.checked_mul(b).ok_or_else(|| String::from("Multiplication overflow")) } pub fn checked_div(a: u64, b: u64) -> Result<u64, String> { if b == 0 { return Err(String::from("Division by zero")); } Ok(a / b) } }
Security Checklist
- All external calls use checks-effects-interactions
- All arithmetic uses checked operations
- All functions check permissions
- All inputs are validated
- Reentrancy guards where needed
- Events emitted for important actions
- Error messages are clear
- Gas optimization considered
- Code reviewed by security experts
- Comprehensive test coverage
Testing Security
RUST#[cfg(test)] mod security_tests { use super::*; #[test] fn test_reentrancy_protection() { // Test that reentrancy is prevented } #[test] fn test_overflow_protection() { // Test overflow handling let max = u64::MAX; assert!(safe_math::checked_add(max, 1).is_err()); } #[test] fn test_access_control() { // Test unauthorized access fails let contract = Contract::new(String::from("owner")); assert!(contract.withdraw("hacker", 100).is_err()); } }
Best Practices
- Minimize External Calls: Reduce attack surface
- Use Libraries: Don't reinvent security
- Keep It Simple: Complex code = more bugs
- Test Extensively: Test edge cases
- Code Review: Get security review
- Audit: Professional security audit
- Document: Document security assumptions
- Monitor: Monitor for unusual activity
Code Examples
Reentrancy Protection
Implementing reentrancy guard
struct ReentrancyGuard {
locked: bool,
}
impl ReentrancyGuard {
fn new() -> Self {
ReentrancyGuard { locked: false }
}
fn lock(&mut self) -> Result<(), String> {
if self.locked {
return Err(String::from("Reentrancy detected"));
}
self.locked = true;
Ok(())
}
fn unlock(&mut self) {
self.locked = false;
}
}
struct Bank {
balances: std::collections::HashMap<String, u64>,
guard: ReentrancyGuard,
}
impl Bank {
fn withdraw(&mut self, user: &str, amount: u64) -> Result<(), String> {
// Lock to prevent reentrancy
self.guard.lock()?;
// Checks
let balance = self.balances.get(user).copied().unwrap_or(0);
if balance < amount {
self.guard.unlock();
return Err(String::from("Insufficient balance"));
}
// Effects: Update state FIRST
*self.balances.entry(user.to_string()).or_insert(0) -= amount;
// Unlock before external call
self.guard.unlock();
// Interactions: External call LAST
// external_transfer(user, amount)?;
Ok(())
}
}
fn main() {
let mut bank = Bank {
balances: std::collections::HashMap::new(),
guard: ReentrancyGuard::new(),
};
bank.balances.insert(String::from("alice"), 1000);
match bank.withdraw("alice", 500) {
Ok(_) => println!("Withdrawal successful"),
Err(e) => println!("Error: {}", e),
}
}Explanation:
Reentrancy guards prevent recursive calls that could exploit state updates. The guard locks during critical sections and unlocks after state is updated.
Safe Math Operations
Using checked arithmetic to prevent overflows
fn safe_add(a: u64, b: u64) -> Result<u64, String> {
a.checked_add(b).ok_or_else(|| String::from("Addition overflow"))
}
fn safe_sub(a: u64, b: u64) -> Result<u64, String> {
a.checked_sub(b).ok_or_else(|| String::from("Subtraction underflow"))
}
fn safe_mul(a: u64, b: u64) -> Result<u64, String> {
a.checked_mul(b).ok_or_else(|| String::from("Multiplication overflow"))
}
fn safe_div(a: u64, b: u64) -> Result<u64, String> {
if b == 0 {
return Err(String::from("Division by zero"));
}
Ok(a / b)
}
struct TokenContract {
balances: std::collections::HashMap<String, u64>,
}
impl TokenContract {
fn transfer(
&mut self,
from: &str,
to: &str,
amount: u64,
) -> Result<(), String> {
let from_balance = self.balances.get(from).copied().unwrap_or(0);
// Safe subtraction
let new_from_balance = safe_sub(from_balance, amount)?;
// Safe addition
let to_balance = self.balances.get(to).copied().unwrap_or(0);
let new_to_balance = safe_add(to_balance, amount)?;
self.balances.insert(from.to_string(), new_from_balance);
self.balances.insert(to.to_string(), new_to_balance);
Ok(())
}
}
fn main() {
let mut contract = TokenContract {
balances: std::collections::HashMap::new(),
};
contract.balances.insert(String::from("alice"), 1000);
match contract.transfer("alice", "bob", 500) {
Ok(_) => println!("Transfer successful"),
Err(e) => println!("Error: {}", e),
}
}Explanation:
Checked arithmetic prevents integer overflow and underflow. Always use checked operations for financial calculations in smart contracts.
Exercises
Implement Access Control
Add access control to a contract function!
Starter Code:
struct Contract {
owner: String,
value: u64,
}
impl Contract {
fn set_value(&mut self, caller: &str, new_value: u64) -> Result<(), String> {
// Add owner check
self.value = new_value;
Ok(())
}
}
fn main() {
let mut contract = Contract {
owner: String::from("owner"),
value: 0,
};
contract.set_value("owner", 100).unwrap();
println!("Value: {}", contract.value);
}