Skip to main content

evm_oracle_state/
cache_reader.rs

1use std::{
2    cell::RefCell,
3    collections::{BTreeMap, BTreeSet},
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7#[cfg(feature = "aave")]
8use alloy_primitives::I256;
9use alloy_primitives::{Address, Bytes, U256};
10use alloy_sol_types::{SolCall, sol};
11use evm_fork_cache::{
12    cache::EvmCache,
13    multicall::{IMulticall3, MULTICALL3_ADDRESS, execute_batched, try_decode_result},
14};
15
16use crate::{
17    AggregatorLayoutEvidence, ChainlinkFeedProvider, Feed, FeedConfig, FeedId, FeedMetadata,
18    FeedRegistration, FeedSource, OracleError, OracleFeedStatus, OracleRegistry, OracleTracker,
19    ProviderFuture, RoundData, registry::derive_feed_id,
20};
21
22sol! {
23    interface AggregatorProxyInterface {
24        function latestRoundData() external view returns (
25            uint80 roundId,
26            int256 answer,
27            uint256 startedAt,
28            uint256 updatedAt,
29            uint80 answeredInRound
30        );
31        function decimals() external view returns (uint8);
32        function description() external view returns (string);
33        function version() external view returns (uint256);
34        function aggregator() external view returns (address);
35        function typeAndVersion() external view returns (string);
36    }
37}
38
39use AggregatorProxyInterface::{
40    aggregatorCall, decimalsCall, descriptionCall, latestRoundDataCall, typeAndVersionCall,
41    versionCall,
42};
43
44#[derive(Debug)]
45struct ChainlinkCoreRead {
46    decimals: u8,
47    description: String,
48    version: U256,
49    round: RoundData,
50    aggregator: Option<Address>,
51}
52
53#[cfg(feature = "aave")]
54sol! {
55    interface AaveOracleInterface {
56        function getSourceOfAsset(address asset) external view returns (address);
57    }
58
59    interface AavePriceCapAdapterStableInterface {
60        function ASSET_TO_USD_AGGREGATOR() external view returns (address);
61        function getPriceCap() external view returns (int256);
62        function decimals() external view returns (uint8);
63        function description() external view returns (string);
64        function latestAnswer() external view returns (int256);
65    }
66
67    interface AaveRatioCapAdapterInterface {
68        function BASE_TO_USD_AGGREGATOR() external view returns (address);
69        function RATIO_PROVIDER() external view returns (address);
70        function RATIO_DECIMALS() external view returns (uint8);
71        function getRatio() external view returns (int256);
72        function isCapped() external view returns (bool);
73        function decimals() external view returns (uint8);
74        function description() external view returns (string);
75        function latestAnswer() external view returns (int256);
76    }
77
78    interface AaveSynchronicityPegToBaseInterface {
79        function ASSET_TO_PEG() external view returns (address);
80        function PEG_TO_BASE() external view returns (address);
81        function decimals() external view returns (uint8);
82        function description() external view returns (string);
83        function latestAnswer() external view returns (int256);
84    }
85
86    interface AaveFixedPriceSourceInterface {
87        function price() external view returns (int256);
88        function decimals() external view returns (uint8);
89        function description() external view returns (string);
90        function latestAnswer() external view returns (int256);
91    }
92
93    interface AaveConstantPriceSourceInterface {
94        function PRICE() external view returns (int256);
95    }
96
97    interface AaveBaseToPegProbeInterface {
98        function BASE_TO_PEG() external view returns (address);
99    }
100
101    interface AaveDynamicSourceProbeInterface {
102        function REFERENCE_FEED() external view returns (address);
103        function DISCOUNT_RATE() external view returns (uint256);
104        function discount() external view returns (uint256);
105        function EXCHANGE_RATE() external view returns (uint256);
106        function PENDLE_PRINCIPAL_TOKEN() external view returns (address);
107        function PENDLE_ORACLE() external view returns (address);
108    }
109}
110
111#[cfg(feature = "aave")]
112use AaveBaseToPegProbeInterface::BASE_TO_PEGCall;
113#[cfg(feature = "aave")]
114use AaveConstantPriceSourceInterface::PRICECall;
115#[cfg(feature = "aave")]
116use AaveDynamicSourceProbeInterface::{
117    DISCOUNT_RATECall, EXCHANGE_RATECall, PENDLE_ORACLECall, PENDLE_PRINCIPAL_TOKENCall,
118    REFERENCE_FEEDCall, discountCall,
119};
120#[cfg(feature = "aave")]
121use AaveFixedPriceSourceInterface::{
122    decimalsCall as aaveFixedDecimalsCall, descriptionCall as aaveFixedDescriptionCall,
123    latestAnswerCall as aaveFixedLatestAnswerCall, priceCall as aaveFixedPriceCall,
124};
125#[cfg(feature = "aave")]
126use AaveOracleInterface::getSourceOfAssetCall;
127#[cfg(feature = "aave")]
128use AavePriceCapAdapterStableInterface::{
129    ASSET_TO_USD_AGGREGATORCall, decimalsCall as aaveDecimalsCall,
130    descriptionCall as aaveDescriptionCall, getPriceCapCall, latestAnswerCall,
131};
132#[cfg(feature = "aave")]
133use AaveRatioCapAdapterInterface::{
134    BASE_TO_USD_AGGREGATORCall, RATIO_DECIMALSCall, RATIO_PROVIDERCall,
135    decimalsCall as aaveRatioDecimalsCall, descriptionCall as aaveRatioDescriptionCall,
136    getRatioCall, isCappedCall, latestAnswerCall as aaveRatioLatestAnswerCall,
137};
138#[cfg(feature = "aave")]
139use AaveSynchronicityPegToBaseInterface::{
140    ASSET_TO_PEGCall, PEG_TO_BASECall, decimalsCall as aaveSynchronicityDecimalsCall,
141    descriptionCall as aaveSynchronicityDescriptionCall,
142    latestAnswerCall as aaveSynchronicityLatestAnswerCall,
143};
144
145#[cfg(feature = "aave")]
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub(crate) struct AavePriceCapStableSource {
148    pub(crate) underlying_proxy: Address,
149    pub(crate) price_cap: I256,
150    pub(crate) decimals: u8,
151    pub(crate) description: String,
152    pub(crate) latest_answer: I256,
153}
154
155#[cfg(feature = "aave")]
156#[derive(Clone, Debug, PartialEq, Eq)]
157pub(crate) struct AaveRatioCapSource {
158    pub(crate) base_to_usd_proxy: Address,
159    pub(crate) ratio_provider: Address,
160    pub(crate) current_ratio: I256,
161    pub(crate) is_capped: bool,
162    pub(crate) ratio_decimals: u8,
163    pub(crate) decimals: u8,
164    pub(crate) description: String,
165    pub(crate) latest_answer: I256,
166}
167
168#[cfg(feature = "aave")]
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub(crate) struct AaveSynchronicityPegToBaseSource {
171    pub(crate) asset_to_peg_proxy: Address,
172    pub(crate) peg_to_base_proxy: Address,
173    pub(crate) decimals: u8,
174    pub(crate) description: String,
175    pub(crate) latest_answer: I256,
176}
177
178#[cfg(feature = "aave")]
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub(crate) struct AaveFixedPriceSource {
181    pub(crate) decimals: u8,
182    pub(crate) description: String,
183    pub(crate) latest_answer: I256,
184}
185
186/// Chainlink view reader backed by an `evm-fork-cache` fork cache.
187pub struct EvmCacheChainlinkReader<'a> {
188    cache: RefCell<&'a mut EvmCache>,
189    multicall_reads: bool,
190}
191
192impl<'a> EvmCacheChainlinkReader<'a> {
193    /// Create a reader around an existing mutable EVM cache.
194    pub fn new(cache: &'a mut EvmCache) -> Self {
195        Self {
196            cache: RefCell::new(cache),
197            multicall_reads: true,
198        }
199    }
200
201    /// Create a reader with direct/fork Multicall3 view batching disabled.
202    pub fn without_multicall(cache: &'a mut EvmCache) -> Self {
203        Self {
204            cache: RefCell::new(cache),
205            multicall_reads: false,
206        }
207    }
208
209    /// Enable or disable direct/fork Multicall3 view batching.
210    pub fn with_multicall_reads(mut self, enabled: bool) -> Self {
211        self.multicall_reads = enabled;
212        self
213    }
214
215    /// Register a typed feed into an existing oracle registry.
216    pub async fn register_feed(
217        &self,
218        registry: &mut OracleRegistry,
219        feed: Feed,
220    ) -> Result<FeedId, OracleError> {
221        let config = FeedConfig::try_from(feed)?;
222        self.register_config(registry, config).await
223    }
224
225    /// Register multiple Chainlink-compatible feeds, batching proxy reads when possible.
226    pub async fn register_feeds(
227        &self,
228        registry: &mut OracleRegistry,
229        feeds: Vec<Feed>,
230    ) -> Result<Vec<OracleAdapterFeedSkip>, OracleError> {
231        let mut prepared = Vec::with_capacity(feeds.len());
232        for feed in feeds {
233            let proxy = feed.proxy().unwrap_or_default();
234            let config = FeedConfig::try_from(feed.clone())?;
235            let read_proxy = FeedSource::Chainlink.read_proxy(config.proxy);
236            prepared.push((feed, config, proxy, read_proxy));
237        }
238
239        let read_proxies = prepared
240            .iter()
241            .map(|(_, _, _, read_proxy)| *read_proxy)
242            .collect::<Vec<_>>();
243        let mut cores = self
244            .read_chainlink_cores_multicall(&read_proxies)
245            .unwrap_or_default();
246        let mut layouts = self.layout_evidence_for_cores(&cores);
247
248        let mut skipped = Vec::new();
249        for (index, (feed, config, proxy, read_proxy)) in prepared.into_iter().enumerate() {
250            let mut from_batched_core = false;
251            let core = match cores.get_mut(index).and_then(Option::take) {
252                Some(Ok(core)) => {
253                    from_batched_core = true;
254                    Ok(core)
255                }
256                Some(Err(error)) => Err(error),
257                None => self.read_chainlink_core(read_proxy),
258            };
259
260            match core {
261                Ok(core) => {
262                    if from_batched_core {
263                        if let Some(layouts) = layouts.as_mut() {
264                            let layout = layouts.get_mut(index).and_then(Option::take);
265                            self.insert_config_core_with_layout(registry, config, core, layout)?;
266                        } else {
267                            self.insert_config_core(registry, config, core).await?;
268                        }
269                    } else {
270                        self.insert_config_core(registry, config, core).await?;
271                    }
272                }
273                Err(OracleError::Provider(error)) => {
274                    skipped.push(OracleAdapterFeedSkip {
275                        feed,
276                        proxy,
277                        reason: OracleAdapterSkipReason::NotChainlinkCompatible { error },
278                    });
279                }
280                // A proxy whose reads decode but whose values do not fit the
281                // crate's typed round representation is equally not usable as
282                // a Chainlink-compatible feed: classify it as a skip instead
283                // of failing the whole best-effort registration batch.
284                Err(error @ OracleError::Decode(_)) => {
285                    skipped.push(OracleAdapterFeedSkip {
286                        feed,
287                        proxy,
288                        reason: OracleAdapterSkipReason::NotChainlinkCompatible {
289                            error: error.to_string(),
290                        },
291                    });
292                }
293                Err(error) => return Err(error),
294            }
295        }
296
297        Ok(skipped)
298    }
299
300    /// Register a Chainlink-compatible feed config into an existing registry.
301    pub async fn register_config(
302        &self,
303        registry: &mut OracleRegistry,
304        config: FeedConfig,
305    ) -> Result<FeedId, OracleError> {
306        let source = FeedSource::Chainlink;
307        let read_proxy = source.read_proxy(config.proxy);
308        let core = self.read_chainlink_core(read_proxy)?;
309        self.insert_config_core(registry, config, core).await
310    }
311
312    fn layout_evidence_for_cores(
313        &self,
314        cores: &[Option<Result<ChainlinkCoreRead, OracleError>>],
315    ) -> Option<Vec<Option<AggregatorLayoutEvidence>>> {
316        let mut aggregators = Vec::new();
317        let mut seen_aggregators = BTreeSet::new();
318        for core in cores.iter().filter_map(|core| match core {
319            Some(Ok(core)) => core.aggregator,
320            _ => None,
321        }) {
322            if seen_aggregators.insert(core) {
323                aggregators.push(core);
324            }
325        }
326        if aggregators.is_empty() {
327            return Some(vec![None; cores.len()]);
328        }
329
330        let mut type_and_versions = self.read_type_and_versions_multicall(&aggregators)?;
331        let mut evidence_by_aggregator = BTreeMap::new();
332        for (index, aggregator) in aggregators.iter().copied().enumerate() {
333            let evidence = type_and_versions
334                .get_mut(index)
335                .and_then(Option::take)
336                .map(|type_and_version| {
337                    AggregatorLayoutEvidence::from_type_and_version(
338                        aggregator,
339                        type_and_version,
340                        None,
341                    )
342                })
343                .unwrap_or_else(|| AggregatorLayoutEvidence::unknown(aggregator, None));
344            evidence_by_aggregator.insert(aggregator, evidence);
345        }
346
347        Some(
348            cores
349                .iter()
350                .map(|core| {
351                    let aggregator = match core {
352                        Some(Ok(core)) => core.aggregator?,
353                        _ => return None,
354                    };
355                    evidence_by_aggregator.get(&aggregator).cloned()
356                })
357                .collect(),
358        )
359    }
360
361    async fn insert_config_core(
362        &self,
363        registry: &mut OracleRegistry,
364        config: FeedConfig,
365        core: ChainlinkCoreRead,
366    ) -> Result<FeedId, OracleError> {
367        let aggregator_layout = registry
368            .detect_aggregator_layout(self, core.aggregator, None)
369            .await;
370        self.insert_config_core_with_layout(registry, config, core, aggregator_layout)
371    }
372
373    fn insert_config_core_with_layout(
374        &self,
375        registry: &mut OracleRegistry,
376        config: FeedConfig,
377        core: ChainlinkCoreRead,
378        aggregator_layout: Option<AggregatorLayoutEvidence>,
379    ) -> Result<FeedId, OracleError> {
380        let id = config
381            .id
382            .unwrap_or_else(|| derive_feed_id(config.label.as_deref(), config.proxy));
383        let source = FeedSource::Chainlink;
384        let registration = FeedRegistration {
385            id: id.clone(),
386            proxy: config.proxy,
387            label: config.label,
388            base: config.base,
389            quote: config.quote,
390            staleness: config.staleness,
391            current_aggregator: core.aggregator,
392            aggregator_layout,
393            metadata: FeedMetadata {
394                decimals: core.decimals,
395                description: core.description,
396                version: core.version,
397            },
398            source,
399            status: OracleFeedStatus::Ready,
400        };
401        registry.insert_seeded_registration(registration, core.round)?;
402        Ok(id)
403    }
404
405    /// Read `decimals()` from a Chainlink-compatible proxy.
406    pub fn read_decimals(&self, proxy: Address) -> Result<u8, OracleError> {
407        self.cache
408            .borrow_mut()
409            .call_sol(proxy, decimalsCall {})
410            .map_err(provider_error)
411    }
412
413    /// Read `description()` from a Chainlink-compatible proxy.
414    pub fn read_description(&self, proxy: Address) -> Result<String, OracleError> {
415        self.cache
416            .borrow_mut()
417            .call_sol(proxy, descriptionCall {})
418            .map_err(provider_error)
419    }
420
421    /// Read `version()` from a Chainlink-compatible proxy.
422    pub fn read_version(&self, proxy: Address) -> Result<U256, OracleError> {
423        self.cache
424            .borrow_mut()
425            .call_sol(proxy, versionCall {})
426            .map_err(provider_error)
427    }
428
429    /// Read `latestRoundData()` from a Chainlink-compatible proxy.
430    pub fn read_latest_round_data(&self, proxy: Address) -> Result<RoundData, OracleError> {
431        let round = self
432            .cache
433            .borrow_mut()
434            .call_sol(proxy, latestRoundDataCall {})
435            .map_err(provider_error)?;
436        round_from_raw(round)
437    }
438
439    /// Best-effort read of the proxy's current aggregator.
440    pub fn read_aggregator(&self, proxy: Address) -> Result<Option<Address>, OracleError> {
441        let aggregator = self
442            .cache
443            .borrow_mut()
444            .call_sol(proxy, aggregatorCall {})
445            .map_err(provider_error)?;
446        Ok(Some(aggregator))
447    }
448
449    /// Best-effort read of an aggregator implementation `typeAndVersion()`.
450    pub fn read_type_and_version(
451        &self,
452        aggregator: Address,
453    ) -> Result<Option<String>, OracleError> {
454        let type_and_version = self
455            .cache
456            .borrow_mut()
457            .call_sol(aggregator, typeAndVersionCall {})
458            .map_err(provider_error)?;
459        Ok(Some(type_and_version))
460    }
461
462    fn read_chainlink_core(&self, proxy: Address) -> Result<ChainlinkCoreRead, OracleError> {
463        if let Some(read) = self.read_chainlink_core_multicall(proxy) {
464            return read;
465        }
466
467        Ok(ChainlinkCoreRead {
468            decimals: self.read_decimals(proxy)?,
469            description: self.read_description(proxy)?,
470            version: self.read_version(proxy)?,
471            round: self.read_latest_round_data(proxy)?,
472            aggregator: self.read_aggregator(proxy)?,
473        })
474    }
475
476    fn read_chainlink_core_multicall(
477        &self,
478        proxy: Address,
479    ) -> Option<Result<ChainlinkCoreRead, OracleError>> {
480        let results = self.execute_multicall([
481            multicall_call(proxy, decimalsCall {}, true),
482            multicall_call(proxy, descriptionCall {}, true),
483            multicall_call(proxy, versionCall {}, true),
484            multicall_call(proxy, latestRoundDataCall {}, true),
485            multicall_call(proxy, aggregatorCall {}, true),
486        ])?;
487        if results.len() != 5 {
488            return None;
489        }
490
491        let decimals = try_decode_result::<decimalsCall>(&results[0])?;
492        let description = try_decode_result::<descriptionCall>(&results[1])?;
493        let version = try_decode_result::<versionCall>(&results[2])?;
494        let raw_round = try_decode_result::<latestRoundDataCall>(&results[3])?;
495        let aggregator = try_decode_result::<aggregatorCall>(&results[4]);
496        let round = match round_from_raw(raw_round) {
497            Ok(round) => round,
498            Err(error) => return Some(Err(error)),
499        };
500        Some(Ok(ChainlinkCoreRead {
501            decimals,
502            description,
503            version,
504            round,
505            aggregator,
506        }))
507    }
508
509    fn read_chainlink_cores_multicall(
510        &self,
511        proxies: &[Address],
512    ) -> Option<Vec<Option<Result<ChainlinkCoreRead, OracleError>>>> {
513        let calls = proxies
514            .iter()
515            .flat_map(|proxy| {
516                [
517                    multicall_call(*proxy, decimalsCall {}, true),
518                    multicall_call(*proxy, descriptionCall {}, true),
519                    multicall_call(*proxy, versionCall {}, true),
520                    multicall_call(*proxy, latestRoundDataCall {}, true),
521                    multicall_call(*proxy, aggregatorCall {}, true),
522                ]
523            })
524            .collect::<Vec<_>>();
525        let results = self.execute_multicall(calls)?;
526        if results.len() != proxies.len() * 5 {
527            return None;
528        }
529
530        let mut cores = Vec::with_capacity(proxies.len());
531        for chunk in results.chunks_exact(5) {
532            let decimals = try_decode_result::<decimalsCall>(&chunk[0]);
533            let description = try_decode_result::<descriptionCall>(&chunk[1]);
534            let version = try_decode_result::<versionCall>(&chunk[2]);
535            let raw_round = try_decode_result::<latestRoundDataCall>(&chunk[3]);
536            let aggregator = try_decode_result::<aggregatorCall>(&chunk[4]);
537            let Some((decimals, description, version, raw_round)) =
538                decimals.zip(description).zip(version).zip(raw_round).map(
539                    |(((decimals, description), version), raw_round)| {
540                        (decimals, description, version, raw_round)
541                    },
542                )
543            else {
544                cores.push(None);
545                continue;
546            };
547
548            let round = match round_from_raw(raw_round) {
549                Ok(round) => round,
550                Err(error) => {
551                    cores.push(Some(Err(error)));
552                    continue;
553                }
554            };
555            cores.push(Some(Ok(ChainlinkCoreRead {
556                decimals,
557                description,
558                version,
559                round,
560                aggregator,
561            })));
562        }
563        Some(cores)
564    }
565
566    fn read_type_and_versions_multicall(
567        &self,
568        aggregators: &[Address],
569    ) -> Option<Vec<Option<String>>> {
570        let calls = aggregators
571            .iter()
572            .map(|aggregator| multicall_call(*aggregator, typeAndVersionCall {}, true))
573            .collect::<Vec<_>>();
574        let results = self.execute_multicall(calls)?;
575        if results.len() != aggregators.len() {
576            return None;
577        }
578        Some(
579            results
580                .iter()
581                .map(try_decode_result::<typeAndVersionCall>)
582                .collect(),
583        )
584    }
585
586    fn execute_multicall<I>(&self, calls: I) -> Option<Vec<IMulticall3::Result>>
587    where
588        I: IntoIterator<Item = (Address, Bytes, bool)>,
589    {
590        if !self.multicall_reads {
591            return None;
592        }
593        let calls = calls.into_iter().collect::<Vec<_>>();
594        if calls.is_empty() {
595            return Some(Vec::new());
596        }
597        if let Some(results) = self.execute_direct_rpc_multicall(&calls) {
598            return Some(results);
599        }
600        let mut cache = self.cache.borrow_mut();
601        execute_batched(&mut cache, calls).ok()
602    }
603
604    fn execute_direct_rpc_multicall(
605        &self,
606        calls: &[(Address, Bytes, bool)],
607    ) -> Option<Vec<IMulticall3::Result>> {
608        let calls = calls
609            .iter()
610            .map(|(target, call_data, allow_failure)| IMulticall3::Call3 {
611                target: *target,
612                allowFailure: *allow_failure,
613                callData: call_data.clone(),
614            })
615            .collect::<Vec<_>>();
616        let call = IMulticall3::aggregate3Call { calls };
617        let cache = self.cache.borrow();
618        let bytes = cache
619            .rpc_call(MULTICALL3_ADDRESS, Bytes::from(call.abi_encode()))
620            .and_then(Result::ok)?;
621        IMulticall3::aggregate3Call::abi_decode_returns(&bytes).ok()
622    }
623
624    #[cfg(feature = "aave")]
625    pub(crate) fn read_aave_source(
626        &self,
627        oracle: Address,
628        asset: Address,
629    ) -> Result<Address, OracleError> {
630        self.cache
631            .borrow_mut()
632            .call_sol(oracle, getSourceOfAssetCall { asset })
633            .map_err(provider_error)
634    }
635
636    #[cfg(feature = "aave")]
637    pub(crate) fn read_aave_price_cap_stable(
638        &self,
639        source: Address,
640    ) -> Result<AavePriceCapStableSource, OracleError> {
641        let underlying_proxy = self
642            .cache
643            .borrow_mut()
644            .call_sol(source, ASSET_TO_USD_AGGREGATORCall {})
645            .map_err(provider_error)?;
646        let price_cap = self
647            .cache
648            .borrow_mut()
649            .call_sol(source, getPriceCapCall {})
650            .map_err(provider_error)?;
651        let decimals = self
652            .cache
653            .borrow_mut()
654            .call_sol(source, aaveDecimalsCall {})
655            .map_err(provider_error)?;
656        let description = self
657            .cache
658            .borrow_mut()
659            .call_sol(source, aaveDescriptionCall {})
660            .map_err(provider_error)?;
661        let latest_answer = self
662            .cache
663            .borrow_mut()
664            .call_sol(source, latestAnswerCall {})
665            .map_err(provider_error)?;
666
667        Ok(AavePriceCapStableSource {
668            underlying_proxy,
669            price_cap,
670            decimals,
671            description,
672            latest_answer,
673        })
674    }
675
676    #[cfg(feature = "aave")]
677    pub(crate) fn read_aave_ratio_cap(
678        &self,
679        source: Address,
680    ) -> Result<AaveRatioCapSource, OracleError> {
681        let base_to_usd_proxy = self
682            .cache
683            .borrow_mut()
684            .call_sol(source, BASE_TO_USD_AGGREGATORCall {})
685            .map_err(provider_error)?;
686        let ratio_provider = self
687            .cache
688            .borrow_mut()
689            .call_sol(source, RATIO_PROVIDERCall {})
690            .map_err(provider_error)?;
691        let current_ratio = self
692            .cache
693            .borrow_mut()
694            .call_sol(source, getRatioCall {})
695            .map_err(provider_error)?;
696        let is_capped = self
697            .cache
698            .borrow_mut()
699            .call_sol(source, isCappedCall {})
700            .map_err(provider_error)?;
701        let ratio_decimals = self
702            .cache
703            .borrow_mut()
704            .call_sol(source, RATIO_DECIMALSCall {})
705            .map_err(provider_error)?;
706        let decimals = self
707            .cache
708            .borrow_mut()
709            .call_sol(source, aaveRatioDecimalsCall {})
710            .map_err(provider_error)?;
711        let description = self
712            .cache
713            .borrow_mut()
714            .call_sol(source, aaveRatioDescriptionCall {})
715            .map_err(provider_error)?;
716        let latest_answer = self
717            .cache
718            .borrow_mut()
719            .call_sol(source, aaveRatioLatestAnswerCall {})
720            .map_err(provider_error)?;
721
722        Ok(AaveRatioCapSource {
723            base_to_usd_proxy,
724            ratio_provider,
725            current_ratio,
726            is_capped,
727            ratio_decimals,
728            decimals,
729            description,
730            latest_answer,
731        })
732    }
733
734    #[cfg(feature = "aave")]
735    pub(crate) fn read_aave_synchronicity_peg_to_base(
736        &self,
737        source: Address,
738    ) -> Result<AaveSynchronicityPegToBaseSource, OracleError> {
739        let asset_to_peg_proxy = self
740            .cache
741            .borrow_mut()
742            .call_sol(source, ASSET_TO_PEGCall {})
743            .map_err(provider_error)?;
744        let peg_to_base_proxy = self
745            .cache
746            .borrow_mut()
747            .call_sol(source, PEG_TO_BASECall {})
748            .map_err(provider_error)?;
749        let decimals = self
750            .cache
751            .borrow_mut()
752            .call_sol(source, aaveSynchronicityDecimalsCall {})
753            .map_err(provider_error)?;
754        let description = self
755            .cache
756            .borrow_mut()
757            .call_sol(source, aaveSynchronicityDescriptionCall {})
758            .map_err(provider_error)?;
759        let latest_answer = self
760            .cache
761            .borrow_mut()
762            .call_sol(source, aaveSynchronicityLatestAnswerCall {})
763            .map_err(provider_error)?;
764
765        Ok(AaveSynchronicityPegToBaseSource {
766            asset_to_peg_proxy,
767            peg_to_base_proxy,
768            decimals,
769            description,
770            latest_answer,
771        })
772    }
773
774    #[cfg(feature = "aave")]
775    pub(crate) fn read_aave_fixed_price(
776        &self,
777        source: Address,
778    ) -> Result<AaveFixedPriceSource, OracleError> {
779        if self.has_known_aave_dependency_or_config(source) {
780            return Err(OracleError::Unsupported(
781                "source exposes dependency/config view(s); not fixed-price".to_string(),
782            ));
783        }
784        let fixed_price = self.read_fixed_price_value(source)?;
785        let decimals = self
786            .cache
787            .borrow_mut()
788            .call_sol(source, aaveFixedDecimalsCall {})
789            .map_err(provider_error)?;
790        let description = self
791            .cache
792            .borrow_mut()
793            .call_sol(source, aaveFixedDescriptionCall {})
794            .map_err(provider_error)?;
795        let latest_answer = self
796            .cache
797            .borrow_mut()
798            .call_sol(source, aaveFixedLatestAnswerCall {})
799            .map_err(provider_error)?;
800        if latest_answer != fixed_price {
801            return Err(OracleError::Unsupported(
802                "latestAnswer() does not match fixed price getter".to_string(),
803            ));
804        }
805
806        Ok(AaveFixedPriceSource {
807            decimals,
808            description,
809            latest_answer,
810        })
811    }
812
813    #[cfg(feature = "aave")]
814    fn read_fixed_price_value(&self, source: Address) -> Result<I256, OracleError> {
815        match self
816            .cache
817            .borrow_mut()
818            .call_sol(source, aaveFixedPriceCall {})
819        {
820            Ok(price) => Ok(price),
821            Err(price_error) => match self.cache.borrow_mut().call_sol(source, PRICECall {}) {
822                Ok(price) => Ok(price),
823                Err(constant_error) => Err(OracleError::Unsupported(format!(
824                    "missing fixed-price getter price() ({price_error:?}) or PRICE() ({constant_error:?})"
825                ))),
826            },
827        }
828    }
829
830    #[cfg(feature = "aave")]
831    fn has_known_aave_dependency_or_config(&self, source: Address) -> bool {
832        self.cache
833            .borrow_mut()
834            .call_sol(source, ASSET_TO_USD_AGGREGATORCall {})
835            .is_ok()
836            || self
837                .cache
838                .borrow_mut()
839                .call_sol(source, BASE_TO_USD_AGGREGATORCall {})
840                .is_ok()
841            || self
842                .cache
843                .borrow_mut()
844                .call_sol(source, ASSET_TO_PEGCall {})
845                .is_ok()
846            || self
847                .cache
848                .borrow_mut()
849                .call_sol(source, PEG_TO_BASECall {})
850                .is_ok()
851            || self
852                .cache
853                .borrow_mut()
854                .call_sol(source, BASE_TO_PEGCall {})
855                .is_ok()
856            || self
857                .cache
858                .borrow_mut()
859                .call_sol(source, REFERENCE_FEEDCall {})
860                .is_ok()
861            || self
862                .cache
863                .borrow_mut()
864                .call_sol(source, DISCOUNT_RATECall {})
865                .is_ok()
866            || self
867                .cache
868                .borrow_mut()
869                .call_sol(source, discountCall {})
870                .is_ok()
871            || self
872                .cache
873                .borrow_mut()
874                .call_sol(source, EXCHANGE_RATECall {})
875                .is_ok()
876            || self
877                .cache
878                .borrow_mut()
879                .call_sol(source, PENDLE_PRINCIPAL_TOKENCall {})
880                .is_ok()
881            || self
882                .cache
883                .borrow_mut()
884                .call_sol(source, PENDLE_ORACLECall {})
885                .is_ok()
886    }
887}
888
889impl ChainlinkFeedProvider for EvmCacheChainlinkReader<'_> {
890    fn decimals(&self, proxy: Address) -> ProviderFuture<'_, u8> {
891        let result = self.read_decimals(proxy);
892        Box::pin(async move { result })
893    }
894
895    fn description(&self, proxy: Address) -> ProviderFuture<'_, String> {
896        let result = self.read_description(proxy);
897        Box::pin(async move { result })
898    }
899
900    fn version(&self, proxy: Address) -> ProviderFuture<'_, U256> {
901        let result = self.read_version(proxy);
902        Box::pin(async move { result })
903    }
904
905    fn latest_round_data(&self, proxy: Address) -> ProviderFuture<'_, RoundData> {
906        let result = self.read_latest_round_data(proxy);
907        Box::pin(async move { result })
908    }
909
910    fn aggregator(&self, proxy: Address) -> ProviderFuture<'_, Option<Address>> {
911        let result = self.read_aggregator(proxy).or(Ok(None));
912        Box::pin(async move { result })
913    }
914
915    fn aggregator_type_and_version(
916        &self,
917        aggregator: Address,
918    ) -> ProviderFuture<'_, Option<String>> {
919        let result = self.read_type_and_version(aggregator).or(Ok(None));
920        Box::pin(async move { result })
921    }
922}
923
924/// Cache-native oracle adapter entry point.
925pub struct OracleAdapter;
926
927impl OracleAdapter {
928    /// Start building a cache-backed oracle tracker.
929    pub fn builder() -> OracleAdapterBuilder {
930        OracleAdapterBuilder::default()
931    }
932}
933
934/// Builder for cache-backed oracle tracker registration.
935#[derive(Clone, Debug)]
936pub struct OracleAdapterBuilder {
937    feeds: Vec<Feed>,
938    now_timestamp: Option<u64>,
939    multicall_reads: bool,
940}
941
942impl Default for OracleAdapterBuilder {
943    fn default() -> Self {
944        Self {
945            feeds: Vec::new(),
946            now_timestamp: None,
947            multicall_reads: true,
948        }
949    }
950}
951
952/// Best-effort cache-native oracle registration report.
953#[non_exhaustive]
954#[derive(Clone, Debug)]
955pub struct OracleAdapterBuildReport {
956    /// Tracker containing every successfully registered feed.
957    pub tracker: OracleTracker,
958    /// Feeds that were skipped because the source could not be registered.
959    pub skipped: Vec<OracleAdapterFeedSkip>,
960}
961
962/// Feed skipped during best-effort cache-native registration.
963#[derive(Clone, Debug)]
964pub struct OracleAdapterFeedSkip {
965    /// Feed definition supplied by the caller.
966    pub feed: Feed,
967    /// Proxy/source address that failed registration.
968    pub proxy: Address,
969    /// Classified skip reason.
970    pub reason: OracleAdapterSkipReason,
971}
972
973/// Complete description of one adapter feed skip, carried by
974/// [`OracleError::FeedSkipped`] when a fail-fast `build` surfaces a skip as an
975/// error. Use the `build_report` methods to receive skips as data instead.
976#[derive(Clone, Debug, PartialEq, Eq)]
977pub struct OracleFeedSkip {
978    /// Stable feed id, when the caller supplied one.
979    pub feed_id: Option<FeedId>,
980    /// Human-readable feed label, when the caller supplied one.
981    pub label: Option<String>,
982    /// Proxy/source address that failed registration.
983    pub proxy: Address,
984    /// Classified skip reason.
985    pub reason: OracleAdapterSkipReason,
986}
987
988impl OracleFeedSkip {
989    pub(crate) fn from_adapter_skip(skipped: &OracleAdapterFeedSkip) -> Self {
990        Self {
991            feed_id: skipped.feed.id(),
992            label: skipped.feed.label().map(str::to_string),
993            proxy: skipped.proxy,
994            reason: skipped.reason.clone(),
995        }
996    }
997
998    fn display_name(&self) -> &str {
999        self.label
1000            .as_deref()
1001            .or_else(|| self.feed_id.as_ref().map(|id| id.as_str()))
1002            .unwrap_or("unknown")
1003    }
1004}
1005
1006impl std::fmt::Display for OracleFeedSkip {
1007    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1008        write!(
1009            f,
1010            "adapter feed `{}` (proxy {:?}) was skipped: {}",
1011            self.display_name(),
1012            self.proxy,
1013            self.reason
1014        )
1015    }
1016}
1017
1018/// Classified reason for skipping a feed in best-effort registration.
1019#[non_exhaustive]
1020#[derive(Clone, Debug, PartialEq, Eq)]
1021pub enum OracleAdapterSkipReason {
1022    /// The source did not expose the Chainlink proxy methods required by this crate.
1023    NotChainlinkCompatible {
1024        /// Underlying read/decode error.
1025        error: String,
1026    },
1027    /// The Aave oracle source variant is not supported by this MVP.
1028    UnsupportedAaveSource {
1029        /// Classified discovery or compatibility error.
1030        error: String,
1031    },
1032    /// The Morpho oracle source variant is not supported by this adapter.
1033    UnsupportedMorphoSource {
1034        /// Classified discovery or compatibility error.
1035        error: String,
1036    },
1037    /// The Euler oracle source variant is not supported by this adapter.
1038    UnsupportedEulerSource {
1039        /// Classified discovery or compatibility error.
1040        error: String,
1041    },
1042    /// The RedStone push source variant is not supported by this adapter.
1043    #[cfg(feature = "redstone")]
1044    UnsupportedRedstoneSource {
1045        /// Classified discovery or compatibility error.
1046        error: String,
1047    },
1048}
1049
1050impl OracleAdapterBuilder {
1051    /// Add one typed feed registration.
1052    pub fn feed(mut self, feed: Feed) -> Self {
1053        self.feeds.push(feed);
1054        self
1055    }
1056
1057    /// Add multiple typed feed registrations.
1058    pub fn feeds(mut self, feeds: impl IntoIterator<Item = Feed>) -> Self {
1059        self.feeds.extend(feeds);
1060        self
1061    }
1062
1063    /// Set a fixed timestamp for deterministic registration status classification.
1064    pub fn now_timestamp(mut self, now_timestamp: u64) -> Self {
1065        self.now_timestamp = Some(now_timestamp);
1066        self
1067    }
1068
1069    /// Enable or disable direct/fork Multicall3 batching for cache-native Chainlink view reads.
1070    ///
1071    /// Enabled by default. When enabled, feed registration first batches proxy
1072    /// `decimals`, `description`, `version`, `latestRoundData`, `aggregator`,
1073    /// and aggregator `typeAndVersion` reads through direct RPC Multicall3,
1074    /// falling back to fork-cache Multicall3 and then sequential cache calls
1075    /// when direct RPC batching cannot be used.
1076    pub fn multicall_reads(mut self, enabled: bool) -> Self {
1077        self.multicall_reads = enabled;
1078        self
1079    }
1080
1081    /// Disable direct/fork Multicall3 view batching for cache-native Chainlink registration.
1082    pub fn disable_multicall_reads(self) -> Self {
1083        self.multicall_reads(false)
1084    }
1085
1086    /// Register feeds from cache reads and return a typed tracker.
1087    ///
1088    /// # Errors
1089    ///
1090    /// Returns [`OracleError::FeedSkipped`] when any feed could not be
1091    /// registered (use [`Self::build_report`] to receive skips as data),
1092    /// [`OracleError::DuplicateFeedId`]/[`OracleError::DuplicateProxy`] when
1093    /// two feeds collide, and [`OracleError::Config`] when the system clock
1094    /// is before the UNIX epoch and no `now_timestamp` was set.
1095    pub async fn build(self, cache: &mut EvmCache) -> Result<OracleTracker, OracleError> {
1096        let report = self.build_report(cache).await?;
1097        if let Some(skipped) = report.skipped.first() {
1098            return Err(OracleError::FeedSkipped(Box::new(
1099                OracleFeedSkip::from_adapter_skip(skipped),
1100            )));
1101        }
1102        Ok(report.tracker)
1103    }
1104
1105    /// Register every compatible feed and return skipped feeds instead of failing fast.
1106    pub async fn build_report(
1107        self,
1108        cache: &mut EvmCache,
1109    ) -> Result<OracleAdapterBuildReport, OracleError> {
1110        let now_timestamp = match self.now_timestamp {
1111            Some(now_timestamp) => now_timestamp,
1112            None => SystemTime::now()
1113                .duration_since(UNIX_EPOCH)
1114                .map_err(crate::error::clock_error)?
1115                .as_secs(),
1116        };
1117        let reader = EvmCacheChainlinkReader::new(cache).with_multicall_reads(self.multicall_reads);
1118        let mut registry = OracleRegistry::new_at_timestamp(now_timestamp);
1119        let skipped = reader.register_feeds(&mut registry, self.feeds).await?;
1120        Ok(OracleAdapterBuildReport {
1121            tracker: OracleTracker::new(registry),
1122            skipped,
1123        })
1124    }
1125}
1126
1127impl std::fmt::Display for OracleAdapterSkipReason {
1128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1129        match self {
1130            Self::NotChainlinkCompatible { error } => {
1131                write!(f, "not Chainlink-compatible ({error})")
1132            }
1133            Self::UnsupportedAaveSource { error } => {
1134                write!(f, "unsupported Aave oracle source ({error})")
1135            }
1136            Self::UnsupportedMorphoSource { error } => {
1137                write!(f, "unsupported Morpho oracle source ({error})")
1138            }
1139            Self::UnsupportedEulerSource { error } => {
1140                write!(f, "unsupported Euler oracle source ({error})")
1141            }
1142            #[cfg(feature = "redstone")]
1143            Self::UnsupportedRedstoneSource { error } => {
1144                write!(f, "unsupported RedStone oracle source ({error})")
1145            }
1146        }
1147    }
1148}
1149
1150fn u64_from_u256(value: U256, field: &'static str) -> Result<u64, OracleError> {
1151    u64::try_from(value).map_err(|_| {
1152        OracleError::Decode(crate::ChainlinkEventDecodeError::Uint64Overflow { field, value })
1153    })
1154}
1155
1156fn round_from_raw(
1157    round: <latestRoundDataCall as SolCall>::Return,
1158) -> Result<RoundData, OracleError> {
1159    Ok(RoundData {
1160        round_id: U256::from(round.roundId),
1161        answer: round.answer,
1162        started_at: u64_from_u256(round.startedAt, "startedAt")?,
1163        updated_at: u64_from_u256(round.updatedAt, "updatedAt")?,
1164        answered_in_round: U256::from(round.answeredInRound),
1165    })
1166}
1167
1168fn multicall_call<C: SolCall>(
1169    target: Address,
1170    call: C,
1171    allow_failure: bool,
1172) -> (Address, Bytes, bool) {
1173    (target, Bytes::from(call.abi_encode()), allow_failure)
1174}
1175
1176fn provider_error(error: impl std::fmt::Debug) -> OracleError {
1177    OracleError::Provider(format!("{error:?}"))
1178}