1use alloy_network::{Ethereum, Network};
2use alloy_primitives::{Address, B256, I256, U256};
3use evm_fork_cache::reactive::{ReactiveBatchReport, ReactiveReport, ReportTag};
4
5use crate::{
6 AggregatorChange, AggregatorLayoutEvidence, ChainlinkFeedProvider, FeedId, FeedRegistration,
7 FeedSource, ORACLE_LEGACY_ANSWER_UPDATED_KIND, ORACLE_SIGNAL_NAMESPACE, OracleBlockRef,
8 OracleError, OracleFeedReadinessReport, OraclePrice, OracleRegistry, OracleRoundStatus,
9 OracleSignalKind, OracleSnapshot, OracleValueSource, OracleValueStatus, RoundData,
10 registry::snapshot_from_proxy_read, state::EventSnapshotInput,
11};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct OracleUpdate {
16 pub id: FeedId,
18 pub proxy: Address,
20 pub aggregator: Address,
22 pub round: RoundData,
24 pub block_number: Option<u64>,
26 pub log_index: Option<u64>,
28 pub value_status: OracleValueStatus,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct OraclePriceUpdate {
35 pub id: FeedId,
37 pub proxy: Address,
39 pub aggregator: Address,
41 pub label: Option<String>,
43 pub base: Option<String>,
45 pub quote: Option<String>,
47 pub raw_answer: I256,
49 pub decimals: u8,
51 pub event_round_id: U256,
53 pub started_at: u64,
55 pub updated_at: u64,
57 pub block_number: Option<u64>,
59 pub block_hash: Option<B256>,
61 pub log_index: Option<u64>,
63 pub round_status: OracleRoundStatus,
65 pub value_status: OracleValueStatus,
67 pub source: OracleValueSource,
69}
70
71impl OraclePriceUpdate {
72 fn round(&self) -> RoundData {
73 RoundData {
74 round_id: self.event_round_id,
75 answer: self.raw_answer,
76 started_at: self.started_at,
77 updated_at: self.updated_at,
78 answered_in_round: self.event_round_id,
79 }
80 }
81}
82
83#[non_exhaustive]
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
86pub enum OracleReconciliationKind {
87 #[default]
91 Proxy,
92 DerivedProtocolRead,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct OracleReconciliationRequest {
102 pub id: FeedId,
104 pub proxy: Address,
106 pub aggregator: Option<Address>,
108 pub event_round: RoundData,
110 pub block_number: Option<u64>,
112 pub block_hash: Option<B256>,
114 pub log_index: Option<u64>,
116 pub kind: OracleReconciliationKind,
118}
119
120impl OracleReconciliationRequest {
121 pub fn block_ref(&self) -> Option<OracleBlockRef> {
123 Some(OracleBlockRef {
124 number: self.block_number?,
125 hash: self.block_hash?,
126 })
127 }
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct OraclePriceConfirmed {
133 pub id: FeedId,
135 pub proxy: Address,
137 pub aggregator: Option<Address>,
139 pub label: Option<String>,
141 pub base: Option<String>,
143 pub quote: Option<String>,
145 pub raw_answer: I256,
147 pub decimals: u8,
149 pub event_round_id: U256,
151 pub updated_at: u64,
153 pub block_number: Option<u64>,
155 pub block_hash: Option<B256>,
157 pub log_index: Option<u64>,
159 pub round_status: OracleRoundStatus,
161 pub value_status: OracleValueStatus,
163 pub source: OracleValueSource,
165 pub proxy_round: RoundData,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct OraclePriceCorrected {
172 pub id: FeedId,
174 pub proxy: Address,
176 pub aggregator: Option<Address>,
178 pub label: Option<String>,
180 pub base: Option<String>,
182 pub quote: Option<String>,
184 pub event_answer: I256,
186 pub raw_answer: I256,
188 pub decimals: u8,
190 pub event_round_id: U256,
192 pub updated_at: u64,
194 pub block_number: Option<u64>,
196 pub block_hash: Option<B256>,
198 pub log_index: Option<u64>,
200 pub round_status: OracleRoundStatus,
202 pub value_status: OracleValueStatus,
204 pub source: OracleValueSource,
206 pub corrected_round: RoundData,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct OraclePriceStale {
213 pub id: FeedId,
215 pub proxy: Address,
217 pub aggregator: Option<Address>,
219 pub label: Option<String>,
221 pub base: Option<String>,
223 pub quote: Option<String>,
225 pub raw_answer: I256,
227 pub decimals: u8,
229 pub event_round_id: U256,
231 pub updated_at: u64,
233 pub block_number: Option<u64>,
235 pub block_hash: Option<B256>,
237 pub log_index: Option<u64>,
239 pub round_status: OracleRoundStatus,
241 pub value_status: OracleValueStatus,
243 pub source: OracleValueSource,
245 pub proxy_round: RoundData,
247}
248
249#[non_exhaustive]
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub enum OracleHookEvent {
253 PriceUpdate(OraclePriceUpdate),
255 PriceConfirmed(OraclePriceConfirmed),
257 PriceCorrected(OraclePriceCorrected),
259 PriceStale(OraclePriceStale),
261 AggregatorChanged(AggregatorChange),
263}
264
265pub type OracleAggregatorChanged = AggregatorChange;
267
268#[derive(Clone, Debug, PartialEq, Eq)]
270pub struct OracleReconciliationResult {
271 pub request: OracleReconciliationRequest,
273 pub hooks: Vec<OracleHookEvent>,
275}
276
277#[derive(Clone, Debug, Default, PartialEq, Eq)]
279pub struct ReconcileReport {
280 pub checked_feeds: usize,
282 pub feed_statuses: Vec<OracleFeedReadinessReport>,
284 pub changed_feeds: Vec<FeedId>,
286 pub aggregator_changes: Vec<AggregatorChange>,
288}
289
290#[derive(Clone, Debug, PartialEq, Eq)]
293pub struct DerivedReconcileFailure {
294 pub request: OracleReconciliationRequest,
296 pub error: OracleError,
298}
299
300#[derive(Clone, Debug, Default, PartialEq, Eq)]
302pub struct DerivedReconcileReport {
303 pub reconciled: Vec<OracleReconciliationResult>,
305 pub failed: Vec<DerivedReconcileFailure>,
307}
308
309#[derive(Clone, Debug)]
311pub struct OracleTracker {
312 registry: OracleRegistry,
313 pending_reconciliations: Vec<OracleReconciliationRequest>,
314}
315
316impl OracleTracker {
317 pub fn new(registry: OracleRegistry) -> Self {
319 Self {
320 registry,
321 pending_reconciliations: Vec::new(),
322 }
323 }
324
325 pub fn from_registrations_at_timestamp(
327 feeds: Vec<(FeedRegistration, RoundData)>,
328 now_timestamp: u64,
329 ) -> Result<Self, OracleError> {
330 let mut registry = OracleRegistry::new_at_timestamp(now_timestamp);
331 for (registration, round) in feeds {
332 let id = registration.id.clone();
333 if registry.registrations_map().contains_key(&id) {
334 return Err(OracleError::DuplicateFeedId(id.to_string()));
335 }
336 if registry.id_by_proxy(registration.proxy).is_some() {
337 return Err(OracleError::DuplicateProxy(registration.proxy));
338 }
339
340 let snapshot = OracleSnapshot::proxy_read(
341 id.clone(),
342 registration.proxy,
343 registration.current_aggregator,
344 registration.metadata.clone(),
345 round,
346 now_timestamp,
347 ®istration.staleness,
348 );
349 registry
350 .registrations_map_mut()
351 .insert(id.clone(), registration.clone());
352 registry
353 .snapshots_by_proxy_mut()
354 .insert(registration.proxy, snapshot);
355 registry.record_proxy_id(registration.proxy, id);
356 }
357 Ok(Self {
358 registry,
359 pending_reconciliations: Vec::new(),
360 })
361 }
362
363 pub fn registrations(&self) -> impl Iterator<Item = FeedRegistration> + '_ {
368 self.registry.registrations()
369 }
370
371 pub fn registrations_iter(&self) -> impl Iterator<Item = &FeedRegistration> {
373 self.registry.registrations_iter()
374 }
375
376 pub fn feed_readiness(&self) -> Vec<OracleFeedReadinessReport> {
378 self.registry.feed_readiness()
379 }
380
381 pub fn insert_seeded_registration(
383 &mut self,
384 registration: FeedRegistration,
385 round: RoundData,
386 ) -> Result<(), OracleError> {
387 self.registry
388 .insert_seeded_registration(registration, round)
389 }
390
391 pub fn remove_by_id(&mut self, id: FeedId) -> Option<FeedRegistration> {
393 let registration = self.registry.remove_registration_by_id(id)?;
394 self.clear_pending_for_proxy(registration.proxy);
395 Some(registration)
396 }
397
398 pub fn remove_by_proxy(&mut self, proxy: Address) -> Option<FeedRegistration> {
400 let registration = self.registry.remove_registration_by_proxy(proxy)?;
401 self.clear_pending_for_proxy(registration.proxy);
402 Some(registration)
403 }
404
405 pub fn now_timestamp(&self) -> u64 {
407 self.registry.now_timestamp()
408 }
409
410 pub fn latest(&self, proxy: Address) -> Option<&OracleSnapshot> {
412 self.registry.latest(proxy)
413 }
414
415 pub fn registration_for_proxy(&self, proxy: Address) -> Option<&FeedRegistration> {
417 let id = self.registry.id_by_proxy(proxy)?;
418 self.registry.registrations_map().get(id)
419 }
420
421 pub fn price(&self, id: impl AsRef<str>) -> Result<OraclePrice, OracleError> {
428 let registration = self
429 .registration_by_id_str(id.as_ref())
430 .ok_or(OracleError::FeedNotFound)?;
431 self.price_for_registration(registration)
432 }
433
434 pub fn price_by_id(&self, id: FeedId) -> Result<OraclePrice, OracleError> {
441 let registration = self
442 .registry
443 .registrations_map()
444 .get(&id)
445 .ok_or(OracleError::FeedNotFound)?;
446 self.price_for_registration(registration)
447 }
448
449 pub fn price_by_proxy(&self, proxy: Address) -> Result<OraclePrice, OracleError> {
456 let id = self
457 .registry
458 .id_by_proxy(proxy)
459 .cloned()
460 .ok_or(OracleError::FeedNotFound)?;
461 self.price_by_id(id)
462 }
463
464 pub fn latest_round(&self, id: impl AsRef<str>) -> Result<RoundData, OracleError> {
471 Ok(self.price(id)?.round_data())
472 }
473
474 pub fn latest_round_by_proxy(&self, proxy: Address) -> Result<RoundData, OracleError> {
476 Ok(self.price_by_proxy(proxy)?.round_data())
477 }
478
479 pub fn pending_reconciliations(&self) -> &[OracleReconciliationRequest] {
481 &self.pending_reconciliations
482 }
483
484 pub fn apply_batch_report<N: Network>(
495 &mut self,
496 report: &ReactiveBatchReport<N>,
497 ) -> Result<(), OracleError> {
498 let rich_price_update_keys = report
503 .applied
504 .iter()
505 .flat_map(|applied| applied.hook_signals.iter())
506 .filter(|signal| {
507 signal.namespace.as_ref() == ORACLE_SIGNAL_NAMESPACE
508 && signal.kind.as_ref() == OracleSignalKind::PriceUpdate.as_str()
509 })
510 .filter_map(|signal| signal.payload.as_ref()?.downcast_ref::<OraclePriceUpdate>())
511 .map(|update| (update.proxy, update.event_round_id))
512 .collect::<std::collections::BTreeSet<_>>();
513
514 for applied in &report.applied {
515 for signal in &applied.hook_signals {
516 if signal.namespace.as_ref() != ORACLE_SIGNAL_NAMESPACE {
517 continue;
518 }
519
520 let Some(payload) = signal.payload.as_ref() else {
521 continue;
522 };
523
524 match signal.kind.as_ref() {
525 kind if kind == OracleSignalKind::PriceUpdate.as_str() => {
526 if let Some(update) = payload.downcast_ref::<OraclePriceUpdate>() {
527 self.apply_price_update_with_tags(update.clone(), &signal.labels)?;
528 }
529 }
530 kind if kind == ORACLE_LEGACY_ANSWER_UPDATED_KIND => {
531 if let Some(update) = payload.downcast_ref::<OracleUpdate>()
532 && !rich_price_update_keys
533 .contains(&(update.proxy, update.round.round_id))
534 {
535 self.apply_update(update.clone())?;
536 }
537 }
538 _ => {}
539 }
540 }
541 }
542
543 if report.reports.iter().any(|report| {
544 matches!(
545 report.as_ref(),
546 ReactiveReport::Reorg(reorg) if !reorg.dropped_blocks.is_empty()
547 )
548 }) {
549 self.mark_event_snapshots_unknown();
550 }
551
552 Ok(())
553 }
554
555 pub async fn reconcile<P: ChainlinkFeedProvider>(
557 &mut self,
558 provider: &P,
559 ) -> Result<ReconcileReport, OracleError> {
560 let registrations: Vec<_> = self.registry.registrations().collect();
561 let mut report = ReconcileReport::default();
562
563 for mut registration in registrations {
564 report.checked_feeds += 1;
565 if !registration.source.supports_proxy_reconciliation() {
566 continue;
567 }
568 let mut new_source = None;
569 let (round, new_aggregator, new_layout) = if matches!(
570 registration.source,
571 FeedSource::AaveSynchronicityPegToBase { .. }
572 ) {
573 let reconciled =
574 reconcile_current_aave_synchronicity_peg_to_base(self, provider, ®istration)
575 .await?;
576 new_source = reconciled.new_source;
577 (
578 reconciled.proxy_round,
579 reconciled.new_aggregator,
580 reconciled.new_layout,
581 )
582 } else {
583 let read_proxy = registration.source.read_proxy(registration.proxy);
584 let round = registration
585 .source
586 .normalize_round(provider.latest_round_data(read_proxy).await?);
587 let new_aggregator = provider
588 .aggregator(read_proxy)
589 .await
590 .unwrap_or(registration.current_aggregator);
591 let new_layout = self
592 .registry
593 .detect_aggregator_layout(provider, new_aggregator, None)
594 .await;
595 (round, new_aggregator, new_layout)
596 };
597 let old_aggregator = registration.current_aggregator;
598 let snapshot = snapshot_from_proxy_read(
599 ®istration,
600 round,
601 new_aggregator,
602 self.registry.now_timestamp(),
603 );
604
605 let changed = self
606 .registry
607 .latest(registration.proxy)
608 .is_none_or(|old| old != &snapshot);
609 if changed {
610 report.changed_feeds.push(registration.id.clone());
611 }
612
613 if old_aggregator != new_aggregator {
614 report.aggregator_changes.push(AggregatorChange {
615 id: registration.id.clone(),
616 proxy: registration.proxy,
617 old: old_aggregator,
618 new: new_aggregator,
619 });
620 }
621
622 registration.current_aggregator = new_aggregator;
623 registration.aggregator_layout = new_layout.clone();
624 if let Some(stored) = self
625 .registry
626 .registrations_map_mut()
627 .get_mut(®istration.id)
628 {
629 stored.current_aggregator = new_aggregator;
630 stored.aggregator_layout = new_layout;
631 if let Some(new_source) = new_source {
632 stored.source = new_source;
633 }
634 }
635 self.registry.replace_snapshot(snapshot);
636 self.clear_pending_for_proxy(registration.proxy);
637 }
638
639 report.feed_statuses = self
640 .registry
641 .registrations_iter()
642 .map(|registration| OracleFeedReadinessReport {
643 id: Some(registration.id.clone()),
644 proxy: registration.proxy,
645 status: registration.status,
646 reason: None,
647 })
648 .collect();
649
650 Ok(report)
651 }
652
653 pub(crate) fn registration_by_id_str(&self, id: &str) -> Option<&FeedRegistration> {
655 self.registry.registrations_map().get(id)
656 }
657
658 fn price_for_registration(
659 &self,
660 registration: &FeedRegistration,
661 ) -> Result<OraclePrice, OracleError> {
662 let snapshot = self
663 .registry
664 .latest(registration.proxy)
665 .ok_or(OracleError::FeedNotFound)?;
666 Ok(OraclePrice::from_snapshot(snapshot, registration))
667 }
668
669 fn apply_update(&mut self, update: OracleUpdate) -> Result<(), OracleError> {
670 let Some(registration) = self.registry.registrations_map().get(&update.id).cloned() else {
671 return Ok(());
672 };
673
674 if !registration
675 .source
676 .accepts_event_from(registration.current_aggregator, update.aggregator)
677 {
678 return Ok(());
679 }
680
681 let snapshot = OracleSnapshot::event(EventSnapshotInput {
682 id: update.id.clone(),
683 proxy: update.proxy,
684 aggregator: Some(update.aggregator),
685 metadata: registration.metadata,
686 round: update.round,
687 now_timestamp: self.registry.now_timestamp(),
688 staleness: ®istration.staleness,
689 block_number: update.block_number,
690 block_hash: None,
691 value_status: update.value_status,
692 source: registration.source.event_value_source(),
693 });
694 if let Some(kind) = Self::reconciliation_kind(®istration.source) {
695 self.queue_reconciliation(OracleReconciliationRequest {
696 id: update.id.clone(),
697 proxy: update.proxy,
698 aggregator: Some(update.aggregator),
699 event_round: snapshot.round.clone(),
700 block_number: update.block_number,
701 block_hash: None,
702 log_index: update.log_index,
703 kind,
704 });
705 }
706 self.registry.replace_snapshot(snapshot);
707 Ok(())
708 }
709
710 fn apply_price_update_with_tags(
711 &mut self,
712 update: OraclePriceUpdate,
713 labels: &[ReportTag],
714 ) -> Result<(), OracleError> {
715 let Some(mut registration) = self.registry.registrations_map().get(&update.id).cloned()
716 else {
717 return Ok(());
718 };
719
720 if let Some(new_source) = pyth_source_from_event_tags(®istration.source, labels) {
721 if let Some(stored) = self.registry.registrations_map_mut().get_mut(&update.id) {
722 stored.source = new_source.clone();
723 }
724 registration.source = new_source;
725 }
726
727 if !registration
728 .source
729 .accepts_event_from(registration.current_aggregator, update.aggregator)
730 {
731 return Ok(());
732 }
733
734 let event_round = update.round();
735 if update.value_status == OracleValueStatus::Unknown {
736 self.mark_matching_event_snapshot_unknown(
737 update.proxy,
738 &event_round,
739 update.block_number,
740 update.block_hash,
741 );
742 return Ok(());
743 }
744
745 let snapshot = OracleSnapshot::event(EventSnapshotInput {
746 id: update.id.clone(),
747 proxy: update.proxy,
748 aggregator: Some(update.aggregator),
749 metadata: registration.metadata,
750 round: event_round.clone(),
751 now_timestamp: self.registry.now_timestamp(),
752 staleness: ®istration.staleness,
753 block_number: update.block_number,
754 block_hash: update.block_hash,
755 value_status: update.value_status,
756 source: update.source,
757 });
758 if let Some(kind) = Self::reconciliation_kind(®istration.source) {
759 self.queue_reconciliation(OracleReconciliationRequest {
760 id: update.id.clone(),
761 proxy: update.proxy,
762 aggregator: Some(update.aggregator),
763 event_round,
764 block_number: update.block_number,
765 block_hash: update.block_hash,
766 log_index: update.log_index,
767 kind,
768 });
769 }
770 self.registry.replace_snapshot(snapshot);
771 Ok(())
772 }
773
774 fn mark_matching_event_snapshot_unknown(
775 &mut self,
776 proxy: Address,
777 event_round: &RoundData,
778 block_number: Option<u64>,
779 block_hash: Option<B256>,
780 ) {
781 let Some(snapshot) = self.registry.snapshots_by_proxy_mut().get_mut(&proxy) else {
782 return;
783 };
784 if !is_event_originated_source(snapshot.source) {
785 return;
786 }
787 if !proxy_round_matches_event(&snapshot.round, event_round) {
788 return;
789 }
790 if block_number.is_some() && snapshot.block_number != block_number {
791 return;
792 }
793 if block_hash.is_some() && snapshot.block_hash != block_hash {
794 return;
795 }
796
797 snapshot.mark_unknown();
798 self.clear_pending_for_proxy(proxy);
799 }
800
801 fn mark_event_snapshots_unknown(&mut self) {
802 let mut unknown_proxies = Vec::new();
803 for snapshot in self.registry.snapshots_by_proxy_mut().values_mut() {
804 if is_event_originated_source(snapshot.source) {
805 unknown_proxies.push(snapshot.proxy);
806 snapshot.mark_unknown();
807 }
808 }
809 for proxy in unknown_proxies {
810 self.clear_pending_for_proxy(proxy);
811 }
812 }
813
814 pub fn reconcile_derived_pending_with<R: crate::OracleDerivedReader>(
835 &mut self,
836 reader: &mut R,
837 ) -> DerivedReconcileReport {
838 let requests: Vec<OracleReconciliationRequest> = self
839 .pending_reconciliations
840 .iter()
841 .filter(|request| request.kind == OracleReconciliationKind::DerivedProtocolRead)
842 .cloned()
843 .collect();
844 let mut report = DerivedReconcileReport::default();
845 for request in requests {
846 let Some(registration) = self.registry.registrations_map().get(&request.id).cloned()
847 else {
848 report.failed.push(DerivedReconcileFailure {
849 request,
850 error: OracleError::FeedNotFound,
851 });
852 continue;
853 };
854 let answer = match reader.read_derived_value(®istration) {
855 Ok(answer) => answer,
856 Err(error) => {
857 report
858 .failed
859 .push(DerivedReconcileFailure { request, error });
860 continue;
861 }
862 };
863 let proxy_round = if answer == request.event_round.answer {
868 request.event_round.clone()
869 } else {
870 let now_timestamp = self.registry.now_timestamp();
871 RoundData {
872 round_id: request.event_round.round_id,
873 answer,
874 started_at: now_timestamp,
875 updated_at: now_timestamp,
876 answered_in_round: request.event_round.answered_in_round,
877 }
878 };
879 let apply = self.apply_reconciled_request(
883 &request,
884 proxy_round,
885 registration.current_aggregator,
886 registration.aggregator_layout.clone(),
887 None,
888 true,
889 );
890 match apply {
891 Ok(hooks) => report
892 .reconciled
893 .push(OracleReconciliationResult { request, hooks }),
894 Err(error) => report
895 .failed
896 .push(DerivedReconcileFailure { request, error }),
897 }
898 }
899 report
900 }
901
902 fn queue_reconciliation(&mut self, request: OracleReconciliationRequest) {
903 self.clear_pending_for_proxy(request.proxy);
904 self.pending_reconciliations.push(request);
905 }
906
907 fn reconciliation_kind(source: &FeedSource) -> Option<OracleReconciliationKind> {
911 if source.supports_proxy_reconciliation() {
912 Some(OracleReconciliationKind::Proxy)
913 } else if source.supports_derived_reconciliation() {
914 Some(OracleReconciliationKind::DerivedProtocolRead)
915 } else {
916 None
917 }
918 }
919
920 fn clear_pending_for_proxy(&mut self, proxy: Address) {
921 self.pending_reconciliations
922 .retain(|request| request.proxy != proxy);
923 }
924
925 fn complete_pending_reconciliation(&mut self, request: &OracleReconciliationRequest) {
926 self.pending_reconciliations
927 .retain(|pending| pending != request);
928 }
929
930 fn apply_reconciled_request(
931 &mut self,
932 request: &OracleReconciliationRequest,
933 proxy_round: RoundData,
934 new_aggregator: Option<Address>,
935 new_layout: Option<AggregatorLayoutEvidence>,
936 new_source: Option<FeedSource>,
937 round_is_normalized: bool,
938 ) -> Result<Vec<OracleHookEvent>, OracleError> {
939 let Some(registration) = self.registry.registrations_map().get(&request.id).cloned() else {
940 return Err(OracleError::FeedNotFound);
941 };
942
943 let proxy_round = if round_is_normalized {
944 proxy_round
945 } else {
946 registration.source.normalize_round(proxy_round)
947 };
948 let old_aggregator = registration.current_aggregator;
949 let mut snapshot = snapshot_from_proxy_read(
950 ®istration,
951 proxy_round.clone(),
952 new_aggregator,
953 self.registry.now_timestamp(),
954 );
955 let value_status =
956 reconciliation_value_status(&snapshot.round_status, request, &proxy_round);
957 snapshot.set_value_status(value_status);
958
959 let mut hooks = Vec::new();
960 if old_aggregator != new_aggregator {
961 hooks.push(OracleHookEvent::AggregatorChanged(AggregatorChange {
962 id: registration.id.clone(),
963 proxy: registration.proxy,
964 old: old_aggregator,
965 new: new_aggregator,
966 }));
967 }
968
969 match value_status {
970 OracleValueStatus::Confirmed => {
971 hooks.push(OracleHookEvent::PriceConfirmed(OraclePriceConfirmed {
972 id: registration.id.clone(),
973 proxy: registration.proxy,
974 aggregator: new_aggregator,
975 label: registration.label.clone(),
976 base: registration.base.clone(),
977 quote: registration.quote.clone(),
978 raw_answer: proxy_round.answer,
979 decimals: registration.metadata.decimals,
980 event_round_id: request.event_round.round_id,
981 updated_at: request.event_round.updated_at,
982 block_number: request.block_number,
983 block_hash: request.block_hash,
984 log_index: request.log_index,
985 round_status: snapshot.round_status.clone(),
986 value_status,
987 source: OracleValueSource::Proxy,
988 proxy_round: proxy_round.clone(),
989 }));
990 }
991 OracleValueStatus::Corrected => {
992 hooks.push(OracleHookEvent::PriceCorrected(OraclePriceCorrected {
993 id: registration.id.clone(),
994 proxy: registration.proxy,
995 aggregator: new_aggregator,
996 label: registration.label.clone(),
997 base: registration.base.clone(),
998 quote: registration.quote.clone(),
999 event_answer: request.event_round.answer,
1000 raw_answer: proxy_round.answer,
1001 decimals: registration.metadata.decimals,
1002 event_round_id: request.event_round.round_id,
1003 updated_at: request.event_round.updated_at,
1004 block_number: request.block_number,
1005 block_hash: request.block_hash,
1006 log_index: request.log_index,
1007 round_status: snapshot.round_status.clone(),
1008 value_status,
1009 source: OracleValueSource::Proxy,
1010 corrected_round: proxy_round.clone(),
1011 }));
1012 }
1013 OracleValueStatus::EventPending
1014 | OracleValueStatus::RequiresRepair
1015 | OracleValueStatus::Unknown => {}
1016 }
1017
1018 if matches!(snapshot.round_status, OracleRoundStatus::Stale { .. }) {
1019 hooks.push(OracleHookEvent::PriceStale(OraclePriceStale {
1020 id: registration.id.clone(),
1021 proxy: registration.proxy,
1022 aggregator: new_aggregator,
1023 label: registration.label.clone(),
1024 base: registration.base.clone(),
1025 quote: registration.quote.clone(),
1026 raw_answer: proxy_round.answer,
1027 decimals: registration.metadata.decimals,
1028 event_round_id: request.event_round.round_id,
1029 updated_at: request.event_round.updated_at,
1030 block_number: request.block_number,
1031 block_hash: request.block_hash,
1032 log_index: request.log_index,
1033 round_status: snapshot.round_status.clone(),
1034 value_status,
1035 source: OracleValueSource::Proxy,
1036 proxy_round: proxy_round.clone(),
1037 }));
1038 }
1039
1040 if let Some(stored) = self
1041 .registry
1042 .registrations_map_mut()
1043 .get_mut(®istration.id)
1044 {
1045 stored.current_aggregator = new_aggregator;
1046 stored.aggregator_layout = new_layout;
1047 if let Some(new_source) = new_source {
1048 stored.source = new_source;
1049 }
1050 }
1051 self.registry.replace_snapshot(snapshot);
1052 self.complete_pending_reconciliation(request);
1053 Ok(hooks)
1054 }
1055}
1056
1057#[derive(Clone, Debug, Default)]
1059pub struct OracleReconciler {
1060 queue: std::collections::VecDeque<OracleReconciliationRequest>,
1061}
1062
1063impl OracleReconciler {
1064 pub fn enqueue(&mut self, request: OracleReconciliationRequest) {
1066 self.queue.push_back(request);
1067 }
1068
1069 pub async fn reconcile_next<P: ChainlinkFeedProvider>(
1071 &mut self,
1072 tracker: &mut OracleTracker,
1073 provider: &P,
1074 ) -> Result<Option<OracleReconciliationResult>, OracleError> {
1075 let request = loop {
1076 let Some(front) = self.queue.front().cloned() else {
1077 return Ok(None);
1078 };
1079 if front.kind == OracleReconciliationKind::DerivedProtocolRead {
1080 self.queue.pop_front();
1084 continue;
1085 }
1086 break front;
1087 };
1088
1089 let Some(registration) = tracker
1090 .registry
1091 .registrations_map()
1092 .get(&request.id)
1093 .cloned()
1094 else {
1095 return Err(OracleError::FeedNotFound);
1096 };
1097 if !registration.source.supports_proxy_reconciliation() {
1098 self.queue.pop_front();
1099 return Ok(Some(OracleReconciliationResult {
1100 request,
1101 hooks: Vec::new(),
1102 }));
1103 }
1104 let block_ref = request.block_ref();
1105 let reconciled = if matches!(
1106 registration.source,
1107 FeedSource::AaveSynchronicityPegToBase { .. }
1108 ) {
1109 reconcile_aave_synchronicity_peg_to_base(
1110 tracker,
1111 provider,
1112 ®istration,
1113 &request,
1114 block_ref,
1115 )
1116 .await?
1117 } else {
1118 reconcile_single_proxy_source(tracker, provider, ®istration, &request, block_ref)
1119 .await?
1120 };
1121 let hooks = tracker.apply_reconciled_request(
1122 &request,
1123 reconciled.proxy_round,
1124 reconciled.new_aggregator,
1125 reconciled.new_layout,
1126 reconciled.new_source,
1127 reconciled.round_is_normalized,
1128 )?;
1129 self.queue.pop_front();
1130
1131 Ok(Some(OracleReconciliationResult { request, hooks }))
1132 }
1133}
1134
1135struct ReconciledSourceRead {
1136 proxy_round: RoundData,
1137 new_aggregator: Option<Address>,
1138 new_layout: Option<AggregatorLayoutEvidence>,
1139 new_source: Option<FeedSource>,
1140 round_is_normalized: bool,
1141}
1142
1143async fn reconcile_single_proxy_source<P: ChainlinkFeedProvider>(
1144 tracker: &mut OracleTracker,
1145 provider: &P,
1146 registration: &FeedRegistration,
1147 request: &OracleReconciliationRequest,
1148 block_ref: Option<OracleBlockRef>,
1149) -> Result<ReconciledSourceRead, OracleError> {
1150 let read_proxy = registration.source.read_proxy(registration.proxy);
1151 let proxy_round = provider.latest_round_data_at(read_proxy, block_ref).await?;
1152 let new_aggregator = provider
1153 .aggregator_at(read_proxy, block_ref)
1154 .await
1155 .unwrap_or_else(|_| {
1156 tracker
1157 .current_aggregator(&request.id)
1158 .or(request.aggregator)
1159 });
1160 let new_layout = tracker
1161 .detect_aggregator_layout(provider, new_aggregator, block_ref)
1162 .await;
1163 Ok(ReconciledSourceRead {
1164 proxy_round,
1165 new_aggregator,
1166 new_layout,
1167 new_source: None,
1168 round_is_normalized: false,
1169 })
1170}
1171
1172async fn reconcile_aave_synchronicity_peg_to_base<P: ChainlinkFeedProvider>(
1173 tracker: &mut OracleTracker,
1174 provider: &P,
1175 registration: &FeedRegistration,
1176 request: &OracleReconciliationRequest,
1177 block_ref: Option<OracleBlockRef>,
1178) -> Result<ReconciledSourceRead, OracleError> {
1179 let FeedSource::AaveSynchronicityPegToBase {
1180 asset_to_peg_proxy,
1181 asset_to_peg_aggregator,
1182 peg_to_base_proxy,
1183 peg_to_base_aggregator,
1184 ..
1185 } = registration.source.clone()
1186 else {
1187 unreachable!("caller checked source family")
1188 };
1189
1190 let asset_round = provider
1191 .latest_round_data_at(asset_to_peg_proxy, block_ref)
1192 .await?;
1193 let peg_round = provider
1194 .latest_round_data_at(peg_to_base_proxy, block_ref)
1195 .await?;
1196 let new_asset_aggregator = provider
1197 .aggregator_at(asset_to_peg_proxy, block_ref)
1198 .await
1199 .unwrap_or(Some(asset_to_peg_aggregator));
1200 let new_peg_aggregator = provider
1201 .aggregator_at(peg_to_base_proxy, block_ref)
1202 .await
1203 .unwrap_or(Some(peg_to_base_aggregator));
1204 let changed_dependency_round = if new_peg_aggregator == request.aggregator {
1205 &peg_round
1206 } else {
1207 &asset_round
1208 };
1209 let derived_answer = registration
1210 .source
1211 .normalize_dependency_answers(asset_round.answer, peg_round.answer)
1212 .ok_or_else(|| {
1213 OracleError::Config(crate::error::OracleConfigError::Other(
1214 "source is not an Aave peg-to-base synchronicity source".to_string(),
1215 ))
1216 })?;
1217 let proxy_round = RoundData {
1218 round_id: changed_dependency_round.round_id,
1219 answer: derived_answer,
1220 started_at: changed_dependency_round.started_at,
1221 updated_at: changed_dependency_round.updated_at,
1222 answered_in_round: changed_dependency_round.answered_in_round,
1223 };
1224 let new_source = match (new_asset_aggregator, new_peg_aggregator) {
1225 (Some(asset_aggregator), Some(peg_aggregator)) => {
1226 registration.source.with_synchronicity_dependency_state(
1227 asset_aggregator,
1228 asset_round.answer,
1229 peg_aggregator,
1230 peg_round.answer,
1231 )
1232 }
1233 _ => None,
1234 };
1235 let new_layout = tracker
1236 .detect_aggregator_layout(provider, new_asset_aggregator, block_ref)
1237 .await;
1238
1239 Ok(ReconciledSourceRead {
1240 proxy_round,
1241 new_aggregator: new_asset_aggregator,
1242 new_layout,
1243 new_source,
1244 round_is_normalized: true,
1245 })
1246}
1247
1248async fn reconcile_current_aave_synchronicity_peg_to_base<P: ChainlinkFeedProvider>(
1249 tracker: &mut OracleTracker,
1250 provider: &P,
1251 registration: &FeedRegistration,
1252) -> Result<ReconciledSourceRead, OracleError> {
1253 let FeedSource::AaveSynchronicityPegToBase {
1254 asset_to_peg_proxy,
1255 asset_to_peg_aggregator,
1256 peg_to_base_proxy,
1257 peg_to_base_aggregator,
1258 ..
1259 } = registration.source.clone()
1260 else {
1261 unreachable!("caller checked source family")
1262 };
1263
1264 let asset_round = provider.latest_round_data(asset_to_peg_proxy).await?;
1265 let peg_round = provider.latest_round_data(peg_to_base_proxy).await?;
1266 let new_asset_aggregator = provider
1267 .aggregator(asset_to_peg_proxy)
1268 .await
1269 .unwrap_or(Some(asset_to_peg_aggregator));
1270 let new_peg_aggregator = provider
1271 .aggregator(peg_to_base_proxy)
1272 .await
1273 .unwrap_or(Some(peg_to_base_aggregator));
1274 let representative_round = if peg_round.updated_at >= asset_round.updated_at {
1275 &peg_round
1276 } else {
1277 &asset_round
1278 };
1279 let derived_answer = registration
1280 .source
1281 .normalize_dependency_answers(asset_round.answer, peg_round.answer)
1282 .ok_or_else(|| {
1283 OracleError::Config(crate::error::OracleConfigError::Other(
1284 "source is not an Aave peg-to-base synchronicity source".to_string(),
1285 ))
1286 })?;
1287 let proxy_round = RoundData {
1288 round_id: representative_round.round_id,
1289 answer: derived_answer,
1290 started_at: representative_round.started_at,
1291 updated_at: representative_round.updated_at,
1292 answered_in_round: representative_round.answered_in_round,
1293 };
1294 let new_source = match (new_asset_aggregator, new_peg_aggregator) {
1295 (Some(asset_aggregator), Some(peg_aggregator)) => {
1296 registration.source.with_synchronicity_dependency_state(
1297 asset_aggregator,
1298 asset_round.answer,
1299 peg_aggregator,
1300 peg_round.answer,
1301 )
1302 }
1303 _ => None,
1304 };
1305 let new_layout = tracker
1306 .detect_aggregator_layout(provider, new_asset_aggregator, None)
1307 .await;
1308
1309 Ok(ReconciledSourceRead {
1310 proxy_round,
1311 new_aggregator: new_asset_aggregator,
1312 new_layout,
1313 new_source,
1314 round_is_normalized: true,
1315 })
1316}
1317
1318impl OracleTracker {
1319 async fn detect_aggregator_layout<P: ChainlinkFeedProvider>(
1320 &mut self,
1321 provider: &P,
1322 aggregator: Option<Address>,
1323 block: Option<OracleBlockRef>,
1324 ) -> Option<AggregatorLayoutEvidence> {
1325 self.registry
1326 .detect_aggregator_layout(provider, aggregator, block)
1327 .await
1328 }
1329
1330 fn current_aggregator(&self, id: &FeedId) -> Option<Address> {
1331 self.registry
1332 .registrations_map()
1333 .get(id)
1334 .and_then(|registration| registration.current_aggregator)
1335 }
1336}
1337
1338fn reconciliation_value_status(
1339 round_status: &OracleRoundStatus,
1340 request: &OracleReconciliationRequest,
1341 proxy_round: &RoundData,
1342) -> OracleValueStatus {
1343 match round_status {
1344 OracleRoundStatus::Unknown => OracleValueStatus::RequiresRepair,
1345 OracleRoundStatus::Fresh
1346 | OracleRoundStatus::Stale { .. }
1347 | OracleRoundStatus::IncompleteRound
1348 | OracleRoundStatus::InvalidAnswer
1349 if proxy_round_matches_event(proxy_round, &request.event_round) =>
1350 {
1351 OracleValueStatus::Confirmed
1352 }
1353 OracleRoundStatus::Fresh
1354 | OracleRoundStatus::Stale { .. }
1355 | OracleRoundStatus::IncompleteRound
1356 | OracleRoundStatus::InvalidAnswer => OracleValueStatus::Corrected,
1357 }
1358}
1359
1360fn is_event_originated_source(source: OracleValueSource) -> bool {
1361 matches!(
1362 source,
1363 OracleValueSource::Event | OracleValueSource::Derived
1364 )
1365}
1366
1367fn proxy_round_matches_event(proxy_round: &RoundData, event_round: &RoundData) -> bool {
1368 proxy_round.round_id == event_round.round_id
1369 && proxy_round.answer == event_round.answer
1370 && proxy_round.updated_at == event_round.updated_at
1371}
1372
1373fn pyth_source_from_event_tags(source: &FeedSource, labels: &[ReportTag]) -> Option<FeedSource> {
1374 let (_pyth, price_id, current_expo, current_conf) = source.pyth_source()?;
1375 let mut expo = current_expo;
1376 let mut conf = current_conf;
1377 let mut saw_pyth_label = false;
1378
1379 for label in labels {
1380 match label.key.as_str() {
1381 "pyth_expo" => {
1382 if let Ok(value) = label.value.parse::<i32>() {
1383 expo = value;
1384 saw_pyth_label = true;
1385 }
1386 }
1387 "pyth_conf" => {
1388 if let Ok(value) = label.value.parse::<u64>() {
1389 conf = value;
1390 saw_pyth_label = true;
1391 }
1392 }
1393 _ => {}
1394 }
1395 }
1396
1397 saw_pyth_label.then(|| {
1398 source
1399 .with_pyth_event_metadata(price_id, expo, conf)
1400 .expect("pyth_source returned Some for Pyth source")
1401 })
1402}
1403
1404#[allow(dead_code)]
1405fn _ethereum_batch_report_type_is_supported(_: &ReactiveBatchReport<Ethereum>) {}