1use alloy_primitives::{Address, I256, U256};
2use thiserror::Error;
3
4use crate::{
5 AssetId, Denomination, FeedRegistration, OracleError, OracleRoundStatus, OracleSnapshot,
6 OracleValueSource, OracleValueStatus, RoundData, TokenAmount, ValuedAmount,
7};
8
9#[non_exhaustive]
15#[derive(Clone, Debug, Error, PartialEq, Eq)]
16pub enum PricePolicyViolation {
17 #[error("price base asset is unknown")]
19 MissingBase,
20 #[error("price quote denomination is unknown")]
22 MissingQuote,
23 #[error("price quote {actual} does not match required quote {required}")]
26 QuoteMismatch {
27 required: Denomination,
29 actual: Denomination,
31 },
32 #[error("price is stale: age {age_secs}s exceeds max age {max_age_secs}s")]
34 Stale {
35 age_secs: u64,
37 max_age_secs: u64,
39 },
40 #[error("price round is incomplete")]
42 IncompleteRound,
43 #[error("price answer is invalid")]
45 InvalidAnswer,
46 #[error("price round status is unknown")]
48 UnknownRoundStatus,
49 #[error("{}", value_status_violation_message(.0))]
53 ValueStatusNotAllowed(OracleValueStatus),
54 #[error("{}", source_violation_message(.0))]
57 SourceNotAllowed(OracleValueSource),
58 #[error("liquidation price requires a positive market price")]
60 NonPositivePrice,
61 #[error("cannot value an amount with a negative market price")]
63 NegativePrice,
64 #[error("token amount base asset {actual} does not match price base {expected}")]
66 BaseAssetMismatch {
67 expected: AssetId,
69 actual: AssetId,
71 },
72 #[error("valued amount cannot be negative")]
74 NegativeValuedAmount,
75 #[error("{0}")]
80 ValueOutOfRange(&'static str),
81}
82
83fn value_status_violation_message(status: &OracleValueStatus) -> &'static str {
84 match status {
85 OracleValueStatus::EventPending => "price is pending authoritative proxy reconciliation",
86 OracleValueStatus::RequiresRepair => "price requires authoritative repair",
87 OracleValueStatus::Unknown => "price value status is unknown",
88 _ => "price value status is not allowed",
89 }
90}
91
92fn source_violation_message(source: &OracleValueSource) -> &'static str {
93 match source {
94 OracleValueSource::Event => "event-derived price source is not allowed",
95 OracleValueSource::Mock => "mock price source is not allowed",
96 OracleValueSource::Derived => "derived price source is not allowed",
97 OracleValueSource::Unknown => "price source is unknown",
98 _ => "price source is not allowed",
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct OraclePrice {
105 pub id: crate::FeedId,
107 pub proxy: Address,
109 pub aggregator: Option<Address>,
111 pub label: Option<String>,
113 pub base: Option<String>,
115 pub quote: Option<String>,
117 pub raw_answer: I256,
119 pub decimals: u8,
121 pub round_id: U256,
123 pub updated_at: u64,
125 pub round: RoundData,
127 pub round_status: OracleRoundStatus,
129 pub value_status: OracleValueStatus,
131 pub source: OracleValueSource,
133}
134
135impl OraclePrice {
136 pub(crate) fn from_snapshot(
137 snapshot: &OracleSnapshot,
138 registration: &FeedRegistration,
139 ) -> Self {
140 Self {
141 id: snapshot.id.clone(),
142 proxy: snapshot.proxy,
143 aggregator: snapshot.aggregator,
144 label: registration.label.clone(),
145 base: registration.base.clone(),
146 quote: registration.quote.clone(),
147 raw_answer: snapshot.round.answer,
148 decimals: snapshot.metadata.decimals,
149 round_id: snapshot.round.round_id,
150 updated_at: snapshot.round.updated_at,
151 round: snapshot.round.clone(),
152 round_status: snapshot.round_status.clone(),
153 value_status: snapshot.value_status,
154 source: snapshot.source,
155 }
156 }
157
158 pub fn is_actionable(&self) -> bool {
160 self.round_status == OracleRoundStatus::Fresh
161 && matches!(
162 self.value_status,
163 OracleValueStatus::EventPending
164 | OracleValueStatus::Confirmed
165 | OracleValueStatus::Corrected
166 )
167 }
168
169 pub fn is_event_pending(&self) -> bool {
171 self.value_status == OracleValueStatus::EventPending
172 }
173
174 pub fn is_confirmed(&self) -> bool {
176 self.value_status == OracleValueStatus::Confirmed
177 }
178
179 pub fn is_corrected(&self) -> bool {
181 self.value_status == OracleValueStatus::Corrected
182 }
183
184 pub fn round_data(&self) -> RoundData {
186 self.round.clone()
187 }
188
189 pub fn scaled_to(&self, target_decimals: u8) -> Result<I256, OracleError> {
191 scale_i256(self.raw_answer, self.decimals, target_decimals)
192 }
193
194 pub fn require(&self, policy: PricePolicy) -> Result<CheckedPrice, OracleError> {
196 policy.check(self)
197 }
198}
199
200#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct PricePolicy {
203 kind: PricePolicyKind,
204 quote: Option<Denomination>,
205 allow_event_pending: bool,
206 allow_mock_source: bool,
207 allow_derived_source: bool,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq)]
211enum PricePolicyKind {
212 Liquidation,
213}
214
215impl PricePolicy {
216 pub fn liquidation() -> Self {
218 Self {
219 kind: PricePolicyKind::Liquidation,
220 quote: None,
221 allow_event_pending: false,
222 allow_mock_source: false,
223 allow_derived_source: false,
224 }
225 }
226
227 pub fn quote(mut self, quote: Denomination) -> Self {
229 self.quote = Some(quote);
230 self
231 }
232
233 pub fn allow_event_pending(mut self) -> Self {
235 self.allow_event_pending = true;
236 self
237 }
238
239 pub fn allow_mock_source(mut self) -> Self {
241 self.allow_mock_source = true;
242 self
243 }
244
245 pub fn allow_derived_source(mut self) -> Self {
247 self.allow_derived_source = true;
248 self
249 }
250
251 fn check(&self, price: &OraclePrice) -> Result<CheckedPrice, OracleError> {
252 let base = price
253 .base
254 .as_deref()
255 .map(AssetId::symbol)
256 .ok_or(OracleError::Policy(PricePolicyViolation::MissingBase))?;
257 let quote = price
258 .quote
259 .as_deref()
260 .map(Denomination::from)
261 .ok_or(OracleError::Policy(PricePolicyViolation::MissingQuote))?;
262
263 if let Some(required_quote) = &self.quote
264 && "e != required_quote
265 {
266 return Err(OracleError::Policy(PricePolicyViolation::QuoteMismatch {
267 required: required_quote.clone(),
268 actual: quote.clone(),
269 }));
270 }
271
272 match &price.round_status {
273 OracleRoundStatus::Fresh => {}
274 OracleRoundStatus::Stale {
275 age_secs,
276 max_age_secs,
277 } => {
278 return Err(OracleError::Policy(PricePolicyViolation::Stale {
279 age_secs: *age_secs,
280 max_age_secs: *max_age_secs,
281 }));
282 }
283 OracleRoundStatus::IncompleteRound => {
284 return Err(OracleError::Policy(PricePolicyViolation::IncompleteRound));
285 }
286 OracleRoundStatus::InvalidAnswer => {
287 return Err(OracleError::Policy(PricePolicyViolation::InvalidAnswer));
288 }
289 OracleRoundStatus::Unknown => {
290 return Err(OracleError::Policy(
291 PricePolicyViolation::UnknownRoundStatus,
292 ));
293 }
294 }
295
296 match price.value_status {
297 OracleValueStatus::Confirmed | OracleValueStatus::Corrected => {}
298 OracleValueStatus::EventPending if self.allow_event_pending => {}
299 OracleValueStatus::EventPending => {
300 return Err(OracleError::Policy(
301 PricePolicyViolation::ValueStatusNotAllowed(OracleValueStatus::EventPending),
302 ));
303 }
304 OracleValueStatus::RequiresRepair => {
305 return Err(OracleError::Policy(
306 PricePolicyViolation::ValueStatusNotAllowed(OracleValueStatus::RequiresRepair),
307 ));
308 }
309 OracleValueStatus::Unknown => {
310 return Err(OracleError::Policy(
311 PricePolicyViolation::ValueStatusNotAllowed(OracleValueStatus::Unknown),
312 ));
313 }
314 }
315
316 match price.source {
317 OracleValueSource::Proxy => {}
318 OracleValueSource::Event if self.allow_event_pending => {}
319 OracleValueSource::Event => {
320 return Err(OracleError::Policy(PricePolicyViolation::SourceNotAllowed(
321 OracleValueSource::Event,
322 )));
323 }
324 OracleValueSource::Mock if self.allow_mock_source => {}
325 OracleValueSource::Mock => {
326 return Err(OracleError::Policy(PricePolicyViolation::SourceNotAllowed(
327 OracleValueSource::Mock,
328 )));
329 }
330 OracleValueSource::Derived if self.allow_derived_source => {}
331 OracleValueSource::Derived => {
332 return Err(OracleError::Policy(PricePolicyViolation::SourceNotAllowed(
333 OracleValueSource::Derived,
334 )));
335 }
336 OracleValueSource::Unknown => {
337 return Err(OracleError::Policy(PricePolicyViolation::SourceNotAllowed(
338 OracleValueSource::Unknown,
339 )));
340 }
341 }
342
343 match self.kind {
344 PricePolicyKind::Liquidation if price.raw_answer <= I256::ZERO => {
345 return Err(OracleError::Policy(PricePolicyViolation::NonPositivePrice));
346 }
347 PricePolicyKind::Liquidation => {}
348 }
349
350 Ok(CheckedPrice {
351 base,
352 quote,
353 raw_answer: price.raw_answer,
354 decimals: price.decimals,
355 })
356 }
357}
358
359#[derive(Clone, Debug, PartialEq, Eq)]
361pub struct CheckedPrice {
362 base: AssetId,
363 quote: Denomination,
364 raw_answer: I256,
365 decimals: u8,
366}
367
368impl CheckedPrice {
369 pub fn base(&self) -> &AssetId {
371 &self.base
372 }
373
374 pub fn quote(&self) -> &Denomination {
376 &self.quote
377 }
378
379 pub fn raw_answer(&self) -> I256 {
381 self.raw_answer
382 }
383
384 pub fn value_of(
386 &self,
387 amount: TokenAmount,
388 output_decimals: u8,
389 ) -> Result<ValuedAmount, OracleError> {
390 if amount.asset() != &self.base {
391 return Err(OracleError::Policy(
392 PricePolicyViolation::BaseAssetMismatch {
393 expected: self.base.clone(),
394 actual: amount.asset().clone(),
395 },
396 ));
397 }
398 if self.raw_answer.is_negative() {
399 return Err(OracleError::Policy(PricePolicyViolation::NegativePrice));
400 }
401
402 let amount_raw = I256::try_from(amount.raw()).map_err(|_| {
403 OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
404 "token amount does not fit in int256",
405 ))
406 })?;
407 let product = amount_raw
408 .checked_mul(self.raw_answer)
409 .ok_or(OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
410 "valuation multiplication overflowed",
411 )))?;
412
413 let input_decimals = u16::from(amount.decimals()) + u16::from(self.decimals);
414 let raw = if input_decimals >= u16::from(output_decimals) {
415 let scale = pow10_i256(input_decimals - u16::from(output_decimals))?;
416 product.checked_div(scale).ok_or(OracleError::Policy(
417 PricePolicyViolation::ValueOutOfRange("valuation division failed"),
418 ))?
419 } else {
420 let scale = pow10_i256(u16::from(output_decimals) - input_decimals)?;
421 product.checked_mul(scale).ok_or(OracleError::Policy(
422 PricePolicyViolation::ValueOutOfRange("valuation scale-up overflowed"),
423 ))?
424 };
425
426 ValuedAmount::checked_new(self.quote.clone(), raw, output_decimals)
427 }
428}
429
430fn scale_i256(value: I256, from_decimals: u8, to_decimals: u8) -> Result<I256, OracleError> {
431 if from_decimals == to_decimals {
432 return Ok(value);
433 }
434
435 let diff = from_decimals.abs_diff(to_decimals);
436 let factor = I256::unchecked_from(10_i64)
437 .checked_pow(U256::from(diff))
438 .ok_or(OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
439 "decimal scale factor overflowed",
440 )))?;
441
442 if to_decimals > from_decimals {
443 value
444 .checked_mul(factor)
445 .ok_or(OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
446 "scaled oracle price overflowed",
447 )))
448 } else {
449 value
450 .checked_div(factor)
451 .ok_or(OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
452 "scaled oracle price division failed",
453 )))
454 }
455}
456
457fn pow10_i256(exp: u16) -> Result<I256, OracleError> {
458 I256::unchecked_from(10_i64)
459 .checked_pow(U256::from(exp))
460 .ok_or(OracleError::Policy(PricePolicyViolation::ValueOutOfRange(
461 "decimal scale factor overflowed",
462 )))
463}