Smart Contract

BandPriceOracle

A.17ae3b1b0b0d50db.BandPriceOracle

Valid From

132,672,751

Deployed

1w ago
Feb 21, 2026, 05:42:05 PM UTC

Dependents

0 imports
1import PriceOracle from 0x17ae3b1b0b0d50db
2
3/// BandPriceOracle: Production-ready oracle using Band Protocol
4///
5/// This oracle fetches real-time price data from Band Protocol's decentralized
6/// oracle network on Flow blockchain.
7///
8/// Band Protocol provides:
9/// - Decentralized price feeds from multiple data sources
10/// - Real-time price updates
11/// - High reliability and accuracy
12/// - Multiple cryptocurrency and forex pairs
13/// 
14/// Official Documentation: 
15/// - Flow DeFi Contracts: https://developers.flow.com/ecosystem/defi-liquidity/defi-contracts-mainnet
16/// - Band Protocol: https://docs.bandchain.org/integration/band-standard-dataset/using-band-dataset/
17/// 
18/// Note: BandOracle at 0x6801a6222ebf784a wraps StdReference. We use StdReference directly.
19///
20access(all) contract BandPriceOracle: PriceOracle {
21
22    /// Price data structure (matches PriceOracle interface)
23    access(all) struct PriceData {
24        access(all) let symbol: String
25        access(all) let price: UFix64
26        access(all) let timestamp: UFix64
27        access(all) let confidence: UFix64?
28
29        init(symbol: String, price: UFix64, timestamp: UFix64, confidence: UFix64?) {
30            self.symbol = symbol
31            self.price = price
32            self.timestamp = timestamp
33            self.confidence = confidence
34        }
35    }
36
37    /// Band Protocol StdReference contract interface
38    /// This interface defines how to interact with Band's oracle
39    access(all) resource interface StdReference {
40        access(all) fun getReferenceData(base: String, quote: String): ReferenceData
41        access(all) fun getReferenceDataBulk(bases: [String], quotes: [String]): [ReferenceData]
42    }
43
44    /// Reference data structure from Band Protocol
45    access(all) struct ReferenceData {
46        access(all) let rate: UInt256      // Price * 10^18
47        access(all) let lastUpdatedBase: UInt256
48        access(all) let lastUpdatedQuote: UInt256
49
50        init(rate: UInt256, lastUpdatedBase: UInt256, lastUpdatedQuote: UInt256) {
51            self.rate = rate
52            self.lastUpdatedBase = lastUpdatedBase
53            self.lastUpdatedQuote = lastUpdatedQuote
54        }
55    }
56
57    /// Storage paths
58    access(all) let OracleStoragePath: StoragePath
59    access(all) let OraclePublicPath: PublicPath
60    access(all) let AdminStoragePath: StoragePath
61
62    /// Band Protocol StdReference contract address
63    access(all) var bandReferenceAddress: Address
64
65    /// Price cache to reduce Band Protocol calls
66    /// Maps symbol -> cached price data
67    access(self) var priceCache: {String: PriceData}
68
69    /// Cache expiry time (60 seconds default)
70    access(all) var cacheExpiry: UFix64
71
72    /// Maximum price age to accept from Band (5 minutes)
73    access(all) var maxPriceAge: UFix64
74
75    /// Symbol mapping: DCA symbol -> Band symbol
76    /// Example: "FLOW" -> "FLOW", "USDC" -> "USDC"
77    access(self) var symbolMapping: {String: String}
78
79    /// Events
80    access(all) event PriceFetched(symbol: String, price: UFix64, timestamp: UFix64, source: String)
81    access(all) event PriceCached(symbol: String, price: UFix64, expiresAt: UFix64)
82    access(all) event PriceFeedStale(symbol: String, age: UFix64, maxAge: UFix64)
83    access(all) event BandReferenceUpdated(oldAddress: Address, newAddress: Address)
84    access(all) event SymbolMappingUpdated(dcaSymbol: String, bandSymbol: String)
85    access(all) event CacheExpiryUpdated(newExpiry: UFix64)
86    access(all) event MaxPriceAgeUpdated(newMaxAge: UFix64)
87    access(all) event AdminTransferred(oldAdmin: Address, newAdmin: Address, transferredBy: Address)
88
89    /// Admin resource for configuration management
90    /// Only the contract deployer receives this capability
91    access(all) resource Admin {
92
93        /// Update Band Protocol reference address
94        /// Use this to switch between testnet/mainnet or update to new Band contract
95        access(all) fun setBandReferenceAddress(_ newAddress: Address) {
96            pre {
97                newAddress != Address(0x0): "Invalid Band reference address"
98            }
99
100            let oldAddress = BandPriceOracle.bandReferenceAddress
101            BandPriceOracle.bandReferenceAddress = newAddress
102            emit BandReferenceUpdated(oldAddress: oldAddress, newAddress: newAddress)
103        }
104
105        /// Add symbol mapping between DCA symbol and Band symbol
106        /// Example: addSymbolMapping("FLOW", "FLOW") or addSymbolMapping("USDC.e", "USDC")
107        access(all) fun addSymbolMapping(dcaSymbol: String, bandSymbol: String) {
108            pre {
109                dcaSymbol.length > 0: "DCA symbol cannot be empty"
110                bandSymbol.length > 0: "Band symbol cannot be empty"
111            }
112
113            BandPriceOracle.symbolMapping[dcaSymbol] = bandSymbol
114            emit SymbolMappingUpdated(dcaSymbol: dcaSymbol, bandSymbol: bandSymbol)
115        }
116
117        /// Set cache expiry time (how long to cache prices before refetching)
118        access(all) fun setCacheExpiry(_ newExpiry: UFix64) {
119            pre {
120                newExpiry > 0.0 && newExpiry <= 3600.0: "Cache expiry must be between 0 and 1 hour"
121            }
122
123            BandPriceOracle.cacheExpiry = newExpiry
124            emit CacheExpiryUpdated(newExpiry: newExpiry)
125        }
126
127        /// Set max price age (how old a price can be before considered stale)
128        access(all) fun setMaxPriceAge(_ newMaxAge: UFix64) {
129            pre {
130                newMaxAge > 0.0 && newMaxAge <= 3600.0: "Max price age must be between 0 and 1 hour"
131            }
132
133            BandPriceOracle.maxPriceAge = newMaxAge
134            emit MaxPriceAgeUpdated(newMaxAge: newMaxAge)
135        }
136    }
137
138    /// Oracle implementation
139    access(all) resource Oracle: PriceOracle.Oracle {
140
141        /// Get current price for a token from Band Protocol
142        access(all) fun getPrice(symbol: String): AnyStruct? {
143            if let priceData = BandPriceOracle.fetchPrice(symbol: symbol, maxAge: BandPriceOracle.maxPriceAge) {
144                return priceData as AnyStruct
145            }
146            return nil
147        }
148
149        /// Get price with custom staleness check
150        access(all) fun getPriceWithMaxAge(symbol: String, maxAge: UFix64): AnyStruct? {
151            if let priceData = BandPriceOracle.fetchPrice(symbol: symbol, maxAge: maxAge) {
152                return priceData as AnyStruct
153            }
154            return nil
155        }
156
157        /// Check if token is supported
158        access(all) fun supportsToken(symbol: String): Bool {
159            return BandPriceOracle.symbolMapping.containsKey(symbol)
160        }
161
162        /// Get all supported tokens
163        access(all) fun getSupportedTokens(): [String] {
164            return BandPriceOracle.symbolMapping.keys
165        }
166    }
167
168    /// Fetch price from Band Protocol with caching
169    access(all) fun fetchPrice(symbol: String, maxAge: UFix64): PriceData? {
170        let currentTime = getCurrentBlock().timestamp
171
172        // Check cache first
173        if let cachedPrice = self.priceCache[symbol] {
174            let cacheAge = currentTime - cachedPrice.timestamp
175            if cacheAge <= self.cacheExpiry {
176                // Cache hit - return cached price
177                return cachedPrice
178            }
179        }
180
181        // Cache miss or expired - fetch from Band Protocol
182        return self.fetchFromBand(symbol: symbol, maxAge: maxAge)
183    }
184
185    /// Fetch price directly from Band Protocol
186    access(self) fun fetchFromBand(symbol: String, maxAge: UFix64): PriceData? {
187        // Get Band symbol mapping
188        let bandSymbol = self.symbolMapping[symbol]
189        if bandSymbol == nil {
190            return nil
191        }
192
193        // Get Band StdReference capability
194        let bandReference = getAccount(self.bandReferenceAddress)
195            .capabilities.get<&{StdReference}>(/public/BandStdReference)
196            .borrow()
197
198        if bandReference == nil {
199            // Band Protocol not available - return cached if available
200            return self.priceCache[symbol]
201        }
202
203        // Fetch price from Band: symbol/USD
204        let refData = bandReference!.getReferenceData(
205            base: bandSymbol!,
206            quote: "USD"
207        )
208
209        // Convert Band's rate (UInt256 with 18 decimals) to UFix64
210        // Band rate is price * 10^18, we need UFix64 price
211        let price = self.convertBandRate(refData.rate)
212
213        // Check staleness
214        let currentTime = getCurrentBlock().timestamp
215        let bandUpdateTime = UFix64(refData.lastUpdatedBase)
216        let age = currentTime - bandUpdateTime
217
218        if age > maxAge {
219            emit PriceFeedStale(symbol: symbol, age: age, maxAge: maxAge)
220            return nil
221        }
222
223        // Create PriceData
224        let priceData = PriceData(
225            symbol: symbol,
226            price: price,
227            timestamp: bandUpdateTime,
228            confidence: nil  // Band doesn't provide confidence intervals
229        )
230
231        // Cache the price
232        self.priceCache[symbol] = priceData
233        emit PriceCached(
234            symbol: symbol,
235            price: price,
236            expiresAt: currentTime + self.cacheExpiry
237        )
238
239        emit PriceFetched(
240            symbol: symbol,
241            price: price,
242            timestamp: bandUpdateTime,
243            source: "Band Protocol"
244        )
245
246        return priceData
247    }
248
249    /// Convert Band's UInt256 rate (with 18 decimals) to UFix64 price
250    /// Band rate format: price * 10^18
251    /// Example: FLOW at $1.25 = 1250000000000000000
252    access(self) fun convertBandRate(_ rate: UInt256): UFix64 {
253        // Convert UInt256 to UFix64
254        // Divide by 10^18 to get actual price
255        // Note: This is a simplified conversion
256        // In production, use proper decimal handling
257
258        let rateString = rate.toString()
259        let rateLength = rateString.length
260
261        // If rate is less than 18 digits, price is less than 1
262        if rateLength <= 18 {
263            // Pad with leading zeros
264            let padding = "000000000000000000".slice(from: 0, upTo: 18 - rateLength)
265            let fullRate = padding.concat(rateString)
266            let integerPart = "0"
267            let decimalPart = fullRate
268
269            // Construct UFix64 string
270            let priceString = integerPart.concat(".").concat(decimalPart)
271            return UFix64.fromString(priceString) ?? 0.0
272        }
273
274        // Extract integer and decimal parts
275        let splitPoint = rateLength - 18
276        let integerPart = rateString.slice(from: 0, upTo: splitPoint)
277        let decimalPart = rateString.slice(from: splitPoint, upTo: rateLength)
278
279        // Construct UFix64 string
280        let priceString = integerPart.concat(".").concat(decimalPart)
281
282        return UFix64.fromString(priceString) ?? 0.0
283    }
284
285    /// Public function to get price (convenience)
286    access(all) fun getPrice(symbol: String): PriceData? {
287        return self.fetchPrice(symbol: symbol, maxAge: self.maxPriceAge)
288    }
289
290    /// Public function to get price with staleness check
291    access(all) fun getPriceWithMaxAge(symbol: String, maxAge: UFix64): PriceData? {
292        return self.fetchPrice(symbol: symbol, maxAge: maxAge)
293    }
294
295    /// Create a new Oracle resource
296    access(all) fun createOracle(): @Oracle {
297        return <- create Oracle()
298    }
299
300    init() {
301        self.OracleStoragePath = /storage/BandPriceOracle
302        self.OraclePublicPath = /public/BandPriceOracle
303        self.AdminStoragePath = /storage/BandPriceOracleAdmin
304
305        // Set Band Protocol StdReference address
306        // Testnet: 0x9f857c97e8c50809
307        // Mainnet: 0x1a94aed0e4e6c2a7
308        self.bandReferenceAddress = 0x9f857c97e8c50809  // Testnet default
309
310        // Cache settings
311        self.cacheExpiry = 60.0      // 60 seconds
312        self.maxPriceAge = 300.0     // 5 minutes
313
314        // Initialize price cache
315        self.priceCache = {}
316
317        // Initialize symbol mappings (Band symbol names)
318        self.symbolMapping = {
319            "FLOW": "FLOW",
320            "USDC": "USDC",
321            "USDT": "USDT",
322            "BTC": "BTC",
323            "ETH": "ETH",
324            "FUSD": "FUSD"
325        }
326
327        self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
328    }
329}
330