Oracle Integration: Chainlink and Price Feeds

Integrating oracles for off-chain data: Chainlink price feeds, VRF, and custom oracle implementations.

Advanced⏱️ 55 min📚 Prerequisites: 2

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

  1. Data Source: External API, database, etc.
  2. Oracle Node: Fetches and signs data
  3. Aggregator: Combines multiple oracle responses
  4. 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

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

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

  1. Multiple Sources: Use multiple oracles
  2. Aggregation: Median or weighted average
  3. Staleness Checks: Reject old data
  4. Outlier Detection: Reject extreme values
  5. 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

RUST
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

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

Medium

Starter Code:

RUST
struct PriceData {
    price: u64,
    timestamp: u64,
}

fn main() {
    // Create price data
    // Check if stale
}