Smart Contract

BandOracle

A.6801a6222ebf784a.BandOracle

Valid From

86,794,168

Deployed

3d ago
Feb 24, 2026, 11:42:25 PM UTC

Dependents

12 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3
4/// The Flow blockchain contract for the Band Protocol Oracle.
5/// https://docs.bandchain.org/
6///
7access(all) contract BandOracle {
8    
9    /// Paths
10
11    // OracleAdmin resource path.
12    access(all) let OracleAdminStoragePath: StoragePath
13
14    // Relay resource path.
15    access(all) let RelayStoragePath: StoragePath
16
17    // FeeCollector resource path.
18    access(all) let FeeCollectorStoragePath: StoragePath
19
20
21    /// Fields
22    
23    // String as base private path for data updater capabilities.
24    access(contract) let dataUpdaterBasePath: String
25
26    // Mapping from symbol to data struct.
27    access(contract) let symbolsRefData: {String: RefData}
28
29    // Aux constant for holding the 10^18 value.
30    access(all) let e18: UInt256
31
32    // Aux constant for holding the 10^9 value.
33    access(all) let e9: UInt256
34
35    // Vault for storing service fees.
36    access(contract) let payments: @{FungibleToken.Vault}
37
38    // Service fee per request.
39    access(contract) var fee: UFix64
40
41    // Mapping of Relayer address to their issued capability ID
42    access(contract) let relayersCapabilityID: {Address: UInt64}
43
44
45    /// Events
46    
47    // Emitted when a relayer updates a set of symbols.
48    access(all) event BandOracleSymbolsUpdated(symbols: [String], relayerID: UInt64, requestID: UInt64)
49
50    // Emitted when a symbol is removed from the oracle.
51    access(all) event BandOracleSymbolRemoved(symbol: String)
52
53    // Emitted when fees are collected.
54    access(all) event FeesCollected(amount: UFix64, to: Address?, collectorUUID: UInt64, collectorAddress: Address)
55
56    // Emitted when fees are updated.
57    access(all) event FeeUpdated(old: UFix64, new: UFix64)
58
59
60    /// Structs
61    
62    /// Structure for storing any symbol USD-rate.
63    ///
64    access(all) struct RefData {
65        /// USD-rate, multiplied by 1e9.
66        access(all) var rate: UInt64
67        /// UNIX epoch when data is last resolved. 
68        access(all) var timestamp: UInt64
69        /// BandChain request identifier for this data.
70        access(all) var requestID: UInt64
71
72        init(rate: UInt64, timestamp: UInt64, requestID: UInt64) {
73            self.rate = rate
74            self.timestamp = timestamp
75            self.requestID = requestID
76        }
77    }
78
79    /// Structure for consuming data as quote / base symbols.
80    ///
81    access(all) struct ReferenceData {
82        /// Base / quote symbols rate multiplied by 10^18.
83        access(all) var integerE18Rate: UInt256
84        /// Base / quote symbols rate as a fixed point number.
85        access(all) var fixedPointRate: UFix64
86        /// UNIX epoch when base data is last resolved. 
87        access(all) var baseTimestamp: UInt64
88        /// UNIX epoch when quote data is last resolved. 
89        access(all) var quoteTimestamp: UInt64
90
91        init(rate: UInt256, baseTimestamp: UInt64, quoteTimestamp: UInt64) {
92            self.integerE18Rate = rate
93            self.fixedPointRate = BandOracle.e18ToFixedPoint(rate: rate)
94            self.baseTimestamp = baseTimestamp
95            self.quoteTimestamp = quoteTimestamp
96        }
97    }
98
99
100    /// Resources
101
102    /// Admin only operations.
103    ///
104    access(all) resource interface OracleAdmin {
105        access(all) fun setRelayerCapabilityID (relayer: Address, capabilityID: UInt64)
106        access(all) fun removeRelayerCapabilityID (relayer: Address)
107        access(all) fun getUpdaterCapabilityIDFromAddress (relayer: Address): UInt64?
108        access(all) fun removeSymbol (symbol: String)
109        access(all) fun createNewFeeCollector (): @BandOracle.FeeCollector
110    }
111
112    /// Relayer operations.
113    ///
114    access(all) resource interface DataUpdater {
115        access(all) fun updateData (symbolsRates: {String: UInt64}, resolveTime: UInt64, 
116                            requestID: UInt64, relayerID: UInt64)
117        access(all) fun forceUpdateData (symbolsRates: {String: UInt64}, resolveTime: UInt64, 
118                            requestID: UInt64, relayerID: UInt64)
119    }
120
121    /// The `BandOracleAdmin` will be created on the contract deployment, and will allow 
122    /// the own admin to manage the oracle and the relayers to update prices on it.
123    ///
124    access(all) resource BandOracleAdmin: OracleAdmin, DataUpdater {
125
126        /// Stores in contract the data updater capability ID along with the address
127        /// of the relayer who got the capability
128        ///
129        /// @param relayer: The entitled relayer account address
130        /// @param capabilityID: The ID of the data updater capability
131        ///
132        access(all) fun setRelayerCapabilityID (relayer: Address, capabilityID: UInt64) {
133            BandOracle.relayersCapabilityID[relayer] = capabilityID
134        }
135
136        /// Deletes a relayer's CapabilityID from `BandOracle.relayersCapabilityID` mapping for traceability purposes
137        /// NOTE: Does not revoke the underlying Capability - this must be done in a separate call from the issuing account
138        ///
139        /// @param relayer: The entitled relayer account address
140        ///
141        access(all) fun removeRelayerCapabilityID (relayer: Address) {
142            BandOracle.relayersCapabilityID.remove(key: relayer)
143        }
144
145        /// Method to retrieve the data updater capability ID from the relayer
146        ///
147        /// @param relayer: The entitled relayer account address
148        ///
149        access(all) fun getUpdaterCapabilityIDFromAddress (relayer: Address): UInt64? {
150            return BandOracle.relayersCapabilityID[relayer]
151        }
152
153        /// Removes a symbol and its quotes from the contract storage.
154        ///
155        /// @param symbol: The string representing the symbol to be removed from the contract.
156        ///
157        access(all) fun removeSymbol (symbol: String) {
158            BandOracle.removeSymbol(symbol: symbol)
159        }
160        
161        /// Relayers can call this method to update rates.
162        ///
163        /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
164        /// @param resolveTime: The registered time for the rates.
165        /// @param requestID: The Band Protocol request ID.
166        /// @param relayerID: The ID of the relayer carrying the update.
167        ///
168        access(all) fun updateData (symbolsRates: {String: UInt64}, resolveTime: UInt64, 
169                            requestID: UInt64, relayerID: UInt64) {
170            BandOracle.updateRefData(symbolsRates: symbolsRates, resolveTime: resolveTime, 
171                                    requestID: requestID, relayerID: relayerID)
172        }
173
174        /// Relayers can call this method to force update rates.
175        ///
176        /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
177        /// @param resolveTime: The registered time for the rates.
178        /// @param requestID: The Band Protocol request ID.
179        /// @param relayerID: The ID of the relayer carrying the update.
180        ///
181        access(all) fun forceUpdateData (symbolsRates: {String: UInt64}, resolveTime: UInt64, 
182                                requestID: UInt64, relayerID: UInt64) {
183            BandOracle.forceUpdateRefData(symbolsRates: symbolsRates, resolveTime: resolveTime, 
184                                    requestID: requestID, relayerID: relayerID)
185        }
186
187        /// Creates a fee collector, meant to be called once after contract deployment
188        /// for storing the resource on the maintainer's account.
189        ///
190        /// @return The `FeeCollector` resource
191        ///
192        access(all) fun createNewFeeCollector (): @FeeCollector {
193            return <- create FeeCollector()
194        }
195
196    }
197
198    /// The resource that will allow an account to make quote updates
199    ///
200    access(all) resource Relay {
201        
202        // Capability linked to the OracleAdmin allowing relayers to relay rate updates
203        access(self) let updaterCapability: Capability<&{DataUpdater}>
204    
205        /// Relay updated rates to the Oracle Admin
206        ///
207        /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
208        /// @param resolveTime: The registered time for the rates.
209        /// @param requestID: The Band Protocol request ID.
210        ///
211        access(all) fun relayRates (symbolsRates: {String: UInt64}, resolveTime: UInt64, requestID: UInt64) {
212            let updaterRef = self.updaterCapability.borrow() 
213                ?? panic ("Can't borrow reference to data updater while processing request ".concat(requestID.toString()))
214            updaterRef.updateData(symbolsRates: symbolsRates, resolveTime: resolveTime, requestID: requestID, relayerID: self.uuid)
215        }
216
217        /// Relay updated rates to the Oracle Admin forcing the update of the symbols even if the `resolveTime` is older than the last update.
218        ///
219        /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
220        /// @param resolveTime: The registered time for the rates.
221        /// @param requestID: The Band Protocol request ID.
222        ///
223        access(all) fun forceRelayRates (symbolsRates: {String: UInt64}, resolveTime: UInt64, requestID: UInt64) {
224            let updaterRef = self.updaterCapability.borrow() 
225                ?? panic ("Can't borrow reference to data updater while processing request ".concat(requestID.toString()))
226            updaterRef.forceUpdateData(symbolsRates: symbolsRates, resolveTime: resolveTime, requestID: requestID, relayerID: self.uuid)
227        }
228
229        init(updaterCapability: Capability<&{DataUpdater}>) {
230            self.updaterCapability = updaterCapability
231            let updaterRef = self.updaterCapability.borrow() 
232                ?? panic ("Can't borrow linked updater")
233        }
234    }
235
236    /// The resource that allows the maintainer account to charge a fee for the use of the oracle.
237    ///
238    access(all) resource FeeCollector {
239
240        /// Sets the fee in Flow tokens for the oracle use.
241        ///
242        /// @param fee: The amount of Flow tokens.
243        ///
244        access(all) fun setFee (fee: UFix64) {
245            BandOracle.setFee(fee: fee)
246            emit FeeUpdated(old: BandOracle.fee, new: fee)
247        }
248
249        /// Extracts the fees from the contract's vault.
250        /// 
251        /// @return A vault containing the funds obtained for the oracle use.
252        ///
253        access(all) fun collectFees (receiver: &{FungibleToken.Receiver}) {
254            let fees <- BandOracle.collectFees()
255            emit FeesCollected(amount: fees.balance, to: receiver.owner?.address, collectorUUID: self.uuid, collectorAddress: self.owner!.address)
256            receiver.deposit(from: <-fees)
257        }
258
259    }
260
261
262    /// Functions
263
264    /// Aux access(contract) functions
265
266    /// Auxiliary private function for the `OracleAdmin` to update the rates.
267    ///
268    /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
269    /// @param resolveTime: The registered time for the rates.
270    /// @param requestID: The Band Protocol request ID.
271    /// @param relayerID: The ID of the relayer carrying the update.
272    ///
273    access(contract) fun updateRefData (symbolsRates: {String: UInt64}, resolveTime: UInt64, requestID: UInt64, relayerID: UInt64) {
274        let updatedSymbols: [String] = []
275        // For each symbol rate relayed
276        for symbol in symbolsRates.keys {
277            // If the symbol hasn't stored rates yet, or the stored records are older
278            // than the new relayed rates
279            if (BandOracle.symbolsRefData[symbol] == nil ) ||
280                (BandOracle.symbolsRefData[symbol]!.timestamp < resolveTime) {
281                // Store the relayed rate
282                BandOracle.symbolsRefData[symbol] = 
283                    RefData(rate: symbolsRates[symbol]!, timestamp: resolveTime, requestID: requestID)
284                updatedSymbols.append(symbol)
285            }
286        }
287        emit BandOracleSymbolsUpdated(symbols: updatedSymbols, relayerID: relayerID, requestID: requestID)
288    }
289
290    /// Auxiliary private function for the `OracleAdmin` to force update the rates.
291    ///
292    /// @param symbolsRates: Set of symbols and corresponding usd rates to update.
293    /// @param resolveTime: The registered time for the rates.
294    /// @param requestID: The Band Protocol request ID.
295    /// @param relayerID: The ID of the relayer carrying the update.
296    ///
297    access(contract) fun forceUpdateRefData (symbolsRates: {String: UInt64}, resolveTime: UInt64, requestID: UInt64, relayerID: UInt64) {
298        // For each symbol rate relayed, store it no matter what was the previous
299        // records for it
300        for symbol in symbolsRates.keys {
301            BandOracle.symbolsRefData[symbol] = 
302                RefData(rate: symbolsRates[symbol]!, timestamp: resolveTime, requestID: requestID)
303        }
304        emit BandOracleSymbolsUpdated(symbols: symbolsRates.keys, relayerID: relayerID, requestID: requestID)
305    }
306
307    /// Auxiliary private function for removing a stored symbol
308    ///
309    /// @param symbol: The string representing the symbol to delete
310    ///
311    access(contract) fun removeSymbol (symbol: String) {
312        BandOracle.symbolsRefData.remove(key: symbol)
313        emit BandOracleSymbolRemoved(symbol: symbol)
314    }
315
316    /// Auxiliary private function for checking and retrieving data for a given symbol.
317    ///
318    /// @param symbol: String representing a symbol.
319    /// @return Optional `RefData` struct if there is any quote stored for the requested symbol.
320    ///
321    access(contract) fun _getRefData (symbol: String): RefData? {
322        // If the requested symbol is USD just return 10^9
323        if (symbol == "USD") {
324            return RefData(rate: UInt64(BandOracle.e9), timestamp: UInt64(getCurrentBlock().timestamp), requestID: 0)
325        } else {
326            return self.symbolsRefData[symbol] ?? nil
327        }
328    }
329
330    /// Private function that calculates the reference data between two base and quote symbols.
331    ///
332    /// @param baseRefData: Base ref data.
333    /// @param quoteRefData: Quote ref data.
334    /// @return Calculated `ReferenceData` structure.
335    ///
336    access(contract) fun calculateReferenceData (baseRefData: RefData, quoteRefData: RefData): ReferenceData {
337            let rate = UInt256((UInt256(baseRefData.rate) * BandOracle.e18) / UInt256(quoteRefData.rate))
338            return ReferenceData (rate: rate,
339                            baseTimestamp: baseRefData.timestamp,
340                            quoteTimestamp: quoteRefData.timestamp)
341    }
342
343    /// Private method for the `FeeCollector` to be able to set the fee for using the oracle
344    ///
345    /// @param fee: The amount of flow tokens to set as fee.
346    ///
347    access(contract) fun setFee (fee: UFix64) {
348        BandOracle.fee = fee
349    }
350
351    /// Private method for the `FeeCollector` to be able to collect the fees from the contract vault.
352    ///
353    /// @return A flow token vault with the collected fees so far.
354    ///
355    access(contract) fun collectFees (): @{FungibleToken.Vault} {
356        let collectedFees <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
357        collectedFees.deposit(from: <- BandOracle.payments.withdraw(amount: BandOracle.payments.balance))
358        return <- collectedFees
359    }
360
361
362    /// Public access functions.
363
364    /// Public method for creating a relay and become a relayer.
365    ///
366    /// @param updaterCapability: The capability pointing to the OracleAdmin resource needed to create the relay.
367    /// @return The new relay resource.
368    ///
369    access(all) fun createRelay (updaterCapability: Capability<&{DataUpdater}>): @Relay {
370        return <- create Relay(updaterCapability: updaterCapability)
371    }
372
373    /// Auxiliary method to ensure that the formation of the capability name that 
374    /// identifies data updater capability for relayers is done in a uniform way
375    /// by both admin and relayers.
376    ///
377    /// @param relayer: Address of the account who will be granted with a relayer.
378    /// @return The capability name.
379    ///
380    access(all) view fun getUpdaterCapabilityNameFromAddress (relayer: Address): String {
381        // Create the string that will form the private path concatenating the base
382        // path and the relayer identifying address.
383        let capabilityName = 
384            BandOracle.dataUpdaterBasePath.concat(relayer.toString())
385        return capabilityName
386    }
387
388    /// This function returns the current fee for using the oracle in Flow tokens.
389    ///
390    /// @return The fee to be charged for every request made to the oracle.
391    ///
392    access(all) view fun getFee (): UFix64 {
393        return BandOracle.fee
394    }
395
396    /// The entry point for consumers to query the oracle in exchange of a fee.
397    ///
398    /// @param baseSymbol: String representing base symbol.
399    /// @param quoteSymbol: String representing quote symbol.
400    /// @param payment: Flow token vault containing the service fee.
401    /// @return The `ReferenceData` containing the requested data.
402    ///
403    access(all) fun getReferenceData (baseSymbol: String, quoteSymbol: String, payment: @{FungibleToken.Vault}): ReferenceData {
404        pre {
405            payment.balance >= BandOracle.fee : "Insufficient balance"
406        }
407        if (BandOracle._getRefData(symbol: baseSymbol) != nil && BandOracle._getRefData(symbol: quoteSymbol) != nil){
408            let baseRefData = BandOracle._getRefData(symbol: baseSymbol)!
409            let quoteRefData = BandOracle._getRefData(symbol: quoteSymbol)!
410            BandOracle.payments.deposit(from: <- payment)
411            return BandOracle.calculateReferenceData (baseRefData: baseRefData, quoteRefData: quoteRefData)
412        } else {
413            panic("Cannot get a quote for the requested symbol pair.")
414        }
415    }
416
417    /// Turn scientific notation numbers as `UInt256` multiplied by e8 into `UFix64`
418    /// fixed point numbers. Exceptionally large integer rates may lose some precision
419    /// when converted to a decimal number.
420    ///
421    /// @param rate: The symbol rate as an integer.
422    /// @return The symbol rate as a decimal.
423    ///
424    access(all) view fun e18ToFixedPoint (rate: UInt256): UFix64 {
425        return  (
426                    UFix64(
427                        rate / BandOracle.e18
428                    ) 
429                        + 
430                    (
431                        UFix64(
432                            (rate 
433                                / 
434                            BandOracle.e9) 
435                                % 
436                            BandOracle.e9
437                        )
438                            /
439                        UFix64(BandOracle.e9)
440                    ) 
441                )
442    }
443
444    init() {
445        self.OracleAdminStoragePath = /storage/BandOracleAdmin
446        self.RelayStoragePath = /storage/BandOracleRelay
447        self.FeeCollectorStoragePath = /storage/BandOracleFeeCollector
448        self.dataUpdaterBasePath = "BandOracleDataUpdater_"
449        self.account.storage.save(<- create BandOracleAdmin(), to: self.OracleAdminStoragePath)
450        self.symbolsRefData = {}
451        self.relayersCapabilityID = {}
452        self.payments <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
453        self.fee = 0.0
454        self.e18 = 1_000_000_000_000_000_000
455        self.e9 = 1_000_000_000
456        // Create a relayer on the admin account so the relay methods are never accessed directly.
457        // The admin could decide to build a transaction borrowing the whole BandOracleAdmin
458        // resource and call updateData methods bypassing relayData methods but we are explicitly
459        // discouraging that by giving the admin a regular relay resource on contract deployment.
460        let oracleAdminRef = self.account.storage.borrow<&{OracleAdmin}>(from: BandOracle.OracleAdminStoragePath)
461            ?? panic("Can't borrow a reference to the Oracle Admin")
462        let updaterCapability = self.account.capabilities.storage.issue<&{BandOracle.DataUpdater}>(BandOracle.OracleAdminStoragePath)
463        let relayer <- BandOracle.createRelay(updaterCapability: updaterCapability)
464        self.account.storage.save(<- relayer, to: BandOracle.RelayStoragePath)
465    }
466}