Smart Contract
OracleAggregator
A.17ae3b1b0b0d50db.OracleAggregator
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