Smart Contract

PriceOracle

A.07e2f8fc48632ece.PriceOracle

Deployed

1w ago
Feb 17, 2026, 05:50:06 PM UTC

Dependents

0 imports
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}