1use std::{collections::BTreeMap, sync::Arc};
2
3use alloy_primitives::{Address, B256, I256, U256, U512};
4
5use crate::{
6 AggregatorLayout, AggregatorLayoutConfidence, AggregatorLayoutEvidence, ChainlinkFeedProvider,
7 FeedConfig, FeedMetadata, OracleBlockRef, OracleError, OracleFeedStatus, OracleSnapshot,
8 OracleSourceDescriptor, OracleTransformDescriptor, OracleValueSource, RoundData,
9 classify_type_and_version,
10};
11
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct FeedId(Arc<str>);
15
16impl FeedId {
17 pub fn new(id: impl Into<String>) -> Self {
20 Self(Arc::from(id.into()))
21 }
22
23 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27}
28
29impl std::borrow::Borrow<str> for FeedId {
33 fn borrow(&self) -> &str {
34 self.as_str()
35 }
36}
37
38impl std::fmt::Display for FeedId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 self.0.fmt(f)
41 }
42}
43
44impl AsRef<str> for FeedId {
45 fn as_ref(&self) -> &str {
46 self.as_str()
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct OracleDependency {
53 pub proxy: Address,
55 pub aggregator: Address,
57 pub answer: I256,
59}
60
61impl OracleDependency {
62 pub fn new(proxy: Address, aggregator: Address, answer: I256) -> Self {
65 Self {
66 proxy,
67 aggregator,
68 answer,
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum MorphoChainlinkFeedRole {
76 BaseFeed1,
78 BaseFeed2,
80 QuoteFeed1,
82 QuoteFeed2,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct MorphoChainlinkFeed {
89 pub role: MorphoChainlinkFeedRole,
91 pub dependency: OracleDependency,
93}
94
95impl MorphoChainlinkFeed {
96 pub fn new(role: MorphoChainlinkFeedRole, dependency: OracleDependency) -> Self {
98 Self { role, dependency }
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum EulerQuoteLeg {
105 Chainlink {
107 oracle: Address,
109 feed: Address,
111 aggregator: Address,
113 answer: I256,
115 feed_decimals: u8,
117 inverse: bool,
119 quote_decimals: u8,
121 },
122 FixedRate {
124 oracle: Address,
126 rate: I256,
128 inverse: bool,
130 base_decimals: u8,
132 quote_decimals: u8,
134 },
135 RateProvider {
137 oracle: Address,
139 rate_provider: Address,
141 rate: I256,
143 inverse: bool,
145 quote_decimals: u8,
147 },
148}
149
150impl EulerQuoteLeg {
151 pub fn event_aggregator(&self) -> Option<Address> {
153 match self {
154 Self::Chainlink { aggregator, .. } => Some(*aggregator),
155 Self::FixedRate { .. } | Self::RateProvider { .. } => None,
156 }
157 }
158
159 pub fn price(&self) -> I256 {
161 self.price_from_event(None, I256::ZERO)
162 }
163
164 pub fn price_from_event(&self, aggregator: Option<Address>, answer: I256) -> I256 {
166 match self {
167 Self::Chainlink {
168 aggregator: dependency_aggregator,
169 answer: stored_answer,
170 feed_decimals,
171 inverse,
172 quote_decimals,
173 ..
174 } => {
175 let answer = if aggregator == Some(*dependency_aggregator) {
176 answer
177 } else {
178 *stored_answer
179 };
180 if *inverse {
181 inverse_positive_price(answer, *feed_decimals, *quote_decimals)
182 } else {
183 scale_positive_price(answer, *feed_decimals, *quote_decimals)
184 }
185 }
186 Self::FixedRate {
187 rate,
188 inverse,
189 base_decimals,
190 quote_decimals,
191 ..
192 } => {
193 if *inverse {
194 inverse_fixed_rate(*rate, *base_decimals, *quote_decimals)
195 } else {
196 *rate
197 }
198 }
199 Self::RateProvider {
200 rate,
201 inverse,
202 quote_decimals,
203 ..
204 } => {
205 if *inverse {
206 inverse_positive_price(*rate, 18, *quote_decimals)
207 } else {
208 scale_positive_price(*rate, 18, *quote_decimals)
209 }
210 }
211 }
212 }
213}
214
215#[non_exhaustive]
217#[derive(Clone, Debug, Default, PartialEq, Eq)]
218pub enum FeedSource {
219 #[default]
221 Chainlink,
222 AavePriceCapStable {
224 source: Address,
226 underlying_proxy: Address,
228 price_cap: I256,
230 },
231 AaveRatioCap {
233 source: Address,
235 base_to_usd_proxy: Address,
237 ratio_provider: Address,
239 current_ratio: I256,
241 max_ratio: I256,
243 ratio_decimals: u8,
245 },
246 AaveSynchronicityPegToBase {
248 source: Address,
250 asset_to_peg_proxy: Address,
252 asset_to_peg_aggregator: Address,
254 asset_to_peg_answer: I256,
256 asset_to_peg_decimals: u8,
258 peg_to_base_proxy: Address,
260 peg_to_base_aggregator: Address,
262 peg_to_base_answer: I256,
264 peg_to_base_decimals: u8,
266 output_decimals: u8,
268 },
269 AaveFixedPrice {
271 source: Address,
273 price: I256,
275 },
276 MorphoChainlinkV2 {
278 source: Address,
280 base_vault_assets: I256,
282 quote_vault_assets: I256,
284 scale_factor: I256,
286 feeds: Vec<MorphoChainlinkFeed>,
288 },
289 EulerQuote {
291 source: Address,
293 base: Address,
295 quote: Address,
297 base_decimals: u8,
299 quote_decimals: u8,
301 leg: EulerQuoteLeg,
303 },
304 EulerCross {
306 source: Address,
308 base: Address,
310 cross: Address,
312 quote: Address,
314 base_decimals: u8,
316 cross_decimals: u8,
318 quote_decimals: u8,
320 base_cross: EulerQuoteLeg,
322 cross_quote: EulerQuoteLeg,
324 },
325 Pyth {
327 pyth: Address,
329 price_id: B256,
331 expo: i32,
333 conf: u64,
335 },
336 #[cfg(feature = "redstone")]
338 RedstonePush {
339 price_feed: Address,
341 adapter: Address,
343 data_feed_id: B256,
345 },
346 Custom(OracleSourceDescriptor),
348}
349
350impl FeedSource {
351 pub fn aave_price_cap_stable(
353 source: Address,
354 underlying_proxy: Address,
355 price_cap: I256,
356 ) -> Self {
357 Self::AavePriceCapStable {
358 source,
359 underlying_proxy,
360 price_cap,
361 }
362 }
363
364 pub fn aave_ratio_cap(
366 source: Address,
367 base_to_usd_proxy: Address,
368 ratio_provider: Address,
369 current_ratio: I256,
370 max_ratio: I256,
371 ratio_decimals: u8,
372 ) -> Self {
373 Self::AaveRatioCap {
374 source,
375 base_to_usd_proxy,
376 ratio_provider,
377 current_ratio,
378 max_ratio,
379 ratio_decimals,
380 }
381 }
382
383 #[allow(clippy::too_many_arguments)]
385 pub fn aave_synchronicity_peg_to_base(
386 source: Address,
387 asset_to_peg_proxy: Address,
388 asset_to_peg_aggregator: Address,
389 asset_to_peg_answer: I256,
390 asset_to_peg_decimals: u8,
391 peg_to_base_proxy: Address,
392 peg_to_base_aggregator: Address,
393 peg_to_base_answer: I256,
394 peg_to_base_decimals: u8,
395 output_decimals: u8,
396 ) -> Self {
397 Self::AaveSynchronicityPegToBase {
398 source,
399 asset_to_peg_proxy,
400 asset_to_peg_aggregator,
401 asset_to_peg_answer,
402 asset_to_peg_decimals,
403 peg_to_base_proxy,
404 peg_to_base_aggregator,
405 peg_to_base_answer,
406 peg_to_base_decimals,
407 output_decimals,
408 }
409 }
410
411 pub fn aave_fixed_price(source: Address, price: I256) -> Self {
413 Self::AaveFixedPrice { source, price }
414 }
415
416 pub fn morpho_chainlink_v2(
418 source: Address,
419 base_vault_assets: I256,
420 quote_vault_assets: I256,
421 scale_factor: I256,
422 feeds: Vec<MorphoChainlinkFeed>,
423 ) -> Self {
424 Self::MorphoChainlinkV2 {
425 source,
426 base_vault_assets,
427 quote_vault_assets,
428 scale_factor,
429 feeds,
430 }
431 }
432
433 pub fn euler_quote(
435 source: Address,
436 base: Address,
437 quote: Address,
438 base_decimals: u8,
439 quote_decimals: u8,
440 leg: EulerQuoteLeg,
441 ) -> Self {
442 Self::EulerQuote {
443 source,
444 base,
445 quote,
446 base_decimals,
447 quote_decimals,
448 leg,
449 }
450 }
451
452 #[allow(clippy::too_many_arguments)]
454 pub fn euler_cross(
455 source: Address,
456 base: Address,
457 cross: Address,
458 quote: Address,
459 base_decimals: u8,
460 cross_decimals: u8,
461 quote_decimals: u8,
462 base_cross: EulerQuoteLeg,
463 cross_quote: EulerQuoteLeg,
464 ) -> Self {
465 Self::EulerCross {
466 source,
467 base,
468 cross,
469 quote,
470 base_decimals,
471 cross_decimals,
472 quote_decimals,
473 base_cross,
474 cross_quote,
475 }
476 }
477
478 #[cfg(feature = "euler")]
485 pub(crate) fn with_euler_user_source(self, user_source: Address) -> Self {
486 match self {
487 Self::EulerQuote {
488 base,
489 quote,
490 base_decimals,
491 quote_decimals,
492 leg,
493 ..
494 } => Self::EulerQuote {
495 source: user_source,
496 base,
497 quote,
498 base_decimals,
499 quote_decimals,
500 leg,
501 },
502 Self::EulerCross {
503 base,
504 cross,
505 quote,
506 base_decimals,
507 cross_decimals,
508 quote_decimals,
509 base_cross,
510 cross_quote,
511 ..
512 } => Self::EulerCross {
513 source: user_source,
514 base,
515 cross,
516 quote,
517 base_decimals,
518 cross_decimals,
519 quote_decimals,
520 base_cross,
521 cross_quote,
522 },
523 other => other,
524 }
525 }
526
527 pub fn pyth(pyth: Address, price_id: B256, expo: i32, conf: u64) -> Self {
529 Self::Pyth {
530 pyth,
531 price_id,
532 expo,
533 conf,
534 }
535 }
536
537 #[cfg(feature = "redstone")]
539 pub fn redstone_push(price_feed: Address, adapter: Address, data_feed_id: B256) -> Self {
540 Self::RedstonePush {
541 price_feed,
542 adapter,
543 data_feed_id,
544 }
545 }
546
547 pub fn custom(descriptor: OracleSourceDescriptor) -> Self {
561 Self::Custom(descriptor)
562 }
563
564 pub fn is_identity(&self) -> bool {
566 matches!(self, Self::Chainlink)
567 }
568
569 pub fn uses_builtin_chainlink_handler(&self) -> bool {
571 matches!(
572 self,
573 Self::Chainlink
574 | Self::AavePriceCapStable { .. }
575 | Self::AaveRatioCap { .. }
576 | Self::AaveSynchronicityPegToBase { .. }
577 | Self::MorphoChainlinkV2 { .. }
578 | Self::EulerQuote { .. }
579 | Self::EulerCross { .. }
580 )
581 }
582
583 pub fn supports_proxy_reconciliation(&self) -> bool {
589 !matches!(
590 self,
591 Self::AaveFixedPrice { .. }
592 | Self::MorphoChainlinkV2 { .. }
593 | Self::EulerQuote { .. }
594 | Self::EulerCross { .. }
595 | Self::Pyth { .. }
596 )
597 }
598
599 pub fn supports_derived_reconciliation(&self) -> bool {
610 #[cfg(feature = "morpho")]
611 if matches!(self, Self::MorphoChainlinkV2 { .. }) {
612 return true;
613 }
614 #[cfg(feature = "euler")]
615 if matches!(self, Self::EulerQuote { .. } | Self::EulerCross { .. }) {
616 return true;
617 }
618 false
619 }
620
621 pub fn read_proxy(&self, registration_proxy: Address) -> Address {
623 match self {
624 Self::Chainlink => registration_proxy,
625 Self::AavePriceCapStable {
626 underlying_proxy, ..
627 } => *underlying_proxy,
628 Self::AaveRatioCap {
629 base_to_usd_proxy, ..
630 } => *base_to_usd_proxy,
631 Self::AaveSynchronicityPegToBase {
632 asset_to_peg_proxy, ..
633 } => *asset_to_peg_proxy,
634 Self::AaveFixedPrice { source, .. } => *source,
635 Self::MorphoChainlinkV2 { source, .. }
636 | Self::EulerQuote { source, .. }
637 | Self::EulerCross { source, .. } => *source,
638 Self::Pyth { pyth, .. } => *pyth,
639 #[cfg(feature = "redstone")]
640 Self::RedstonePush { price_feed, .. } => *price_feed,
641 Self::Custom(descriptor) => descriptor.read_proxy,
642 }
643 }
644
645 pub fn event_aggregators(&self, current_aggregator: Option<Address>) -> Vec<Address> {
647 match self {
648 Self::AaveSynchronicityPegToBase {
649 asset_to_peg_aggregator,
650 peg_to_base_aggregator,
651 ..
652 } => vec![*asset_to_peg_aggregator, *peg_to_base_aggregator],
653 Self::MorphoChainlinkV2 { feeds, .. } => feeds
654 .iter()
655 .map(|feed| feed.dependency.aggregator)
656 .collect(),
657 Self::EulerQuote { leg, .. } => leg.event_aggregator().into_iter().collect(),
658 Self::EulerCross {
659 base_cross,
660 cross_quote,
661 ..
662 } => base_cross
663 .event_aggregator()
664 .into_iter()
665 .chain(cross_quote.event_aggregator())
666 .collect(),
667 Self::Pyth { pyth, .. } => vec![*pyth],
668 #[cfg(feature = "redstone")]
669 Self::RedstonePush {
670 price_feed,
671 adapter,
672 ..
673 } => {
674 let mut aggregators = vec![*adapter];
675 if price_feed != adapter {
676 aggregators.push(*price_feed);
677 }
678 aggregators
679 }
680 Self::AaveFixedPrice { .. } => Vec::new(),
681 _ => current_aggregator.into_iter().collect(),
682 }
683 }
684
685 pub fn event_value_source(&self) -> OracleValueSource {
687 match self {
688 Self::AavePriceCapStable { .. }
689 | Self::AaveRatioCap { .. }
690 | Self::AaveSynchronicityPegToBase { .. }
691 | Self::AaveFixedPrice { .. }
692 | Self::MorphoChainlinkV2 { .. }
693 | Self::EulerQuote { .. }
694 | Self::EulerCross { .. } => OracleValueSource::Derived,
695 _ => OracleValueSource::Event,
696 }
697 }
698
699 pub fn accepts_event_from(
701 &self,
702 current_aggregator: Option<Address>,
703 aggregator: Address,
704 ) -> bool {
705 self.event_aggregators(current_aggregator)
706 .into_iter()
707 .any(|event_aggregator| event_aggregator == aggregator)
708 }
709
710 pub fn wants_answer_updated_from(
712 &self,
713 current_aggregator: Option<Address>,
714 aggregator: Address,
715 ) -> bool {
716 match self {
717 Self::AaveRatioCap { .. }
718 | Self::AaveSynchronicityPegToBase { .. }
719 | Self::MorphoChainlinkV2 { .. }
720 | Self::EulerQuote { .. }
721 | Self::EulerCross { .. } => self.accepts_event_from(current_aggregator, aggregator),
722 _ => false,
723 }
724 }
725
726 pub fn supports_direct_chainlink_storage_effects(&self) -> bool {
728 match self {
729 Self::AaveRatioCap { .. }
730 | Self::AaveSynchronicityPegToBase { .. }
731 | Self::MorphoChainlinkV2 { .. }
732 | Self::EulerQuote { .. }
733 | Self::EulerCross { .. }
734 | Self::Pyth { .. } => false,
735 #[cfg(feature = "redstone")]
736 Self::RedstonePush { .. } => false,
737 _ => true,
738 }
739 }
740
741 pub fn normalize_answer(&self, answer: I256) -> I256 {
743 self.normalize_answer_from_event(None, answer)
744 }
745
746 pub fn normalize_answer_from_event(&self, aggregator: Option<Address>, answer: I256) -> I256 {
748 match self {
749 Self::Chainlink => answer,
750 Self::AavePriceCapStable { price_cap, .. } if answer > *price_cap => *price_cap,
751 Self::AavePriceCapStable { .. } => answer,
752 Self::AaveRatioCap {
753 current_ratio,
754 max_ratio,
755 ratio_decimals,
756 ..
757 } => {
758 if answer <= I256::ZERO || *current_ratio <= I256::ZERO {
759 return I256::ZERO;
760 }
761 let capped_ratio = if current_ratio < max_ratio {
762 *current_ratio
763 } else {
764 *max_ratio
765 };
766 if capped_ratio <= I256::ZERO {
767 return I256::ZERO;
768 }
769 div_or_zero(
770 answer.saturating_mul(capped_ratio),
771 decimal_scale(*ratio_decimals),
772 )
773 }
774 Self::AaveSynchronicityPegToBase {
775 asset_to_peg_aggregator,
776 asset_to_peg_answer,
777 asset_to_peg_decimals,
778 peg_to_base_aggregator,
779 peg_to_base_answer,
780 peg_to_base_decimals,
781 output_decimals,
782 ..
783 } => {
784 let asset_to_peg = if aggregator == Some(*asset_to_peg_aggregator) {
785 answer
786 } else {
787 *asset_to_peg_answer
788 };
789 let peg_to_base = if aggregator == Some(*peg_to_base_aggregator) {
790 answer
791 } else {
792 *peg_to_base_answer
793 };
794 normalize_synchronicity_answer(
795 asset_to_peg,
796 *asset_to_peg_decimals,
797 peg_to_base,
798 *peg_to_base_decimals,
799 *output_decimals,
800 )
801 }
802 Self::AaveFixedPrice { price, .. } => *price,
803 Self::MorphoChainlinkV2 {
804 base_vault_assets,
805 quote_vault_assets,
806 scale_factor,
807 feeds,
808 ..
809 } => normalize_morpho_chainlink_v2_answer(
810 *base_vault_assets,
811 *quote_vault_assets,
812 *scale_factor,
813 feeds,
814 aggregator,
815 answer,
816 ),
817 Self::EulerQuote { leg, .. } => leg.price_from_event(aggregator, answer),
818 Self::EulerCross {
819 cross_decimals,
820 base_cross,
821 cross_quote,
822 ..
823 } => normalize_euler_cross_answer(
824 base_cross.price_from_event(aggregator, answer),
825 cross_quote.price_from_event(aggregator, answer),
826 *cross_decimals,
827 ),
828 Self::Pyth { .. } => answer,
829 #[cfg(feature = "redstone")]
830 Self::RedstonePush { .. } => answer,
831 Self::Custom(descriptor) => match &descriptor.transform {
832 OracleTransformDescriptor::Identity | OracleTransformDescriptor::Custom { .. } => {
833 answer
834 }
835 OracleTransformDescriptor::PriceCap { cap } if answer > *cap => *cap,
836 OracleTransformDescriptor::PriceCap { .. } => answer,
837 },
838 }
839 }
840
841 pub fn normalize_round(&self, mut round: RoundData) -> RoundData {
843 round.answer = match self {
844 Self::AaveSynchronicityPegToBase {
845 asset_to_peg_aggregator,
846 ..
847 } => self.normalize_answer_from_event(Some(*asset_to_peg_aggregator), round.answer),
848 Self::MorphoChainlinkV2 { .. }
849 | Self::EulerQuote { .. }
850 | Self::EulerCross { .. }
851 | Self::Pyth { .. } => round.answer,
852 _ => self.normalize_answer(round.answer),
853 };
854 round
855 }
856
857 pub(crate) fn normalize_dependency_answers(
858 &self,
859 asset_to_peg_answer: I256,
860 peg_to_base_answer: I256,
861 ) -> Option<I256> {
862 match self {
863 Self::AaveSynchronicityPegToBase {
864 asset_to_peg_decimals,
865 peg_to_base_decimals,
866 output_decimals,
867 ..
868 } => Some(normalize_synchronicity_answer(
869 asset_to_peg_answer,
870 *asset_to_peg_decimals,
871 peg_to_base_answer,
872 *peg_to_base_decimals,
873 *output_decimals,
874 )),
875 _ => None,
876 }
877 }
878
879 pub(crate) fn with_synchronicity_dependency_state(
880 &self,
881 asset_to_peg_aggregator: Address,
882 asset_to_peg_answer: I256,
883 peg_to_base_aggregator: Address,
884 peg_to_base_answer: I256,
885 ) -> Option<Self> {
886 match self {
887 Self::AaveSynchronicityPegToBase {
888 source,
889 asset_to_peg_proxy,
890 asset_to_peg_decimals,
891 peg_to_base_proxy,
892 peg_to_base_decimals,
893 output_decimals,
894 ..
895 } => Some(Self::AaveSynchronicityPegToBase {
896 source: *source,
897 asset_to_peg_proxy: *asset_to_peg_proxy,
898 asset_to_peg_aggregator,
899 asset_to_peg_answer,
900 asset_to_peg_decimals: *asset_to_peg_decimals,
901 peg_to_base_proxy: *peg_to_base_proxy,
902 peg_to_base_aggregator,
903 peg_to_base_answer,
904 peg_to_base_decimals: *peg_to_base_decimals,
905 output_decimals: *output_decimals,
906 }),
907 _ => None,
908 }
909 }
910
911 pub fn pyth_source(&self) -> Option<(Address, B256, i32, u64)> {
913 match self {
914 Self::Pyth {
915 pyth,
916 price_id,
917 expo,
918 conf,
919 } => Some((*pyth, *price_id, *expo, *conf)),
920 _ => None,
921 }
922 }
923
924 pub fn with_pyth_event_metadata(&self, price_id: B256, expo: i32, conf: u64) -> Option<Self> {
926 match self {
927 Self::Pyth { pyth, .. } => Some(Self::Pyth {
928 pyth: *pyth,
929 price_id,
930 expo,
931 conf,
932 }),
933 _ => None,
934 }
935 }
936}
937
938fn decimal_scale(decimals: u8) -> I256 {
939 let mut scale = I256::unchecked_from(1_i8);
940 for _ in 0..decimals {
941 scale = scale.saturating_mul(I256::unchecked_from(10_i8));
942 }
943 scale
944}
945
946fn div_or_zero(numerator: I256, denominator: I256) -> I256 {
947 if denominator == I256::ZERO {
948 I256::ZERO
949 } else {
950 numerator / denominator
951 }
952}
953
954fn normalize_synchronicity_answer(
955 asset_to_peg: I256,
956 asset_to_peg_decimals: u8,
957 peg_to_base: I256,
958 peg_to_base_decimals: u8,
959 output_decimals: u8,
960) -> I256 {
961 if asset_to_peg <= I256::ZERO || peg_to_base <= I256::ZERO {
962 return I256::ZERO;
963 }
964 let numerator = asset_to_peg
965 .saturating_mul(peg_to_base)
966 .saturating_mul(decimal_scale(output_decimals));
967 let denominator = decimal_scale(asset_to_peg_decimals.saturating_add(peg_to_base_decimals));
968 div_or_zero(numerator, denominator)
969}
970
971fn normalize_morpho_chainlink_v2_answer(
972 base_vault_assets: I256,
973 quote_vault_assets: I256,
974 scale_factor: I256,
975 feeds: &[MorphoChainlinkFeed],
976 aggregator: Option<Address>,
977 answer: I256,
978) -> I256 {
979 if base_vault_assets < I256::ZERO
980 || quote_vault_assets <= I256::ZERO
981 || scale_factor <= I256::ZERO
982 {
983 return I256::ZERO;
984 }
985
986 let mut base_product = i256_magnitude(base_vault_assets);
996 let mut quote_product = i256_magnitude(quote_vault_assets);
997 for feed in feeds {
998 let feed_answer = if aggregator == Some(feed.dependency.aggregator) {
999 answer
1000 } else {
1001 feed.dependency.answer
1002 };
1003 if feed_answer < I256::ZERO {
1004 return I256::ZERO;
1005 }
1006 let magnitude = i256_magnitude(feed_answer);
1007 let product = match feed.role {
1008 MorphoChainlinkFeedRole::BaseFeed1 | MorphoChainlinkFeedRole::BaseFeed2 => {
1009 &mut base_product
1010 }
1011 MorphoChainlinkFeedRole::QuoteFeed1 | MorphoChainlinkFeedRole::QuoteFeed2 => {
1012 &mut quote_product
1013 }
1014 };
1015 *product = match product.checked_mul(magnitude) {
1016 Some(value) => value,
1017 None => return I256::ZERO,
1018 };
1019 }
1020 if quote_product.is_zero() {
1021 return I256::ZERO;
1022 }
1023
1024 let numerator = u512_from_u256(i256_magnitude(scale_factor)) * u512_from_u256(base_product);
1026 let quotient = numerator / u512_from_u256(quote_product);
1027 let limbs = quotient.into_limbs();
1028 if limbs[4..].iter().any(|limb| *limb != 0) {
1029 return I256::ZERO;
1030 }
1031 let low = U256::from_limbs([limbs[0], limbs[1], limbs[2], limbs[3]]);
1032 I256::try_from(low).unwrap_or(I256::ZERO)
1033}
1034
1035fn i256_magnitude(value: I256) -> U256 {
1037 value.into_raw()
1038}
1039
1040fn u512_from_u256(value: U256) -> U512 {
1041 let limbs = value.into_limbs();
1042 U512::from_limbs([limbs[0], limbs[1], limbs[2], limbs[3], 0, 0, 0, 0])
1043}
1044
1045fn normalize_euler_cross_answer(
1046 base_cross_price: I256,
1047 cross_quote_price: I256,
1048 cross_decimals: u8,
1049) -> I256 {
1050 if base_cross_price <= I256::ZERO || cross_quote_price <= I256::ZERO {
1051 return I256::ZERO;
1052 }
1053 div_or_zero(
1054 base_cross_price.saturating_mul(cross_quote_price),
1055 decimal_scale(cross_decimals),
1056 )
1057}
1058
1059fn scale_positive_price(answer: I256, from_decimals: u8, to_decimals: u8) -> I256 {
1060 if answer <= I256::ZERO {
1061 return I256::ZERO;
1062 }
1063 if from_decimals == to_decimals {
1064 answer
1065 } else if from_decimals < to_decimals {
1066 answer.saturating_mul(decimal_scale(to_decimals - from_decimals))
1067 } else {
1068 div_or_zero(answer, decimal_scale(from_decimals - to_decimals))
1069 }
1070}
1071
1072fn inverse_positive_price(answer: I256, feed_decimals: u8, quote_decimals: u8) -> I256 {
1073 if answer <= I256::ZERO {
1074 return I256::ZERO;
1075 }
1076 div_or_zero(
1077 decimal_scale(feed_decimals.saturating_add(quote_decimals)),
1078 answer,
1079 )
1080}
1081
1082fn inverse_fixed_rate(rate: I256, base_decimals: u8, quote_decimals: u8) -> I256 {
1083 if rate <= I256::ZERO {
1084 return I256::ZERO;
1085 }
1086 div_or_zero(
1087 decimal_scale(base_decimals.saturating_add(quote_decimals)),
1088 rate,
1089 )
1090}
1091
1092#[derive(Clone, Debug, PartialEq, Eq)]
1094pub struct FeedRegistration {
1095 pub id: FeedId,
1097 pub proxy: Address,
1099 pub label: Option<String>,
1101 pub base: Option<String>,
1103 pub quote: Option<String>,
1105 pub staleness: crate::StalenessPolicy,
1107 pub current_aggregator: Option<Address>,
1109 pub aggregator_layout: Option<AggregatorLayoutEvidence>,
1111 pub metadata: FeedMetadata,
1113 pub source: FeedSource,
1115 pub status: OracleFeedStatus,
1117}
1118
1119#[derive(Clone, Debug, PartialEq, Eq)]
1121pub struct OracleFeedReadinessReport {
1122 pub id: Option<FeedId>,
1124 pub proxy: Address,
1126 pub status: OracleFeedStatus,
1128 pub reason: Option<String>,
1130}
1131
1132#[derive(Clone, Debug, PartialEq, Eq)]
1134pub struct AggregatorChange {
1135 pub id: FeedId,
1137 pub proxy: Address,
1139 pub old: Option<Address>,
1141 pub new: Option<Address>,
1143}
1144
1145struct PreparedFeed {
1146 id: FeedId,
1147 config: FeedConfig,
1148 source: FeedSource,
1149 metadata: FeedMetadata,
1150 round: RoundData,
1151 current_aggregator: Option<Address>,
1152}
1153
1154#[derive(Clone, Debug)]
1156pub struct OracleRegistry {
1157 registrations: BTreeMap<FeedId, FeedRegistration>,
1158 ids_by_proxy: BTreeMap<Address, FeedId>,
1159 snapshots_by_proxy: BTreeMap<Address, OracleSnapshot>,
1160 layouts_by_code_hash: BTreeMap<B256, AggregatorLayout>,
1161 now_timestamp: u64,
1162}
1163
1164impl OracleRegistry {
1165 pub fn new_at_timestamp(now_timestamp: u64) -> Self {
1167 Self {
1168 registrations: BTreeMap::new(),
1169 ids_by_proxy: BTreeMap::new(),
1170 snapshots_by_proxy: BTreeMap::new(),
1171 layouts_by_code_hash: BTreeMap::new(),
1172 now_timestamp,
1173 }
1174 }
1175
1176 pub async fn register_chainlink_feed<P: ChainlinkFeedProvider>(
1178 &mut self,
1179 provider: &P,
1180 config: FeedConfig,
1181 ) -> Result<FeedId, OracleError> {
1182 let id = self.prepare_feed_id(&config)?;
1183 let source = FeedSource::Chainlink;
1184 let read_proxy = source.read_proxy(config.proxy);
1185 let metadata = FeedMetadata {
1186 decimals: provider.decimals(read_proxy).await?,
1187 description: provider.description(read_proxy).await?,
1188 version: provider.version(read_proxy).await?,
1189 };
1190 let round = source.normalize_round(provider.latest_round_data(read_proxy).await?);
1191 let current_aggregator = provider.aggregator(read_proxy).await.unwrap_or(None);
1192 self.insert_prepared_feed(
1193 provider,
1194 PreparedFeed {
1195 id,
1196 config,
1197 source,
1198 metadata,
1199 round,
1200 current_aggregator,
1201 },
1202 )
1203 .await
1204 }
1205
1206 #[allow(dead_code)]
1207 pub(crate) async fn register_discovered_feed<P: ChainlinkFeedProvider>(
1208 &mut self,
1209 provider: &P,
1210 config: FeedConfig,
1211 source: FeedSource,
1212 metadata: FeedMetadata,
1213 round: RoundData,
1214 current_aggregator: Option<Address>,
1215 ) -> Result<FeedId, OracleError> {
1216 let id = self.prepare_feed_id(&config)?;
1217 self.insert_prepared_feed(
1218 provider,
1219 PreparedFeed {
1220 id,
1221 config,
1222 source,
1223 metadata,
1224 round,
1225 current_aggregator,
1226 },
1227 )
1228 .await
1229 }
1230
1231 async fn insert_prepared_feed<P: ChainlinkFeedProvider>(
1232 &mut self,
1233 provider: &P,
1234 prepared: PreparedFeed,
1235 ) -> Result<FeedId, OracleError> {
1236 let PreparedFeed {
1237 id,
1238 config,
1239 source,
1240 metadata,
1241 round,
1242 current_aggregator,
1243 } = prepared;
1244 let aggregator_layout = self
1245 .detect_aggregator_layout(provider, current_aggregator, None)
1246 .await;
1247
1248 let registration = FeedRegistration {
1249 id: id.clone(),
1250 proxy: config.proxy,
1251 label: config.label,
1252 base: config.base,
1253 quote: config.quote,
1254 staleness: config.staleness,
1255 current_aggregator,
1256 aggregator_layout,
1257 metadata: metadata.clone(),
1258 source,
1259 status: OracleFeedStatus::Ready,
1260 };
1261 let snapshot = OracleSnapshot::proxy_read(
1262 id.clone(),
1263 registration.proxy,
1264 current_aggregator,
1265 metadata,
1266 round,
1267 self.now_timestamp,
1268 ®istration.staleness,
1269 );
1270
1271 self.ids_by_proxy.insert(registration.proxy, id.clone());
1272 self.snapshots_by_proxy.insert(registration.proxy, snapshot);
1273 self.registrations.insert(id.clone(), registration);
1274 Ok(id)
1275 }
1276
1277 fn prepare_feed_id(&self, config: &FeedConfig) -> Result<FeedId, OracleError> {
1278 if self.ids_by_proxy.contains_key(&config.proxy) {
1279 return Err(OracleError::DuplicateProxy(config.proxy));
1280 }
1281
1282 let id = config
1283 .id
1284 .clone()
1285 .unwrap_or_else(|| derive_feed_id(config.label.as_deref(), config.proxy));
1286 if self.registrations.contains_key(&id) {
1287 return Err(OracleError::DuplicateFeedId(id.to_string()));
1288 }
1289 Ok(id)
1290 }
1291
1292 pub fn registration(&self, id: FeedId) -> Option<&FeedRegistration> {
1294 self.registrations.get(&id)
1295 }
1296
1297 pub fn feed_readiness(&self) -> Vec<OracleFeedReadinessReport> {
1299 self.registrations
1300 .values()
1301 .map(|registration| OracleFeedReadinessReport {
1302 id: Some(registration.id.clone()),
1303 proxy: registration.proxy,
1304 status: registration.status,
1305 reason: None,
1306 })
1307 .collect()
1308 }
1309
1310 pub fn latest(&self, proxy: Address) -> Option<&OracleSnapshot> {
1312 self.snapshots_by_proxy.get(&proxy)
1313 }
1314
1315 pub(crate) fn now_timestamp(&self) -> u64 {
1316 self.now_timestamp
1317 }
1318
1319 pub(crate) fn registrations_map(&self) -> &BTreeMap<FeedId, FeedRegistration> {
1320 &self.registrations
1321 }
1322
1323 pub(crate) fn registrations_map_mut(&mut self) -> &mut BTreeMap<FeedId, FeedRegistration> {
1324 &mut self.registrations
1325 }
1326
1327 pub(crate) fn snapshots_by_proxy_mut(&mut self) -> &mut BTreeMap<Address, OracleSnapshot> {
1328 &mut self.snapshots_by_proxy
1329 }
1330
1331 pub(crate) fn id_by_proxy(&self, proxy: Address) -> Option<&FeedId> {
1332 self.ids_by_proxy.get(&proxy)
1333 }
1334
1335 pub(crate) fn record_proxy_id(&mut self, proxy: Address, id: FeedId) {
1336 self.ids_by_proxy.insert(proxy, id);
1337 }
1338
1339 pub(crate) fn replace_snapshot(&mut self, snapshot: OracleSnapshot) {
1340 self.snapshots_by_proxy.insert(snapshot.proxy, snapshot);
1341 }
1342
1343 pub(crate) fn insert_seeded_registration(
1344 &mut self,
1345 registration: FeedRegistration,
1346 round: RoundData,
1347 ) -> Result<(), OracleError> {
1348 if self.ids_by_proxy.contains_key(®istration.proxy) {
1349 return Err(OracleError::DuplicateProxy(registration.proxy));
1350 }
1351 if self.registrations.contains_key(®istration.id) {
1352 return Err(OracleError::DuplicateFeedId(registration.id.to_string()));
1353 }
1354
1355 let snapshot = OracleSnapshot::proxy_read(
1356 registration.id.clone(),
1357 registration.proxy,
1358 registration.current_aggregator,
1359 registration.metadata.clone(),
1360 registration.source.normalize_round(round),
1361 self.now_timestamp,
1362 ®istration.staleness,
1363 );
1364 self.ids_by_proxy
1365 .insert(registration.proxy, registration.id.clone());
1366 self.snapshots_by_proxy.insert(registration.proxy, snapshot);
1367 self.registrations
1368 .insert(registration.id.clone(), registration);
1369 Ok(())
1370 }
1371
1372 pub(crate) fn remove_registration_by_id(&mut self, id: FeedId) -> Option<FeedRegistration> {
1373 let registration = self.registrations.remove(&id)?;
1374 self.ids_by_proxy.remove(®istration.proxy);
1375 self.snapshots_by_proxy.remove(®istration.proxy);
1376 Some(registration)
1377 }
1378
1379 pub(crate) fn remove_registration_by_proxy(
1380 &mut self,
1381 proxy: Address,
1382 ) -> Option<FeedRegistration> {
1383 let id = self.ids_by_proxy.get(&proxy)?.clone();
1384 self.remove_registration_by_id(id)
1385 }
1386
1387 pub(crate) async fn detect_aggregator_layout<P: ChainlinkFeedProvider>(
1388 &mut self,
1389 provider: &P,
1390 aggregator: Option<Address>,
1391 block: Option<OracleBlockRef>,
1392 ) -> Option<AggregatorLayoutEvidence> {
1393 let aggregator = aggregator?;
1394 let code_hash = provider
1395 .aggregator_code_hash_at(aggregator, block)
1396 .await
1397 .unwrap_or(None);
1398 let type_and_version = provider
1399 .aggregator_type_and_version_at(aggregator, block)
1400 .await
1401 .unwrap_or(None);
1402
1403 if let Some(type_and_version) = type_and_version {
1404 let layout = classify_type_and_version(&type_and_version);
1405 if let Some(code_hash) = code_hash
1406 && is_cacheable_layout(layout)
1407 {
1408 self.layouts_by_code_hash.insert(code_hash, layout);
1409 }
1410 return Some(AggregatorLayoutEvidence {
1411 aggregator,
1412 type_and_version: Some(type_and_version),
1413 code_hash,
1414 layout,
1415 confidence: AggregatorLayoutConfidence::TypeAndVersion,
1416 });
1417 }
1418
1419 if let Some(code_hash) = code_hash {
1420 if let Some(layout) = self.layouts_by_code_hash.get(&code_hash).copied() {
1421 return Some(AggregatorLayoutEvidence::from_code_hash_cache(
1422 aggregator, code_hash, layout,
1423 ));
1424 }
1425 return Some(AggregatorLayoutEvidence::unknown(
1426 aggregator,
1427 Some(code_hash),
1428 ));
1429 }
1430
1431 Some(AggregatorLayoutEvidence::unknown(aggregator, None))
1432 }
1433
1434 pub fn registrations(&self) -> impl Iterator<Item = FeedRegistration> + '_ {
1439 self.registrations_iter().cloned()
1440 }
1441
1442 pub fn registrations_iter(&self) -> impl Iterator<Item = &FeedRegistration> {
1444 self.registrations.values()
1445 }
1446}
1447
1448fn is_cacheable_layout(layout: AggregatorLayout) -> bool {
1449 matches!(
1450 layout,
1451 AggregatorLayout::ChainlinkOcr2V1
1452 | AggregatorLayout::ChainlinkOcr1V2
1453 | AggregatorLayout::ChainlinkOcr1V3
1454 | AggregatorLayout::ChainlinkOcr1V4
1455 )
1456}
1457
1458pub(crate) fn snapshot_from_proxy_read(
1459 registration: &FeedRegistration,
1460 round: RoundData,
1461 aggregator: Option<Address>,
1462 now_timestamp: u64,
1463) -> OracleSnapshot {
1464 OracleSnapshot::proxy_read(
1465 registration.id.clone(),
1466 registration.proxy,
1467 aggregator,
1468 registration.metadata.clone(),
1469 round,
1470 now_timestamp,
1471 ®istration.staleness,
1472 )
1473}
1474
1475pub(crate) fn derive_feed_id(label: Option<&str>, proxy: Address) -> FeedId {
1476 if let Some(label) = label {
1477 let slug = label
1478 .chars()
1479 .map(|ch| {
1480 if ch.is_ascii_alphanumeric() {
1481 ch.to_ascii_lowercase()
1482 } else {
1483 '-'
1484 }
1485 })
1486 .collect::<String>()
1487 .trim_matches('-')
1488 .to_string();
1489 if !slug.is_empty() {
1490 return FeedId::new(slug);
1491 }
1492 }
1493
1494 FeedId::new(format!("{proxy:?}"))
1495}
1496
1497#[allow(dead_code)]
1498fn _u256_is_part_of_public_metadata(_: U256) {}