Smart Contract
PriceOracle
A.e385412159992e11.PriceOracle
1/**
2
3# This contract is the interface description of PriceOracle.
4 The oracle includes an medianizer, which obtains prices from multiple feeds and calculate the median as the final price.
5
6# Structure
7 Feeder1(off-chain) --> PriceFeeder(resource) 3.4$ PriceReader1(resource)
8 Feeder2(off-chain) --> PriceFeeder(resource) 3.2$ --> PriceOracle(contract) cal median 3.4$ --> PriceReader2(resource)
9 Feeder3(off-chain) --> PriceFeeder(resource) 3.6$ PriceReader3(resource)
10
11# Robustness
12 1. Median value is the current aggregation strategy.
13 2. _MinFeederNumber determines the minimum number of feeds required to provide a valid price. As long as there're more than 50% of the nodes are honest the median data is trustworthy.
14 3. The feeder needs to set a price expiration time, after which the price will be invalid (0.0). Dev is responsible to detect and deal with this abnormal data in the contract logic.
15
16# More price-feeding institutions and partners are welcome to join and build a more decentralized oracle on flow.
17# To apply to join the Feeder whitelist, please follow: https://docs.increment.fi/protocols/decentralized-price-feed-oracle/apply-as-feeder
18# On-chain price data can be publicly & freely accessed through the PublicPriceOracle contract.
19
20# Author Increment Labs
21
22*/
23
24import OracleInterface from 0xcec15c814971c1dc
25import OracleConfig from 0xcec15c814971c1dc
26
27access(all) contract PriceOracle: OracleInterface {
28
29 /// The identifier of the token type, eg: BTC/USD
30 access(all) var _PriceIdentifier: String?
31
32 /// Storage path of local oracle certificate
33 access(all) let _CertificateStoragePath: StoragePath
34 /// Storage path of public interface resource
35 access(all) let _OraclePublicStoragePath: StoragePath
36
37 /// The contract will fetch the price according to this path on the feed node
38 access(all) var _PriceFeederPublicPath: PublicPath?
39 access(all) var _PriceFeederStoragePath: StoragePath?
40 /// Recommended path for PriceReader, users can manage resources by themselves
41 access(all) var _PriceReaderStoragePath: StoragePath?
42
43 /// Address white list of feeders and readers
44 access(self) let _FeederWhiteList: {Address: Bool}
45 access(self) let _ReaderWhiteList: {Address: Bool}
46
47 /// The minimum number of feeders to provide a valid price.
48 access(all) var _MinFeederNumber: Int
49
50 /// Reserved parameter fields: {ParamName: Value}
51 access(self) let _reservedFields: {String: AnyStruct}
52
53 /// events
54 access(all) event PublishOraclePrice(price: UFix64, tokenType: String, feederAddr: Address)
55 access(all) event MintPriceReader()
56 access(all) event MintPriceFeeder()
57 access(all) event ConfigOracle(oldType: String?, newType: String?, oldMinFeederNumber: Int, newMinFeederNumber: Int)
58 access(all) event AddFeederWhiteList(addr: Address)
59 access(all) event DelFeederWhiteList(addr: Address)
60 access(all) event AddReaderWhiteList(addr: Address)
61 access(all) event DelReaderWhiteList(addr: Address)
62
63
64 /// Oracle price reader, users need to save this resource in their local storage
65 ///
66 /// Only readers in the addr whitelist have permission to read prices
67 /// Please do not share your PriceReader capability with others and take the responsibility of community governance.
68 ///
69 access(all) resource PriceReader: OracleInterface.PriceReader {
70 access(all) let _PriceIdentifier: String
71
72 /// Get the median price of all current feeds.
73 ///
74 /// @Return Median price, returns 0.0 if the current price is invalid
75 ///
76 access(all) view fun getMedianPrice(): UFix64 {
77 pre {
78 self.owner != nil: "PriceReader resource must be stored in local storage."
79 PriceOracle._ReaderWhiteList.containsKey(self.owner!.address): "Reader addr is not on the whitelist."
80 }
81
82 let priceMedian = PriceOracle.takeMedianPrice()
83
84 return priceMedian
85 }
86
87 access(all) view fun getPriceIdentifier(): String {
88 return self._PriceIdentifier
89 }
90
91 access(all) view fun getRawMedianPrice(): UFix64 {
92 return PriceOracle.getRawMedianPrice()
93 }
94
95 access(all) view fun getRawMedianBlockHeight(): UInt64 {
96 return PriceOracle.getRawMedianBlockHeight()
97 }
98
99 init() {
100 self._PriceIdentifier = PriceOracle._PriceIdentifier!
101 }
102 }
103
104 /// Panel for publishing price. Every feeder needs to mint this resource locally.
105 ///
106 /// TODO: to confirm if contract can be upgraded with change: `OracleInterface.PriceFeederPublic -> OracleInterface.PriceFeeder`
107 access(all) resource PriceFeeder: OracleInterface.PriceFeeder {
108 access(self) var _Price: UFix64
109 access(self) var _LastPublishBlockHeight: UInt64
110 /// seconds
111 access(self) var _ExpiredDuration: UInt64
112
113 access(all) let _PriceIdentifier: String
114
115 /// The feeder uses this function to offer price at the price panel
116 ///
117 /// @Param price - price from off-chain
118 ///
119 access(OracleInterface.FeederAuth) fun publishPrice(price: UFix64) {
120 self._Price = price
121
122 self._LastPublishBlockHeight = getCurrentBlock().height
123 emit PublishOraclePrice(price: price, tokenType: PriceOracle._PriceIdentifier!, feederAddr: self.owner!.address)
124 }
125
126 /// Set valid duration of price. If there is no update within the duration, the price will expire.
127 ///
128 /// @Param blockheightDuration by the block numbers
129 ///
130 access(OracleInterface.FeederAuth) fun setExpiredDuration(blockheightDuration: UInt64) {
131 self._ExpiredDuration = blockheightDuration
132 }
133
134 /// Get the current feed price, returns 0 if the data is expired.
135 /// This function can only be called by the PriceOracle contract
136 ///
137 access(all) view fun fetchPrice(certificate: &{OracleInterface.OracleCertificate}): UFix64 {
138 pre {
139 certificate.getType() == Type<@OracleCertificate>(): "PriceOracle certificate does not match."
140 }
141 if (getCurrentBlock().height - self._LastPublishBlockHeight > self._ExpiredDuration) {
142 return 0.0
143 }
144 return self._Price
145 }
146
147 /// Get the current feed price regardless of whether it's expired or not.
148 ///
149 access(all) view fun getRawPrice(certificate: &{OracleInterface.OracleCertificate}): UFix64 {
150 pre {
151 certificate.getType() == Type<@OracleCertificate>(): "PriceOracle certificate does not match."
152 }
153 return self._Price
154 }
155
156 access(all) view fun getLatestPublishBlockHeight(): UInt64 {
157 return self._LastPublishBlockHeight
158 }
159
160 access(all) view fun getExpiredHeightDuration(): UInt64 {
161 return self._ExpiredDuration
162 }
163
164 init() {
165 self._Price = 0.0
166 self._PriceIdentifier = PriceOracle._PriceIdentifier!
167 self._LastPublishBlockHeight = 0
168 self._ExpiredDuration = 60 * 40
169 }
170 }
171
172 /// All external interfaces of this contract
173 ///
174 access(all) resource OraclePublic: OracleInterface.OraclePublicInterface_Reader, OracleInterface.OraclePublicInterface_Feeder {
175 /// Users who need to read the oracle price should mint this resource and save locally.
176 ///
177 access(all) fun mintPriceReader(): @PriceReader {
178 emit MintPriceReader()
179
180 return <- create PriceReader()
181 }
182
183 /// Feeders need to mint their own price panels and expose the exact public path to oracle contract
184 ///
185 /// @Return Resource of price panel
186 ///
187 access(all) fun mintPriceFeeder(): @{OracleInterface.PriceFeeder} {
188 emit MintPriceFeeder()
189
190 return <- create PriceFeeder()
191 }
192
193 /// Recommended path for PriceReader, users can manage resources by themselves
194 ///
195 access(all) view fun getPriceReaderStoragePath(): StoragePath { return PriceOracle._PriceReaderStoragePath! }
196
197 /// The oracle contract will get the feeding-price based on this path
198 /// Feeders need to expose their price panel capabilities at this public path
199 access(all) view fun getPriceFeederStoragePath(): StoragePath { return PriceOracle._PriceFeederStoragePath! }
200 access(all) view fun getPriceFeederPublicPath(): PublicPath { return PriceOracle._PriceFeederPublicPath! }
201 }
202
203 /// Each oracle contract will hold its own certificate to identify itself.
204 ///
205 /// Only the oracle contract can mint the certificate.
206 ///
207 /// TODO: to confirm if contract can be upgraded with change: `OracleInterface.IdentityCertificate -> OracleInterface.OracleCertificate`
208 access(all) resource OracleCertificate: OracleInterface.OracleCertificate {}
209
210 /// Reader certificate is used to provide proof of its address.In fact, anyone can mint their reader certificate.
211 ///
212 /// Readers only need to apply for a certificate to any oracle contract once.
213 /// The contract will control the read permission of the readers according to the address whitelist.
214 /// Please do not share your certificate capability with others and take the responsibility of community governance.
215 ///
216 access(all) resource ReaderCertificate: OracleInterface.IdentityCertificate {}
217
218
219 /// Calculate the median of the price feed after filtering out expired data
220 ///
221 access(contract) view fun takeMedianPrice(): UFix64 {
222 let certificateRef = self.account.storage.borrow<&OracleCertificate>(from: self._CertificateStoragePath)
223 ?? panic("Lost PriceOracle certificate resource.")
224
225 var priceList: [UFix64] = []
226
227 for oracleAddr in self._FeederWhiteList.keys {
228 let pricePanelCap = getAccount(oracleAddr).capabilities.get<&{OracleInterface.PriceFeederPublic}>(PriceOracle._PriceFeederPublicPath!)
229 // Get valid feeding-price
230 if (pricePanelCap.check()) {
231 let price = pricePanelCap.borrow()!.fetchPrice(certificate: certificateRef)
232 if(price > 0.0) {
233 /// cannot use append() as this is inside view function
234 priceList = priceList.concat([price])
235 }
236 }
237 }
238
239 let len = priceList.length
240 // If the number of valid prices is insufficient
241 if (len < self._MinFeederNumber) {
242 return 0.0
243 }
244 // sort
245 let sortPriceList = OracleConfig.sortUFix64List(list: priceList)
246
247 // find median
248 var mid = 0.0
249 if (len % 2 == 0) {
250 let v1 = sortPriceList[len/2-1]
251 let v2 = sortPriceList[len/2]
252 mid = UFix64(v1+v2)/2.0
253 } else {
254 mid = sortPriceList[(len-1)/2]
255 }
256 return mid
257 }
258
259 access(contract) view fun getFeederWhiteListPrice(): [UFix64] {
260 let certificateRef = self.account.storage.borrow<&OracleCertificate>(from: self._CertificateStoragePath)
261 ?? panic("Lost PriceOracle certificate resource.")
262 var priceList: [UFix64] = []
263
264 for oracleAddr in PriceOracle._FeederWhiteList.keys {
265 let pricePanelCap = getAccount(oracleAddr).capabilities.get<&{OracleInterface.PriceFeederPublic}>(PriceOracle._PriceFeederPublicPath!)
266 if (pricePanelCap.check()) {
267 let price = pricePanelCap.borrow()!.fetchPrice(certificate: certificateRef)
268 if(price > 0.0) {
269 priceList = priceList.concat([price])
270 } else {
271 priceList = priceList.concat([0.0])
272 }
273 } else {
274 priceList = priceList.concat([0.0])
275 }
276 }
277 return priceList
278 }
279
280 /// Calculate the *raw* median of the price feed with no filtering of expired data.
281 ///
282 access(contract) view fun getRawMedianPrice(): UFix64 {
283 let certificateRef = self.account.storage.borrow<&OracleCertificate>(from: self._CertificateStoragePath) ?? panic("Lost PriceOracle certificate resource.")
284 var priceList: [UFix64] = []
285 for oracleAddr in PriceOracle._FeederWhiteList.keys {
286 let pricePanelCap = getAccount(oracleAddr).capabilities.get<&{OracleInterface.PriceFeederPublic}>(PriceOracle._PriceFeederPublicPath!)
287 if (pricePanelCap.check()) {
288 let price = pricePanelCap.borrow()!.getRawPrice(certificate: certificateRef)
289 priceList = priceList.concat([price])
290 } else {
291 priceList = priceList.concat([0.0])
292 }
293 }
294 // sort
295 let sortPriceList = OracleConfig.sortUFix64List(list: priceList)
296
297 // find median
298 let len = priceList.length
299 var mid = 0.0
300 if (len % 2 == 0) {
301 let v1 = sortPriceList[len/2-1]
302 let v2 = sortPriceList[len/2]
303 mid = UFix64(v1+v2)/2.0
304 } else {
305 mid = sortPriceList[(len-1)/2]
306 }
307 return mid
308 }
309
310 /// Calculate the published block height of the *raw* median data. If it's an even list, it is the smaller one of the two middle value.
311 ///
312 access(contract) view fun getRawMedianBlockHeight(): UInt64 {
313 let certificateRef = self.account.storage.borrow<&OracleCertificate>(from: self._CertificateStoragePath) ?? panic("Lost PriceOracle certificate resource.")
314 var latestBlockHeightList: [UInt64] = []
315 for oracleAddr in PriceOracle._FeederWhiteList.keys {
316 let pricePanelCap = getAccount(oracleAddr).capabilities.get<&{OracleInterface.PriceFeederPublic}>(PriceOracle._PriceFeederPublicPath!)
317 if (pricePanelCap.check()) {
318 let latestPublishBlockHeight = pricePanelCap.borrow()!.getLatestPublishBlockHeight()
319 latestBlockHeightList = latestBlockHeightList.concat([latestPublishBlockHeight])
320 } else {
321 latestBlockHeightList = latestBlockHeightList.concat([0])
322 }
323 }
324 // sort
325 let sortHeightList = OracleConfig.sortUInt64List(list: latestBlockHeightList)
326
327 // find median
328 let len = sortHeightList.length
329 var midHeight: UInt64 = 0
330 if (len % 2 == 0) {
331 let h1 = sortHeightList[len/2-1]
332 let h2 = sortHeightList[len/2]
333 midHeight = (h1 < h2)? h1:h2
334 } else {
335 midHeight = sortHeightList[(len-1)/2]
336 }
337 return midHeight
338 }
339
340 access(contract) fun configOracle(
341 priceIdentifier: String,
342 minFeederNumber: Int,
343 feederStoragePath: StoragePath,
344 feederPublicPath: PublicPath,
345 readerStoragePath: StoragePath
346 ) {
347 emit ConfigOracle(
348 oldType: self._PriceIdentifier,
349 newType: priceIdentifier,
350 oldMinFeederNumber: self._MinFeederNumber,
351 newMinFeederNumber: minFeederNumber
352 )
353
354 self._PriceIdentifier = priceIdentifier
355 self._MinFeederNumber = minFeederNumber
356 self._PriceFeederStoragePath = feederStoragePath
357 self._PriceFeederPublicPath = feederPublicPath
358 self._PriceReaderStoragePath = readerStoragePath
359 }
360
361 access(all) view fun getFeederWhiteList(): [Address] {
362 return PriceOracle._FeederWhiteList.keys
363 }
364
365 access(all) view fun getReaderWhiteList(from: UInt64, to: UInt64): [Address] {
366 let readerAddrs = PriceOracle._ReaderWhiteList.keys
367 let readerLen = UInt64(readerAddrs.length)
368 assert(from <= to && from < readerLen, message: "Index out of range")
369 var _to = to
370 if _to == 0 || _to == UInt64.max || _to >= readerLen {
371 _to = readerLen-1
372 }
373 return readerAddrs.slice(from: Int(from), upTo: Int(_to+1))
374 }
375
376 /// Community administrator, Increment Labs will then collect community feedback and initiate voting for governance.
377 ///
378 access(all) resource Admin: OracleInterface.Admin {
379 ///
380 access(all) fun configOracle(
381 priceIdentifier: String,
382 minFeederNumber: Int,
383 feederStoragePath: StoragePath,
384 feederPublicPath: PublicPath,
385 readerStoragePath: StoragePath
386 ) {
387 PriceOracle.configOracle(
388 priceIdentifier: priceIdentifier,
389 minFeederNumber: minFeederNumber,
390 feederStoragePath: feederStoragePath,
391 feederPublicPath: feederPublicPath,
392 readerStoragePath: readerStoragePath
393 )
394 }
395
396 access(all) fun addFeederWhiteList(feederAddr: Address) {
397 // Check if feeder prepared price panel first
398 let PriceFeederCap = getAccount(feederAddr).capabilities.get<&{OracleInterface.PriceFeederPublic}>(PriceOracle._PriceFeederPublicPath!)
399 assert(PriceFeederCap.check(), message: "Need to prepare data feeder resource capability first.")
400
401 PriceOracle._FeederWhiteList[feederAddr] = true
402
403 emit AddFeederWhiteList(addr: feederAddr)
404 }
405
406 access(all) fun addReaderWhiteList(readerAddr: Address) {
407 PriceOracle._ReaderWhiteList[readerAddr] = true
408 emit AddReaderWhiteList(addr: readerAddr)
409 }
410
411 access(all) fun delFeederWhiteList(feederAddr: Address) {
412 PriceOracle._FeederWhiteList.remove(key: feederAddr)
413 emit DelFeederWhiteList(addr: feederAddr)
414 }
415
416 access(all) fun delReaderWhiteList(readerAddr: Address) {
417 PriceOracle._ReaderWhiteList.remove(key: readerAddr)
418 emit DelReaderWhiteList(addr: readerAddr)
419 }
420
421 access(all) view fun getFeederWhiteListPrice(): [UFix64] {
422 return PriceOracle.getFeederWhiteListPrice()
423 }
424
425 access(all) view fun getFeederWhiteList(): [Address] {
426 return PriceOracle._FeederWhiteList.keys
427 }
428
429 access(all) view fun getReaderWhiteList(): [Address] {
430 return PriceOracle._ReaderWhiteList.keys
431 }
432 }
433
434 init() {
435 self._FeederWhiteList = {}
436 self._ReaderWhiteList = {}
437 self._MinFeederNumber = 1
438 self._PriceIdentifier = nil
439
440 self._CertificateStoragePath = /storage/oracle_certificate
441 self._OraclePublicStoragePath = /storage/oracle_public
442
443 self._PriceFeederStoragePath = nil
444 self._PriceFeederPublicPath = nil
445 self._PriceReaderStoragePath = nil
446 self._reservedFields = {}
447
448 // Local admin resource
449 destroy <- self.account.storage.load<@AnyResource>(from: OracleConfig.OracleAdminPath)
450 self.account.storage.save(<-create Admin(), to: OracleConfig.OracleAdminPath)
451 // Create oracle ceritifcate
452 destroy <- self.account.storage.load<@AnyResource>(from: self._CertificateStoragePath)
453 self.account.storage.save(<-create OracleCertificate(), to: self._CertificateStoragePath)
454 // Public interface
455 destroy <- self.account.storage.load<@AnyResource>(from: self._OraclePublicStoragePath)
456 self.account.storage.save(<-create OraclePublic(), to: self._OraclePublicStoragePath)
457 self.account.capabilities.publish(
458 self.account.capabilities.storage.issue<&{OracleInterface.OraclePublicInterface_Reader}>(self._OraclePublicStoragePath),
459 at: OracleConfig.OraclePublicInterface_ReaderPath
460 )
461 self.account.capabilities.publish(
462 self.account.capabilities.storage.issue<&{OracleInterface.OraclePublicInterface_Feeder}>(self._OraclePublicStoragePath),
463 at: OracleConfig.OraclePublicInterface_FeederPath
464 )
465 }
466}