Smart Contract
BandOracle
A.6801a6222ebf784a.BandOracle
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}