Smart Contract

OracleAggregator

A.17ae3b1b0b0d50db.OracleAggregator

Valid From

132,672,779

Deployed

6d ago
Feb 21, 2026, 05:42:05 PM UTC

Dependents

0 imports
1import PriceOracle from 0x17ae3b1b0b0d50db
2import BandPriceOracle from 0x17ae3b1b0b0d50db
3import PoolPriceOracle from 0x17ae3b1b0b0d50db
4import SimplePriceOracle from 0x17ae3b1b0b0d50db
5
6/// OracleAggregator: Multi-oracle price aggregation with waterfall fallback
7///
8/// This contract implements a robust multi-oracle strategy that queries multiple
9/// price sources in priority order, ensuring maximum reliability and token coverage.
10///
11/// **Waterfall Strategy**:
12/// 1. **Band Protocol** (Primary) - For major tokens (FLOW, BTC, ETH, USDC, etc.)
13///    - Decentralized oracle network
14///    - Real-time updates
15///    - High reliability
16///
17/// 2. **Pool Price Oracle** (Secondary) - For DEX-specific tokens (Froth, etc.)
18///    - Direct liquidity pool queries
19///    - Real-time pool prices
20///    - Supports any token with a pool
21///
22/// 3. **Simple Price Oracle** (Tertiary) - For manual overrides/emergencies
23///    - Admin-controlled prices
24///    - Fallback when others unavailable
25///    - Testing and development
26///
27/// **Example Usage**:
28/// - FLOW: Band Protocol → Pool (if Band fails) → Simple (emergency)
29/// - Froth: Pool Price → Simple (if pool query fails)
30/// - New Token: Pool Price → Simple (manual price)
31///
32access(all) contract OracleAggregator: PriceOracle {
33
34    /// Price data structure (matches PriceOracle interface)
35    /// Uses AnyStruct to handle different oracle implementations
36    access(all) struct PriceData {
37        access(all) let symbol: String
38        access(all) let price: UFix64
39        access(all) let timestamp: UFix64
40        access(all) let confidence: UFix64?
41
42        init(symbol: String, price: UFix64, timestamp: UFix64, confidence: UFix64?) {
43            self.symbol = symbol
44            self.price = price
45            self.timestamp = timestamp
46            self.confidence = confidence
47        }
48    }
49
50    /// Oracle priority levels
51    access(all) enum OraclePriority: UInt8 {
52        access(all) case band        // Highest priority
53        access(all) case pool        // Medium priority
54        access(all) case simple      // Lowest priority (fallback)
55    }
56
57    /// Oracle source tracking for transparency
58    access(all) struct PriceResult {
59        access(all) let priceData: PriceData
60        access(all) let source: String  // "Band", "Pool", "Simple"
61        access(all) let priority: OraclePriority
62
63        init(priceData: PriceData, source: String, priority: OraclePriority) {
64            self.priceData = priceData
65            self.source = source
66            self.priority = priority
67        }
68    }
69
70    /// Storage paths
71    access(all) let OracleStoragePath: StoragePath
72    access(all) let OraclePublicPath: PublicPath
73    access(all) let AdminStoragePath: StoragePath
74
75    /// Oracle configuration
76    access(all) struct OracleConfig {
77        access(all) let useBand: Bool
78        access(all) let usePool: Bool
79        access(all) let useSimple: Bool
80
81        init(useBand: Bool, usePool: Bool, useSimple: Bool) {
82            self.useBand = useBand
83            self.usePool = usePool
84            self.useSimple = useSimple
85        }
86    }
87
88    /// Active oracle configuration
89    access(all) var oracleConfig: OracleConfig
90
91    /// Symbol-specific oracle preferences
92    /// Allows configuring which oracle to prefer for specific tokens
93    /// Example: "FROTH" -> OraclePriority.pool (skip Band, go straight to Pool)
94    access(self) var symbolPreferences: {String: OraclePriority}
95
96    /// Price cache (combined from all oracles)
97    access(self) var aggregateCache: {String: PriceResult}
98
99    /// Cache expiry (60 seconds - aligned with Band)
100    access(all) var cacheExpiry: UFix64
101
102    /// Events
103    access(all) event PriceFetched(
104        symbol: String,
105        price: UFix64,
106        source: String,
107        priority: UInt8,
108        timestamp: UFix64
109    )
110    access(all) event OracleFallback(
111        symbol: String,
112        failedSource: String,
113        fallbackSource: String
114    )
115    access(all) event SymbolPreferenceSet(
116        symbol: String,
117        preferredOracle: UInt8
118    )
119    access(all) event OracleConfigUpdated(
120        useBand: Bool,
121        usePool: Bool,
122        useSimple: Bool
123    )
124    access(all) event CacheExpiryUpdated(newExpiry: UFix64)
125    access(all) event AdminTransferred(oldAdmin: Address, newAdmin: Address, transferredBy: Address)
126
127    /// Admin resource for configuration management
128    /// Only the contract deployer receives this capability
129    access(all) resource Admin {
130
131        /// Set oracle preference for a specific symbol
132        /// Example: setSymbolPreference("FROTH", OraclePriority.pool)
133        /// This will make Froth always query Pool first, skipping Band
134        access(all) fun setSymbolPreference(symbol: String, priority: OraclePriority) {
135            pre {
136                symbol.length > 0: "Symbol cannot be empty"
137            }
138
139            OracleAggregator.symbolPreferences[symbol] = priority
140            emit SymbolPreferenceSet(symbol: symbol, preferredOracle: priority.rawValue)
141        }
142
143        /// Remove symbol preference (revert to waterfall strategy)
144        access(all) fun removeSymbolPreference(symbol: String) {
145            OracleAggregator.symbolPreferences.remove(key: symbol)
146        }
147
148        /// Enable/disable oracle sources
149        /// At least one oracle must remain enabled
150        access(all) fun configureOracles(useBand: Bool, usePool: Bool, useSimple: Bool) {
151            pre {
152                useBand || usePool || useSimple: "At least one oracle must be enabled"
153            }
154
155            OracleAggregator.oracleConfig = OracleConfig(
156                useBand: useBand,
157                usePool: usePool,
158                useSimple: useSimple
159            )
160
161            emit OracleConfigUpdated(
162                useBand: useBand,
163                usePool: usePool,
164                useSimple: useSimple
165            )
166        }
167
168        /// Set cache expiry time (how long to cache aggregated prices)
169        access(all) fun setCacheExpiry(_ newExpiry: UFix64) {
170            pre {
171                newExpiry > 0.0 && newExpiry <= 3600.0: "Cache expiry must be between 0 and 1 hour"
172            }
173
174            OracleAggregator.cacheExpiry = newExpiry
175            emit CacheExpiryUpdated(newExpiry: newExpiry)
176        }
177    }
178
179    /// Oracle implementation
180    access(all) resource Oracle: PriceOracle.Oracle {
181
182        /// Get current price using waterfall strategy
183        access(all) fun getPrice(symbol: String): AnyStruct? {
184            if let result = OracleAggregator.fetchPriceWithFallback(symbol: symbol, maxAge: 300.0) {
185                return result.priceData as AnyStruct
186            }
187            return nil
188        }
189
190        /// Get price with custom staleness check
191        access(all) fun getPriceWithMaxAge(symbol: String, maxAge: UFix64): AnyStruct? {
192            if let result = OracleAggregator.fetchPriceWithFallback(symbol: symbol, maxAge: maxAge) {
193                return result.priceData as AnyStruct
194            }
195            return nil
196        }
197
198        /// Check if token is supported by any oracle
199        access(all) fun supportsToken(symbol: String): Bool {
200            // Check if any oracle can provide a price for this symbol
201            if OracleAggregator.oracleConfig.useBand {
202                if let _ = BandPriceOracle.getPriceWithMaxAge(symbol: symbol, maxAge: 300.0) {
203                    return true
204                }
205            }
206            if OracleAggregator.oracleConfig.usePool {
207                if let _ = PoolPriceOracle.getPrice(symbol: symbol) {
208                    return true
209                }
210            }
211            if OracleAggregator.oracleConfig.useSimple {
212                if let _ = SimplePriceOracle.getPrice(symbol: symbol) {
213                    return true
214                }
215            }
216            return false
217        }
218
219        /// Get all supported tokens (union of all oracles)
220        access(all) fun getSupportedTokens(): [String] {
221            let symbols: {String: Bool} = {}
222
223            if OracleAggregator.oracleConfig.useBand {
224                // Band supported tokens would be listed here
225                // For now, add common ones
226                symbols["FLOW"] = true
227                symbols["BTC"] = true
228                symbols["ETH"] = true
229                symbols["USDC"] = true
230                symbols["USDT"] = true
231            }
232
233            if OracleAggregator.oracleConfig.usePool {
234                // Use public function to get supported tokens
235                let poolSymbols = PoolPriceOracle.getSupportedTokens()
236                for symbol in poolSymbols {
237                    symbols[symbol] = true
238                }
239            }
240
241            if OracleAggregator.oracleConfig.useSimple {
242                // SimplePriceOracle has all symbols in its prices dict
243                // Would need to access it - simplified for now
244            }
245
246            return symbols.keys
247        }
248    }
249
250    /// Fetch price with waterfall fallback strategy
251    access(all) fun fetchPriceWithFallback(symbol: String, maxAge: UFix64): PriceResult? {
252        let currentTime = getCurrentBlock().timestamp
253
254        // Check aggregate cache first
255        if let cachedResult = self.aggregateCache[symbol] {
256            let cacheAge = currentTime - cachedResult.priceData.timestamp
257            if cacheAge <= self.cacheExpiry {
258                return cachedResult  // Cache hit
259            }
260        }
261
262        // Check for symbol-specific preference
263        let preference = self.symbolPreferences[symbol]
264
265        // If preference set, try that oracle first
266        if preference != nil {
267            let result = self.tryOracle(priority: preference!, symbol: symbol, maxAge: maxAge)
268            if result != nil {
269                return self.cacheAndReturn(result!)
270            }
271        }
272
273        // Waterfall: Try each oracle in priority order
274        // 1. Band Protocol (if enabled)
275        if self.oracleConfig.useBand {
276            let result = self.tryOracle(priority: OraclePriority.band, symbol: symbol, maxAge: maxAge)
277            if result != nil {
278                return self.cacheAndReturn(result!)
279            }
280        }
281
282        // 2. Pool Price (if enabled)
283        if self.oracleConfig.usePool {
284            let result = self.tryOracle(priority: OraclePriority.pool, symbol: symbol, maxAge: maxAge)
285            if result != nil {
286                if self.oracleConfig.useBand {
287                    // Emit fallback event if we tried Band first
288                    emit OracleFallback(
289                        symbol: symbol,
290                        failedSource: "Band",
291                        fallbackSource: "Pool"
292                    )
293                }
294                return self.cacheAndReturn(result!)
295            }
296        }
297
298        // 3. Simple Oracle (if enabled)
299        if self.oracleConfig.useSimple {
300            let result = self.tryOracle(priority: OraclePriority.simple, symbol: symbol, maxAge: maxAge)
301            if result != nil {
302                emit OracleFallback(
303                    symbol: symbol,
304                    failedSource: self.oracleConfig.usePool ? "Pool" : "Band",
305                    fallbackSource: "Simple"
306                )
307                return self.cacheAndReturn(result!)
308            }
309        }
310
311        // All oracles failed
312        return nil
313    }
314
315    /// Try to fetch price from a specific oracle
316    access(self) fun tryOracle(priority: OraclePriority, symbol: String, maxAge: UFix64): PriceResult? {
317        var priceData: PriceData? = nil
318        var source = ""
319
320        switch priority {
321            case OraclePriority.band:
322                // Convert BandPriceOracle.PriceData to OracleAggregator.PriceData
323                if let bandPriceAny = BandPriceOracle.getPriceWithMaxAge(symbol: symbol, maxAge: maxAge) {
324                    if let bandPrice = bandPriceAny as? BandPriceOracle.PriceData {
325                        priceData = PriceData(
326                            symbol: bandPrice.symbol,
327                            price: bandPrice.price,
328                            timestamp: bandPrice.timestamp,
329                            confidence: bandPrice.confidence
330                        )
331                    }
332                }
333                source = "Band"
334
335            case OraclePriority.pool:
336                // Convert PoolPriceOracle.PriceData to OracleAggregator.PriceData
337                if let poolPriceAny = PoolPriceOracle.getPrice(symbol: symbol) {
338                    if let poolPrice = poolPriceAny as? PoolPriceOracle.PriceData {
339                        priceData = PriceData(
340                            symbol: poolPrice.symbol,
341                            price: poolPrice.price,
342                            timestamp: poolPrice.timestamp,
343                            confidence: poolPrice.confidence
344                        )
345                    }
346                }
347                source = "Pool"
348
349            case OraclePriority.simple:
350                // Convert SimplePriceOracle.PriceData to OracleAggregator.PriceData
351                if let simplePrice = SimplePriceOracle.getPriceWithMaxAge(symbol: symbol, maxAge: maxAge) {
352                    priceData = PriceData(
353                        symbol: simplePrice.symbol,
354                        price: simplePrice.price,
355                        timestamp: simplePrice.timestamp,
356                        confidence: simplePrice.confidence
357                    )
358                }
359                source = "Simple"
360        }
361
362        if priceData == nil {
363            return nil
364        }
365
366        return PriceResult(
367            priceData: priceData!,
368            source: source,
369            priority: priority
370        )
371    }
372
373    /// Cache result and emit event
374    access(self) fun cacheAndReturn(_ result: PriceResult): PriceResult {
375        // Cache the result
376        self.aggregateCache[result.priceData.symbol] = result
377
378        // Emit event
379        emit PriceFetched(
380            symbol: result.priceData.symbol,
381            price: result.priceData.price,
382            source: result.source,
383            priority: result.priority.rawValue,
384            timestamp: result.priceData.timestamp
385        )
386
387        return result
388    }
389
390    /// Public convenience function
391    access(all) fun getPrice(symbol: String): PriceData? {
392        let result = self.fetchPriceWithFallback(symbol: symbol, maxAge: 300.0)
393        return result?.priceData
394    }
395
396    /// Get price with source information
397    access(all) fun getPriceWithSource(symbol: String): PriceResult? {
398        return self.fetchPriceWithFallback(symbol: symbol, maxAge: 300.0)
399    }
400
401    /// Create a new Oracle resource
402    access(all) fun createOracle(): @Oracle {
403        return <- create Oracle()
404    }
405
406
407    init() {
408        self.OracleStoragePath = /storage/OracleAggregator
409        self.OraclePublicPath = /public/OracleAggregator
410        self.AdminStoragePath = /storage/OracleAggregatorAdmin
411
412        // Enable all oracles by default
413        self.oracleConfig = OracleConfig(useBand: true, usePool: true, useSimple: true)
414
415        // Cache settings
416        self.cacheExpiry = 60.0  // 60 seconds
417
418        // Initialize storage
419        self.symbolPreferences = {}
420        self.aggregateCache = {}
421
422        // Example: Set Pool as preferred oracle for Flow ecosystem tokens
423        // self.symbolPreferences["FROTH"] = OraclePriority.pool
424        // self.symbolPreferences["EMERALD"] = OraclePriority.pool
425
426        self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
427    }
428}
429