1use std::{
2 sync::Arc,
3 time::{SystemTime, UNIX_EPOCH},
4};
5
6use alloy_network::Ethereum;
7use alloy_primitives::{Address, B256, I256, U256};
8use alloy_sol_types::sol;
9use evm_fork_cache::{cache::EvmCache, reactive::ReactiveHandler};
10
11use crate::{
12 AdapterFuture, AssetId, Denomination, EvmCacheChainlinkReader, FeedId, FeedMetadata,
13 FeedRegistration, FeedSource, MorphoChainlinkFeed, MorphoChainlinkFeedRole,
14 OracleAdapterFeedSkip, OracleAdapterId, OracleAdapterPlugin, OracleAdapterSkipReason,
15 OracleDependency, OracleDiscoveredFeed, OracleDiscoveryContext, OracleDiscoveryReport,
16 OracleError, OracleFeedStatus, OracleReactiveHandler, OracleStorageSync, RoundData,
17 StalenessPolicy,
18};
19
20sol! {
21 interface MorphoBlueInterface {
22 function idToMarketParams(bytes32 id) external view returns (
23 address loanToken,
24 address collateralToken,
25 address oracle,
26 address irm,
27 uint256 lltv
28 );
29 }
30
31 interface MorphoChainlinkOracleV2Interface {
32 function BASE_VAULT() external view returns (address);
33 function BASE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
34 function QUOTE_VAULT() external view returns (address);
35 function QUOTE_VAULT_CONVERSION_SAMPLE() external view returns (uint256);
36 function BASE_FEED_1() external view returns (address);
37 function BASE_FEED_2() external view returns (address);
38 function QUOTE_FEED_1() external view returns (address);
39 function QUOTE_FEED_2() external view returns (address);
40 function SCALE_FACTOR() external view returns (uint256);
41 function price() external view returns (uint256);
42 }
43
44 interface Erc4626Like {
45 function convertToAssets(uint256 shares) external view returns (uint256);
46 }
47}
48
49use Erc4626Like::convertToAssetsCall;
50use MorphoBlueInterface::idToMarketParamsCall;
51use MorphoChainlinkOracleV2Interface::{
52 BASE_FEED_1Call, BASE_FEED_2Call, BASE_VAULT_CONVERSION_SAMPLECall, BASE_VAULTCall,
53 QUOTE_FEED_1Call, QUOTE_FEED_2Call, QUOTE_VAULT_CONVERSION_SAMPLECall, QUOTE_VAULTCall,
54 SCALE_FACTORCall, priceCall,
55};
56
57const ADAPTER_ID: &str = "evm-oracle-state.morpho-blue";
58const MORPHO_PRICE_DECIMALS: u8 = 36;
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct MorphoMarketParams {
63 pub loan_token: Address,
65 pub collateral_token: Address,
67 pub oracle: Address,
69 pub irm: Address,
71 pub lltv: U256,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct MorphoBlueMarket {
78 id: Option<B256>,
79 params: Option<MorphoMarketParams>,
80 oracle: Option<Address>,
81 feed_id: Option<FeedId>,
82 label: Option<String>,
83 base: Option<AssetId>,
84 quote: Option<Denomination>,
85 staleness: StalenessPolicy,
86}
87
88impl MorphoBlueMarket {
89 pub fn market_id(id: B256) -> Self {
91 Self {
92 id: Some(id),
93 params: None,
94 oracle: None,
95 feed_id: None,
96 label: None,
97 base: None,
98 quote: None,
99 staleness: StalenessPolicy::default(),
100 }
101 }
102
103 pub fn market_params(params: MorphoMarketParams) -> Self {
105 Self {
106 id: None,
107 oracle: Some(params.oracle),
108 params: Some(params),
109 feed_id: None,
110 label: None,
111 base: None,
112 quote: None,
113 staleness: StalenessPolicy::default(),
114 }
115 }
116
117 pub fn oracle(oracle: Address) -> Self {
119 Self {
120 id: None,
121 params: None,
122 oracle: Some(oracle),
123 feed_id: None,
124 label: None,
125 base: None,
126 quote: None,
127 staleness: StalenessPolicy::default(),
128 }
129 }
130
131 pub fn id(mut self, id: impl Into<String>) -> Self {
133 self.feed_id = Some(FeedId::new(id));
134 self
135 }
136
137 pub fn feed_id(mut self, id: FeedId) -> Self {
139 self.feed_id = Some(id);
140 self
141 }
142
143 pub fn label(mut self, label: impl Into<String>) -> Self {
145 self.label = Some(label.into());
146 self
147 }
148
149 pub fn base(mut self, base: AssetId) -> Self {
151 self.base = Some(base);
152 self
153 }
154
155 pub fn quote(mut self, quote: Denomination) -> Self {
157 self.quote = Some(quote);
158 self
159 }
160
161 pub fn max_age_secs(mut self, max_age_secs: u64) -> Self {
163 self.staleness = StalenessPolicy::max_age(max_age_secs);
164 self
165 }
166
167 pub fn staleness(mut self, staleness: StalenessPolicy) -> Self {
169 self.staleness = staleness;
170 self
171 }
172}
173
174#[derive(Clone, Debug, Default)]
176pub struct MorphoBlueOracleAdapter {
177 morpho: Option<Address>,
178 markets: Vec<MorphoBlueMarket>,
179 now_timestamp: Option<u64>,
180}
181
182impl MorphoBlueOracleAdapter {
183 pub fn new(morpho: Address) -> Self {
185 Self {
186 morpho: Some(morpho),
187 markets: Vec::new(),
188 now_timestamp: None,
189 }
190 }
191
192 pub fn from_oracle(oracle: Address) -> Self {
194 Self {
195 morpho: None,
196 markets: vec![MorphoBlueMarket::oracle(oracle)],
197 now_timestamp: None,
198 }
199 }
200
201 pub fn market_id(self, id: B256) -> Self {
203 self.market(MorphoBlueMarket::market_id(id))
204 }
205
206 pub fn market(mut self, market: MorphoBlueMarket) -> Self {
208 self.markets.push(market);
209 self
210 }
211
212 pub fn markets(mut self, markets: impl IntoIterator<Item = MorphoBlueMarket>) -> Self {
214 self.markets.extend(markets);
215 self
216 }
217
218 pub fn now_timestamp(mut self, now_timestamp: u64) -> Self {
220 self.now_timestamp = Some(now_timestamp);
221 self
222 }
223
224 fn timestamp(&self, fallback: Option<u64>) -> Result<u64, OracleError> {
225 if let Some(now_timestamp) = self.now_timestamp.or(fallback) {
226 return Ok(now_timestamp);
227 }
228 Ok(SystemTime::now()
229 .duration_since(UNIX_EPOCH)
230 .map_err(crate::error::clock_error)?
231 .as_secs())
232 }
233
234 fn discover_markets(
235 &self,
236 cache: &mut EvmCache,
237 now_timestamp: u64,
238 ) -> Result<OracleDiscoveryReport, OracleError> {
239 let mut report = OracleDiscoveryReport::new();
240 for market in &self.markets {
241 match self.discover_market(cache, market, now_timestamp) {
242 Ok(feed) => report = report.with_feed(feed),
243 Err(error) => {
244 report = report.with_skip(OracleAdapterFeedSkip {
245 feed: crate::Feed::proxy(market.oracle.unwrap_or_default()).build(),
246 proxy: market.oracle.unwrap_or_default(),
247 reason: OracleAdapterSkipReason::UnsupportedMorphoSource {
248 error: format!("unsupported Morpho source ({error})"),
249 },
250 });
251 }
252 }
253 }
254 Ok(report)
255 }
256
257 fn discover_market(
258 &self,
259 cache: &mut EvmCache,
260 market: &MorphoBlueMarket,
261 now_timestamp: u64,
262 ) -> Result<OracleDiscoveredFeed, OracleError> {
263 let params = if let Some(params) = &market.params {
264 Some(params.clone())
265 } else if let Some(id) = market.id {
266 Some(self.read_market_params(cache, id)?)
267 } else {
268 None
269 };
270 let oracle = market
271 .oracle
272 .or_else(|| params.as_ref().map(|params| params.oracle))
273 .ok_or_else(|| {
274 OracleError::Config(crate::error::OracleConfigError::Other(
275 "Morpho market oracle is missing".to_string(),
276 ))
277 })?;
278
279 let probed = self.read_chainlink_v2(cache, oracle)?;
280 let id = market
281 .feed_id
282 .clone()
283 .unwrap_or_else(|| derive_morpho_feed_id(market.id, oracle));
284 let label = market
285 .label
286 .clone()
287 .or_else(|| Some(format!("Morpho {oracle:?}")));
288 let base = market.base.clone().map(String::from).or_else(|| {
289 params
290 .as_ref()
291 .map(|params| format!("{:?}", params.collateral_token))
292 });
293 let quote = market.quote.clone().map(String::from).or_else(|| {
294 params
295 .as_ref()
296 .map(|params| format!("{:?}", params.loan_token))
297 });
298 let metadata = FeedMetadata {
299 decimals: MORPHO_PRICE_DECIMALS,
300 description: label
301 .clone()
302 .unwrap_or_else(|| "MorphoChainlinkOracleV2".to_string()),
303 version: U256::ZERO,
304 };
305 let source = FeedSource::morpho_chainlink_v2(
306 oracle,
307 probed.base_vault_assets,
308 probed.quote_vault_assets,
309 probed.scale_factor,
310 probed.feeds,
311 );
312 let registration = FeedRegistration {
313 id,
314 proxy: oracle,
315 label,
316 base,
317 quote,
318 staleness: market.staleness,
319 current_aggregator: source.event_aggregators(None).first().copied(),
320 aggregator_layout: None,
321 metadata,
322 source,
323 status: OracleFeedStatus::Ready,
324 };
325 let round = RoundData {
326 round_id: U256::ZERO,
327 answer: probed.price,
328 started_at: now_timestamp,
329 updated_at: now_timestamp,
330 answered_in_round: U256::ZERO,
331 };
332
333 Ok(OracleDiscoveredFeed::new(registration, round))
334 }
335
336 fn read_market_params(
337 &self,
338 cache: &mut EvmCache,
339 id: B256,
340 ) -> Result<MorphoMarketParams, OracleError> {
341 let morpho = self.morpho.ok_or_else(|| {
342 OracleError::Config(crate::error::OracleConfigError::Other(
343 "Morpho contract is required for market-id discovery".to_string(),
344 ))
345 })?;
346 let params = cache
347 .call_sol(morpho, idToMarketParamsCall { id })
348 .map_err(provider_error)?;
349 Ok(MorphoMarketParams {
350 loan_token: params.loanToken,
351 collateral_token: params.collateralToken,
352 oracle: params.oracle,
353 irm: params.irm,
354 lltv: params.lltv,
355 })
356 }
357
358 fn read_chainlink_v2(
359 &self,
360 cache: &mut EvmCache,
361 oracle: Address,
362 ) -> Result<MorphoChainlinkV2Read, OracleError> {
363 let base_vault = cache
364 .call_sol(oracle, BASE_VAULTCall {})
365 .map_err(provider_error)?;
366 let base_sample = cache
367 .call_sol(oracle, BASE_VAULT_CONVERSION_SAMPLECall {})
368 .map_err(provider_error)?;
369 let quote_vault = cache
370 .call_sol(oracle, QUOTE_VAULTCall {})
371 .map_err(provider_error)?;
372 let quote_sample = cache
373 .call_sol(oracle, QUOTE_VAULT_CONVERSION_SAMPLECall {})
374 .map_err(provider_error)?;
375 let scale_factor = cache
376 .call_sol(oracle, SCALE_FACTORCall {})
377 .map_err(provider_error)?;
378 let price = cache
379 .call_sol(oracle, priceCall {})
380 .map_err(provider_error)?;
381 let base_vault_assets = read_vault_assets(cache, base_vault, base_sample)?;
382 let quote_vault_assets = read_vault_assets(cache, quote_vault, quote_sample)?;
383 let feed_addresses = [
384 (
385 MorphoChainlinkFeedRole::BaseFeed1,
386 cache
387 .call_sol(oracle, BASE_FEED_1Call {})
388 .map_err(provider_error)?,
389 ),
390 (
391 MorphoChainlinkFeedRole::BaseFeed2,
392 cache
393 .call_sol(oracle, BASE_FEED_2Call {})
394 .map_err(provider_error)?,
395 ),
396 (
397 MorphoChainlinkFeedRole::QuoteFeed1,
398 cache
399 .call_sol(oracle, QUOTE_FEED_1Call {})
400 .map_err(provider_error)?,
401 ),
402 (
403 MorphoChainlinkFeedRole::QuoteFeed2,
404 cache
405 .call_sol(oracle, QUOTE_FEED_2Call {})
406 .map_err(provider_error)?,
407 ),
408 ];
409
410 let reader = EvmCacheChainlinkReader::new(cache);
411 let mut feeds = Vec::new();
412 for (role, feed) in feed_addresses {
413 if feed == Address::ZERO {
414 continue;
415 }
416 let round = reader.read_latest_round_data(feed)?;
417 let aggregator = reader.read_aggregator(feed)?.ok_or_else(|| {
418 OracleError::Unsupported("Morpho dependency aggregator() returned none".to_string())
419 })?;
420 feeds.push(MorphoChainlinkFeed::new(
421 role,
422 OracleDependency::new(feed, aggregator, round.answer),
423 ));
424 }
425
426 Ok(MorphoChainlinkV2Read {
427 base_vault_assets: i256_from_u256(base_vault_assets, "base vault assets")?,
428 quote_vault_assets: i256_from_u256(quote_vault_assets, "quote vault assets")?,
429 scale_factor: i256_from_u256(scale_factor, "scale factor")?,
430 price: i256_from_u256(price, "price")?,
431 feeds,
432 })
433 }
434}
435
436impl OracleAdapterPlugin for MorphoBlueOracleAdapter {
437 fn adapter_id(&self) -> OracleAdapterId {
438 OracleAdapterId::new(ADAPTER_ID)
439 }
440
441 fn discover<'a>(
442 &'a self,
443 ctx: OracleDiscoveryContext<'a>,
444 ) -> AdapterFuture<'a, OracleDiscoveryReport> {
445 Box::pin(async move {
446 let now_timestamp = self.timestamp(Some(ctx.now_timestamp))?;
447 self.discover_markets(ctx.cache, now_timestamp)
448 })
449 }
450
451 fn reactive_handler(
452 &self,
453 registrations: Vec<FeedRegistration>,
454 storage_sync: OracleStorageSync,
455 ) -> Arc<dyn ReactiveHandler<Ethereum>> {
456 Arc::new(OracleReactiveHandler::with_storage_sync(
457 registrations,
458 storage_sync,
459 ))
460 }
461}
462
463#[derive(Clone, Debug)]
464struct MorphoChainlinkV2Read {
465 base_vault_assets: I256,
466 quote_vault_assets: I256,
467 scale_factor: I256,
468 price: I256,
469 feeds: Vec<MorphoChainlinkFeed>,
470}
471
472fn read_vault_assets(
473 cache: &mut EvmCache,
474 vault: Address,
475 sample: U256,
476) -> Result<U256, OracleError> {
477 if vault == Address::ZERO {
478 return Ok(U256::from(1_u8));
479 }
480 cache
481 .call_sol(vault, convertToAssetsCall { shares: sample })
482 .map_err(provider_error)
483}
484
485fn derive_morpho_feed_id(id: Option<B256>, oracle: Address) -> FeedId {
486 match id {
487 Some(id) => FeedId::new(format!("morpho-{id:?}")),
488 None => FeedId::new(format!("morpho-{oracle:?}")),
489 }
490}
491
492fn i256_from_u256(value: U256, field: &'static str) -> Result<I256, OracleError> {
493 I256::try_from(value)
494 .map_err(|_| OracleError::Unsupported(format!("{field} does not fit int256")))
495}
496
497fn provider_error(error: impl ToString) -> OracleError {
498 OracleError::Provider(error.to_string())
499}