1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
use alloy_primitives::{Address, B256, I256, U256};
use crate::{FeedId, StalenessPolicy};
/// Static metadata read from a Chainlink-compatible proxy.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FeedMetadata {
/// Number of decimals used by the answer.
pub decimals: u8,
/// Human-readable feed description.
pub description: String,
/// Feed contract version.
pub version: U256,
}
/// Round tuple returned by `latestRoundData()`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoundData {
/// Proxy or aggregator round id, depending on the source.
pub round_id: U256,
/// Signed answer.
pub answer: I256,
/// `startedAt` timestamp.
pub started_at: u64,
/// `updatedAt` timestamp.
pub updated_at: u64,
/// `answeredInRound`.
pub answered_in_round: U256,
}
/// Lifecycle readiness of a registered oracle feed.
///
/// This is a feed/runtime readiness signal, not a price freshness signal. A
/// feed can be [`Ready`](Self::Ready) while its current round is
/// [`Stale`](OracleRoundStatus::Stale).
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum OracleFeedStatus {
/// Registration was requested but has not been initialized.
#[default]
Pending,
/// Metadata or registration exists, but usable state/warmup is incomplete.
Cold,
/// Feed is structurally usable.
Ready,
/// Feed remains registered, but a repair, reconciliation, source, layout, or warmup path failed.
Degraded,
/// Feed was explicitly disabled by the caller/runtime.
Disabled,
/// Feed source/layout/protocol is not supported.
Unsupported,
}
/// Classified usability of an oracle round under the feed policy.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OracleRoundStatus {
/// Round is usable under its configured policy.
Fresh,
/// Round is older than the configured max age.
Stale {
/// Observed age in seconds.
age_secs: u64,
/// Configured max age in seconds.
max_age_secs: u64,
},
/// Round data is incomplete.
IncompleteRound,
/// Answer is disallowed by policy.
InvalidAnswer,
/// Round validity is not currently known.
Unknown,
}
/// Reconciliation lifecycle for an oracle value.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OracleValueStatus {
/// Snapshot was derived from an event and still needs proxy reconciliation.
EventPending,
/// Value came from the authoritative source path without needing event correction.
///
/// This does not imply the round is fresh; inspect [`OracleRoundStatus`].
Confirmed,
/// Event-derived value was superseded by an authoritative source read.
///
/// This does not imply the corrected round is fresh; inspect [`OracleRoundStatus`].
Corrected,
/// Value/cache path is not trusted as final and needs authoritative repair.
///
/// Payloads may still carry a best-available diagnostic value, but
/// [`crate::PricePolicy`] rejects this status by default.
RequiresRepair,
/// Value lifecycle is not currently known.
Unknown,
}
/// Source that produced an oracle value.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OracleValueSource {
/// Value came from the authoritative proxy/source path.
Proxy,
/// Value came from an oracle event and must be reconciled before final trust.
Event,
/// Value was derived from one or more dependency feeds.
Derived,
/// Value came from a caller-controlled mock override.
Mock,
/// Value source is not currently trusted or known.
Unknown,
}
/// Typed current oracle state for one registered proxy feed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OracleSnapshot {
/// Registered feed id.
pub id: FeedId,
/// User-facing proxy address.
pub proxy: Address,
/// Best-known current aggregator address.
pub aggregator: Option<Address>,
/// Feed metadata read from the proxy.
pub metadata: FeedMetadata,
/// Current round data.
pub round: RoundData,
/// Current round validity status.
pub round_status: OracleRoundStatus,
/// Current value reconciliation lifecycle.
pub value_status: OracleValueStatus,
/// State source.
pub source: OracleValueSource,
/// Block number associated with the observation, when known.
pub block_number: Option<u64>,
/// Block hash associated with the observation, when known.
pub block_hash: Option<B256>,
requires_reconciliation: bool,
}
impl OracleSnapshot {
pub(crate) fn proxy_read(
id: FeedId,
proxy: Address,
aggregator: Option<Address>,
metadata: FeedMetadata,
round: RoundData,
now_timestamp: u64,
staleness: &StalenessPolicy,
) -> Self {
let round_status = classify_round(&round, now_timestamp, staleness);
Self {
id,
proxy,
aggregator,
metadata,
round,
round_status,
value_status: OracleValueStatus::Confirmed,
source: OracleValueSource::Proxy,
block_number: None,
block_hash: None,
requires_reconciliation: false,
}
}
pub(crate) fn event(input: EventSnapshotInput<'_>) -> Self {
let round_status = classify_round(&input.round, input.now_timestamp, input.staleness);
Self {
id: input.id,
proxy: input.proxy,
aggregator: input.aggregator,
metadata: input.metadata,
round: input.round,
round_status,
value_status: input.value_status,
source: input.source,
block_number: input.block_number,
block_hash: input.block_hash,
requires_reconciliation: matches!(
input.value_status,
OracleValueStatus::EventPending
| OracleValueStatus::RequiresRepair
| OracleValueStatus::Unknown
),
}
}
pub(crate) fn mark_unknown(&mut self) {
self.round_status = OracleRoundStatus::Unknown;
self.value_status = OracleValueStatus::RequiresRepair;
self.source = OracleValueSource::Unknown;
self.requires_reconciliation = true;
}
pub(crate) fn set_value_status(&mut self, value_status: OracleValueStatus) {
self.value_status = value_status;
self.requires_reconciliation = matches!(
value_status,
OracleValueStatus::EventPending
| OracleValueStatus::RequiresRepair
| OracleValueStatus::Unknown
);
}
/// Return true when callers should repair this snapshot via proxy read.
pub fn requires_reconciliation(&self) -> bool {
self.requires_reconciliation
}
}
pub(crate) struct EventSnapshotInput<'a> {
pub(crate) id: FeedId,
pub(crate) proxy: Address,
pub(crate) aggregator: Option<Address>,
pub(crate) metadata: FeedMetadata,
pub(crate) round: RoundData,
pub(crate) now_timestamp: u64,
pub(crate) staleness: &'a StalenessPolicy,
pub(crate) block_number: Option<u64>,
pub(crate) block_hash: Option<B256>,
pub(crate) value_status: OracleValueStatus,
pub(crate) source: OracleValueSource,
}
pub(crate) fn classify_round(
round: &RoundData,
now_timestamp: u64,
staleness: &StalenessPolicy,
) -> OracleRoundStatus {
if round.updated_at == 0 || round.answered_in_round < round.round_id {
return OracleRoundStatus::IncompleteRound;
}
if !staleness.allows_zero_or_negative_answer() && round.answer <= I256::ZERO {
return OracleRoundStatus::InvalidAnswer;
}
if let Some(max_age_secs) = staleness.max_age_secs() {
let age_secs = now_timestamp.saturating_sub(round.updated_at);
if age_secs > max_age_secs {
return OracleRoundStatus::Stale {
age_secs,
max_age_secs,
};
}
}
OracleRoundStatus::Fresh
}