diff --git a/Cargo.lock b/Cargo.lock index 8a774c7d..a8a8e51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7110,12 +7110,15 @@ dependencies = [ "frame-support", "frame-system", "log", + "orml-currencies", + "orml-tokens", "orml-traits", "pallet-balances", "pallet-xcm", "parachain-info", "parity-scale-codec", "polkadot-parachain", + "primitives", "scale-info", "serde", "sp-core", @@ -8767,11 +8770,15 @@ dependencies = [ name = "primitives" version = "1.0.0" dependencies = [ + "frame-support", + "orml-traits", "parity-scale-codec", "scale-info", "sp-consensus-aura", "sp-core", "sp-runtime", + "sp-std", + "xcm", ] [[package]] diff --git a/pallets/automation-price/src/fees.rs b/pallets/automation-price/src/fees.rs index 4c62774d..630ce46b 100644 --- a/pallets/automation-price/src/fees.rs +++ b/pallets/automation-price/src/fees.rs @@ -101,9 +101,8 @@ where let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location) .ok_or("IncoveribleMultilocation")?; - let currency_id = currency_id.into(); - match T::MultiCurrency::withdraw(currency_id, &self.owner, fee) { + match T::MultiCurrency::withdraw(currency_id.into(), &self.owner, fee) { Ok(_) => { TR::take_revenue(MultiAsset { id: AssetId::Concrete(self.schedule_fee_location), @@ -112,6 +111,7 @@ where if self.execution_fee_amount > MultiBalanceOf::::zero() { T::XcmpTransactor::pay_xcm_fee( + currency_id, self.owner.clone(), self.execution_fee_amount.saturated_into(), )?; diff --git a/pallets/automation-price/src/mock.rs b/pallets/automation-price/src/mock.rs index 865c2d0f..0ca7b693 100644 --- a/pallets/automation-price/src/mock.rs +++ b/pallets/automation-price/src/mock.rs @@ -322,7 +322,11 @@ where Ok(()) } - fn pay_xcm_fee(_: T::AccountId, _: u128) -> Result<(), sp_runtime::DispatchError> { + fn pay_xcm_fee( + _: CurrencyId, + _: T::AccountId, + _: u128, + ) -> Result<(), sp_runtime::DispatchError> { Ok(()) } } diff --git a/pallets/automation-time/src/fees.rs b/pallets/automation-time/src/fees.rs index 7c1ed26b..7ff2e35c 100644 --- a/pallets/automation-time/src/fees.rs +++ b/pallets/automation-time/src/fees.rs @@ -18,6 +18,8 @@ /// ! Traits and default implementation for paying execution fees. use crate::{AccountOf, Action, ActionOf, Config, Error, MultiBalanceOf, Pallet}; +use frame_support::traits::Get; +use frame_system::RawOrigin; use orml_traits::MultiCurrency; use pallet_xcmp_handler::{InstructionSequence, XcmpTransactor}; use sp_runtime::{ @@ -39,11 +41,17 @@ pub trait HandleFees { ) -> Result; } +#[derive(Clone)] +pub struct FeePayment { + pub asset_location: MultiLocation, + pub amount: MultiBalanceOf, + pub is_local: bool, +} + pub struct FeeHandler { owner: T::AccountId, - pub schedule_fee_location: MultiLocation, - pub schedule_fee_amount: MultiBalanceOf, - pub execution_fee_amount: MultiBalanceOf, + pub schedule_fee: FeePayment, + pub execution_fee: Option>, _phantom_data: PhantomData, } @@ -71,59 +79,115 @@ where T: Config, TR: TakeRevenue, { - /// Ensure the fee can be paid. - fn can_pay_fee(&self) -> Result<(), DispatchError> { - let fee = self.schedule_fee_amount.saturating_add(self.execution_fee_amount); - - if fee.is_zero() { + fn ensure_can_withdraw( + &self, + asset_location: MultiLocation, + amount: MultiBalanceOf, + ) -> Result<(), DispatchError> { + if amount.is_zero() { return Ok(()) } - // Manually check for ExistenceRequirement since MultiCurrency doesn't currently support it - let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location) - .ok_or("IncoveribleMultilocation")?; - let currency_id = currency_id.into(); + let currency_id = T::CurrencyIdConvert::convert(asset_location) + .ok_or("IncoveribleMultilocation")? + .into(); let free_balance = T::MultiCurrency::free_balance(currency_id, &self.owner); + let min_balance = T::MultiCurrency::minimum_balance(currency_id); free_balance - .checked_sub(&fee) - .ok_or(DispatchError::Token(BelowMinimum))? - .checked_sub(&T::MultiCurrency::minimum_balance(currency_id)) + .checked_sub(&amount) + .and_then(|balance_minus_fee| balance_minus_fee.checked_sub(&min_balance)) .ok_or(DispatchError::Token(BelowMinimum))?; - T::MultiCurrency::ensure_can_withdraw(currency_id, &self.owner, fee)?; + + T::MultiCurrency::ensure_can_withdraw(currency_id, &self.owner, amount)?; + + Ok(()) + } + + /// Ensure the fee can be paid. + fn can_pay_fee(&self) -> Result<(), DispatchError> { + match &self.execution_fee { + Some(exec_fee) if exec_fee.is_local => { + // If the locations of schedule_fee and execution_fee are equal, + // we need to add the fees to check whether they are sufficient, + // otherwise check them separately. + let exec_fee_location = exec_fee + .asset_location + .reanchored(&T::SelfLocation::get(), T::UniversalLocation::get()) + .map_err(|_| Error::::CannotReanchor)?; + + let schedule_fee_location = self + .schedule_fee + .asset_location + .reanchored(&T::SelfLocation::get(), T::UniversalLocation::get()) + .map_err(|_| Error::::CannotReanchor)?; + + if exec_fee_location == schedule_fee_location { + let fee = self.schedule_fee.amount.saturating_add(exec_fee.amount); + Self::ensure_can_withdraw(self, exec_fee.asset_location, fee)?; + } else { + Self::ensure_can_withdraw( + self, + self.schedule_fee.asset_location, + self.schedule_fee.amount, + )?; + Self::ensure_can_withdraw(self, exec_fee.asset_location, exec_fee.amount)?; + } + }, + _ => { + Self::ensure_can_withdraw( + self, + self.schedule_fee.asset_location, + self.schedule_fee.amount, + )?; + }, + } + Ok(()) } /// Withdraw the fee. fn withdraw_fee(&self) -> Result<(), DispatchError> { - let fee = self.schedule_fee_amount.saturating_add(self.execution_fee_amount); + log::debug!(target: "FeeHandler", "FeeHandler::withdraw_fee, self.schedule_fee.asset_location: {:?}, self.schedule_fee.amount: {:?}", + self.schedule_fee.asset_location, self.schedule_fee.amount); + // Withdraw schedule fee + // When the expected deduction amount, schedule_fee_amount, is not equal to zero, execute the withdrawal process; + // otherwise, there’s no need to deduct. + if !self.schedule_fee.amount.is_zero() { + let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee.asset_location) + .ok_or("InconvertibleMultilocation")?; - if fee.is_zero() { - return Ok(()) - } + T::MultiCurrency::withdraw(currency_id.into(), &self.owner, self.schedule_fee.amount) + .map_err(|_| DispatchError::Token(BelowMinimum))?; - let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location) - .ok_or("IncoveribleMultilocation")?; - let currency_id = currency_id.into(); + TR::take_revenue(MultiAsset { + id: AssetId::Concrete(self.schedule_fee.asset_location), + fun: Fungibility::Fungible(self.schedule_fee.amount.saturated_into()), + }); + } - match T::MultiCurrency::withdraw(currency_id, &self.owner, fee) { - Ok(_) => { - TR::take_revenue(MultiAsset { - id: AssetId::Concrete(self.schedule_fee_location), - fun: Fungibility::Fungible(self.schedule_fee_amount.saturated_into()), - }); + // Withdraw execution fee + if let Some(execution_fee) = &self.execution_fee { + if execution_fee.is_local { + log::debug!(target: "FeeHandler", "FeeHandler::withdraw_fee, self.execution_fee.asset_location: {:?}, self.execution_fee.amount: {:?}", + execution_fee.asset_location, execution_fee.amount); + let currency_id = T::CurrencyIdConvert::convert(execution_fee.asset_location) + .ok_or("InconvertibleMultilocation")?; - if self.execution_fee_amount > MultiBalanceOf::::zero() { + let execution_fee_amount = execution_fee.amount; + // When the expected deduction amount, execution_fee_amount, is not equal to zero, execute the withdrawal process; + // otherwise, there’s no need to deduct. + if !execution_fee_amount.is_zero() { T::XcmpTransactor::pay_xcm_fee( + currency_id, self.owner.clone(), - self.execution_fee_amount.saturated_into(), + execution_fee_amount.saturated_into(), )?; } - - Ok(()) - }, - Err(_) => Err(DispatchError::Token(BelowMinimum)), + } } + + Ok(()) } /// Builds an instance of the struct @@ -137,18 +201,32 @@ where let schedule_fee_amount: u128 = Pallet::::calculate_schedule_fee_amount(action, executions)?.saturated_into(); - let execution_fee_amount = match action.clone() { - Action::XCMP { execution_fee, instruction_sequence, .. } - if instruction_sequence == InstructionSequence::PayThroughSovereignAccount => - execution_fee.amount.saturating_mul(executions.into()).saturated_into(), - _ => 0u32.saturated_into(), + let schedule_fee = FeePayment { + asset_location: schedule_fee_location, + amount: schedule_fee_amount.saturated_into(), + is_local: true, + }; + + let execution_fee = match action.clone() { + Action::XCMP { execution_fee, instruction_sequence, .. } => { + let location = MultiLocation::try_from(execution_fee.asset_location) + .map_err(|()| Error::::BadVersion)?; + let amount = + execution_fee.amount.saturating_mul(executions.into()).saturated_into(); + Some(FeePayment { + asset_location: location, + amount, + is_local: instruction_sequence == + InstructionSequence::PayThroughSovereignAccount, + }) + }, + _ => None, }; Ok(Self { owner: owner.clone(), - schedule_fee_location, - schedule_fee_amount: schedule_fee_amount.saturated_into(), - execution_fee_amount, + schedule_fee, + execution_fee, _phantom_data: Default::default(), }) } @@ -164,7 +242,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{mock::*, Action}; + use crate::{mock::*, Action, AssetPayment, Weight}; use codec::Encode; use frame_benchmarking::frame_support::assert_err; use frame_support::sp_runtime::AccountId32; @@ -178,22 +256,184 @@ mod tests { let call: ::RuntimeCall = frame_system::Call::remark_with_event { remark: vec![50] }.into(); - let mut spy = 0; + let mut has_callback_run = false; let result = ::FeeHandler::pay_checked_fees_for( &alice, &Action::DynamicDispatch { encoded_call: call.encode() }, 1, || { - spy += 1; + has_callback_run = true; Ok("called") }, ); assert_eq!(result.expect("success"), "called"); - assert_eq!(spy, 1); + assert_eq!(has_callback_run, true); assert!(starting_funds > Balances::free_balance(alice)) }) } + #[test] + fn call_pay_checked_fees_for_with_normal_flow_and_enough_execution_fee_success() { + new_test_ext(0).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + let mut has_callback_run = false; + get_multi_xcmp_funds(alice.clone()); + + let action = Action::XCMP { + destination, + schedule_fee: NATIVE_LOCATION, + execution_fee: AssetPayment { asset_location: destination.into(), amount: 10 }, + encoded_call: vec![3, 4, 5], + encoded_call_weight: Weight::from_parts(100_000, 0), + overall_weight: Weight::from_parts(200_000, 0), + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + }; + + let result = ::FeeHandler::pay_checked_fees_for( + &alice, + &action, + 1, + || { + has_callback_run = true; + Ok("called") + }, + ); + assert_eq!(result.expect("success"), "called"); + assert_eq!(has_callback_run, true); + }) + } + + #[test] + fn call_pay_checked_fees_for_with_normal_flow_and_foreign_schedule_fee_success() { + new_test_ext(0).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + let mut has_callback_run = false; + let _ = Currencies::update_balance( + RawOrigin::Root.into(), + alice.clone(), + FOREIGN_CURRENCY_ID, + XmpFee::get() as i64, + ); + fund_account(&alice, 900_000_000, 1, Some(0)); + + let action = Action::XCMP { + destination, + schedule_fee: destination.into(), + execution_fee: AssetPayment { asset_location: NATIVE_LOCATION.into(), amount: 10 }, + encoded_call: vec![3, 4, 5], + encoded_call_weight: Weight::from_parts(100_000, 0), + overall_weight: Weight::from_parts(200_000, 0), + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + }; + + let result = ::FeeHandler::pay_checked_fees_for( + &alice, + &action, + 1, + || { + has_callback_run = true; + Ok("called") + }, + ); + assert_eq!(result.expect("success"), "called"); + assert_eq!(has_callback_run, true); + }) + } + + #[test] + fn call_pay_checked_fees_for_with_normal_flow_and_foreign_schedule_fee_will_throw_insufficent_balance( + ) { + new_test_ext(0).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + fund_account(&alice, 900_000_000, 1, Some(0)); + + let action = Action::XCMP { + destination, + schedule_fee: destination.clone().into(), + execution_fee: AssetPayment { asset_location: NATIVE_LOCATION.into(), amount: 10 }, + encoded_call: vec![3, 4, 5], + encoded_call_weight: Weight::from_parts(100_000, 0), + overall_weight: Weight::from_parts(200_000, 0), + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + }; + + let result = ::FeeHandler::pay_checked_fees_for( + &alice, + &action, + 1, + || Ok(()), + ); + assert_err!(result, Error::::InsufficientBalance); + }) + } + + #[test] + fn call_pay_checked_fees_for_with_normal_flow_and_insufficent_execution_fee_will_fail() { + new_test_ext(0).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + fund_account(&alice, 900_000_000, 1, Some(0)); + + let action = Action::XCMP { + destination, + schedule_fee: NATIVE_LOCATION, + execution_fee: AssetPayment { asset_location: destination.into(), amount: 10 }, + encoded_call: vec![3, 4, 5], + encoded_call_weight: Weight::from_parts(100_000, 0), + overall_weight: Weight::from_parts(200_000, 0), + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + }; + + let result = ::FeeHandler::pay_checked_fees_for( + &alice, + &action, + 1, + || Ok(()), + ); + assert_err!(result, Error::::InsufficientBalance); + }) + } + + #[test] + fn call_pay_checked_fees_for_with_alternate_flow_and_no_execution_fee_success() { + new_test_ext(0).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + let mut has_callback_run = false; + fund_account(&alice, 900_000_000, 1, Some(0)); + + let action = Action::XCMP { + destination, + schedule_fee: NATIVE_LOCATION, + execution_fee: AssetPayment { asset_location: destination.into(), amount: 10 }, + encoded_call: vec![3, 4, 5], + encoded_call_weight: Weight::from_parts(100_000, 0), + overall_weight: Weight::from_parts(200_000, 0), + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }; + + let result = ::FeeHandler::pay_checked_fees_for( + &alice, + &action, + 1, + || { + has_callback_run = true; + Ok("called") + }, + ); + assert_eq!(result.expect("success"), "called"); + assert_eq!(has_callback_run, true); + }) + } + #[test] fn errors_when_not_enough_funds_for_fee() { new_test_ext(0).execute_with(|| { diff --git a/pallets/automation-time/src/lib.rs b/pallets/automation-time/src/lib.rs index 999acf36..356c122f 100644 --- a/pallets/automation-time/src/lib.rs +++ b/pallets/automation-time/src/lib.rs @@ -59,7 +59,7 @@ use frame_support::{ weights::constants::WEIGHT_REF_TIME_PER_SECOND, }; use frame_system::pallet_prelude::*; -use orml_traits::{FixedConversionRateProvider, MultiCurrency}; +use orml_traits::{location::Reserve, FixedConversionRateProvider, MultiCurrency}; use pallet_parachain_staking::DelegatorActions; use pallet_timestamp::{self as timestamp}; pub use pallet_xcmp_handler::InstructionSequence; @@ -189,14 +189,19 @@ pub mod pallet { /// This chain's Universal Location. type UniversalLocation: Get; - //The paraId of this chain. - type SelfParaId: Get; - type TransferCallCreator: primitives::TransferCallCreator< MultiAddress, BalanceOf, ::RuntimeCall, >; + + /// The way to retreave the reserve of a MultiAsset. This can be + /// configured to accept absolute or relative paths for self tokens + type ReserveProvider: Reserve; + + /// Self chain location. + #[pallet::constant] + type SelfLocation: Get; } const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); @@ -267,8 +272,12 @@ pub mod pallet { /// The version of the `VersionedMultiLocation` value used is not able /// to be interpreted. BadVersion, + // The fee payment asset location is not supported. UnsupportedFeePayment, + // Mulilocation cannot be reanchored. CannotReanchor, + /// Invalid asset location. + InvalidAssetLocation, } #[pallet::event] @@ -375,7 +384,8 @@ pub mod pallet { /// * `DuplicateTask`: There can be no duplicate tasks. /// * `TimeTooFarOut`: Execution time or frequency are past the max time horizon. /// * `TimeSlotFull`: Time slot is full. No more tasks can be scheduled for this time. - /// * `UnsupportedFeePayment`: Time slot is full. No more tasks can be scheduled for this time. + /// * `UnsupportedFeePayment`: Unsupported fee payment. + /// * `InvalidAssetLocation` Invalid asset location. #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::schedule_xcmp_task_full(schedule.number_of_executions()))] pub fn schedule_xcmp_task( @@ -393,10 +403,18 @@ pub mod pallet { MultiLocation::try_from(*destination).map_err(|()| Error::::BadVersion)?; let schedule_fee = MultiLocation::try_from(*schedule_fee).map_err(|()| Error::::BadVersion)?; + + let execution_fee: AssetPayment = *execution_fee; + let execution_fee_location = + MultiLocation::try_from(execution_fee.clone().asset_location) + .map_err(|()| Error::::BadVersion)?; + + Self::ensure_supported_execution_fee_location(&execution_fee_location, &destination)?; + let action = Action::XCMP { destination, schedule_fee, - execution_fee: *execution_fee, + execution_fee, encoded_call, encoded_call_weight, overall_weight, @@ -432,7 +450,6 @@ pub mod pallet { /// * `DuplicateTask`: There can be no duplicate tasks. /// * `TimeTooFarOut`: Execution time or frequency are past the max time horizon. /// * `TimeSlotFull`: Time slot is full. No more tasks can be scheduled for this time. - /// * `UnsupportedFeePayment`: Time slot is full. No more tasks can be scheduled for this time. /// * `Other("proxy error: expected `ProxyType::Any`")`: schedule_as must be a proxy account of type "any" for the caller. #[pallet::call_index(3)] #[pallet::weight(::WeightInfo::schedule_xcmp_task_full(schedule.number_of_executions()).saturating_add(T::DbWeight::get().reads(1)))] @@ -1357,21 +1374,12 @@ pub mod pallet { abort_errors: Vec>, ) -> DispatchResult { match action.clone() { - Action::XCMP { execution_fee, instruction_sequence, .. } => { + Action::XCMP { execution_fee, .. } => { let asset_location = MultiLocation::try_from(execution_fee.asset_location) .map_err(|()| Error::::BadVersion)?; - let asset_location = asset_location - .reanchored( - &MultiLocation::new(1, X1(Parachain(T::SelfParaId::get().into()))), - T::UniversalLocation::get(), - ) + let _asset_location = asset_location + .reanchored(&T::SelfLocation::get().into(), T::UniversalLocation::get()) .map_err(|_| Error::::CannotReanchor)?; - // Only native token are supported as the XCMP fee for local deductions - if instruction_sequence == InstructionSequence::PayThroughSovereignAccount && - asset_location != MultiLocation::new(0, Here) - { - Err(Error::::UnsupportedFeePayment)? - } }, _ => (), }; @@ -1530,10 +1538,7 @@ pub mod pallet { let schedule_fee_location = action.schedule_fee_location::(); let schedule_fee_location = schedule_fee_location - .reanchored( - &MultiLocation::new(1, X1(Parachain(T::SelfParaId::get().into()))), - T::UniversalLocation::get(), - ) + .reanchored(&T::SelfLocation::get().into(), T::UniversalLocation::get()) .map_err(|_| Error::::CannotReanchor)?; let fee = if schedule_fee_location == MultiLocation::default() { @@ -1551,6 +1556,24 @@ pub mod pallet { Ok(fee) } + + /// Checks if the execution fee location is supported for scheduling a task + /// + /// if the locations can not be verified, an error such as InvalidAssetLocation or UnsupportedFeePayment will be thrown + pub fn ensure_supported_execution_fee_location( + exeuction_fee_location: &MultiLocation, + destination: &MultiLocation, + ) -> Result<(), DispatchError> { + let exeuction_fee = + MultiAsset { id: Concrete(*exeuction_fee_location), fun: Fungibility::Fungible(0) }; + let reserve = T::ReserveProvider::reserve(&exeuction_fee) + .ok_or(Error::::InvalidAssetLocation)?; + if reserve != MultiLocation::here() && &reserve != destination { + return Err(Error::::UnsupportedFeePayment.into()) + } + + Ok(()) + } } impl pallet_valve::Shutdown for Pallet { diff --git a/pallets/automation-time/src/mock.rs b/pallets/automation-time/src/mock.rs index 16c744bd..3dd50812 100644 --- a/pallets/automation-time/src/mock.rs +++ b/pallets/automation-time/src/mock.rs @@ -28,7 +28,7 @@ use frame_support::{ }; use frame_system::{self as system, EnsureRoot, RawOrigin}; use orml_traits::parameter_type_with_key; -use primitives::{EnsureProxy, TransferCallCreator}; +use primitives::{AbsoluteAndRelativeReserveProvider, EnsureProxy, TransferCallCreator}; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -398,7 +398,11 @@ where Ok(()) } - fn pay_xcm_fee(_: T::AccountId, _: u128) -> Result<(), sp_runtime::DispatchError> { + fn pay_xcm_fee( + _: CurrencyId, + _: T::AccountId, + _: u128, + ) -> Result<(), sp_runtime::DispatchError> { Ok(()) } } @@ -504,6 +508,7 @@ parameter_types! { // The universal location within the global consensus system pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())); + pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainInfo::parachain_id().into()))); } impl pallet_automation_time::Config for Test { @@ -530,8 +535,9 @@ impl pallet_automation_time::Config for Test { type FeeConversionRateProvider = MockConversionRateProvider; type EnsureProxy = MockEnsureProxy; type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; type TransferCallCreator = MockTransferCallCreator; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } // Build genesis storage according to the mock runtime. @@ -715,6 +721,21 @@ pub fn get_xcmp_funds(account: AccountId) { Balances::force_set_balance(RawOrigin::Root.into(), account, with_xcm_fees).unwrap(); } +pub fn get_multi_xcmp_funds(account: AccountId) { + let double_action_weight = MockWeight::::run_xcmp_task() * 2; + let action_fee = ExecutionWeightFee::get() * u128::from(double_action_weight.ref_time()); + let max_execution_fee = action_fee * u128::from(MaxExecutionTimes::get()); + Balances::force_set_balance(RawOrigin::Root.into(), account.clone(), max_execution_fee) + .unwrap(); + Currencies::update_balance( + RawOrigin::Root.into(), + account, + FOREIGN_CURRENCY_ID, + XmpFee::get() as i64, + ) + .unwrap(); +} + // TODO: swap above to this pattern pub fn fund_account_dynamic_dispatch( account: &AccountId, @@ -742,10 +763,7 @@ pub fn fund_account( pub fn get_fee_per_second(location: &MultiLocation) -> Option { let location = location - .reanchored( - &MultiLocation::new(1, X1(Parachain(::SelfParaId::get().into()))), - ::UniversalLocation::get(), - ) + .reanchored(&SelfLocation::get(), ::UniversalLocation::get()) .expect("Reanchor location failed"); let found_asset = ASSET_FEE_PER_SECOND.into_iter().find(|item| match item { diff --git a/pallets/automation-time/src/tests.rs b/pallets/automation-time/src/tests.rs index db02bf43..3e707634 100644 --- a/pallets/automation-time/src/tests.rs +++ b/pallets/automation-time/src/tests.rs @@ -30,7 +30,6 @@ use frame_support::{ }; use frame_system::{self, RawOrigin}; use rand::Rng; -use sp_core::Get; use sp_runtime::{ traits::{BlakeTwo256, Hash}, AccountId32, @@ -117,10 +116,7 @@ fn calculate_expected_xcmp_action_schedule_fee( num_of_execution: u32, ) -> u128 { let schedule_fee_location = schedule_fee_location - .reanchored( - &MultiLocation::new(1, X1(Parachain(::SelfParaId::get().into()))), - ::UniversalLocation::get(), - ) + .reanchored(&SelfLocation::get(), ::UniversalLocation::get()) .expect("Location reanchor failed"); let weight = ::WeightInfo::run_xcmp_task(); @@ -859,10 +855,7 @@ fn calculate_xcmp_action_schedule_fee_amount_with_absolute_or_relative_native_sc let num_of_execution = generate_random_num(1, 20); let action_absolute = create_xcmp_action(XcmpActionParams { - schedule_fee: MultiLocation::new( - 1, - X1(Parachain(::SelfParaId::get().into())), - ), + schedule_fee: SelfLocation::get(), ..XcmpActionParams::default() }); let fee_amount_abosolute = @@ -1036,6 +1029,56 @@ fn schedule_xcmp_works() { }) } +#[test] +fn schedule_xcmp_works_with_multi_currency() { + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + // Funds including XCM fees + get_multi_xcmp_funds(alice.clone()); + + assert_ok!(AutomationTime::schedule_xcmp_task( + RuntimeOrigin::signed(alice), + ScheduleParam::Fixed { execution_times: vec![SCHEDULED_TIME] }, + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { asset_location: destination.into(), amount: 10 }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0), + )); + }) +} + +#[test] +fn schedule_xcmp_works_with_unsupported_currency_will_fail() { + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let alice = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + // Funds including XCM fees + get_multi_xcmp_funds(alice.clone()); + + assert_noop!( + AutomationTime::schedule_xcmp_task( + RuntimeOrigin::signed(alice), + ScheduleParam::Fixed { execution_times: vec![SCHEDULED_TIME] }, + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: MultiLocation::new(1, X1(Parachain(3000))).into(), + amount: 10 + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0), + ), + Error::::UnsupportedFeePayment, + ); + }) +} + #[test] fn schedule_xcmp_through_proxy_works() { new_test_ext(START_BLOCK_TIME).execute_with(|| { @@ -1052,10 +1095,7 @@ fn schedule_xcmp_through_proxy_works() { ScheduleParam::Fixed { execution_times: vec![SCHEDULED_TIME] }, Box::new(destination.into()), Box::new(MultiLocation::default().into()), - Box::new(AssetPayment { - asset_location: destination.into(), - amount: 10, - }), + Box::new(AssetPayment { asset_location: destination.into(), amount: 10 }), call, Weight::from_parts(100_000, 0), Weight::from_parts(200_000, 0), diff --git a/pallets/xcmp-handler/Cargo.toml b/pallets/xcmp-handler/Cargo.toml index db61aff0..dded1b2a 100644 --- a/pallets/xcmp-handler/Cargo.toml +++ b/pallets/xcmp-handler/Cargo.toml @@ -42,6 +42,8 @@ xcm-executor = { git = "https://github.com/paritytech/polkadot", default-feature # ORML orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } +orml-currencies = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } +orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } [dev-dependencies] # Substrate @@ -58,6 +60,8 @@ pallet-xcm = { git = 'https://github.com/paritytech/polkadot', default-features xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.43" } xcm-executor = { git = 'https://github.com/paritytech/polkadot', default-features = false, branch = "release-v0.9.43" } +primitives = { path = "../../primitives", default-features = false } + [features] default = ["std"] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] diff --git a/pallets/xcmp-handler/src/lib.rs b/pallets/xcmp-handler/src/lib.rs index c1d4a298..d72ae367 100644 --- a/pallets/xcmp-handler/src/lib.rs +++ b/pallets/xcmp-handler/src/lib.rs @@ -46,14 +46,18 @@ use xcm::latest::prelude::*; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::traits::Currency; + use orml_traits::{location::Reserve, MultiCurrency}; use polkadot_parachain::primitives::Sibling; - use sp_runtime::traits::{AccountIdConversion, Convert, SaturatedConversion}; + use sp_runtime::{ + traits::{AccountIdConversion, CheckedSub, Convert, SaturatedConversion}, + TokenError::BelowMinimum, + }; use sp_std::prelude::*; use xcm_executor::traits::WeightBounds; - pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; + pub type MultiCurrencyId = <::MultiCurrency as MultiCurrency< + ::AccountId, + >>::CurrencyId; #[pallet::config] pub trait Config: frame_system::Config { @@ -61,8 +65,8 @@ pub mod pallet { type RuntimeCall: From> + Encode; - /// The Currency type for interacting with balances - type Currency: Currency; + /// The MultiCurrency type for interacting with balances + type MultiCurrency: MultiCurrency; /// The currencyIds that our chain supports. type CurrencyId: Parameter @@ -71,7 +75,9 @@ pub mod pallet { + MaybeSerializeDeserialize + Ord + TypeInfo - + MaxEncodedLen; + + MaxEncodedLen + + From> + + Into>; /// The currencyId for the native currency. #[pallet::constant] @@ -97,6 +103,14 @@ pub mod pallet { /// Utility for determining XCM instruction weights. type Weigher: WeightBounds<::RuntimeCall>; + + /// The way to retreave the reserve of a MultiAsset. This can be + /// configured to accept absolute or relative paths for self tokens + type ReserveProvider: Reserve; + + /// Self chain location. + #[pallet::constant] + type SelfLocation: Get; } const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); @@ -155,6 +169,10 @@ pub mod pallet { BadVersion, // Asset not found TransactInfoNotFound, + // Invalid asset location. + InvalidAssetLocation, + // The fee payment asset location is not supported. + UnsupportedFeePayment, } #[pallet::call] @@ -210,6 +228,32 @@ pub mod pallet { Ok(instructions) } + fn get_local_xcm( + asset: MultiAsset, + destination: MultiLocation, + ) -> Result::RuntimeCall>, DispatchError> { + let reserve = + T::ReserveProvider::reserve(&asset).ok_or(Error::::InvalidAssetLocation)?; + let local_xcm = if reserve == MultiLocation::here() { + Xcm(vec![ + WithdrawAsset::<::RuntimeCall>(asset.into()), + DepositAsset::<::RuntimeCall> { + assets: Wild(All), + beneficiary: destination, + }, + ]) + } else if reserve == destination { + Xcm(vec![ + WithdrawAsset::<::RuntimeCall>(asset.clone().into()), + BurnAsset::<::RuntimeCall>(asset.into()), + ]) + } else { + return Err(Error::::UnsupportedFeePayment.into()) + }; + + Ok(local_xcm) + } + /// Construct the instructions for a transact xcm with our local currency. /// /// Local instructions @@ -235,41 +279,71 @@ pub mod pallet { (xcm::latest::Xcm<::RuntimeCall>, xcm::latest::Xcm<()>), DispatchError, > { - // XCM for local chain let local_asset = MultiAsset { id: Concrete(asset_location), fun: Fungibility::Fungible(fee) }; - let local_xcm = Xcm(vec![ - WithdrawAsset::<::RuntimeCall>(local_asset.clone().into()), - DepositAsset::<::RuntimeCall> { - assets: Wild(All), - beneficiary: destination, - }, - ]); - - // XCM for target chain let target_asset = local_asset + .clone() .reanchored(&destination, T::UniversalLocation::get()) .map_err(|_| Error::::CannotReanchor)?; - let target_xcm = Xcm(vec![ - ReserveAssetDeposited::<()>(target_asset.clone().into()), - BuyExecution::<()> { fees: target_asset, weight_limit: Limited(overall_weight) }, - DescendOrigin::<()>(descend_location), - Transact::<()> { - origin_kind: OriginKind::SovereignAccount, - require_weight_at_most: transact_encoded_call_weight, - call: transact_encoded_call.into(), - }, - RefundSurplus::<()>, - DepositAsset::<()> { - assets: Wild(AllCounted(1)), - beneficiary: MultiLocation { - parents: 1, - interior: X1(Parachain(T::SelfParaId::get().into())), + let reserve = T::ReserveProvider::reserve(&local_asset) + .ok_or(Error::::InvalidAssetLocation)?; + + let (local_xcm, target_xcm) = if reserve == MultiLocation::here() { + let local_xcm = Xcm(vec![ + WithdrawAsset::<::RuntimeCall>(local_asset.into()), + DepositAsset::<::RuntimeCall> { + assets: Wild(All), + beneficiary: destination, }, - }, - ]); + ]); + let target_xcm = Xcm(vec![ + ReserveAssetDeposited::<()>(target_asset.clone().into()), + BuyExecution::<()> { + fees: target_asset, + weight_limit: Limited(overall_weight), + }, + DescendOrigin::<()>(descend_location), + Transact::<()> { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: transact_encoded_call_weight, + call: transact_encoded_call.into(), + }, + RefundSurplus::<()>, + DepositAsset::<()> { + assets: Wild(AllCounted(1)), + beneficiary: T::SelfLocation::get(), + }, + ]); + (local_xcm, target_xcm) + } else if reserve == destination { + let local_xcm = Xcm(vec![ + WithdrawAsset::<::RuntimeCall>(local_asset.clone().into()), + BurnAsset::<::RuntimeCall>(local_asset.into()), + ]); + let target_xcm = Xcm(vec![ + WithdrawAsset::<()>(target_asset.clone().into()), + BuyExecution::<()> { + fees: target_asset, + weight_limit: Limited(overall_weight), + }, + DescendOrigin::<()>(descend_location), + Transact::<()> { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: transact_encoded_call_weight, + call: transact_encoded_call.into(), + }, + RefundSurplus::<()>, + DepositAsset::<()> { + assets: Wild(AllCounted(1)), + beneficiary: T::SelfLocation::get(), + }, + ]); + (local_xcm, target_xcm) + } else { + return Err(Error::::UnsupportedFeePayment.into()) + }; Ok((local_xcm, target_xcm)) } @@ -322,8 +396,7 @@ pub mod pallet { pub fn transact_in_local_chain( internal_instructions: xcm::latest::Xcm<::RuntimeCall>, ) -> Result<(), DispatchError> { - let local_sovereign_account = - MultiLocation::new(1, X1(Parachain(T::SelfParaId::get().into()))); + let local_sovereign_account = T::SelfLocation::get(); let weight = T::Weigher::weight(&mut internal_instructions.clone().into()) .map_err(|_| Error::::ErrorGettingCallWeight)?; let hash = internal_instructions.using_encoded(sp_io::hashing::blake2_256); @@ -405,18 +478,46 @@ pub mod pallet { Ok(()) } + fn do_pay_xcm_fee( + currency_id: T::CurrencyId, + source: T::AccountId, + dest: T::AccountId, + fee: u128, + ) -> Result<(), DispatchError> { + let free_balance = T::MultiCurrency::free_balance(currency_id.into(), &source); + let min_balance = T::MultiCurrency::minimum_balance(currency_id.into()); + + free_balance + .checked_sub(&fee.saturated_into()) + .and_then(|balance_minus_fee| balance_minus_fee.checked_sub(&min_balance)) + .ok_or(DispatchError::Token(BelowMinimum))?; + + T::MultiCurrency::ensure_can_withdraw( + currency_id.into(), + &source, + fee.saturated_into(), + )?; + + T::MultiCurrency::transfer(currency_id.into(), &source, &dest, fee.saturated_into())?; + + Ok(()) + } + /// Pay for XCMP fees. /// Transfers fee from payer account to the local chain sovereign account. /// - pub fn pay_xcm_fee(source: T::AccountId, fee: u128) -> Result<(), DispatchError> { - let local_sovereign_account = + pub fn pay_xcm_fee( + currency_id: T::CurrencyId, + source: T::AccountId, + fee: u128, + ) -> Result<(), DispatchError> { + let local_sovereign_account: T::AccountId = Sibling::from(T::SelfParaId::get()).into_account_truncating(); - - match T::Currency::transfer( - &source, - &local_sovereign_account, - >::saturated_from(fee), - frame_support::traits::ExistenceRequirement::KeepAlive, + match Self::do_pay_xcm_fee( + currency_id, + source.clone(), + local_sovereign_account.clone(), + fee, ) { Ok(_number) => Self::deposit_event(Event::XcmFeesPaid { source, @@ -446,7 +547,11 @@ pub trait XcmpTransactor { flow: InstructionSequence, ) -> Result<(), sp_runtime::DispatchError>; - fn pay_xcm_fee(source: AccountId, fee: u128) -> Result<(), sp_runtime::DispatchError>; + fn pay_xcm_fee( + currency_id: CurrencyId, + source: AccountId, + fee: u128, + ) -> Result<(), sp_runtime::DispatchError>; } impl XcmpTransactor for Pallet { @@ -474,8 +579,12 @@ impl XcmpTransactor for Pallet { Ok(()) } - fn pay_xcm_fee(source: T::AccountId, fee: u128) -> Result<(), sp_runtime::DispatchError> { - Self::pay_xcm_fee(source, fee)?; + fn pay_xcm_fee( + currency_id: T::CurrencyId, + source: T::AccountId, + fee: u128, + ) -> Result<(), sp_runtime::DispatchError> { + Self::pay_xcm_fee(currency_id, source, fee)?; Ok(()) } diff --git a/pallets/xcmp-handler/src/mock.rs b/pallets/xcmp-handler/src/mock.rs index b7402403..7327d646 100644 --- a/pallets/xcmp-handler/src/mock.rs +++ b/pallets/xcmp-handler/src/mock.rs @@ -41,6 +41,10 @@ use xcm_executor::{ Assets, XcmExecutor, }; +use orml_traits::parameter_type_with_key; + +use primitives::AbsoluteAndRelativeReserveProvider; + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; pub type AccountId = AccountId32; @@ -63,6 +67,8 @@ frame_support::construct_runtime!( XcmpHandler: pallet_xcmp_handler::{Pallet, Call, Storage, Event}, XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin}, CumulusXcm: cumulus_pallet_xcm::{Pallet, Call, Event, Origin}, + Currencies: orml_currencies::{Pallet, Call}, + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, } ); @@ -125,6 +131,34 @@ impl pallet_balances::Config for Test { impl parachain_info::Config for Test {} +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = i64; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = ConstU32<100_000>; + type MaxReserves = ConstU32<100_000>; + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = frame_support::traits::Nothing; +} + +impl orml_currencies::Config for Test { + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); +} +pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; + pub struct AccountIdToMultiLocation; impl Convert for AccountIdToMultiLocation { fn convert(account: AccountId) -> MultiLocation { @@ -306,13 +340,14 @@ impl Convert> for TokenIdConvert { parameter_types! { pub const GetNativeCurrencyId: CurrencyId = NATIVE; pub Ancestry: MultiLocation = Parachain(ParachainInfo::parachain_id().into()).into(); + pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainInfo::parachain_id().into()))); } impl pallet_xcmp_handler::Config for Test { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; type CurrencyId = CurrencyId; - type Currency = Balances; + type MultiCurrency = Currencies; type GetNativeCurrencyId = GetNativeCurrencyId; type SelfParaId = parachain_info::Pallet; type AccountIdToMultiLocation = AccountIdToMultiLocation; @@ -321,6 +356,8 @@ impl pallet_xcmp_handler::Config for Test { type XcmExecutor = XcmExecutor; type XcmSender = TestSendXcm; type Weigher = FixedWeightBounds; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } // Build genesis storage according to the mock runtime. diff --git a/pallets/xcmp-handler/src/tests.rs b/pallets/xcmp-handler/src/tests.rs index c0ffe3b2..26fa5130 100644 --- a/pallets/xcmp-handler/src/tests.rs +++ b/pallets/xcmp-handler/src/tests.rs @@ -14,8 +14,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crate::{mock::*, InstructionSequence}; -use frame_support::assert_ok; +use crate::{mock::*, Error, InstructionSequence}; +use frame_support::{assert_noop, assert_ok}; use frame_system::RawOrigin; use polkadot_parachain::primitives::Sibling; use sp_runtime::traits::{AccountIdConversion, Convert}; @@ -138,8 +138,6 @@ fn transact_in_local_chain_works() { asset.clone(), MultiLocation { parents: 1, interior: X1(Parachain(LOCAL_PARA_ID)) } ), - // Depositing asset - (asset, MultiLocation { parents: 1, interior: X1(Parachain(PARA_ID)) }), ] ); assert_eq!(events(), [RuntimeEvent::XcmpHandler(crate::Event::XcmTransactedLocally)]); @@ -211,6 +209,94 @@ fn transact_in_target_chain_works() { }); } +#[test] +fn transact_in_target_chain_with_to_reserved_currency_works() { + new_test_ext().execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let asset_location = MultiLocation { parents: 1, interior: X1(Parachain(PARA_ID)) }; + let transact_encoded_call: Vec = vec![0, 1, 2]; + let transact_encoded_call_weight = Weight::from_parts(100_000_000, 0); + let xcm_weight = transact_encoded_call_weight + .checked_add(&Weight::from_parts(100_000_000, 0)) + .expect("xcm_weight overflow"); + let xcm_fee = (xcm_weight.ref_time() as u128) * 5_000_000_000; + let target_asset = MultiAsset { + id: Concrete(MultiLocation { parents: 0, interior: Here }), + fun: Fungible(xcm_fee), + }; + let descend_location: Junctions = + AccountIdToMultiLocation::convert(ALICE).try_into().unwrap(); + + let (_, target_instructions) = XcmpHandler::get_local_currency_instructions( + destination, + asset_location, + descend_location, + transact_encoded_call.clone(), + transact_encoded_call_weight, + xcm_weight, + xcm_fee, + ) + .unwrap(); + + assert_ok!(XcmpHandler::transact_in_target_chain(destination, target_instructions)); + assert_eq!( + sent_xcm(), + vec![( + MultiLocation { parents: 1, interior: X1(Parachain(PARA_ID)) }, + Xcm([ + WithdrawAsset(target_asset.clone().into()), + BuyExecution { fees: target_asset, weight_limit: Limited(xcm_weight) }, + DescendOrigin(X1(AccountId32 { network: None, id: ALICE.into() }),), + Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: transact_encoded_call_weight, + call: transact_encoded_call.into(), + }, + RefundSurplus, + DepositAsset { + assets: Wild(AllCounted(1)), + beneficiary: MultiLocation { + parents: 1, + interior: X1(Parachain(LOCAL_PARA_ID)), + }, + }, + ] + .to_vec()), + )] + ); + assert_eq!(events(), [RuntimeEvent::XcmpHandler(crate::Event::XcmSent { destination })]); + }); +} + +#[test] +fn transact_in_target_chain_with_non_reserved_currency_will_throw_unsupported_fee_payment_error() { + new_test_ext().execute_with(|| { + let destination = MultiLocation::new(1, X1(Parachain(PARA_ID))); + let asset_location = MultiLocation { parents: 1, interior: X1(Parachain(3000)) }; + let transact_encoded_call: Vec = vec![0, 1, 2]; + let transact_encoded_call_weight = Weight::from_parts(100_000_000, 0); + let xcm_weight = transact_encoded_call_weight + .checked_add(&Weight::from_parts(100_000_000, 0)) + .expect("xcm_weight overflow"); + let xcm_fee = (xcm_weight.ref_time() as u128) * 5_000_000_000; + let descend_location: Junctions = + AccountIdToMultiLocation::convert(ALICE).try_into().unwrap(); + + assert_noop!( + XcmpHandler::get_local_currency_instructions( + destination, + asset_location, + descend_location, + transact_encoded_call.clone(), + transact_encoded_call_weight, + xcm_weight, + xcm_fee, + ), + Error::::UnsupportedFeePayment, + ); + }); +} + #[test] fn pay_xcm_fee_works() { new_test_ext().execute_with(|| { @@ -218,10 +304,11 @@ fn pay_xcm_fee_works() { Sibling::from(LOCAL_PARA_ID).into_account_truncating(); let fee = 3_500_000; let alice_balance = 8_000_000; + let currency_id = 0; Balances::force_set_balance(RawOrigin::Root.into(), ALICE, alice_balance).unwrap(); - assert_ok!(XcmpHandler::pay_xcm_fee(ALICE, fee)); + assert_ok!(XcmpHandler::pay_xcm_fee(currency_id, ALICE, fee)); assert_eq!(Balances::free_balance(ALICE), alice_balance - fee); assert_eq!(Balances::free_balance(local_sovereign_account), fee); }); @@ -234,10 +321,11 @@ fn pay_xcm_fee_keeps_wallet_alive() { Sibling::from(LOCAL_PARA_ID).into_account_truncating(); let fee = 3_500_000; let alice_balance = fee; + let currency_id = 0; Balances::force_set_balance(RawOrigin::Root.into(), ALICE, alice_balance).unwrap(); - assert_ok!(XcmpHandler::pay_xcm_fee(ALICE, fee)); + assert_ok!(XcmpHandler::pay_xcm_fee(currency_id, ALICE, fee)); assert_eq!(Balances::free_balance(ALICE), alice_balance); assert_eq!(Balances::free_balance(local_sovereign_account), 0); }); diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 81cdbbec..6df79612 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -15,10 +15,23 @@ codec = { package = "parity-scale-codec", version = "3.0.0", features = [ scale-info = { version = "2.1", default-features = false, features = [ "derive", ] } + +# Substrate Dependencies +## Substrate Primitive Dependencies +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } sp-consensus-aura = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } +## Substrate FRAME Dependencies +frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.43" } + +## Polkdadot deps +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.43", default-features = false } + +## ORML deps +orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } + [features] default = ["std"] std = [ diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 032723eb..fee2c132 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -22,6 +22,12 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentifyAccount, Verify}, MultiAddress, MultiSignature, }; +use sp_std::marker::PhantomData; + +use frame_support::traits::Get; + +use orml_traits::location::{RelativeReserveProvider, Reserve}; +use xcm::latest::prelude::*; pub mod assets; @@ -71,3 +77,20 @@ pub trait EnsureProxy { pub trait TransferCallCreator { fn create_transfer_call(dest: AccountId, value: Balance) -> RuntimeCall; } + +/// `MultiAsset` reserve location provider. It's based on `RelativeReserveProvider` and in +/// addition will convert self absolute location to relative location. +pub struct AbsoluteAndRelativeReserveProvider(PhantomData); +impl> Reserve + for AbsoluteAndRelativeReserveProvider +{ + fn reserve(asset: &MultiAsset) -> Option { + RelativeReserveProvider::reserve(asset).map(|reserve_location| { + if reserve_location == AbsoluteLocation::get() { + MultiLocation::here() + } else { + reserve_location + } + }) + } +} diff --git a/runtime/neumann/src/lib.rs b/runtime/neumann/src/lib.rs index e1839626..2d03e756 100644 --- a/runtime/neumann/src/lib.rs +++ b/runtime/neumann/src/lib.rs @@ -81,7 +81,7 @@ use polkadot_runtime_common::BlockHashCount; // XCM configurations. pub mod xcm_config; -use xcm_config::{FeePerSecondProvider, ToTreasury, TokenIdConvert}; +use xcm_config::{FeePerSecondProvider, SelfLocation, ToTreasury, TokenIdConvert}; pub mod weights; @@ -100,8 +100,8 @@ use common_runtime::{ CurrencyHooks, }; use primitives::{ - AccountId, Address, Amount, AuraId, Balance, BlockNumber, EnsureProxy, Hash, Header, Index, - Signature, TransferCallCreator, + AbsoluteAndRelativeReserveProvider, AccountId, Address, Amount, AuraId, Balance, BlockNumber, + EnsureProxy, Hash, Header, Index, Signature, TransferCallCreator, }; // Custom pallet imports @@ -919,8 +919,9 @@ impl pallet_automation_time::Config for Runtime { type ScheduleAllowList = ScheduleAllowList; type EnsureProxy = AutomationEnsureProxy; type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; type TransferCallCreator = MigrationTransferCallCreator; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } impl pallet_automation_price::Config for Runtime { @@ -1213,9 +1214,10 @@ impl_runtime_apis! { let fee_handler = ::FeeHandler::new(&nobody, &action, executions) .map_err(|_| "Unable to parse fee".as_bytes())?; + let execution_fee = fee_handler.execution_fee.map(|fee| fee.amount).unwrap_or(0); Ok(AutomationFeeDetails { - schedule_fee: fee_handler.schedule_fee_amount, - execution_fee: fee_handler.execution_fee_amount + schedule_fee: fee_handler.schedule_fee.amount, + execution_fee, }) } diff --git a/runtime/neumann/src/xcm_config.rs b/runtime/neumann/src/xcm_config.rs index e249a2b0..ef751ed1 100644 --- a/runtime/neumann/src/xcm_config.rs +++ b/runtime/neumann/src/xcm_config.rs @@ -36,6 +36,8 @@ use orml_xcm_support::{ DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset, }; +use primitives::AbsoluteAndRelativeReserveProvider; + parameter_types! { pub const RelayLocation: MultiLocation = MultiLocation::parent(); pub const RelayNetwork: NetworkId = NetworkId::Rococo; @@ -301,7 +303,7 @@ impl pallet_xcmp_handler::Config for Runtime { type RuntimeCall = RuntimeCall; type CurrencyId = TokenId; type GetNativeCurrencyId = GetNativeCurrencyId; - type Currency = pallet_balances::Pallet; + type MultiCurrency = Currencies; type SelfParaId = parachain_info::Pallet; type AccountIdToMultiLocation = AccountIdToMultiLocation; type CurrencyIdToMultiLocation = TokenIdConvert; @@ -309,6 +311,8 @@ impl pallet_xcmp_handler::Config for Runtime { type XcmSender = XcmRouter; type XcmExecutor = XcmExecutor; type Weigher = FixedWeightBounds; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } pub struct TokenIdConvert; diff --git a/runtime/oak/src/lib.rs b/runtime/oak/src/lib.rs index abbdaa3a..ba14a995 100644 --- a/runtime/oak/src/lib.rs +++ b/runtime/oak/src/lib.rs @@ -32,7 +32,7 @@ use pallet_automation_time_rpc_runtime_api::{ // to their standalone Fee RPC that can handle both use pallet_automation_price_rpc_runtime_api::FeeDetails as AutomationPriceFeeDetails; -use primitives::{assets::CustomMetadata, TokenId}; +use primitives::{assets::CustomMetadata, AbsoluteAndRelativeReserveProvider, TokenId}; use scale_info::prelude::format; use sp_api::impl_runtime_apis; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; @@ -87,7 +87,7 @@ use polkadot_runtime_common::BlockHashCount; // XCM configurations. pub mod xcm_config; -use xcm_config::{FeePerSecondProvider, ToTreasury, TokenIdConvert}; +use xcm_config::{FeePerSecondProvider, SelfLocation, ToTreasury, TokenIdConvert}; pub mod weights; @@ -944,8 +944,9 @@ impl pallet_automation_time::Config for Runtime { type ScheduleAllowList = ScheduleAllowList; type EnsureProxy = AutomationEnsureProxy; type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; type TransferCallCreator = MigrationTransferCallCreator; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } impl pallet_automation_price::Config for Runtime { @@ -1242,9 +1243,10 @@ impl_runtime_apis! { let fee_handler = ::FeeHandler::new(&nobody, &action, executions) .map_err(|_| "Unable to parse fee".as_bytes())?; + let execution_fee = fee_handler.execution_fee.map(|fee| fee.amount).unwrap_or(0); Ok(AutomationFeeDetails { - schedule_fee: fee_handler.schedule_fee_amount, - execution_fee: fee_handler.execution_fee_amount + schedule_fee: fee_handler.schedule_fee.amount, + execution_fee, }) } diff --git a/runtime/oak/src/xcm_config.rs b/runtime/oak/src/xcm_config.rs index 6dfdd2a3..34c0bef7 100644 --- a/runtime/oak/src/xcm_config.rs +++ b/runtime/oak/src/xcm_config.rs @@ -38,6 +38,8 @@ use orml_xcm_support::{ DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset, }; +use primitives::AbsoluteAndRelativeReserveProvider; + parameter_types! { pub const RelayLocation: MultiLocation = MultiLocation::parent(); pub const RelayNetwork: NetworkId = NetworkId::Polkadot; @@ -303,7 +305,7 @@ parameter_types! { impl pallet_xcmp_handler::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; - type Currency = pallet_balances::Pallet; + type MultiCurrency = Currencies; type CurrencyId = TokenId; type GetNativeCurrencyId = GetNativeCurrencyId; type SelfParaId = parachain_info::Pallet; @@ -313,6 +315,8 @@ impl pallet_xcmp_handler::Config for Runtime { type XcmSender = XcmRouter; type XcmExecutor = XcmExecutor; type Weigher = FixedWeightBounds; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } pub struct TokenIdConvert; diff --git a/runtime/turing/src/lib.rs b/runtime/turing/src/lib.rs index 7224e8c0..226afe83 100644 --- a/runtime/turing/src/lib.rs +++ b/runtime/turing/src/lib.rs @@ -31,7 +31,7 @@ use pallet_automation_price_rpc_runtime_api::FeeDetails as AutomationPriceFeeDet use pallet_automation_time_rpc_runtime_api::{ AutomationAction, AutostakingResult, FeeDetails as AutomationFeeDetails, }; -use primitives::{assets::CustomMetadata, TokenId}; +use primitives::{assets::CustomMetadata, AbsoluteAndRelativeReserveProvider, TokenId}; use scale_info::prelude::format; use sp_api::impl_runtime_apis; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; @@ -84,7 +84,7 @@ use polkadot_runtime_common::BlockHashCount; // XCM configurations. pub mod xcm_config; -use xcm_config::{FeePerSecondProvider, ToTreasury, TokenIdConvert}; +use xcm_config::{FeePerSecondProvider, SelfLocation, ToTreasury, TokenIdConvert}; pub mod weights; @@ -1061,8 +1061,9 @@ impl pallet_automation_time::Config for Runtime { type ScheduleAllowList = ScheduleAllowList; type EnsureProxy = AutomationEnsureProxy; type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; type TransferCallCreator = MigrationTransferCallCreator; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } impl pallet_automation_price::Config for Runtime { @@ -1358,9 +1359,10 @@ impl_runtime_apis! { let fee_handler = ::FeeHandler::new(&nobody, &action, executions) .map_err(|_| "Unable to parse fee".as_bytes())?; + let execution_fee = fee_handler.execution_fee.map(|fee| fee.amount).unwrap_or(0); Ok(AutomationFeeDetails { - schedule_fee: fee_handler.schedule_fee_amount, - execution_fee: fee_handler.execution_fee_amount + schedule_fee: fee_handler.schedule_fee.amount, + execution_fee, }) } diff --git a/runtime/turing/src/xcm_config.rs b/runtime/turing/src/xcm_config.rs index 7ad640c3..a0915481 100644 --- a/runtime/turing/src/xcm_config.rs +++ b/runtime/turing/src/xcm_config.rs @@ -37,6 +37,8 @@ use orml_xcm_support::{ DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset, }; +use primitives::AbsoluteAndRelativeReserveProvider; + parameter_types! { pub const RelayLocation: MultiLocation = MultiLocation::parent(); pub const RelayNetwork: NetworkId = NetworkId::Kusama; @@ -303,7 +305,7 @@ parameter_types! { impl pallet_xcmp_handler::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; - type Currency = pallet_balances::Pallet; + type MultiCurrency = Currencies; type CurrencyId = TokenId; type GetNativeCurrencyId = GetNativeCurrencyId; type SelfParaId = parachain_info::Pallet; @@ -313,6 +315,8 @@ impl pallet_xcmp_handler::Config for Runtime { type XcmSender = XcmRouter; type XcmExecutor = XcmExecutor; type Weigher = FixedWeightBounds; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; + type SelfLocation = SelfLocation; } pub struct TokenIdConvert;