1use std::{
2 sync::Arc,
3 time::{SystemTime, UNIX_EPOCH},
4};
5
6use alloy_network::Ethereum;
7use alloy_primitives::{Address, I256, U256, keccak256};
8use alloy_sol_types::sol;
9use evm_fork_cache::{cache::EvmCache, reactive::ReactiveHandler};
10
11use crate::{
12 AdapterFuture, AssetId, Denomination, EulerQuoteLeg, EvmCacheChainlinkReader, FeedId,
13 FeedMetadata, FeedRegistration, FeedSource, OracleAdapterFeedSkip, OracleAdapterId,
14 OracleAdapterPlugin, OracleAdapterSkipReason, OracleDiscoveredFeed, OracleDiscoveryContext,
15 OracleDiscoveryReport, OracleError, OracleFeedStatus, OracleReactiveHandler, OracleStorageSync,
16 RoundData, StalenessPolicy,
17};
18
19sol! {
20 interface EulerRouterInterface {
21 function getConfiguredOracle(address base, address quote) external view returns (address);
22 function fallbackOracle() external view returns (address);
23 }
24
25 interface EulerPriceOracleInterface {
26 function name() external view returns (string);
27 function getQuote(uint256 inAmount, address base, address quote) external view returns (uint256 outAmount);
28 }
29
30 interface EulerChainlinkOracleInterface {
31 function base() external view returns (address);
32 function quote() external view returns (address);
33 function feed() external view returns (address);
34 function maxStaleness() external view returns (uint256);
35 }
36
37 interface EulerFixedRateOracleInterface {
38 function base() external view returns (address);
39 function quote() external view returns (address);
40 function rate() external view returns (uint256);
41 }
42
43 interface EulerRateProviderOracleInterface {
44 function base() external view returns (address);
45 function quote() external view returns (address);
46 function rateProvider() external view returns (address);
47 }
48
49 interface EulerCrossAdapterInterface {
50 function base() external view returns (address);
51 function cross() external view returns (address);
52 function quote() external view returns (address);
53 function oracleBaseCross() external view returns (address);
54 function oracleCrossQuote() external view returns (address);
55 }
56
57 interface RateProviderInterface {
58 function getRate() external view returns (uint256);
59 }
60
61 interface Erc20Metadata {
62 function decimals() external view returns (uint8);
63 }
64}
65
66use Erc20Metadata::decimalsCall;
67use EulerChainlinkOracleInterface::{
68 baseCall as chainlinkBaseCall, feedCall, maxStalenessCall, quoteCall as chainlinkQuoteCall,
69};
70use EulerCrossAdapterInterface::{
71 baseCall as crossBaseCall, crossCall, oracleBaseCrossCall, oracleCrossQuoteCall,
72 quoteCall as crossQuoteCall,
73};
74use EulerFixedRateOracleInterface::{
75 baseCall as fixedBaseCall, quoteCall as fixedQuoteCall, rateCall,
76};
77use EulerPriceOracleInterface::{getQuoteCall, nameCall};
78use EulerRateProviderOracleInterface::{
79 baseCall as rateBaseCall, quoteCall as rateQuoteCall, rateProviderCall,
80};
81use EulerRouterInterface::{fallbackOracleCall, getConfiguredOracleCall};
82use RateProviderInterface::getRateCall;
83
84const ADAPTER_ID: &str = "evm-oracle-state.euler";
85
86#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct EulerQuotePair {
89 base: Address,
90 quote: Address,
91 oracle: Option<Address>,
92 state_key: Option<Address>,
93 id: Option<FeedId>,
94 label: Option<String>,
95 base_label: Option<AssetId>,
96 quote_label: Option<Denomination>,
97 staleness: StalenessPolicy,
98}
99
100impl EulerQuotePair {
101 pub fn new(base: Address, quote: Address) -> Self {
103 Self {
104 base,
105 quote,
106 oracle: None,
107 state_key: None,
108 id: None,
109 label: None,
110 base_label: None,
111 quote_label: None,
112 staleness: StalenessPolicy::default(),
113 }
114 }
115
116 pub fn with_oracle(base: Address, quote: Address, oracle: Address) -> Self {
118 Self {
119 oracle: Some(oracle),
120 ..Self::new(base, quote)
121 }
122 }
123
124 pub fn id(mut self, id: impl Into<String>) -> Self {
126 self.id = Some(FeedId::new(id));
127 self
128 }
129
130 pub fn feed_id(mut self, id: FeedId) -> Self {
132 self.id = Some(id);
133 self
134 }
135
136 pub fn label(mut self, label: impl Into<String>) -> Self {
138 self.label = Some(label.into());
139 self
140 }
141
142 pub fn base_label(mut self, base: AssetId) -> Self {
144 self.base_label = Some(base);
145 self
146 }
147
148 pub fn quote_label(mut self, quote: Denomination) -> Self {
150 self.quote_label = Some(quote);
151 self
152 }
153
154 pub fn max_age_secs(mut self, max_age_secs: u64) -> Self {
156 self.staleness = StalenessPolicy::max_age(max_age_secs);
157 self
158 }
159
160 pub fn staleness(mut self, staleness: StalenessPolicy) -> Self {
162 self.staleness = staleness;
163 self
164 }
165
166 pub fn state_key(mut self, state_key: Address) -> Self {
172 self.state_key = Some(state_key);
173 self
174 }
175
176 pub fn state_key_for(quote_target: Address, base: Address, quote: Address) -> Address {
185 let mut bytes = Vec::with_capacity(ADAPTER_ID.len() + 20 + 20 + 20);
186 bytes.extend_from_slice(ADAPTER_ID.as_bytes());
187 bytes.extend_from_slice(quote_target.as_slice());
188 bytes.extend_from_slice(base.as_slice());
189 bytes.extend_from_slice(quote.as_slice());
190 let hash = keccak256(bytes);
191 Address::from_slice(&hash.as_slice()[12..])
192 }
193}
194
195#[derive(Clone, Debug, Default)]
197pub struct EulerOracleAdapter {
198 router: Option<Address>,
199 pairs: Vec<EulerQuotePair>,
200 now_timestamp: Option<u64>,
201}
202
203impl EulerOracleAdapter {
204 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn router(router: Address) -> Self {
211 Self {
212 router: Some(router),
213 pairs: Vec::new(),
214 now_timestamp: None,
215 }
216 }
217
218 pub fn oracle(oracle: Address, base: Address, quote: Address) -> Self {
220 Self {
221 router: None,
222 pairs: vec![EulerQuotePair::with_oracle(base, quote, oracle)],
223 now_timestamp: None,
224 }
225 }
226
227 pub fn quote_pair(mut self, pair: EulerQuotePair) -> Self {
229 self.pairs.push(pair);
230 self
231 }
232
233 pub fn quote_pairs(mut self, pairs: impl IntoIterator<Item = EulerQuotePair>) -> Self {
235 self.pairs.extend(pairs);
236 self
237 }
238
239 pub fn now_timestamp(mut self, now_timestamp: u64) -> Self {
241 self.now_timestamp = Some(now_timestamp);
242 self
243 }
244
245 fn timestamp(&self, fallback: Option<u64>) -> Result<u64, OracleError> {
246 if let Some(now_timestamp) = self.now_timestamp.or(fallback) {
247 return Ok(now_timestamp);
248 }
249 Ok(SystemTime::now()
250 .duration_since(UNIX_EPOCH)
251 .map_err(crate::error::clock_error)?
252 .as_secs())
253 }
254
255 fn discover_pairs(
256 &self,
257 cache: &mut EvmCache,
258 now_timestamp: u64,
259 ) -> Result<OracleDiscoveryReport, OracleError> {
260 let mut report = OracleDiscoveryReport::new();
261 for pair in &self.pairs {
262 match self.discover_pair(cache, pair, now_timestamp) {
263 Ok(feed) => report = report.with_feed(feed),
264 Err(error) => {
265 let skip_target = pair.oracle.or(self.router).unwrap_or_default();
268 report = report.with_skip(OracleAdapterFeedSkip {
269 feed: crate::Feed::proxy(skip_target).build(),
270 proxy: skip_target,
271 reason: OracleAdapterSkipReason::UnsupportedEulerSource {
272 error: error.to_string(),
273 },
274 });
275 }
276 }
277 }
278 Ok(report)
279 }
280
281 fn discover_pair(
282 &self,
283 cache: &mut EvmCache,
284 pair: &EulerQuotePair,
285 now_timestamp: u64,
286 ) -> Result<OracleDiscoveredFeed, OracleError> {
287 let oracle = match pair.oracle {
288 Some(oracle) => oracle,
289 None => self.resolve_router_oracle(cache, pair.base, pair.quote)?,
290 };
291 let quote_target = pair.oracle.or(self.router).unwrap_or(oracle);
297 let proxy = pair
298 .state_key
299 .unwrap_or_else(|| EulerQuotePair::state_key_for(quote_target, pair.base, pair.quote));
300 let base_decimals = read_asset_decimals(cache, pair.base)?;
301 let quote_decimals = read_asset_decimals(cache, pair.quote)?;
302 let source = self.read_source(
303 cache,
304 oracle,
305 pair.base,
306 pair.quote,
307 base_decimals,
308 quote_decimals,
309 0,
310 )?;
311 let source = source.with_euler_user_source(quote_target);
316 let in_amount = one_base_unit(base_decimals)?;
325 let quote_out = cache
326 .call_sol(
327 quote_target,
328 getQuoteCall {
329 inAmount: in_amount,
330 base: pair.base,
331 quote: pair.quote,
332 },
333 )
334 .map_err(|error| {
335 OracleError::Provider(format!(
336 "Euler getQuote({in_amount}, {:?}, {:?}) via {quote_target:?} failed: {error}",
337 pair.base, pair.quote
338 ))
339 })?;
340 let answer = i256_from_u256(quote_out, "Euler getQuote outAmount")?;
341 let id = pair.id.clone().unwrap_or_else(|| {
342 FeedId::new(format!(
343 "euler-{quote_target:?}-{:?}-{:?}",
344 pair.base, pair.quote
345 ))
346 });
347 let label = pair
348 .label
349 .clone()
350 .or_else(|| Some(format!("Euler {:?}/{:?}", pair.base, pair.quote)));
351 let metadata = FeedMetadata {
352 decimals: quote_decimals,
353 description: label.clone().unwrap_or_else(|| "Euler quote".to_string()),
354 version: U256::ZERO,
355 };
356 let registration = FeedRegistration {
357 id,
358 proxy,
359 label,
360 base: pair
361 .base_label
362 .clone()
363 .map(String::from)
364 .or_else(|| Some(format!("{:?}", pair.base))),
365 quote: pair
366 .quote_label
367 .clone()
368 .map(String::from)
369 .or_else(|| Some(format!("{:?}", pair.quote))),
370 staleness: pair.staleness,
371 current_aggregator: source.event_aggregators(None).first().copied(),
372 aggregator_layout: None,
373 metadata,
374 source,
375 status: OracleFeedStatus::Ready,
376 };
377 let round = RoundData {
378 round_id: U256::ZERO,
379 answer,
380 started_at: now_timestamp,
381 updated_at: now_timestamp,
382 answered_in_round: U256::ZERO,
383 };
384 Ok(OracleDiscoveredFeed::new(registration, round))
385 }
386
387 fn resolve_router_oracle(
388 &self,
389 cache: &mut EvmCache,
390 base: Address,
391 quote: Address,
392 ) -> Result<Address, OracleError> {
393 let router = self.router.ok_or_else(|| {
394 OracleError::Config(crate::error::OracleConfigError::Other(
395 "Euler router or direct oracle is required".to_string(),
396 ))
397 })?;
398 let configured = cache
399 .call_sol(router, getConfiguredOracleCall { base, quote })
400 .map_err(provider_error)?;
401 if configured != Address::ZERO {
402 return Ok(configured);
403 }
404 let fallback = cache
405 .call_sol(router, fallbackOracleCall {})
406 .map_err(provider_error)?;
407 if fallback != Address::ZERO {
408 return Ok(fallback);
409 }
410 Err(OracleError::Unsupported(
411 "Euler router has no configured or fallback oracle for pair".to_string(),
412 ))
413 }
414
415 #[allow(clippy::too_many_arguments)]
416 fn read_source(
417 &self,
418 cache: &mut EvmCache,
419 oracle: Address,
420 requested_base: Address,
421 requested_quote: Address,
422 requested_base_decimals: u8,
423 requested_quote_decimals: u8,
424 depth: usize,
425 ) -> Result<FeedSource, OracleError> {
426 if depth > 4 {
427 return Err(OracleError::Unsupported(
428 "Euler oracle nesting depth exceeded".to_string(),
429 ));
430 }
431 let name = cache
432 .call_sol(oracle, nameCall {})
433 .map_err(provider_error)?;
434 match name.as_str() {
435 "ChainlinkOracle" => {
436 let leg = self.read_chainlink_leg(
437 cache,
438 oracle,
439 requested_base,
440 requested_quote,
441 requested_quote_decimals,
442 )?;
443 Ok(FeedSource::euler_quote(
444 oracle,
445 requested_base,
446 requested_quote,
447 requested_base_decimals,
448 requested_quote_decimals,
449 leg,
450 ))
451 }
452 "FixedRateOracle" => {
453 let leg = self.read_fixed_rate_leg(
454 cache,
455 oracle,
456 requested_base,
457 requested_quote,
458 requested_base_decimals,
459 requested_quote_decimals,
460 )?;
461 Ok(FeedSource::euler_quote(
462 oracle,
463 requested_base,
464 requested_quote,
465 requested_base_decimals,
466 requested_quote_decimals,
467 leg,
468 ))
469 }
470 "RateProviderOracle" => {
471 let leg = self.read_rate_provider_leg(
472 cache,
473 oracle,
474 requested_base,
475 requested_quote,
476 requested_quote_decimals,
477 )?;
478 Ok(FeedSource::euler_quote(
479 oracle,
480 requested_base,
481 requested_quote,
482 requested_base_decimals,
483 requested_quote_decimals,
484 leg,
485 ))
486 }
487 "CrossAdapter" => self.read_cross_source(
488 cache,
489 oracle,
490 requested_base,
491 requested_quote,
492 requested_base_decimals,
493 requested_quote_decimals,
494 depth,
495 ),
496 other => Err(OracleError::Unsupported(format!(
497 "unsupported Euler oracle adapter {other}"
498 ))),
499 }
500 }
501
502 #[allow(clippy::too_many_arguments)]
503 fn read_cross_source(
504 &self,
505 cache: &mut EvmCache,
506 oracle: Address,
507 requested_base: Address,
508 requested_quote: Address,
509 requested_base_decimals: u8,
510 requested_quote_decimals: u8,
511 depth: usize,
512 ) -> Result<FeedSource, OracleError> {
513 let configured_base = cache
514 .call_sol(oracle, crossBaseCall {})
515 .map_err(provider_error)?;
516 let cross = cache
517 .call_sol(oracle, crossCall {})
518 .map_err(provider_error)?;
519 let configured_quote = cache
520 .call_sol(oracle, crossQuoteCall {})
521 .map_err(provider_error)?;
522 let inverse = direction_or_revert(
523 requested_base,
524 configured_base,
525 requested_quote,
526 configured_quote,
527 )?;
528 let oracle_base_cross = cache
529 .call_sol(oracle, oracleBaseCrossCall {})
530 .map_err(provider_error)?;
531 let oracle_cross_quote = cache
532 .call_sol(oracle, oracleCrossQuoteCall {})
533 .map_err(provider_error)?;
534 let cross_decimals = read_asset_decimals(cache, cross)?;
535 let (first_oracle, second_oracle) = if inverse {
536 (oracle_cross_quote, oracle_base_cross)
537 } else {
538 (oracle_base_cross, oracle_cross_quote)
539 };
540 let base_cross = self.read_leg(
541 cache,
542 first_oracle,
543 requested_base,
544 cross,
545 requested_base_decimals,
546 cross_decimals,
547 depth + 1,
548 )?;
549 let cross_quote = self.read_leg(
550 cache,
551 second_oracle,
552 cross,
553 requested_quote,
554 cross_decimals,
555 requested_quote_decimals,
556 depth + 1,
557 )?;
558
559 Ok(FeedSource::euler_cross(
560 oracle,
561 requested_base,
562 cross,
563 requested_quote,
564 requested_base_decimals,
565 cross_decimals,
566 requested_quote_decimals,
567 base_cross,
568 cross_quote,
569 ))
570 }
571
572 #[allow(clippy::too_many_arguments)]
573 fn read_leg(
574 &self,
575 cache: &mut EvmCache,
576 oracle: Address,
577 requested_base: Address,
578 requested_quote: Address,
579 requested_base_decimals: u8,
580 requested_quote_decimals: u8,
581 depth: usize,
582 ) -> Result<EulerQuoteLeg, OracleError> {
583 let source = self.read_source(
584 cache,
585 oracle,
586 requested_base,
587 requested_quote,
588 requested_base_decimals,
589 requested_quote_decimals,
590 depth,
591 )?;
592 match source {
593 FeedSource::EulerQuote { leg, .. } => Ok(leg),
594 FeedSource::EulerCross { .. } => Err(OracleError::Unsupported(
595 "nested Euler CrossAdapter legs are not supported in this MVP".to_string(),
596 )),
597 _ => Err(OracleError::Unsupported(
598 "Euler child source did not produce a quote leg".to_string(),
599 )),
600 }
601 }
602
603 fn read_chainlink_leg(
604 &self,
605 cache: &mut EvmCache,
606 oracle: Address,
607 requested_base: Address,
608 requested_quote: Address,
609 requested_quote_decimals: u8,
610 ) -> Result<EulerQuoteLeg, OracleError> {
611 let configured_base = cache
612 .call_sol(oracle, chainlinkBaseCall {})
613 .map_err(provider_error)?;
614 let configured_quote = cache
615 .call_sol(oracle, chainlinkQuoteCall {})
616 .map_err(provider_error)?;
617 let inverse = direction_or_revert(
618 requested_base,
619 configured_base,
620 requested_quote,
621 configured_quote,
622 )?;
623 let feed = cache
624 .call_sol(oracle, feedCall {})
625 .map_err(provider_error)?;
626 let _max_staleness = cache
627 .call_sol(oracle, maxStalenessCall {})
628 .map_err(provider_error)?;
629 let reader = EvmCacheChainlinkReader::new(cache);
630 let feed_decimals = reader.read_decimals(feed)?;
631 let round = reader.read_latest_round_data(feed)?;
632 let aggregator = reader.read_aggregator(feed)?.ok_or_else(|| {
633 OracleError::Unsupported(
634 "Euler ChainlinkOracle feed aggregator() returned none".to_string(),
635 )
636 })?;
637 Ok(EulerQuoteLeg::Chainlink {
638 oracle,
639 feed,
640 aggregator,
641 answer: round.answer,
642 feed_decimals,
643 inverse,
644 quote_decimals: requested_quote_decimals,
645 })
646 }
647
648 fn read_fixed_rate_leg(
649 &self,
650 cache: &mut EvmCache,
651 oracle: Address,
652 requested_base: Address,
653 requested_quote: Address,
654 requested_base_decimals: u8,
655 requested_quote_decimals: u8,
656 ) -> Result<EulerQuoteLeg, OracleError> {
657 let configured_base = cache
658 .call_sol(oracle, fixedBaseCall {})
659 .map_err(provider_error)?;
660 let configured_quote = cache
661 .call_sol(oracle, fixedQuoteCall {})
662 .map_err(provider_error)?;
663 let inverse = direction_or_revert(
664 requested_base,
665 configured_base,
666 requested_quote,
667 configured_quote,
668 )?;
669 let rate = cache
670 .call_sol(oracle, rateCall {})
671 .map_err(provider_error)?;
672 Ok(EulerQuoteLeg::FixedRate {
673 oracle,
674 rate: i256_from_u256(rate, "rate")?,
675 inverse,
676 base_decimals: requested_base_decimals,
677 quote_decimals: requested_quote_decimals,
678 })
679 }
680
681 fn read_rate_provider_leg(
682 &self,
683 cache: &mut EvmCache,
684 oracle: Address,
685 requested_base: Address,
686 requested_quote: Address,
687 requested_quote_decimals: u8,
688 ) -> Result<EulerQuoteLeg, OracleError> {
689 let configured_base = cache
690 .call_sol(oracle, rateBaseCall {})
691 .map_err(provider_error)?;
692 let configured_quote = cache
693 .call_sol(oracle, rateQuoteCall {})
694 .map_err(provider_error)?;
695 let inverse = direction_or_revert(
696 requested_base,
697 configured_base,
698 requested_quote,
699 configured_quote,
700 )?;
701 let rate_provider = cache
702 .call_sol(oracle, rateProviderCall {})
703 .map_err(provider_error)?;
704 let rate = cache
705 .call_sol(rate_provider, getRateCall {})
706 .map_err(provider_error)?;
707 Ok(EulerQuoteLeg::RateProvider {
708 oracle,
709 rate_provider,
710 rate: i256_from_u256(rate, "rate")?,
711 inverse,
712 quote_decimals: requested_quote_decimals,
713 })
714 }
715}
716
717impl OracleAdapterPlugin for EulerOracleAdapter {
718 fn adapter_id(&self) -> OracleAdapterId {
719 OracleAdapterId::new(ADAPTER_ID)
720 }
721
722 fn discover<'a>(
723 &'a self,
724 ctx: OracleDiscoveryContext<'a>,
725 ) -> AdapterFuture<'a, OracleDiscoveryReport> {
726 Box::pin(async move {
727 let now_timestamp = self.timestamp(Some(ctx.now_timestamp))?;
728 self.discover_pairs(ctx.cache, now_timestamp)
729 })
730 }
731
732 fn reactive_handler(
733 &self,
734 registrations: Vec<FeedRegistration>,
735 storage_sync: OracleStorageSync,
736 ) -> Arc<dyn ReactiveHandler<Ethereum>> {
737 Arc::new(OracleReactiveHandler::with_storage_sync(
738 registrations,
739 storage_sync,
740 ))
741 }
742}
743
744fn direction_or_revert(
745 requested_base: Address,
746 configured_base: Address,
747 requested_quote: Address,
748 configured_quote: Address,
749) -> Result<bool, OracleError> {
750 if requested_base == configured_base && requested_quote == configured_quote {
751 Ok(false)
752 } else if requested_base == configured_quote && requested_quote == configured_base {
753 Ok(true)
754 } else {
755 Err(OracleError::Unsupported(
756 "Euler oracle does not support requested base/quote pair".to_string(),
757 ))
758 }
759}
760
761fn read_asset_decimals(cache: &mut EvmCache, asset: Address) -> Result<u8, OracleError> {
762 if is_reserved_asset_address(asset) {
763 return Ok(18);
764 }
765 match cache.call_sol(asset, decimalsCall {}) {
766 Ok(decimals) => Ok(decimals),
767 Err(_) => Ok(18),
768 }
769}
770
771fn is_reserved_asset_address(asset: Address) -> bool {
772 asset.as_slice()[..16].iter().all(|byte| *byte == 0)
777}
778
779pub(crate) fn one_base_unit(base_decimals: u8) -> Result<U256, OracleError> {
784 U256::from(10_u64)
785 .checked_pow(U256::from(base_decimals))
786 .ok_or_else(|| {
787 OracleError::Unsupported(format!(
788 "Euler base decimals {base_decimals} overflow a uint256 unit amount"
789 ))
790 })
791}
792
793fn i256_from_u256(value: U256, field: &'static str) -> Result<I256, OracleError> {
794 I256::try_from(value)
795 .map_err(|_| OracleError::Unsupported(format!("{field} does not fit int256")))
796}
797
798fn provider_error(error: impl ToString) -> OracleError {
799 OracleError::Provider(error.to_string())
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn state_keys_are_deterministic_and_distinct_per_pair_and_target() {
808 let router = Address::repeat_byte(0x01);
809 let other_router = Address::repeat_byte(0x02);
810 let weth = Address::repeat_byte(0x0a);
811 let usdc = Address::repeat_byte(0x0b);
812 let dai = Address::repeat_byte(0x0c);
813
814 let weth_usdc = EulerQuotePair::state_key_for(router, weth, usdc);
815 assert_eq!(
816 weth_usdc,
817 EulerQuotePair::state_key_for(router, weth, usdc),
818 "state keys must be deterministic"
819 );
820 assert_ne!(weth_usdc, EulerQuotePair::state_key_for(router, weth, dai));
823 assert_ne!(weth_usdc, EulerQuotePair::state_key_for(router, usdc, weth));
824 assert_ne!(
825 weth_usdc,
826 EulerQuotePair::state_key_for(other_router, weth, usdc)
827 );
828 assert_ne!(weth_usdc, router);
830 }
831
832 #[test]
833 fn reserved_asset_addresses_cover_iso4217_pseudo_assets() {
834 let usd = Address::from_slice(&{
836 let mut bytes = [0_u8; 20];
837 bytes[18] = 0x03;
838 bytes[19] = 0x48;
839 bytes
840 });
841 assert!(is_reserved_asset_address(usd));
842 assert!(is_reserved_asset_address(Address::ZERO));
843 let boundary = Address::from_slice(&{
845 let mut bytes = [0_u8; 20];
846 bytes[16..].copy_from_slice(&0xffff_ffff_u32.to_be_bytes());
847 bytes
848 });
849 assert!(is_reserved_asset_address(boundary));
850 assert!(!is_reserved_asset_address(Address::repeat_byte(0x42)));
852 }
853
854 #[test]
855 fn one_base_unit_scales_by_decimals() {
856 assert_eq!(one_base_unit(0).unwrap(), U256::from(1_u64));
857 assert_eq!(
858 one_base_unit(18).unwrap(),
859 U256::from(1_000_000_000_000_000_000_u128)
860 );
861 assert!(one_base_unit(78).is_err(), "10^78 overflows uint256");
862 }
863}