Oracle Integration: Chainlink and Price Feeds
Integrating oracles for off-chain data: Chainlink price feeds, VRF, and custom oracle implementations.
Oracle Integration: Chainlink and Price Feeds
Oracles connect blockchains to external data sources.
Why Oracles?
Blockchains are isolated - they can't access:
- Price Data: Crypto, stock, commodity prices
- Random Numbers: True randomness
- Weather Data: For insurance contracts
- Sports Results: For betting contracts
- API Data: Any external API
Oracle Architecture
Components
- Data Source: External API, database, etc.
- Oracle Node: Fetches and signs data
- Aggregator: Combines multiple oracle responses
- Smart Contract: Consumes oracle data
Chainlink Architecture
RUST// Simplified Chainlink price feed struct PriceFeed { aggregator: Address, decimals: u8, description: String, } struct PriceData { price: i128, updated_at: u64, round_id: u64, } impl PriceFeed { fn latest_round_data(&self) -> PriceData { // Fetch from Chainlink aggregator // Returns: price, timestamp, round_id } fn get_price(&self) -> Result<u64, String> { let data = self.latest_round_data(); // Check staleness (price too old) let current_time = current_timestamp(); if current_time - data.updated_at > 3600 { return Err(String::from("Price data too stale")); } Ok(data.price as u64) } }
Chainlink Price Feeds
Supported Feeds
- Crypto: BTC/USD, ETH/USD, etc.
- Forex: EUR/USD, GBP/USD, etc.
- Commodities: Gold, Oil, etc.
- Stocks: S&P 500, etc.
Implementation
RUST// Chainlink price feed consumer struct PriceConsumer { price_feed: PriceFeed, min_update_interval: u64, last_update: u64, cached_price: Option<u64>, } impl PriceConsumer { fn new(price_feed: PriceFeed) -> Self { PriceConsumer { price_feed, min_update_interval: 3600, // 1 hour last_update: 0, cached_price: None, } } fn get_latest_price(&mut self) -> Result<u64, String> { let current_time = current_timestamp(); // Use cache if recent if let Some(price) = self.cached_price { if current_time - self.last_update < self.min_update_interval { return Ok(price); } } // Fetch new price let price = self.price_feed.get_price()?; self.cached_price = Some(price); self.last_update = current_time; Ok(price) } }
Verifiable Random Function (VRF)
VRF provides provably random numbers.
Use Cases
- Gaming: Random outcomes
- NFTs: Random traits
- Lotteries: Fair selection
- Governance: Random sampling
Implementation
RUSTstruct VRFRequest { key_hash: Vec<u8>, seed: u64, callback_contract: Address, callback_function: Vec<u8>, } struct VRFResponse { randomness: Vec<u8>, proof: Vec<u8>, } impl VRFRequest { fn request_randomness(&self) -> Result<u64, String> { // Request randomness from Chainlink VRF // Returns request ID } fn fulfill_randomness(&self, response: VRFResponse) -> Result<u64, String> { // Verify proof if !self.verify_proof(&response) { return Err(String::from("Invalid VRF proof")); } // Extract random number let random = u64::from_be_bytes( response.randomness[..8].try_into().unwrap() ); Ok(random) } fn verify_proof(&self, response: &VRFResponse) -> bool { // Verify cryptographic proof // In real implementation: use Chainlink's verification true } }
Custom Oracle Implementation
RUSTstruct OracleNode { node_id: String, data_sources: Vec<String>, reputation: u64, } struct OracleAggregator { nodes: Vec<OracleNode>, min_confirmations: usize, } impl OracleAggregator { fn new(min_confirmations: usize) -> Self { OracleAggregator { nodes: Vec::new(), min_confirmations, } } fn add_node(&mut self, node: OracleNode) { self.nodes.push(node); } fn get_price(&self, symbol: &str) -> Result<u64, String> { // Collect responses from multiple nodes let mut prices = Vec::new(); for node in &self.nodes { if let Ok(price) = node.fetch_price(symbol) { prices.push(price); } } if prices.len() < self.min_confirmations { return Err(String::from("Insufficient oracle responses")); } // Aggregate: median or weighted average prices.sort(); let median = prices[prices.len() / 2]; Ok(median) } }
Security Considerations
Oracle Manipulation
- Single Point of Failure: One oracle can be wrong
- Solution: Use multiple oracles, aggregate
Stale Data
- Problem: Old prices can be exploited
- Solution: Check timestamps, reject stale data
Price Deviation
- Problem: One oracle reports wrong price
- Solution: Compare with multiple sources, reject outliers
Best Practices
- Multiple Sources: Use multiple oracles
- Aggregation: Median or weighted average
- Staleness Checks: Reject old data
- Outlier Detection: Reject extreme values
- Circuit Breakers: Pause on anomalies
Real-World Examples
- Chainlink: Most popular oracle network
- Band Protocol: Alternative oracle solution
- UMA: Optimistic oracle
- API3: Decentralized APIs
Use Cases
- DeFi: Price feeds for lending, AMMs
- Insurance: Weather, flight data
- Gaming: Random numbers
- Prediction Markets: Event outcomes
Code Examples
Price Feed Consumer
Simple price feed implementation
use std::collections::HashMap;
struct PriceData {
price: u64,
updated_at: u64,
round_id: u64,
}
struct PriceFeed {
prices: HashMap<String, PriceData>,
decimals: u8,
}
impl PriceFeed {
fn new() -> Self {
PriceFeed {
prices: HashMap::new(),
decimals: 8,
}
}
fn update_price(&mut self, symbol: String, price: u64, timestamp: u64) {
let round_id = self.prices.len() as u64 + 1;
self.prices.insert(symbol, PriceData {
price,
updated_at: timestamp,
round_id,
});
}
fn get_latest_price(&self, symbol: &str) -> Result<u64, String> {
let data = self.prices.get(symbol)
.ok_or_else(|| format!("Price not found for {}", symbol))?;
// Check staleness (1 hour max)
let current_time = 1000000; // Simplified
if current_time - data.updated_at > 3600 {
return Err(String::from("Price data too stale"));
}
Ok(data.price)
}
}
struct PriceConsumer {
price_feed: PriceFeed,
min_price_age: u64,
}
impl PriceConsumer {
fn new(price_feed: PriceFeed) -> Self {
PriceConsumer {
price_feed,
min_price_age: 60, // 1 minute
}
}
fn get_price(&self, symbol: &str) -> Result<u64, String> {
self.price_feed.get_latest_price(symbol)
}
fn calculate_value(&self, symbol: &str, amount: u64) -> Result<u64, String> {
let price = self.get_price(symbol)?;
Ok((price as u128 * amount as u128 / 1e8 as u128) as u64)
}
}
fn main() {
let mut feed = PriceFeed::new();
// Update prices
feed.update_price(String::from("ETH/USD"), 2000_00000000, 1000000);
feed.update_price(String::from("BTC/USD"), 40000_00000000, 1000000);
let consumer = PriceConsumer::new(feed);
// Get prices
match consumer.get_price("ETH/USD") {
Ok(price) => println!("ETH/USD: ${}", price as f64 / 1e8),
Err(e) => println!("Error: {}", e),
}
// Calculate value
match consumer.calculate_value("ETH/USD", 1_00000000) {
Ok(value) => println!("1 ETH = ${}", value as f64 / 1e8),
Err(e) => println!("Error: {}", e),
}
}Explanation:
Price feeds provide external data (like crypto prices) to smart contracts. The consumer checks for stale data and calculates values. In production, this would connect to Chainlink or another oracle network.
VRF Randomness
Verifiable Random Function implementation
// Simplified VRF concept
struct VRFRequest {
request_id: u64,
seed: u64,
callback: String,
}
struct VRFResponse {
request_id: u64,
randomness: Vec<u8>,
proof: Vec<u8>,
}
struct VRFConsumer {
pending_requests: std::collections::HashMap<u64, VRFRequest>,
fulfilled_randomness: std::collections::HashMap<u64, u64>,
}
impl VRFConsumer {
fn new() -> Self {
VRFConsumer {
pending_requests: std::collections::HashMap::new(),
fulfilled_randomness: std::collections::HashMap::new(),
}
}
fn request_randomness(&mut self, seed: u64, callback: String) -> u64 {
let request_id = self.pending_requests.len() as u64 + 1;
self.pending_requests.insert(request_id, VRFRequest {
request_id,
seed,
callback,
});
println!("Requested randomness, ID: {}", request_id);
request_id
}
fn fulfill_randomness(&mut self, response: VRFResponse) -> Result<u64, String> {
// Verify request exists
let request = self.pending_requests.get(&response.request_id)
.ok_or("Request not found")?;
// Verify proof (simplified - in real VRF, verify cryptographic proof)
if !self.verify_proof(&response, request) {
return Err(String::from("Invalid VRF proof"));
}
// Extract random number
let random = if response.randomness.len() >= 8 {
u64::from_be_bytes(
response.randomness[..8].try_into().unwrap()
)
} else {
return Err(String::from("Invalid randomness length"));
};
// Store fulfilled randomness
self.fulfilled_randomness.insert(response.request_id, random);
self.pending_requests.remove(&response.request_id);
Ok(random)
}
fn verify_proof(&self, _response: &VRFResponse, _request: &VRFRequest) -> bool {
// In real implementation: verify cryptographic proof
// This ensures randomness is truly random and not manipulated
true
}
fn get_randomness(&self, request_id: u64) -> Option<u64> {
self.fulfilled_randomness.get(&request_id).copied()
}
}
fn main() {
let mut vrf = VRFConsumer::new();
// Request randomness
let request_id = vrf.request_randomness(12345, String::from("callback"));
// Simulate VRF response (in real system, comes from Chainlink)
let response = VRFResponse {
request_id,
randomness: vec![1, 2, 3, 4, 5, 6, 7, 8],
proof: vec![],
};
// Fulfill randomness
match vrf.fulfill_randomness(response) {
Ok(random) => {
println!("Random number: {}", random);
println!("This number is provably random and verifiable!");
}
Err(e) => println!("Error: {}", e),
}
}Explanation:
VRF provides provably random numbers. The randomness comes with a cryptographic proof that can be verified on-chain. This ensures the randomness is truly random and not manipulated by the oracle or anyone else.
Exercises
Price Feed with Staleness Check
Create a price feed that checks for stale data!
Starter Code:
struct PriceData {
price: u64,
timestamp: u64,
}
fn main() {
// Create price data
// Check if stale
}