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