diff --git a/crates/oracle/Cargo.toml b/crates/oracle/Cargo.toml index 7d40b980c3..81efe41d17 100644 --- a/crates/oracle/Cargo.toml +++ b/crates/oracle/Cargo.toml @@ -22,19 +22,20 @@ pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = " # Parachain dependencies security = { path = "../security", default-features = false } -primitives = { package = "interbtc-primitives", path = "../../primitives", default-features = false } staking = { path = "../staking", default-features = false } currency = { path = "../currency", default-features = false } traits = { path = '../traits', default-features = false } -[dev-dependencies] -mocktopus = "0.8.0" -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } +primitives = { package = "interbtc-primitives", path = "../../primitives", default-features = false } # Orml dependencies orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "dc39cfddefb10ef0de23655e2c3dcdab66a19404", default-features = false } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "dc39cfddefb10ef0de23655e2c3dcdab66a19404", default-features = false } +[dev-dependencies] +mocktopus = "0.8.0" +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } + [features] default = ["std"] std = [ @@ -54,7 +55,12 @@ std = [ "security/std", "staking/std", "currency/std", + "traits/std", + "primitives/std", + + "orml-tokens/std", + "orml-traits/std", ] runtime-benchmarks = [ "frame-benchmarking", diff --git a/crates/oracle/src/lib.rs b/crates/oracle/src/lib.rs index fe288eac2c..d9e0db1708 100644 --- a/crates/oracle/src/lib.rs +++ b/crates/oracle/src/lib.rs @@ -47,6 +47,7 @@ use traits::OracleApi; pub use pallet::*; pub use primitives::{oracle::Key as OracleKey, CurrencyId, TruncateFixedPointToInt}; pub use traits::OnExchangeRateChange; +pub use types::*; #[derive(Encode, Decode, Eq, PartialEq, Clone, Copy, Ord, PartialOrd, TypeInfo, MaxEncodedLen)] pub struct TimestampedValue { @@ -146,6 +147,9 @@ pub mod pallet { #[pallet::getter(fn authorized_oracles)] pub type AuthorizedOracles = StorageMap<_, Blake2_128Concat, T::AccountId, Vec, ValueQuery>; + #[pallet::storage] + pub type RebaseTokens = StorageMap<_, Blake2_128Concat, CurrencyId, CurrencyId, OptionQuery>; + #[pallet::type_value] pub(super) fn DefaultForStorageVersion() -> Version { Version::V0 diff --git a/crates/oracle/src/mock.rs b/crates/oracle/src/mock.rs index eeab5d95e5..fdbbd4dd00 100644 --- a/crates/oracle/src/mock.rs +++ b/crates/oracle/src/mock.rs @@ -1,12 +1,15 @@ use crate as oracle; -use crate::{Config, Error}; +use crate::{types::*, Config, Error}; use frame_support::{ parameter_types, traits::{ConstU32, Everything, GenesisBuild}, }; use mocktopus::mocking::clear_mocks; use orml_traits::parameter_type_with_key; -pub use primitives::{CurrencyId::Token, TokenSymbol::*}; +pub use primitives::{ + CurrencyId::{ForeignAsset, Token}, + TokenSymbol::*, +}; use sp_arithmetic::{FixedI128, FixedU128}; use sp_core::H256; use sp_runtime::{ @@ -14,6 +17,9 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, }; +pub type OracleRebase = + Combiner, Mapper, Tokens>, Tokens>; + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; diff --git a/crates/oracle/src/tests.rs b/crates/oracle/src/tests.rs index 3bcb750331..2b5afcbe07 100644 --- a/crates/oracle/src/tests.rs +++ b/crates/oracle/src/tests.rs @@ -1,6 +1,7 @@ -use crate::{mock::*, CurrencyId, OracleKey}; +use crate::{mock::*, CurrencyId, OracleKey, RebaseTokens}; use frame_support::{assert_err, assert_ok, dispatch::DispatchError}; use mocktopus::mocking::*; +use orml_traits::MultiCurrency; use sp_arithmetic::FixedU128; use sp_runtime::FixedPointNumber; @@ -363,3 +364,43 @@ fn test_median() { assert_eq!(Oracle::median(input_fixedpoint), output_fixedpoint); } } + +#[test] +fn test_rebase_tokens() { + run_test(|| { + Oracle::is_authorized.mock_safe(|_| MockResult::Return(true)); + RebaseTokens::::insert(ForeignAsset(2), Token(KSM)); + assert_ok!(Oracle::feed_values( + RuntimeOrigin::signed(0), + vec![ + // LKSM/BTC => 0.00018807 + ( + OracleKey::ExchangeRate(ForeignAsset(2)), + FixedU128::from_inner(53_171_691_391_503_161_727_385_600) + ), + // KSM/BTC => 0.00148502 + ( + OracleKey::ExchangeRate(Token(KSM)), + FixedU128::from_inner(6_733_916_041_534_794_629_120_000) + ), + ], + )); + + mine_block(); + + assert_ok!(Tokens::set_balance( + RuntimeOrigin::root(), + 0, + ForeignAsset(2), + 1000000000000, + 0 + )); + + // LKSM/KSM => 0.12664475899314487 + assert_eq!(126644758993, OracleRebase::total_issuance(ForeignAsset(2))); + assert_eq!(126644758993, OracleRebase::total_balance(ForeignAsset(2), &0)); + assert_eq!(126644758993, OracleRebase::free_balance(ForeignAsset(2), &0)); + + assert_ok!(OracleRebase::transfer(ForeignAsset(2), &0, &1, 126644758993)); + }); +} diff --git a/crates/oracle/src/types.rs b/crates/oracle/src/types.rs index 12118151c8..8011ce1140 100644 --- a/crates/oracle/src/types.rs +++ b/crates/oracle/src/types.rs @@ -1,5 +1,11 @@ +use crate::{Config, CurrencyId, Pallet, RebaseTokens}; use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::Contains; +use orml_tokens::ConvertBalance; +use orml_traits::MultiCurrency; use scale_info::TypeInfo; +use sp_runtime::DispatchResult; +use sp_std::marker::PhantomData; pub(crate) type BalanceOf = ::Balance; @@ -11,3 +17,198 @@ pub enum Version { /// Initial version. V0, } + +pub struct RebaseAdapter(PhantomData); + +impl ConvertBalance, BalanceOf> for RebaseAdapter +where + T: Config, +{ + type AssetId = CurrencyId; + + fn convert_balance(amount: BalanceOf, from_asset_id: CurrencyId) -> BalanceOf { + if let Some(to_asset_id) = RebaseTokens::::get(&from_asset_id) { + let amount = Pallet::::collateral_to_wrapped(amount, from_asset_id).unwrap_or_default(); + Pallet::::wrapped_to_collateral(amount, to_asset_id).unwrap_or_default() + } else { + amount + } + } + + fn convert_balance_back(amount: BalanceOf, from_asset_id: CurrencyId) -> BalanceOf { + if let Some(to_asset_id) = RebaseTokens::::get(&from_asset_id) { + let amount = Pallet::::collateral_to_wrapped(amount, to_asset_id).unwrap_or_default(); + Pallet::::wrapped_to_collateral(amount, from_asset_id).unwrap_or_default() + } else { + amount + } + } +} + +pub struct IsRebaseToken(PhantomData); + +impl Contains for IsRebaseToken +where + T: Config, +{ + fn contains(currency_id: &CurrencyId) -> bool { + RebaseTokens::::contains_key(currency_id) + } +} + +pub struct Combiner(PhantomData<(AccountId, TestKey, A, B)>); + +impl MultiCurrency for Combiner +where + TestKey: Contains, + A: MultiCurrency>::Balance>, + B: MultiCurrency, +{ + type CurrencyId = CurrencyId; + type Balance = >::Balance; + + fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { + if TestKey::contains(¤cy_id) { + A::minimum_balance(currency_id) + } else { + B::minimum_balance(currency_id) + } + } + + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { + if TestKey::contains(¤cy_id) { + A::total_issuance(currency_id) + } else { + B::total_issuance(currency_id) + } + } + + fn total_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + if TestKey::contains(¤cy_id) { + A::total_balance(currency_id, who) + } else { + B::total_balance(currency_id, who) + } + } + + fn free_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + if TestKey::contains(¤cy_id) { + A::free_balance(currency_id, who) + } else { + B::free_balance(currency_id, who) + } + } + + fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + if TestKey::contains(¤cy_id) { + A::ensure_can_withdraw(currency_id, who, amount) + } else { + B::ensure_can_withdraw(currency_id, who, amount) + } + } + + fn transfer( + currency_id: Self::CurrencyId, + from: &AccountId, + to: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if TestKey::contains(¤cy_id) { + A::transfer(currency_id, from, to, amount) + } else { + B::transfer(currency_id, from, to, amount) + } + } + + fn deposit(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + if TestKey::contains(¤cy_id) { + A::deposit(currency_id, who, amount) + } else { + B::deposit(currency_id, who, amount) + } + } + + fn withdraw(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + if TestKey::contains(¤cy_id) { + A::withdraw(currency_id, who, amount) + } else { + B::withdraw(currency_id, who, amount) + } + } + + fn can_slash(currency_id: Self::CurrencyId, who: &AccountId, value: Self::Balance) -> bool { + if TestKey::contains(¤cy_id) { + A::can_slash(currency_id, who, value) + } else { + B::can_slash(currency_id, who, value) + } + } + + fn slash(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> Self::Balance { + if TestKey::contains(¤cy_id) { + A::slash(currency_id, who, amount) + } else { + B::slash(currency_id, who, amount) + } + } +} + +pub struct Mapper(PhantomData<(AccountId, C, T)>); + +impl MultiCurrency for Mapper +where + C: ConvertBalance< + >::Balance, + >::Balance, + AssetId = CurrencyId, + >, + T: MultiCurrency, +{ + type CurrencyId = CurrencyId; + type Balance = >::Balance; + + fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { + C::convert_balance(T::minimum_balance(currency_id), currency_id) + } + + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { + C::convert_balance(T::total_issuance(currency_id), currency_id) + } + + fn total_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + C::convert_balance(T::total_balance(currency_id, who), currency_id) + } + + fn free_balance(currency_id: Self::CurrencyId, who: &AccountId) -> Self::Balance { + C::convert_balance(T::free_balance(currency_id, who), currency_id) + } + + fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + T::ensure_can_withdraw(currency_id, who, C::convert_balance_back(amount, currency_id)) + } + + fn transfer( + currency_id: Self::CurrencyId, + from: &AccountId, + to: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + T::transfer(currency_id, from, to, C::convert_balance_back(amount, currency_id)) + } + + fn deposit(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + T::deposit(currency_id, who, C::convert_balance_back(amount, currency_id)) + } + + fn withdraw(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> DispatchResult { + T::withdraw(currency_id, who, C::convert_balance_back(amount, currency_id)) + } + + fn can_slash(currency_id: Self::CurrencyId, who: &AccountId, value: Self::Balance) -> bool { + T::can_slash(currency_id, who, C::convert_balance_back(value, currency_id)) + } + + fn slash(currency_id: Self::CurrencyId, who: &AccountId, amount: Self::Balance) -> Self::Balance { + T::slash(currency_id, who, C::convert_balance_back(amount, currency_id)) + } +}