1use std::collections::BTreeMap;
2
3use alloy_primitives::{Address, B256, Bytes, I256, U256, Uint};
4use alloy_sol_types::{SolCall, sol};
5
6use crate::{
7 FeedRegistration, FeedSource, OracleError, OracleRoundStatus, OracleSnapshot, OracleTracker,
8 OracleValueStatus,
9};
10
11sol! {
12 interface AggregatorV3Interface {
13 function latestRoundData() external view returns (
14 uint80 roundId,
15 int256 answer,
16 uint256 startedAt,
17 uint256 updatedAt,
18 uint80 answeredInRound
19 );
20 function decimals() external view returns (uint8);
21 function description() external view returns (string);
22 function version() external view returns (uint256);
23 }
24
25 interface MorphoOracleInterface {
26 function price() external view returns (uint256);
27 }
28
29 interface EulerPriceOracleInterface {
30 function getQuote(uint256 inAmount, address base, address quote) external view returns (uint256 outAmount);
31 function getQuotes(uint256 inAmount, address base, address quote) external view returns (uint256 bidOutAmount, uint256 askOutAmount);
32 }
33
34 struct PythPrice {
35 int64 price;
36 uint64 conf;
37 int32 expo;
38 uint256 publishTime;
39 }
40
41 interface PythInterface {
42 function getPriceUnsafe(bytes32 id) external view returns (PythPrice memory price);
43 function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PythPrice memory price);
44 }
45}
46
47use AggregatorV3Interface::{decimalsCall, descriptionCall, latestRoundDataCall, versionCall};
48use EulerPriceOracleInterface::{getQuoteCall, getQuotesCall, getQuotesReturn};
49use MorphoOracleInterface::priceCall;
50use PythInterface::{getPriceNoOlderThanCall, getPriceUnsafeCall};
51
52pub struct OracleReadOverlay<'a> {
74 tracker: &'a OracleTracker,
75 pyth_feeds: BTreeMap<(Address, B256), PythOverlayFeed>,
76 euler_feeds: BTreeMap<Address, Vec<EulerOverlayFeed>>,
81}
82
83#[derive(Clone, Copy, Debug)]
84struct PythOverlayFeed {
85 proxy: Address,
86 expo: i32,
87 conf: u64,
88}
89
90#[derive(Clone, Copy, Debug)]
91struct EulerOverlayFeed {
92 proxy: Address,
93 base: Address,
94 quote: Address,
95}
96
97impl<'a> OracleReadOverlay<'a> {
98 pub fn new(tracker: &'a OracleTracker) -> Self {
100 let mut pyth_feeds = BTreeMap::new();
101 let mut euler_feeds: BTreeMap<Address, Vec<EulerOverlayFeed>> = BTreeMap::new();
102 for registration in tracker.registrations_iter() {
103 match ®istration.source {
104 FeedSource::EulerQuote {
105 source,
106 base,
107 quote,
108 ..
109 }
110 | FeedSource::EulerCross {
111 source,
112 base,
113 quote,
114 ..
115 } => {
116 euler_feeds
117 .entry(*source)
118 .or_default()
119 .push(EulerOverlayFeed {
120 proxy: registration.proxy,
121 base: *base,
122 quote: *quote,
123 });
124 continue;
125 }
126 _ => {}
127 }
128 let Some((pyth, price_id, expo, conf)) = registration.source.pyth_source() else {
129 continue;
130 };
131 pyth_feeds
132 .entry((pyth, price_id))
133 .or_insert(PythOverlayFeed {
134 proxy: registration.proxy,
135 expo,
136 conf,
137 });
138 }
139 Self {
140 tracker,
141 pyth_feeds,
142 euler_feeds,
143 }
144 }
145
146 pub fn try_call(&self, target: Address, calldata: &[u8]) -> Result<Option<Bytes>, OracleError> {
148 if let Some(bytes) = self.try_pyth_call(target, calldata)? {
149 return Ok(Some(bytes));
150 }
151 if let Some(bytes) = self.try_euler_target_call(target, calldata)? {
152 return Ok(Some(bytes));
153 }
154
155 let Some(snapshot) = self.tracker.latest(target) else {
156 return Ok(None);
157 };
158 let Some(registration) = self.tracker.registration_for_proxy(target) else {
159 return Ok(None);
160 };
161 if !is_actionable(snapshot) || calldata.len() != 4 {
162 if is_actionable(snapshot)
163 && let Some(bytes) = try_euler_call(registration, snapshot, calldata)?
164 {
165 return Ok(Some(bytes));
166 }
167 return Ok(None);
168 }
169
170 if calldata == priceCall::SELECTOR
171 && matches!(registration.source, FeedSource::MorphoChainlinkV2 { .. })
172 {
173 return Ok(Some(Bytes::from(priceCall::abi_encode_returns(
174 &i256_to_u256(snapshot.round.answer, "price")?,
175 ))));
176 }
177
178 if !supports_chainlink_overlay(®istration.source) {
179 return Ok(None);
180 }
181
182 if calldata == latestRoundDataCall::SELECTOR {
183 let round_id = u256_to_uint80(snapshot.round.round_id, "round_id")?;
184 let answered_in_round =
185 u256_to_uint80(snapshot.round.answered_in_round, "answered_in_round")?;
186 return Ok(Some(Bytes::from(
187 latestRoundDataCall::abi_encode_returns_tuple(&(
188 round_id,
189 snapshot.round.answer,
190 U256::from(snapshot.round.started_at),
191 U256::from(snapshot.round.updated_at),
192 answered_in_round,
193 )),
194 )));
195 }
196
197 if calldata == decimalsCall::SELECTOR {
198 return Ok(Some(Bytes::from(decimalsCall::abi_encode_returns(
199 &snapshot.metadata.decimals,
200 ))));
201 }
202
203 if calldata == descriptionCall::SELECTOR {
204 return Ok(Some(Bytes::from(descriptionCall::abi_encode_returns(
205 &snapshot.metadata.description,
206 ))));
207 }
208
209 if calldata == versionCall::SELECTOR {
210 return Ok(Some(Bytes::from(versionCall::abi_encode_returns(
211 &snapshot.metadata.version,
212 ))));
213 }
214
215 Ok(None)
216 }
217
218 fn try_pyth_call(
219 &self,
220 target: Address,
221 calldata: &[u8],
222 ) -> Result<Option<Bytes>, OracleError> {
223 if calldata.len() < 4 {
224 return Ok(None);
225 }
226
227 let price_id = if calldata[..4] == getPriceUnsafeCall::SELECTOR {
228 getPriceUnsafeCall::abi_decode(calldata)
229 .map_err(|error| {
230 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
231 "decode Pyth getPriceUnsafe failed: {error}"
232 )))
233 })?
234 .id
235 } else if calldata[..4] == getPriceNoOlderThanCall::SELECTOR {
236 getPriceNoOlderThanCall::abi_decode(calldata)
237 .map_err(|error| {
238 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
239 "decode Pyth getPriceNoOlderThan failed: {error}"
240 )))
241 })?
242 .id
243 } else {
244 return Ok(None);
245 };
246
247 let Some((snapshot, expo, conf)) = self.pyth_registration_for_call(target, price_id) else {
248 return Ok(None);
249 };
250 if !is_actionable(snapshot) {
251 return Ok(None);
252 }
253
254 let price = pyth_abi_price(snapshot.round.answer, expo)?;
255 let publish_time = U256::from(snapshot.round.updated_at);
256 let pyth_price = PythPrice {
257 price,
258 conf,
259 expo,
260 publishTime: publish_time,
261 };
262
263 let bytes = if calldata[..4] == getPriceUnsafeCall::SELECTOR {
264 getPriceUnsafeCall::abi_encode_returns(&pyth_price)
265 } else {
266 getPriceNoOlderThanCall::abi_encode_returns(&pyth_price)
267 };
268 Ok(Some(Bytes::from(bytes)))
269 }
270
271 fn pyth_registration_for_call(
272 &self,
273 target: Address,
274 price_id: B256,
275 ) -> Option<(&OracleSnapshot, i32, u64)> {
276 let feed = self.pyth_feeds.get(&(target, price_id))?;
277 let snapshot = self.tracker.latest(feed.proxy)?;
278 Some((snapshot, feed.expo, feed.conf))
279 }
280
281 fn try_euler_target_call(
287 &self,
288 target: Address,
289 calldata: &[u8],
290 ) -> Result<Option<Bytes>, OracleError> {
291 let Some(candidates) = self.euler_feeds.get(&target) else {
292 return Ok(None);
293 };
294 let (given_base, given_quote) =
295 if calldata.len() >= 4 && calldata[..4] == getQuoteCall::SELECTOR {
296 let call = getQuoteCall::abi_decode(calldata).map_err(|error| {
297 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
298 "decode Euler getQuote failed: {error}"
299 )))
300 })?;
301 (call.base, call.quote)
302 } else if calldata.len() >= 4 && calldata[..4] == getQuotesCall::SELECTOR {
303 let call = getQuotesCall::abi_decode(calldata).map_err(|error| {
304 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
305 "decode Euler getQuotes failed: {error}"
306 )))
307 })?;
308 (call.base, call.quote)
309 } else {
310 return Ok(None);
311 };
312 let feed = candidates
313 .iter()
314 .find(|feed| feed.base == given_base && feed.quote == given_quote)
315 .or_else(|| {
316 candidates
317 .iter()
318 .find(|feed| feed.base == given_quote && feed.quote == given_base)
319 });
320 let Some(feed) = feed else {
321 return Err(OracleError::Config(
322 crate::error::OracleConfigError::InvalidRequest(
323 "Euler overlay request does not match registered base/quote".to_string(),
324 ),
325 ));
326 };
327 let Some(registration) = self.tracker.registration_for_proxy(feed.proxy) else {
328 return Ok(None);
329 };
330 let Some(snapshot) = self.tracker.latest(feed.proxy) else {
331 return Ok(None);
332 };
333 if !is_actionable(snapshot) {
334 return Ok(None);
335 }
336 try_euler_call(registration, snapshot, calldata)
337 }
338}
339
340fn supports_chainlink_overlay(source: &FeedSource) -> bool {
341 !matches!(
342 source,
343 FeedSource::MorphoChainlinkV2 { .. }
344 | FeedSource::EulerQuote { .. }
345 | FeedSource::EulerCross { .. }
346 | FeedSource::Pyth { .. }
347 )
348}
349
350fn try_euler_call(
351 registration: &FeedRegistration,
352 snapshot: &OracleSnapshot,
353 calldata: &[u8],
354) -> Result<Option<Bytes>, OracleError> {
355 let (base, quote, base_decimals, _quote_decimals) = match ®istration.source {
356 FeedSource::EulerQuote {
357 base,
358 quote,
359 base_decimals,
360 quote_decimals,
361 ..
362 }
363 | FeedSource::EulerCross {
364 base,
365 quote,
366 base_decimals,
367 quote_decimals,
368 ..
369 } => (*base, *quote, *base_decimals, *quote_decimals),
370 _ => return Ok(None),
371 };
372
373 if calldata.len() >= 4 && calldata[..4] == getQuoteCall::SELECTOR {
374 let call = getQuoteCall::abi_decode(calldata).map_err(|error| {
375 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
376 "decode Euler getQuote failed: {error}"
377 )))
378 })?;
379 let out = euler_quote_amount(
380 call.inAmount,
381 call.base,
382 call.quote,
383 base,
384 quote,
385 base_decimals,
386 snapshot.round.answer,
387 )?;
388 return Ok(Some(Bytes::from(getQuoteCall::abi_encode_returns(&out))));
389 }
390
391 if calldata.len() >= 4 && calldata[..4] == getQuotesCall::SELECTOR {
392 let call = getQuotesCall::abi_decode(calldata).map_err(|error| {
393 OracleError::Config(crate::error::OracleConfigError::InvalidRequest(format!(
394 "decode Euler getQuotes failed: {error}"
395 )))
396 })?;
397 let out = euler_quote_amount(
398 call.inAmount,
399 call.base,
400 call.quote,
401 base,
402 quote,
403 base_decimals,
404 snapshot.round.answer,
405 )?;
406 return Ok(Some(Bytes::from(getQuotesCall::abi_encode_returns(
407 &getQuotesReturn {
408 bidOutAmount: out,
409 askOutAmount: out,
410 },
411 ))));
412 }
413
414 Ok(None)
415}
416
417fn euler_quote_amount(
418 in_amount: U256,
419 given_base: Address,
420 given_quote: Address,
421 base: Address,
422 quote: Address,
423 base_decimals: u8,
424 raw_price: I256,
425) -> Result<U256, OracleError> {
426 let price = i256_to_u256(raw_price, "Euler price")?;
427 if given_base == base && given_quote == quote {
428 let scaled = in_amount.checked_mul(price).ok_or_else(|| {
432 OracleError::Unsupported("Euler quote multiplication overflowed".to_string())
433 })?;
434 return scaled
435 .checked_div(decimal_scale_u256(base_decimals))
436 .ok_or_else(|| {
437 OracleError::Unsupported("Euler quote scale division failed".to_string())
438 });
439 }
440 if given_base == quote && given_quote == base {
441 if price.is_zero() {
442 return Err(OracleError::Unsupported(
443 "cannot invert a zero Euler price".to_string(),
444 ));
445 }
446 let scaled = in_amount
447 .checked_mul(decimal_scale_u256(base_decimals))
448 .ok_or_else(|| {
449 OracleError::Unsupported(
450 "Euler inverse quote multiplication overflowed".to_string(),
451 )
452 })?;
453 return scaled.checked_div(price).ok_or_else(|| {
454 OracleError::Unsupported("Euler inverse quote division failed".to_string())
455 });
456 }
457 Err(OracleError::Config(
458 crate::error::OracleConfigError::InvalidRequest(
459 "Euler overlay request does not match registered base/quote".to_string(),
460 ),
461 ))
462}
463
464fn is_actionable(snapshot: &OracleSnapshot) -> bool {
465 snapshot.round_status == OracleRoundStatus::Fresh
466 && matches!(
467 snapshot.value_status,
468 OracleValueStatus::EventPending
469 | OracleValueStatus::Confirmed
470 | OracleValueStatus::Corrected
471 )
472}
473
474fn u256_to_uint80(value: U256, field: &'static str) -> Result<Uint<80, 2>, OracleError> {
475 let value = u128::try_from(value)
476 .map_err(|_| OracleError::Unsupported(format!("{field} does not fit uint80")))?;
477 if value >= (1_u128 << 80) {
478 return Err(OracleError::Unsupported(format!(
479 "{field} does not fit uint80"
480 )));
481 }
482 Uint::<80, 2>::try_from(value)
483 .map_err(|_| OracleError::Unsupported(format!("{field} does not fit uint80")))
484}
485
486fn i256_to_u256(value: I256, field: &'static str) -> Result<U256, OracleError> {
487 U256::try_from(value)
488 .map_err(|_| OracleError::Unsupported(format!("{field} is negative or too large")))
489}
490
491fn i256_to_i64(value: I256, field: &'static str) -> Result<i64, OracleError> {
492 i128::try_from(value)
493 .ok()
494 .and_then(|value| i64::try_from(value).ok())
495 .ok_or_else(|| OracleError::Unsupported(format!("{field} does not fit int64")))
496}
497
498fn pyth_abi_price(raw_answer: I256, expo: i32) -> Result<i64, OracleError> {
499 let price = if expo > 0 {
500 let scale = I256::unchecked_from(10_i8)
501 .checked_pow(U256::from(expo as u32))
502 .ok_or_else(|| {
503 OracleError::Unsupported(format!("Pyth expo {expo} scale overflowed"))
504 })?;
505 raw_answer
506 .checked_div(scale)
507 .ok_or_else(|| OracleError::Unsupported("Pyth price unscale failed".to_string()))?
508 } else {
509 raw_answer
510 };
511 i256_to_i64(price, "Pyth price")
512}
513
514fn decimal_scale_u256(decimals: u8) -> U256 {
515 let mut scale = U256::from(1_u8);
516 for _ in 0..decimals {
517 scale = scale.saturating_mul(U256::from(10_u8));
518 }
519 scale
520}