diff --git a/Cargo.lock b/Cargo.lock index 2d1ae092e..f087c92ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11245,6 +11245,7 @@ dependencies = [ "async-trait", "base64 0.13.1", "bincode", + "cfg-if 1.0.0", "clap 3.2.25", "env_logger 0.9.3", "err-derive", diff --git a/clients/vault/Cargo.toml b/clients/vault/Cargo.toml index 8a20dbc44..dfbd4cc05 100644 --- a/clients/vault/Cargo.toml +++ b/clients/vault/Cargo.toml @@ -25,6 +25,7 @@ async-trait = "0.1.40" base64 = { version = '0.13.0', default-features = false, features = ['alloc'] } bincode = "1.3.3" clap = { version = "3.1", features = ["env"] } +cfg-if = "1.0.0" rand = "0.8.5" futures = "0.3.5" governor = "0.5.0" diff --git a/clients/vault/src/lib.rs b/clients/vault/src/lib.rs index 014d009ba..ad5f14a4e 100644 --- a/clients/vault/src/lib.rs +++ b/clients/vault/src/lib.rs @@ -47,3 +47,15 @@ pub const CHAIN_HEIGHT_POLLING_INTERVAL: Duration = Duration::from_millis(500); /// explicitly yield at most once per second pub const YIELD_RATE: Quota = Quota::per_second(nonzero!(1u32)); + +cfg_if::cfg_if! { + if #[cfg(feature = "standalone-metadata")] { + pub type DecimalsLookupImpl = primitives::DefaultDecimalsLookup; + } else if #[cfg(feature = "parachain-metadata-pendulum")] { + pub type DecimalsLookupImpl = primitives::PendulumDecimalsLookup; + } else if #[cfg(feature = "parachain-metadata-amplitude")] { + pub type DecimalsLookupImpl = primitives::AmplitudeDecimalsLookup; + } else if #[cfg(feature = "parachain-metadata-foucoco")] { + pub type DecimalsLookupImpl = primitives::AmplitudeDecimalsLookup; + } +} diff --git a/clients/vault/src/metrics.rs b/clients/vault/src/metrics.rs index 8f09e3e9d..3bb32cd7f 100644 --- a/clients/vault/src/metrics.rs +++ b/clients/vault/src/metrics.rs @@ -2,11 +2,11 @@ use std::{collections::HashMap, convert::TryInto}; use crate::{ system::{VaultData, VaultIdManager}, - Error, + DecimalsLookupImpl, Error, }; use async_trait::async_trait; use lazy_static::lazy_static; -use primitives::{stellar, Asset}; +use primitives::{stellar, Asset, DecimalsLookup}; use runtime::{ prometheus::{ gather, proto::MetricFamily, Encoder, Gauge, GaugeVec, IntCounter, IntGaugeVec, Opts, @@ -288,7 +288,7 @@ pub async fn metrics_handler() -> Result { } fn raw_value_as_currency(value: u128, currency: CurrencyId) -> Result> { - let scaling_factor = currency.one() as f64; + let scaling_factor = DecimalsLookupImpl::one(currency) as f64; Ok(value as f64 / scaling_factor) } @@ -556,7 +556,7 @@ pub async fn publish_expected_stellar_balance( if let Ok(v) = parachain_rpc.get_vault(&vault.vault_id).await { let lowerbound = v.issued_tokens.saturating_sub(v.to_be_redeemed_tokens); let upperbound = v.issued_tokens.saturating_add(v.to_be_issued_tokens); - let scaling_factor = vault.vault_id.wrapped_currency().one() as f64; + let scaling_factor = DecimalsLookupImpl::one(vault.vault_id.wrapped_currency()) as f64; vault.metrics.asset_balance.lowerbound.set(lowerbound as f64 / scaling_factor); vault.metrics.asset_balance.upperbound.set(upperbound as f64 / scaling_factor); diff --git a/clients/vault/tests/vault_integration_tests.rs b/clients/vault/tests/vault_integration_tests.rs index 4b04e86e4..98660589c 100644 --- a/clients/vault/tests/vault_integration_tests.rs +++ b/clients/vault/tests/vault_integration_tests.rs @@ -17,10 +17,11 @@ use runtime::{ }; use stellar_relay_lib::sdk::PublicKey; -use vault::{service::IssueFilter, Event as CancellationEvent, VaultIdManager}; +use vault::{service::IssueFilter, DecimalsLookupImpl, Event as CancellationEvent, VaultIdManager}; mod helper; use helper::*; +use primitives::DecimalsLookup; #[tokio::test(flavor = "multi_thread")] #[serial] @@ -54,7 +55,7 @@ async fn test_redeem_succeeds() { VaultIdManager::from_map(vault_provider.clone(), vault_wallet.clone(), vault_ids); // We issue 1 (spacewalk-chain) unit - let issue_amount = CurrencyId::Native.one(); + let issue_amount = DecimalsLookupImpl::one(CurrencyId::Native); let vault_collateral = get_required_vault_collateral_for_issue( &vault_provider, issue_amount, @@ -902,7 +903,7 @@ async fn test_execute_open_requests_succeeds() { VaultIdManager::from_map(vault_provider.clone(), vault_wallet.clone(), vault_ids); // We issue 1 (spacewalk-chain) unit - let issue_amount = CurrencyId::Native.one(); + let issue_amount = DecimalsLookupImpl::one(CurrencyId::Native); let vault_collateral = get_required_vault_collateral_for_issue( &vault_provider, issue_amount, diff --git a/clients/wallet/src/operations.rs b/clients/wallet/src/operations.rs index a62613e5b..426b4ac3d 100644 --- a/clients/wallet/src/operations.rs +++ b/clients/wallet/src/operations.rs @@ -8,7 +8,7 @@ use primitives::{ Asset, ClaimPredicate, Claimant, Memo, Operation, PublicKey, StellarSdkError, StroopAmount, Transaction, }, - stellar_stroops_to_u128, StellarStroops, + stellar_stroops_to_u128, DecimalsLookup, StellarStroops, }; pub trait AppendExt { @@ -98,8 +98,11 @@ pub trait RedeemOperationsExt: HorizonClient { // ... and redeeming amount >= 1 XLM, use create account operation if to_be_redeemed_asset == Asset::AssetTypeNative && - to_be_redeemed_amount_u128 >= primitives::CurrencyId::StellarNative.one() - { + to_be_redeemed_amount_u128 >= + // It's okay to use the default here because the Stellar decimals are the same + primitives::DefaultDecimalsLookup::one( + primitives::CurrencyId::StellarNative, + ) { create_account_operation(destination_address, to_be_redeemed_amount) } // else use claimable balance diff --git a/pallets/currency/src/lib.rs b/pallets/currency/src/lib.rs index f836b0e29..b5c68c6b6 100644 --- a/pallets/currency/src/lib.rs +++ b/pallets/currency/src/lib.rs @@ -89,7 +89,8 @@ pub mod pallet { + Copy + Default + Debug - + From; + + From + + From; /// Relay chain currency e.g. DOT/KSM #[pallet::constant] diff --git a/pallets/issue/src/mock.rs b/pallets/issue/src/mock.rs index 24fbbc127..563e010f0 100644 --- a/pallets/issue/src/mock.rs +++ b/pallets/issue/src/mock.rs @@ -29,7 +29,7 @@ pub use currency::{ Amount, }; pub use primitives::CurrencyId; -use primitives::{AmountCompatibility, VaultCurrencyPair, VaultId}; +use primitives::{AmountCompatibility, DefaultDecimalsLookup, VaultCurrencyPair, VaultId}; use crate as issue; use crate::{Config, Error}; @@ -291,6 +291,7 @@ impl staking::Config for Test { impl oracle::Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/issue/src/tests.rs b/pallets/issue/src/tests.rs index a8d4228e0..ea9f36bdb 100644 --- a/pallets/issue/src/tests.rs +++ b/pallets/issue/src/tests.rs @@ -447,14 +447,24 @@ fn test_set_issue_period_only_root() { #[test] fn test_request_issue_fails_exceed_limit_volume_for_issue_request() { run_test(|| { - let volume_limit = 1u128; + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; + let volume_limit = 1_000u128; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); - let issue_amount = volume_limit + 1; + let issue_asset = VAULT.wrapped_currency(); + // First, convert the volume limit to the issue asset + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + + // We set the issue amount as the volume limit + 100 (we have to use at least 100 because + // the issue_amount might get rounded down during the price conversion between assets with + // decimals differing by 2) + let issue_amount = volume_limit_denoted_in_wrapped_asset.amount() + 100; let issue_fee = 1; let griefing_collateral = 1; let amount_transferred = issue_amount; @@ -468,15 +478,22 @@ fn test_request_issue_fails_exceed_limit_volume_for_issue_request() { #[test] fn test_request_issue_fails_after_execute_issue_exceed_limit_volume_for_issue_request() { run_test(|| { + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; let volume_limit = 3u128; crate::Pallet::::_rate_limit_update( - std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + Option::::Some(volume_limit), + volume_currency, 7200u64, ); let issue_asset = VAULT.wrapped_currency(); - let issue_amount = volume_limit; + + // We set the issue amount as exactly the volume limit + // First, convert the volume limit to the issue asset + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + let issue_amount = volume_limit_denoted_in_wrapped_asset.amount(); let issue_fee = 1; let griefing_collateral = 1; let amount_transferred = issue_amount; @@ -519,15 +536,21 @@ fn test_request_issue_fails_after_execute_issue_exceed_limit_volume_for_issue_re #[test] fn test_request_issue_success_with_rate_limit() { run_test(|| { + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; let volume_limit = 3u128; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); let issue_asset = VAULT.wrapped_currency(); - let issue_amount = volume_limit; + + // We set the issue amount as exactly the volume limit + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + let issue_amount = volume_limit_denoted_in_wrapped_asset.amount(); let issue_fee = 1; let griefing_collateral = 1; let amount_transferred = issue_amount; @@ -562,15 +585,20 @@ fn test_request_issue_success_with_rate_limit() { #[test] fn test_request_issue_reset_interval_and_succeeds_with_rate_limit() { run_test(|| { + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; let volume_limit = 3u128; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); let issue_asset = VAULT.wrapped_currency(); - let issue_amount = volume_limit; + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + + let issue_amount = volume_limit_denoted_in_wrapped_asset.amount(); let issue_fee = 1; let griefing_collateral = 1; let amount_transferred = issue_amount; diff --git a/pallets/nomination/src/mock.rs b/pallets/nomination/src/mock.rs index 128fdfe84..1dc902344 100644 --- a/pallets/nomination/src/mock.rs +++ b/pallets/nomination/src/mock.rs @@ -25,7 +25,7 @@ use oracle::{ }, }; pub use primitives::CurrencyId; -use primitives::{VaultCurrencyPair, VaultId}; +use primitives::{DefaultDecimalsLookup, VaultCurrencyPair, VaultId}; use crate as nomination; use crate::{Config, Error}; @@ -273,6 +273,7 @@ impl fee::Config for Test { impl oracle::Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/oracle/src/lib.rs b/pallets/oracle/src/lib.rs index b6c780433..f52409c22 100644 --- a/pallets/oracle/src/lib.rs +++ b/pallets/oracle/src/lib.rs @@ -22,7 +22,9 @@ use currency::Amount; pub use default_weights::{SubstrateWeight, WeightInfo}; use orml_oracle::DataProviderExtended; pub use pallet::*; -pub use primitives::{oracle::Key as OracleKey, CurrencyId, TruncateFixedPointToInt}; +pub use primitives::{ + oracle::Key as OracleKey, CurrencyId, DecimalsLookup, TruncateFixedPointToInt, +}; use security::{ErrorCode, StatusCode}; use crate::types::{BalanceOf, UnsignedFixedPoint, Version}; @@ -57,10 +59,16 @@ pub mod types; pub mod dia; pub mod oracle_api; + pub use crate::oracle_api::*; + #[cfg(feature = "testing-utils")] pub mod oracle_mock; +// We assume this value to be the decimals of our base currency (USD). It doesn't matter too much as +// long as it's consistent. +const USD_DECIMALS: u32 = 12; + #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; @@ -83,6 +91,8 @@ pub mod pallet { /// Weight information for the extrinsics in this module. type WeightInfo: WeightInfo; + type DecimalsLookup: DecimalsLookup; + type DataProvider: DataProviderExtended< OracleKey, orml_oracle::TimestampedValue, @@ -97,7 +107,7 @@ pub mod pallet { } #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] + #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { AggregateUpdated { values: Vec<(OracleKey, T::UnsignedFixedPoint)> }, OracleKeysUpdated { oracle_keys: Vec }, @@ -212,9 +222,9 @@ impl Pallet { let max_delay = Self::get_max_delay(); for key in oracle_keys.iter() { let price = Self::get_timestamped(key); - let Some(price) = price else{ - continue; - }; + let Some(price) = price else { + continue; + }; let is_outdated = current_time > price.timestamp + max_delay; if !is_outdated { updated_items.push((key.clone(), price.value)); @@ -285,9 +295,9 @@ impl Pallet { pub fn get_price(key: OracleKey) -> Result, DispatchError> { ext::security::ensure_parachain_status_running::()?; - let Some(price) = T::DataProvider::get_no_op(&key) else{ - return Err(Error::::MissingExchangeRate.into()); - }; + let Some(price) = T::DataProvider::get_no_op(&key) else { + return Err(Error::::MissingExchangeRate.into()); + }; Ok(price.value) } @@ -297,11 +307,13 @@ impl Pallet { ) -> Result, DispatchError> { let converted = match (amount.currency(), currency_id) { (x, y) if x == y => amount.amount(), - (_, _) => { - // First convert to USD, then convert USD to the desired currency - let base = Self::currency_to_usd(amount.amount(), amount.currency())?; - Self::usd_to_currency(base, currency_id)? - }, + (_, _) => Self::convert_amount( + amount.amount(), + Self::get_price(OracleKey::ExchangeRate(amount.currency()))?, + Self::get_price(OracleKey::ExchangeRate(currency_id))?, + T::DecimalsLookup::decimals(amount.currency()), + T::DecimalsLookup::decimals(currency_id), + )?, }; Ok(Amount::new(converted, currency_id)) } @@ -310,28 +322,88 @@ impl Pallet { amount: BalanceOf, currency_id: CurrencyId, ) -> Result, DispatchError> { - let rate = Self::get_price(OracleKey::ExchangeRate(currency_id))?; - let converted = rate.checked_mul_int(amount).ok_or(ArithmeticError::Overflow)?; - Ok(converted) + // Rate from asset to USD + let asset_rate = Self::get_price(OracleKey::ExchangeRate(currency_id))?; + // Rate from USD to USD is 1 + let usd_rate = UnsignedFixedPoint::::one(); + + Self::convert_amount( + amount, + asset_rate, + usd_rate, + T::DecimalsLookup::decimals(currency_id), + USD_DECIMALS, + ) } pub fn usd_to_currency( amount: BalanceOf, currency_id: CurrencyId, ) -> Result, DispatchError> { - let rate = Self::get_price(OracleKey::ExchangeRate(currency_id))?; - if amount.is_zero() { + // Rate from asset to USD + let asset_rate = Self::get_price(OracleKey::ExchangeRate(currency_id))?; + // Rate from USD to USD is 1 + let usd_rate = UnsignedFixedPoint::::one(); + + Self::convert_amount( + amount, + usd_rate, + asset_rate, + USD_DECIMALS, + T::DecimalsLookup::decimals(currency_id), + ) + } + + fn convert_amount( + from_amount: BalanceOf, + from_price: T::UnsignedFixedPoint, + to_price: T::UnsignedFixedPoint, + from_decimals: u32, + to_decimals: u32, + ) -> Result, DispatchError> { + if from_amount.is_zero() { return Ok(Zero::zero()) } - // The code below performs `amount/rate`, plus necessary type conversions - Ok(T::UnsignedFixedPoint::checked_from_integer(amount) - .ok_or(Error::::TryIntoIntError)? - .checked_div(&rate) - .ok_or(ArithmeticError::Underflow)? - .truncate_to_inner() - .ok_or(Error::::TryIntoIntError)? - .unique_saturated_into()) + let from_amount = T::UnsignedFixedPoint::from_inner(from_amount); + + if from_decimals > to_decimals { + // result = from_amount * from_price / to_price / 10^(from_decimals - to_decimals) + let to_amount = from_price + .checked_mul(&from_amount) + .ok_or(ArithmeticError::Overflow)? + .checked_div(&to_price) + .ok_or(ArithmeticError::Underflow)? + .checked_div( + &UnsignedFixedPoint::::checked_from_integer( + 10u128.pow(from_decimals.saturating_sub(to_decimals)), + ) + .ok_or(Error::::TryIntoIntError)?, + ) + .ok_or(ArithmeticError::Underflow)? + .into_inner() + .unique_saturated_into(); + + Ok(to_amount) + } else { + // result = from_amount * from_price * 10^(to_decimals - from_decimals) / to_price + let to_amount = from_price + .checked_mul(&from_amount) + .ok_or(ArithmeticError::Overflow)? + .checked_mul( + &UnsignedFixedPoint::::checked_from_integer( + 10u128.pow(to_decimals.saturating_sub(from_decimals)), + ) + .ok_or(Error::::TryIntoIntError)?, + ) + .ok_or(ArithmeticError::Overflow)? + .checked_div(&to_price) + .ok_or(ArithmeticError::Underflow)? + .into_inner() + .unique_saturated_into(); + + Ok(to_amount) + } } pub fn get_exchange_rate( diff --git a/pallets/oracle/src/mock.rs b/pallets/oracle/src/mock.rs index e0ec39c11..2c44616ec 100644 --- a/pallets/oracle/src/mock.rs +++ b/pallets/oracle/src/mock.rs @@ -182,6 +182,7 @@ impl currency::Config for Test { impl Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = primitives::DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< crate::testing_utils::MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/oracle/src/tests.rs b/pallets/oracle/src/tests.rs index 54aafdc1e..8d0f6cf1a 100644 --- a/pallets/oracle/src/tests.rs +++ b/pallets/oracle/src/tests.rs @@ -1,6 +1,7 @@ -use crate::{mock::*, CurrencyId, OracleKey}; +use crate::{mock::*, CurrencyId, OracleKey, USD_DECIMALS}; use frame_support::{assert_err, assert_ok}; use mocktopus::mocking::*; +use primitives::{Asset, DecimalsLookup}; use sp_arithmetic::FixedU128; use sp_runtime::FixedPointNumber; @@ -34,12 +35,14 @@ mod oracle_offline_detection { use super::*; type SecurityPallet = security::Pallet; + use security::StatusCode; enum SubmittingOracle { OracleA, OracleB, } + use SubmittingOracle::*; fn set_time(time: u64) { @@ -54,6 +57,7 @@ mod oracle_offline_detection { )); mine_block(); } + fn feed_value_with_value(currency_id: CurrencyId, _oracle: SubmittingOracle, value: u128) { assert_ok!(Oracle::feed_values( 1, @@ -155,12 +159,99 @@ fn getting_exchange_rate_fails_with_missing_exchange_rate() { }); } +#[test] +fn test_amount_conversion() { + run_test(|| { + let source_currency = CurrencyId::XCM(0); + let target_currency = CurrencyId::XCM(1); + + Oracle::get_price.mock_safe(move |key| { + match key { + OracleKey::ExchangeRate(currency) => { + match currency { + // XCM(0) is worth 5 USD + CurrencyId::XCM(0) => + MockResult::Return(Ok(FixedU128::from_rational(5, 1))), + // XCM(1) is worth 1 USD + CurrencyId::XCM(1) => + MockResult::Return(Ok(FixedU128::from_rational(1, 1))), + _ => { + panic!("Unexpected currency") + }, + } + }, + } + }); + + // We get one unit of each currency. The currencies have possibly different decimals, so we + // need to use the `one()` function + let one_unit_source = ::DecimalsLookup::one(source_currency); + let one_unit_target = ::DecimalsLookup::one(target_currency); + + let amount = currency::Amount::new(one_unit_source, source_currency); + // We expect the result to be 5 units of the target currency due to the exchange rates we + // chose + let expected = currency::Amount::::new(5 * one_unit_target, target_currency); + + let result = Oracle::convert(&amount, target_currency).expect("Should convert"); + + assert_eq!(result.amount(), expected.amount()); + assert_eq!(result.currency(), expected.currency()); + + // Convert the target amount back to source again + let result = Oracle::convert(&result, source_currency).expect("Should convert"); + assert_eq!(result.amount(), amount.amount()); + assert_eq!(result.currency(), amount.currency()); + }); +} + +#[test] +fn test_amount_conversion_limits() { + run_test(|| { + // We choose two Stellar assets because we know they have the same decimals + let source_currency = CurrencyId::StellarNative; + let target_currency = + CurrencyId::Stellar(Asset::AlphaNum4 { issuer: [0; 32], code: [0; 4] }); + + Oracle::get_price.mock_safe(move |key| { + match key { + OracleKey::ExchangeRate(currency) => { + // StellarNative is worth 1 USD + if currency == source_currency { + MockResult::Return(Ok(FixedU128::from_rational(1, 1))) + } else { + // Stellar asset is worth 0.5 USD + MockResult::Return(Ok(FixedU128::from_rational(1, 2))) + } + }, + } + }); + + // Range of u128 is 0 to 2^128 - 1 (~= 3.4 * 10^38) + for i in 0..=38 { + let amount = 10u128.pow(i); + let amount = currency::Amount::new(amount, source_currency); + let result = Oracle::convert(&amount, target_currency).expect("Should convert"); + + // We expect the result to be 2 * amount because source is worth twice as much as target + let expected = currency::Amount::::new(amount.amount() * 2, target_currency); + assert_eq!(result.amount(), expected.amount()); + assert_eq!(result.currency(), expected.currency()); + } + }); +} + #[test] fn currency_to_usd() { run_test(|| { + // 1 unit is worth 2 USD Oracle::get_price .mock_safe(|_| MockResult::Return(Ok(FixedU128::checked_from_rational(2, 1).unwrap()))); - let test_cases = [(0, 0), (2, 4), (10, 20)]; + let one_unit_xcm = ::DecimalsLookup::one(CurrencyId::XCM(0)); + let one_unit_usd = 10u128.pow(USD_DECIMALS); + + let test_cases = + [(0, 0), (2 * one_unit_xcm, 4 * one_unit_usd), (10 * one_unit_xcm, 20 * one_unit_usd)]; for (input, expected) in test_cases.iter() { let result = Oracle::currency_to_usd(*input, CurrencyId::XCM(0)); assert_ok!(result, *expected); @@ -171,9 +262,15 @@ fn currency_to_usd() { #[test] fn usd_to_currency() { run_test(|| { + // 1 unit is worth 2 USD Oracle::get_price .mock_safe(|_| MockResult::Return(Ok(FixedU128::checked_from_rational(2, 1).unwrap()))); - let test_cases = [(0, 0), (4, 2), (20, 10), (21, 10)]; + + let one_unit_xcm = ::DecimalsLookup::one(CurrencyId::XCM(0)); + let one_unit_usd = 10u128.pow(USD_DECIMALS); + + let test_cases = + [(0, 0), (4 * one_unit_usd, 2 * one_unit_xcm), (20 * one_unit_usd, 10 * one_unit_xcm)]; for (input, expected) in test_cases.iter() { let result = Oracle::usd_to_currency(*input, CurrencyId::XCM(0)); assert_ok!(result, *expected); diff --git a/pallets/redeem/src/mock.rs b/pallets/redeem/src/mock.rs index 9efbc442f..c44f6bb6b 100644 --- a/pallets/redeem/src/mock.rs +++ b/pallets/redeem/src/mock.rs @@ -27,7 +27,7 @@ use oracle::{ }, }; pub use oracle::{CurrencyId, OracleKey}; -use primitives::{AmountCompatibility, VaultCurrencyPair, VaultId}; +use primitives::{AmountCompatibility, DefaultDecimalsLookup, VaultCurrencyPair, VaultId}; use crate as redeem; use crate::{Config, Error}; @@ -306,6 +306,7 @@ impl pallet_timestamp::Config for Test { impl oracle::Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/redeem/src/tests.rs b/pallets/redeem/src/tests.rs index a8435394e..cb31ac295 100644 --- a/pallets/redeem/src/tests.rs +++ b/pallets/redeem/src/tests.rs @@ -1115,13 +1115,20 @@ mod spec_based_tests { #[test] fn test_request_redeem_fails_limits() { run_test(|| { + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; let volume_limit: u128 = 9; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); + let issue_asset = VAULT.wrapped_currency(); + // First, convert the volume limit to the issue asset + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + convert_to.mock_safe(|_, x| MockResult::Return(Ok(x))); >::insert_vault( &VAULT, @@ -1141,7 +1148,9 @@ fn test_request_redeem_fails_limits() { ); let redeemer = USER; - let amount = volume_limit + 1; + // Choose an amount that is above the limit. Has to be at least 100 because it will get + // rounded down during price conversion with different decimals + let amount = volume_limit_denoted_in_wrapped_asset.amount() + 100; let redeem_fee = 5; let stellar_address = RANDOM_STELLAR_PUBLIC_KEY; @@ -1335,13 +1344,20 @@ fn test_execute_redeem_within_rate_limit_succeeds() { #[test] fn test_execute_redeem_fails_when_exceeds_rate_limit() { run_test(|| { + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; let volume_limit: u128 = 100u128; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); + let issue_asset = VAULT.wrapped_currency(); + // First, convert the volume limit to the issue asset + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_asset) + .expect("Price conversion should work"); + convert_to.mock_safe(|_, x| MockResult::Return(Ok(x))); Security::::set_active_block_number(40); >::insert_vault( @@ -1364,7 +1380,7 @@ fn test_execute_redeem_fails_when_exceeds_rate_limit() { .mock_safe(move |_, _, _| MockResult::Return(Ok(()))); let transfer_fee = Redeem::get_current_inclusion_fee(DEFAULT_WRAPPED_CURRENCY).unwrap(); - let amount = volume_limit; + let amount = volume_limit_denoted_in_wrapped_asset.amount(); let redeem_request = RedeemRequest { period: 0, vault: VAULT, @@ -1415,7 +1431,7 @@ fn test_execute_redeem_fails_when_exceeds_rate_limit() { redeem_id: H256([0; 32]), redeemer: USER, vault_id: VAULT, - amount: 100, + amount: volume_limit_denoted_in_wrapped_asset.amount(), asset: DEFAULT_WRAPPED_CURRENCY, fee: 0, transfer_fee: transfer_fee.amount(), @@ -1426,9 +1442,9 @@ fn test_execute_redeem_fails_when_exceeds_rate_limit() { ); let redeemer = USER; - // Requesting a second redeem request with just 1 unit of collateral should fail because we + // Requesting a second redeem request with a small unit of collateral should fail because we // reached the volume limit already - let amount = 1; + let amount = 1_000; let stellar_address = RANDOM_STELLAR_PUBLIC_KEY; assert_err!( Redeem::request_redeem(RuntimeOrigin::signed(redeemer), amount, stellar_address, VAULT), @@ -1440,13 +1456,20 @@ fn test_execute_redeem_fails_when_exceeds_rate_limit() { #[test] fn test_execute_redeem_after_rate_limit_interval_reset_succeeds() { run_test(|| { - let volume_limit: u128 = 50u128; + let volume_currency = DEFAULT_COLLATERAL_CURRENCY; + let volume_limit: u128 = 5_000u128; crate::Pallet::::_rate_limit_update( std::option::Option::::Some(volume_limit), - DEFAULT_COLLATERAL_CURRENCY, + volume_currency, 7200u64, ); + let issue_currency = VAULT.wrapped_currency(); + // First, convert the volume limit to the issue asset + let volume_limit_denoted_in_wrapped_asset = + Oracle::convert(&Amount::new(volume_limit, volume_currency), issue_currency) + .expect("Price conversion should work"); + convert_to.mock_safe(|_, x| MockResult::Return(Ok(x))); Security::::set_active_block_number(40); >::insert_vault( @@ -1455,7 +1478,7 @@ fn test_execute_redeem_after_rate_limit_interval_reset_succeeds() { id: VAULT, to_be_replaced_tokens: 0, to_be_issued_tokens: 0, - issued_tokens: 400, + issued_tokens: 400000, to_be_redeemed_tokens: 200, replace_collateral: 0, banned_until: None, @@ -1468,15 +1491,15 @@ fn test_execute_redeem_after_rate_limit_interval_reset_succeeds() { ext::stellar_relay::validate_stellar_transaction:: .mock_safe(move |_, _, _| MockResult::Return(Ok(()))); - let transfer_fee = Redeem::get_current_inclusion_fee(DEFAULT_WRAPPED_CURRENCY).unwrap(); - let amount = volume_limit; + let transfer_fee = Redeem::get_current_inclusion_fee(issue_currency).unwrap(); + let amount = volume_limit_denoted_in_wrapped_asset.amount(); let redeem_request = RedeemRequest { period: 0, vault: VAULT, opentime: 40, fee: 0, amount, - asset: DEFAULT_WRAPPED_CURRENCY, + asset: issue_currency, premium: 0, redeemer: USER, stellar_address: RANDOM_STELLAR_PUBLIC_KEY, @@ -1531,8 +1554,9 @@ fn test_execute_redeem_after_rate_limit_interval_reset_succeeds() { ); let redeemer = USER; - let amount = 1; + let amount = 1_000; let stellar_address = RANDOM_STELLAR_PUBLIC_KEY; + // We expect this to fail because we previously reached the volume limit assert_err!( Redeem::request_redeem(RuntimeOrigin::signed(redeemer), amount, stellar_address, VAULT), TestError::ExceedLimitVolumeForRedeemRequest @@ -1545,8 +1569,8 @@ fn test_execute_redeem_after_rate_limit_interval_reset_succeeds() { assert!(>::get() > BalanceOf::::zero()); let redeemer = USER; - let amount = volume_limit; let stellar_address = RANDOM_STELLAR_PUBLIC_KEY; + // We expect this to work now because the volume limit has been reset assert_ok!(Redeem::request_redeem( RuntimeOrigin::signed(redeemer), amount, diff --git a/pallets/replace/src/mock.rs b/pallets/replace/src/mock.rs index d642426fd..97cb673fd 100644 --- a/pallets/replace/src/mock.rs +++ b/pallets/replace/src/mock.rs @@ -27,7 +27,7 @@ use oracle::{ }, }; pub use primitives::CurrencyId; -use primitives::{AmountCompatibility, VaultCurrencyPair, VaultId}; +use primitives::{AmountCompatibility, DefaultDecimalsLookup, VaultCurrencyPair, VaultId}; use crate as replace; use crate::{Config, Error}; @@ -305,6 +305,7 @@ impl pallet_timestamp::Config for Test { impl oracle::Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/vault-registry/src/benchmarking.rs b/pallets/vault-registry/src/benchmarking.rs index b09d1b590..1de6d9257 100644 --- a/pallets/vault-registry/src/benchmarking.rs +++ b/pallets/vault-registry/src/benchmarking.rs @@ -20,8 +20,8 @@ type UnsignedFixedPoint = ::UnsignedFixedPoint; const STELLAR_PUBLIC_KEY_DUMMY: StellarPublicKeyRaw = [0u8; 32]; -fn wrapped(amount: u32) -> Amount { - Amount::new(amount.into(), get_wrapped_currency_id()) +fn amount(amount: u32) -> Amount { + Amount::new(amount.into(), get_collateral_currency_id::()) } fn deposit_tokens( @@ -142,14 +142,17 @@ benchmarks! { let origin: T::AccountId = account("Origin", 0, 0); mint_collateral::(&vault_id.account_id, (1u32 << 31).into()); - register_vault_with_collateral::(vault_id.clone(), 10_000); + let collateral_amount = 10_000; + register_vault_with_collateral::(vault_id.clone(), collateral_amount); let oracle_mock_lock = Oracle::::acquire_lock(); Oracle::::_set_exchange_rate(vault_id.clone().account_id, get_collateral_currency_id::(), UnsignedFixedPoint::::one()).unwrap(); Oracle::::_set_exchange_rate(vault_id.clone().account_id, get_wrapped_currency_id(), UnsignedFixedPoint::::checked_from_rational(1, 10).unwrap()).unwrap(); - VaultRegistry::::try_increase_to_be_issued_tokens(&vault_id, &wrapped(5_000)).unwrap(); - VaultRegistry::::issue_tokens(&vault_id, &wrapped(5_000)).unwrap(); + // Convert the amount to the wrapped currency + let issue_amount = Oracle::::convert(&amount(collateral_amount), get_wrapped_currency_id()).expect("Conversion should work"); + VaultRegistry::::try_increase_to_be_issued_tokens(&vault_id, &issue_amount).unwrap(); + VaultRegistry::::issue_tokens(&vault_id, &issue_amount).unwrap(); Oracle::::_set_exchange_rate(vault_id.clone().account_id, get_collateral_currency_id::(), UnsignedFixedPoint::::checked_from_rational(1, 10).unwrap()).unwrap(); Oracle::::_set_exchange_rate(vault_id.clone().account_id, get_wrapped_currency_id(), UnsignedFixedPoint::::one()).unwrap(); diff --git a/pallets/vault-registry/src/mock.rs b/pallets/vault-registry/src/mock.rs index b6740997c..b7b50d950 100644 --- a/pallets/vault-registry/src/mock.rs +++ b/pallets/vault-registry/src/mock.rs @@ -25,7 +25,7 @@ pub use currency::testing_constants::{ DEFAULT_COLLATERAL_CURRENCY, DEFAULT_NATIVE_CURRENCY, DEFAULT_WRAPPED_CURRENCY, }; pub use primitives::{CurrencyId, CurrencyId::XCM}; -use primitives::{VaultCurrencyPair, VaultId}; +use primitives::{DefaultDecimalsLookup, VaultCurrencyPair, VaultId}; use crate as vault_registry; use crate::{Config, Error}; @@ -195,6 +195,7 @@ impl pallet_timestamp::Config for Test { impl oracle::Config for Test { type RuntimeEvent = TestEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DiaOracleAdapter< MockDiaOracle, UnsignedFixedPoint, diff --git a/pallets/vault-registry/src/tests.rs b/pallets/vault-registry/src/tests.rs index 933aebd3b..78ff27fe9 100644 --- a/pallets/vault-registry/src/tests.rs +++ b/pallets/vault-registry/src/tests.rs @@ -5,7 +5,7 @@ use frame_system::RawOrigin; use mocktopus::mocking::*; use pooled_rewards::RewardsApi; use pretty_assertions::assert_eq; -use primitives::{StellarPublicKeyRaw, VaultCurrencyPair, VaultId}; +use primitives::{DecimalsLookup, StellarPublicKeyRaw, VaultCurrencyPair, VaultId}; use security::Pallet as Security; use sp_arithmetic::{traits::One, FixedPointNumber, FixedU128}; use sp_core::U256; @@ -989,7 +989,7 @@ fn test_threshold_equivalent_to_legacy_calculation() { ) -> Result, DispatchError> { let granularity = 5; // convert the collateral to wrapped - let collateral_in_wrapped = convert_to(DEFAULT_COLLATERAL_CURRENCY, wrapped(collateral))?; + let collateral_in_wrapped = convert_to(DEFAULT_WRAPPED_CURRENCY, amount(collateral))?; let collateral_in_wrapped = U256::from(collateral_in_wrapped.amount()); // calculate how many tokens should be maximally issued given the threshold @@ -1005,11 +1005,13 @@ fn test_threshold_equivalent_to_legacy_calculation() { run_test(|| { let threshold = FixedU128::checked_from_rational(199999, 100000).unwrap(); // 199.999% let random_start = 987529462328_u128; - for xlm in random_start..random_start + 199999 { + for wrapped_amount in random_start..random_start + 199999 { let old = - legacy_calculate_max_wrapped_from_collateral_for_threshold(xlm, 199999).unwrap(); + legacy_calculate_max_wrapped_from_collateral_for_threshold(wrapped_amount, 199999) + .unwrap(); + let new = VaultRegistry::calculate_max_wrapped_from_collateral_for_threshold( - &amount(xlm), + &amount(wrapped_amount), DEFAULT_WRAPPED_CURRENCY, threshold, ) @@ -1069,15 +1071,15 @@ fn get_required_collateral_for_wrapped_with_threshold_succeeds() { run_test(|| { let threshold = FixedU128::checked_from_rational(19999, 10000).unwrap(); // 199.99% let random_start = 987529387592_u128; - for xlm in random_start..random_start + 19999 { + for wrapped_amount in random_start..random_start + 19999 { let min_collateral = VaultRegistry::get_required_collateral_for_wrapped_with_threshold( - &wrapped(xlm), + &wrapped(wrapped_amount), threshold, DEFAULT_COLLATERAL_CURRENCY, ) .unwrap(); - let max_xlm_for_min_collateral = + let max_wrapped_for_min_collateral = VaultRegistry::calculate_max_wrapped_from_collateral_for_threshold( &min_collateral, DEFAULT_WRAPPED_CURRENCY, @@ -1085,7 +1087,7 @@ fn get_required_collateral_for_wrapped_with_threshold_succeeds() { ) .unwrap(); - let max_xlm_for_below_min_collateral = + let max_wrapped_for_below_min_collateral = VaultRegistry::calculate_max_wrapped_from_collateral_for_threshold( &amount(min_collateral.amount() - 1), DEFAULT_WRAPPED_CURRENCY, @@ -1094,9 +1096,18 @@ fn get_required_collateral_for_wrapped_with_threshold_succeeds() { .unwrap(); // Check that the amount we found is indeed the lowest amount that is sufficient for - // `xlm` - assert!(max_xlm_for_min_collateral.amount() >= xlm); - assert!(max_xlm_for_below_min_collateral.amount() < xlm); + // `wrapped` + // We have to be a little generous here, because the conversion from collateral to + // wrapped may be rounded. So we give 10^(wrapped_decimals - collateral_decimals) as + // tolerance. + let tolerance = 10u128.pow( + ::DecimalsLookup::decimals(DEFAULT_WRAPPED_CURRENCY) + .saturating_sub(::DecimalsLookup::decimals( + DEFAULT_COLLATERAL_CURRENCY, + )), + ); + assert!(max_wrapped_for_min_collateral.amount() + tolerance >= wrapped_amount); + assert!(max_wrapped_for_below_min_collateral.amount() - tolerance < wrapped_amount); } }) } @@ -1173,14 +1184,14 @@ mod liquidation_threshold_tests { fn setup() -> Vault { let id = create_sample_vault(); - assert_ok!(VaultRegistry::try_increase_to_be_issued_tokens(&id, &wrapped(50)),); - let res = VaultRegistry::issue_tokens(&id, &wrapped(50)); + assert_ok!(VaultRegistry::try_increase_to_be_issued_tokens(&id, &wrapped(5_000)),); + let res = VaultRegistry::issue_tokens(&id, &wrapped(5_000)); assert_ok!(res); let mut vault = VaultRegistry::get_vault_from_id(&id).unwrap(); - vault.issued_tokens = 50; - vault.to_be_issued_tokens = 40; - vault.to_be_redeemed_tokens = 20; + vault.issued_tokens = 5_000; + vault.to_be_issued_tokens = 4_000; + vault.to_be_redeemed_tokens = 2_000; vault } @@ -1203,7 +1214,11 @@ mod liquidation_threshold_tests { fn is_vault_below_liquidation_threshold_true_succeeds() { run_test(|| { let vault = setup(); - let backing_collateral = vault.issued_tokens * 2 - 1; + let issued_tokens_as_collateral = + Oracle::convert(&wrapped(vault.issued_tokens), vault.id.currencies.collateral) + .expect("Conversion should work") + .amount(); + let backing_collateral = issued_tokens_as_collateral * 2 - 1; VaultRegistry::get_backing_collateral .mock_safe(move |_| MockResult::Return(Ok(amount(backing_collateral)))); assert_eq!( @@ -1271,7 +1286,7 @@ fn get_settled_collateralization_from_vault_succeeds() { }) } -mod get_vaults_below_premium_collaterlization_tests { +mod get_vaults_below_premium_collateralization_tests { use super::{assert_eq, *}; /// sets premium_redeem threshold to 1 @@ -1315,12 +1330,18 @@ mod get_vaults_below_premium_collaterlization_tests { fn get_vaults_below_premium_collateralization_succeeds() { run_test(|| { let id1 = vault_id(3); - let issue_tokens1: u128 = 50; - let collateral1 = 49; + let issue_tokens1 = 5_000; + let collateral1 = Oracle::convert(&wrapped(issue_tokens1), id1.collateral_currency()) + .expect("Conversion should work") + .amount(); + let collateral1 = collateral1 - 1; let id2 = vault_id(4); - let issue_tokens2: u128 = 60; - let collateral2 = 48; + let issue_tokens2: u128 = 6_000; + let collateral2 = Oracle::convert(&wrapped(issue_tokens2), id2.collateral_currency()) + .expect("Conversion should work") + .amount(); + let collateral2 = collateral2 - 12; add_vault(id1.clone(), issue_tokens1, collateral1); add_vault(id2.clone(), issue_tokens2, collateral2); @@ -1336,23 +1357,31 @@ mod get_vaults_below_premium_collaterlization_tests { fn get_vaults_below_premium_collateralization_filters_banned_and_sufficiently_collateralized_vaults( ) { run_test(|| { - // not returned, because is is not under premium threshold (which is set to 100% for + // not returned, because it is not under premium threshold (which is set to 100% for // this test) let id1 = vault_id(3); - let issue_tokens1: u128 = 50; - let collateral1 = 50; + let issue_tokens1 = 5_000; + let collateral1 = Oracle::convert(&wrapped(issue_tokens1), id1.collateral_currency()) + .expect("Conversion should work") + .amount(); add_vault(id1, issue_tokens1, collateral1); // returned let id2 = vault_id(4); - let issue_tokens2: u128 = 50; - let collateral2 = 49; + let issue_tokens2: u128 = 5_000; + let collateral2 = Oracle::convert(&wrapped(issue_tokens2), id2.collateral_currency()) + .expect("Conversion should work") + .amount(); + let collateral2 = collateral2 - 1; add_vault(id2.clone(), issue_tokens2, collateral2); // not returned because it's banned let id3 = vault_id(5); - let issue_tokens3: u128 = 50; - let collateral3 = 49; + let issue_tokens3: u128 = 5_000; + let collateral3 = Oracle::convert(&wrapped(issue_tokens3), id3.collateral_currency()) + .expect("Conversion should work") + .amount(); + let collateral3 = collateral3 - 1; add_vault(id3.clone(), issue_tokens3, collateral3); let mut vault3 = VaultRegistry::get_active_rich_vault_from_id(&id3).unwrap(); vault3.ban_until(1000); diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 21fcf3f2c..bed0b2f6f 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -498,26 +498,74 @@ pub enum CurrencyId { Token(u64), } -impl CurrencyId { - pub const StellarNative: CurrencyId = Self::Stellar(Asset::StellarNative); +pub trait DecimalsLookup { + type CurrencyId; - pub fn decimals(&self) -> u8 { - match self { + fn decimals(currency_id: CurrencyId) -> u32; + + fn one(currency_id: CurrencyId) -> Balance { + 10u128.pow(Self::decimals(currency_id)) + } +} + +// We use the PendulumDecimalsLookup as the default implementation in Spacewalk because it +// is more interesting with the XCM(0) token having 10 decimals vs the default 12 decimals. +pub type DefaultDecimalsLookup = PendulumDecimalsLookup; + +pub struct PendulumDecimalsLookup; +impl DecimalsLookup for PendulumDecimalsLookup { + type CurrencyId = CurrencyId; + + fn decimals(currency_id: CurrencyId) -> u32 { + (match currency_id { CurrencyId::Stellar(asset) => asset.decimals(), + CurrencyId::XCM(index) => match index { + // DOT + 0 => 10, + // Assethub USDT + 1 => 6, + // Assethub USDC + 2 => 6, + // EQD + 3 => 9, + // Moonbeam BRZ + 4 => 18, + // PDEX + 5 => 12, + // GLMR + 6 => 18, + _ => 12, + }, // We assume that all other assets have 12 decimals - CurrencyId::Native | - CurrencyId::XCM(_) | - CurrencyId::ZenlinkLPToken(_, _, _, _) | - CurrencyId::Token(_) => 12, - } + CurrencyId::Native | CurrencyId::ZenlinkLPToken(_, _, _, _) | CurrencyId::Token(_) => + 12, + }) as u32 } +} - pub fn one(&self) -> Balance { - match self { - CurrencyId::Stellar(asset) => asset.one(), - _ => 10u128.pow(self.decimals().into()), - } +pub struct AmplitudeDecimalsLookup; +impl DecimalsLookup for AmplitudeDecimalsLookup { + type CurrencyId = CurrencyId; + + fn decimals(currency_id: CurrencyId) -> u32 { + (match currency_id { + CurrencyId::Stellar(asset) => asset.decimals(), + CurrencyId::XCM(index) => match index { + // KSM + 0 => 12, + // Assethub USDT + 1 => 6, + _ => 12, + }, + // We assume that all other assets have 12 decimals + CurrencyId::Native | CurrencyId::ZenlinkLPToken(_, _, _, _) | CurrencyId::Token(_) => + 12, + }) as u32 } +} + +impl CurrencyId { + pub const StellarNative: CurrencyId = Self::Stellar(Asset::StellarNative); #[allow(non_snake_case)] pub const fn AlphaNum4(code: Bytes4, issuer: AssetIssuer) -> Self { diff --git a/testchain/runtime/src/lib.rs b/testchain/runtime/src/lib.rs index 1bc446545..82a0d95b2 100644 --- a/testchain/runtime/src/lib.rs +++ b/testchain/runtime/src/lib.rs @@ -512,10 +512,12 @@ cfg_if::cfg_if! { #[cfg(any(feature = "runtime-benchmarks", feature = "testing-utils"))] use oracle::testing_utils::MockDataFeeder; +use primitives::DefaultDecimalsLookup; impl oracle::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = oracle::SubstrateWeight; + type DecimalsLookup = DefaultDecimalsLookup; type DataProvider = DataProviderImpl; #[cfg(any(feature = "runtime-benchmarks", feature = "testing-utils"))]