diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5e8a2068..e77f40df84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Asset Hubs: added an AssetExchanger to be able to swap tokens using the xcm executor, even for delivery fees ([polkadot-fellows/runtimes#539](https://github.com/polkadot-fellows/runtimes/pull/539)). - Location conversion tests for relays and parachains ([polkadot-fellows/runtimes#487](https://github.com/polkadot-fellows/runtimes/pull/487)) - Asset Hubs: XcmPaymentApi now returns all assets in a pool with the native token as acceptable as fee payment ([polkadot-fellows/runtimes#523](https://github.com/polkadot-fellows/runtimes/pull/523)) diff --git a/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/hybrid_transfers.rs b/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/hybrid_transfers.rs index 125883cbe8..30926bb422 100644 --- a/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/hybrid_transfers.rs +++ b/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/hybrid_transfers.rs @@ -15,10 +15,12 @@ use super::reserve_transfer::*; use crate::{ + foreign_balance_on, tests::teleport::do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using_xt, *, }; use asset_hub_kusama_runtime::xcm_config::KsmLocation; +use emulated_integration_tests_common::USDT_ID; fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) { type RuntimeEvent = ::RuntimeEvent; @@ -788,3 +790,180 @@ fn transfer_native_asset_from_relay_to_para_through_asset_hub() { // should be non-zero assert!(receiver_assets_after < receiver_assets_before + amount_to_send); } + +// We transfer USDT from PenpalA to PenpalB through Asset Hub. +// The sender on PenpalA pays delivery fees in KSM. +// When the message arrives to Asset Hub, execution and delivery fees are paid in USDT +// swapping for KSM automatically. +// When it arrives to PenpalB, execution fees are paid with USDT by swapping for KSM. +#[test] +fn usdt_only_transfer_from_para_to_para_through_asset_hub() { + // Initialize necessary variables. + let amount_to_send = 1_000_000_000_000; + let sender = PenpalASender::get(); + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let penpal_a_as_seen_by_ah = AssetHubKusama::sibling_location_of(PenpalA::para_id()); + let sov_penpal_on_ah = AssetHubKusama::sovereign_account_id_of(penpal_a_as_seen_by_ah); + let receiver = PenpalBReceiver::get(); + let fee_asset_item = 0; + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + let usdt_location_ah: Location = (PalletInstance(50), GeneralIndex(1984)).into(); + let ksm_location = Location::parent(); + let assets: Vec = vec![(usdt_location.clone(), amount_to_send).into()]; + + // Sender needs some ksm to pay for delivery fees. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + ksm_location.clone(), + sender.clone(), + 10_000_000_000_000, + ); + + // The sovereign account of PenpalA in AssetHubKusama needs to have the same amount of USDT + // since it's the reserve. + AssetHubKusama::mint_asset( + ::RuntimeOrigin::signed(AssetHubKusamaAssetOwner::get()), + USDT_ID, + sov_penpal_on_ah, + 10_000_000_000_000, + ); + + // Mint USDT to sender to be able to transfer. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_location.clone(), + sender.clone(), + 10_000_000_000_000, + ); + + // AssetHubKusama has a pool between USDT and ksm so fees can be paid with USDT by automatically + // swapping them for ksm. + create_pool_with_ksm_on!( + AssetHubKusama, + usdt_location_ah, + false, + AssetHubKusamaAssetOwner::get() + ); + + // PenpalB has a pool between USDT and ksm so fees can be paid with USDT by automatically + // swapping them for ksm. + create_pool_with_ksm_on!(PenpalB, usdt_location.clone(), true, PenpalAssetOwner::get()); + + // Sender starts with a lot of USDT. + let sender_balance_before = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender); + assert_eq!(sender_balance_before, 10_000_000_000_000); + + // Receiver has no USDT. + let receiver_balance_before = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver); + assert_eq!(receiver_balance_before, 0); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination.clone(), + receiver.clone(), + amount_to_send, + assets.into(), + None, + fee_asset_item, + ), + }; + let mut test = ParaToParaThroughAHTest::new(test_args); + + // Assertions executed on the sender, PenpalA. + fn sender_assertions(_: ParaToParaThroughAHTest) { + type Event = ::RuntimeEvent; + + let transfer_amount = 1_000_000_000_000; + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + + assert_expected_events!( + PenpalA, + vec![ + Event::ForeignAssets( + pallet_assets::Event::Burned { asset_id, balance, .. } + ) => { + asset_id: *asset_id == usdt_location.clone(), + balance: *balance == transfer_amount, + }, + ] + ); + } + + // Assertions executed on the intermediate hop, AssetHubKusama. + fn ah_assertions(_: ParaToParaThroughAHTest) { + type Event = ::RuntimeEvent; + + let transfer_amount = 1_000_000_000_000; + let penpal_a_as_seen_by_ah = AssetHubKusama::sibling_location_of(PenpalA::para_id()); + let sov_penpal_on_ah = AssetHubKusama::sovereign_account_id_of(penpal_a_as_seen_by_ah); + + assert_expected_events!( + AssetHubKusama, + vec![ + // USDT is burned from sovereign account of PenpalA. + Event::Assets( + pallet_assets::Event::Burned { asset_id, owner, balance } + ) => { + asset_id: *asset_id == 1984, + owner: *owner == sov_penpal_on_ah, + balance: *balance == transfer_amount, + }, + // Credit is swapped. + Event::AssetConversion( + pallet_asset_conversion::Event::SwapCreditExecuted { .. } + ) => {}, + // Message from PenpalA was processed. + Event::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + } + + // Assertions executed on the receiver, PenpalB. + fn receiver_assertions(_: ParaToParaThroughAHTest) { + type Event = ::RuntimeEvent; + + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + let receiver = PenpalBReceiver::get(); + let final_amount = 990_665_188_940; + + assert_expected_events!( + PenpalB, + vec![ + // Final amount gets deposited to receiver. + Event::ForeignAssets( + pallet_assets::Event::Issued { asset_id, owner, amount } + ) => { + asset_id: *asset_id == usdt_location, + owner: *owner == receiver, + amount: *amount == final_amount, + }, + // Swap was made to pay fees with USDT. + Event::AssetConversion( + pallet_asset_conversion::Event::SwapCreditExecuted { .. } + ) => {}, + ] + ); + } + + // Run test and assert. + test.set_assertion::(sender_assertions); + test.set_assertion::(ah_assertions); + test.set_assertion::(receiver_assertions); + test.set_dispatchable::(para_to_para_transfer_assets_through_ah); + test.assert(); + + // Sender has less USDT after the transfer. + let sender_balance_after = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender); + assert_eq!(sender_balance_after, 9_000_000_000_000); + + // Receiver gets `transfer_amount` minus fees. + let receiver_balance_after = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver); + assert_eq!(receiver_balance_after, 990_665_188_940); +} diff --git a/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/mod.rs b/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/mod.rs index 88fa379c40..81b77b9687 100644 --- a/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/mod.rs +++ b/integration-tests/emulated/tests/assets/asset-hub-kusama/src/tests/mod.rs @@ -22,3 +22,79 @@ mod swap; mod teleport; mod treasury; mod xcm_fee_estimation; + +#[macro_export] +macro_rules! foreign_balance_on { + ( $chain:ident, $id:expr, $who:expr ) => { + emulated_integration_tests_common::impls::paste::paste! { + <$chain>::execute_with(|| { + type ForeignAssets = <$chain as [<$chain Pallet>]>::ForeignAssets; + >::balance($id, $who) + }) + } + }; +} + +#[macro_export] +macro_rules! create_pool_with_ksm_on { + ( $chain:ident, $asset_id:expr, $is_foreign:expr, $asset_owner:expr ) => { + emulated_integration_tests_common::impls::paste::paste! { + <$chain>::execute_with(|| { + type RuntimeEvent = <$chain as Chain>::RuntimeEvent; + let owner = $asset_owner; + let signed_owner = <$chain as Chain>::RuntimeOrigin::signed(owner.clone()); + let ksm_location: Location = Parent.into(); + if $is_foreign { + assert_ok!(<$chain as [<$chain Pallet>]>::ForeignAssets::mint( + signed_owner.clone(), + $asset_id.clone().into(), + owner.clone().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + } else { + let asset_id = match $asset_id.interior.last() { + Some(GeneralIndex(id)) => *id as u32, + _ => unreachable!(), + }; + assert_ok!(<$chain as [<$chain Pallet>]>::Assets::mint( + signed_owner.clone(), + asset_id.into(), + owner.clone().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + } + + assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::create_pool( + signed_owner.clone(), + Box::new(ksm_location.clone()), + Box::new($asset_id.clone()), + )); + + assert_expected_events!( + $chain, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::add_liquidity( + signed_owner, + Box::new(ksm_location), + Box::new($asset_id), + 1_000_000_000_000, + 2_000_000_000_000, // $asset_id is worth half of ksm + 0, + 0, + owner.into() + )); + + assert_expected_events!( + $chain, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + } + }; +} diff --git a/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/hybrid_transfers.rs b/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/hybrid_transfers.rs index cf38d67912..daf4f7a6b1 100644 --- a/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/hybrid_transfers.rs +++ b/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/hybrid_transfers.rs @@ -15,10 +15,12 @@ use super::reserve_transfer::*; use crate::{ + foreign_balance_on, tests::teleport::do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using_xt, *, }; use asset_hub_polkadot_runtime::xcm_config::DotLocation; +use emulated_integration_tests_common::USDT_ID; fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) { type RuntimeEvent = ::RuntimeEvent; @@ -110,6 +112,28 @@ fn para_to_para_transfer_assets_through_ah(t: ParaToParaThroughAHTest) -> Dispat ) } +fn para_to_para_transfer_assets_through_ah_inverted( + t: Test, +) -> DispatchResult { + let fee_idx = t.args.fee_asset_item as usize; + let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap(); + let asset_hub_location: Location = PenpalA::sibling_location_of(AssetHubPolkadot::para_id()); + let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset { + assets: Wild(AllCounted(t.args.assets.len() as u32)), + beneficiary: t.args.beneficiary, + }]); + ::PolkadotXcm::transfer_assets_using_type_and_then( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.assets.into()), + bx!(TransferType::RemoteReserve(asset_hub_location.clone().into())), + bx!(fee.id.into()), + bx!(TransferType::RemoteReserve(asset_hub_location.into())), + bx!(VersionedXcm::from(custom_xcm_on_dest)), + t.args.weight_limit, + ) +} + fn para_to_asset_hub_teleport_foreign_assets(t: ParaToSystemParaTest) -> DispatchResult { let fee_idx = t.args.fee_asset_item as usize; let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap(); @@ -791,3 +815,183 @@ fn transfer_native_asset_from_relay_to_para_through_asset_hub() { // should be non-zero assert!(receiver_assets_after < receiver_assets_before + amount_to_send); } + +// We transfer USDT from PenpalA to PenpalB through Asset Hub. +// The sender on PenpalA pays delivery fees in DOT. +// When the message arrives to Asset Hub, execution and delivery fees are paid in USDT +// swapping for DOT automatically. +// When it arrives to PenpalB, execution fees are paid with USDT by swapping for DOT. +#[test] +fn usdt_only_transfer_from_para_to_para_through_asset_hub() { + // ParaToParaThroughAHTest has the source and destination chains inverted. + type PenpalAToPenpalBTest = Test; + + // Initialize necessary variables. + let amount_to_send = 1_000_000_000_000; + let sender = PenpalASender::get(); + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let penpal_a_as_seen_by_ah = AssetHubPolkadot::sibling_location_of(PenpalA::para_id()); + let sov_penpal_on_ah = AssetHubPolkadot::sovereign_account_id_of(penpal_a_as_seen_by_ah); + let receiver = PenpalBReceiver::get(); + let fee_asset_item = 0; + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + let usdt_location_ah: Location = (PalletInstance(50), GeneralIndex(1984)).into(); + let dot_location = Location::parent(); + let assets: Vec = vec![(usdt_location.clone(), amount_to_send).into()]; + + // Sender needs some DOT to pay for delivery fees. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + dot_location.clone(), + sender.clone(), + 10_000_000_000_000, + ); + + // The sovereign account of PenpalA in AssetHubPolkadot needs to have the same amount of USDT + // since it's the reserve. + AssetHubPolkadot::mint_asset( + ::RuntimeOrigin::signed(AssetHubPolkadotAssetOwner::get()), + USDT_ID, + sov_penpal_on_ah, + 10_000_000_000_000, + ); + + // Mint USDT to sender to be able to transfer. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_location.clone(), + sender.clone(), + 10_000_000_000_000, + ); + + // AssetHubPolkadot has a pool between USDT and DOT so fees can be paid with USDT by + // automatically swapping them for DOT. + create_pool_with_dot_on!( + AssetHubPolkadot, + usdt_location_ah, + false, + AssetHubPolkadotAssetOwner::get() + ); + + // PenpalB has a pool between USDT and DOT so fees can be paid with USDT by automatically + // swapping them for DOT. + create_pool_with_dot_on!(PenpalB, usdt_location.clone(), true, PenpalAssetOwner::get()); + + // Sender starts with a lot of USDT. + let sender_balance_before = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender); + assert_eq!(sender_balance_before, 10_000_000_000_000); + + // Receiver has no USDT. + let receiver_balance_before = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver); + assert_eq!(receiver_balance_before, 0); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination.clone(), + receiver.clone(), + amount_to_send, + assets.into(), + None, + fee_asset_item, + ), + }; + let mut test = PenpalAToPenpalBTest::new(test_args); + + // Assertions executed on the sender, PenpalA. + fn sender_assertions(_: PenpalAToPenpalBTest) { + type Event = ::RuntimeEvent; + + let transfer_amount = 1_000_000_000_000; + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + + assert_expected_events!( + PenpalA, + vec![ + Event::ForeignAssets( + pallet_assets::Event::Burned { asset_id, balance, .. } + ) => { + asset_id: *asset_id == usdt_location.clone(), + balance: *balance == transfer_amount, + }, + ] + ); + } + + // Assertions executed on the intermediate hop, AssetHubPolkadot. + fn ah_assertions(_: PenpalAToPenpalBTest) { + type Event = ::RuntimeEvent; + + let transfer_amount = 1_000_000_000_000; + let penpal_a_as_seen_by_ah = AssetHubPolkadot::sibling_location_of(PenpalA::para_id()); + let sov_penpal_on_ah = AssetHubPolkadot::sovereign_account_id_of(penpal_a_as_seen_by_ah); + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // USDT is burned from sovereign account of PenpalA. + Event::Assets( + pallet_assets::Event::Burned { asset_id, owner, balance } + ) => { + asset_id: *asset_id == 1984, + owner: *owner == sov_penpal_on_ah, + balance: *balance == transfer_amount, + }, + // Credit is swapped. + Event::AssetConversion( + pallet_asset_conversion::Event::SwapCreditExecuted { .. } + ) => {}, + // Message from PenpalA was processed. + Event::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + } + + // Assertions executed on the receiver, PenpalB. + fn receiver_assertions(_: PenpalAToPenpalBTest) { + type Event = ::RuntimeEvent; + + let usdt_location: Location = + (Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into(); + let receiver = PenpalBReceiver::get(); + let final_amount = 990_665_188_940; + + assert_expected_events!( + PenpalB, + vec![ + // Final amount gets deposited to receiver. + Event::ForeignAssets( + pallet_assets::Event::Issued { asset_id, owner, amount } + ) => { + asset_id: *asset_id == usdt_location, + owner: *owner == receiver, + amount: *amount == final_amount, + }, + // Swap was made to pay fees with USDT. + Event::AssetConversion( + pallet_asset_conversion::Event::SwapCreditExecuted { .. } + ) => {}, + ] + ); + } + + // Run test and assert. + test.set_assertion::(sender_assertions); + test.set_assertion::(ah_assertions); + test.set_assertion::(receiver_assertions); + test.set_dispatchable::(para_to_para_transfer_assets_through_ah_inverted); + test.assert(); + + // Sender has less USDT after the transfer. + let sender_balance_after = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender); + assert_eq!(sender_balance_after, 9_000_000_000_000); + + // Receiver gets `transfer_amount` minus fees. + let receiver_balance_after = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver); + assert_eq!(receiver_balance_after, 992_693_493_387); +} diff --git a/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/mod.rs b/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/mod.rs index 73b73b239a..5e7d4e1b4e 100644 --- a/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/mod.rs +++ b/integration-tests/emulated/tests/assets/asset-hub-polkadot/src/tests/mod.rs @@ -23,3 +23,79 @@ mod swap; mod teleport; mod treasury; mod xcm_fee_estimation; + +#[macro_export] +macro_rules! foreign_balance_on { + ( $chain:ident, $id:expr, $who:expr ) => { + emulated_integration_tests_common::impls::paste::paste! { + <$chain>::execute_with(|| { + type ForeignAssets = <$chain as [<$chain Pallet>]>::ForeignAssets; + >::balance($id, $who) + }) + } + }; +} + +#[macro_export] +macro_rules! create_pool_with_dot_on { + ( $chain:ident, $asset_id:expr, $is_foreign:expr, $asset_owner:expr ) => { + emulated_integration_tests_common::impls::paste::paste! { + <$chain>::execute_with(|| { + type RuntimeEvent = <$chain as Chain>::RuntimeEvent; + let owner = $asset_owner; + let signed_owner = <$chain as Chain>::RuntimeOrigin::signed(owner.clone()); + let dot_location: Location = Parent.into(); + if $is_foreign { + assert_ok!(<$chain as [<$chain Pallet>]>::ForeignAssets::mint( + signed_owner.clone(), + $asset_id.clone().into(), + owner.clone().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + } else { + let asset_id = match $asset_id.interior.last() { + Some(GeneralIndex(id)) => *id as u32, + _ => unreachable!(), + }; + assert_ok!(<$chain as [<$chain Pallet>]>::Assets::mint( + signed_owner.clone(), + asset_id.into(), + owner.clone().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + } + + assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::create_pool( + signed_owner.clone(), + Box::new(dot_location.clone()), + Box::new($asset_id.clone()), + )); + + assert_expected_events!( + $chain, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::add_liquidity( + signed_owner, + Box::new(dot_location), + Box::new($asset_id), + 1_000_000_000_000, + 2_000_000_000_000, // $asset_id is worth half of dot + 0, + 0, + owner.into() + )); + + assert_expected_events!( + $chain, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + } + }; +} diff --git a/system-parachains/asset-hubs/asset-hub-kusama/src/xcm_config.rs b/system-parachains/asset-hubs/asset-hub-kusama/src/xcm_config.rs index 4380ba988b..7bfac3a0ca 100644 --- a/system-parachains/asset-hubs/asset-hub-kusama/src/xcm_config.rs +++ b/system-parachains/asset-hubs/asset-hub-kusama/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ }; use crate::{ForeignAssets, ForeignAssetsInstance}; use assets_common::{ - matching::{FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -40,7 +40,7 @@ use parachains_common::xcm_config::{ }; use polkadot_parachain_primitives::primitives::Sibling; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use system_parachains_constants::TREASURY_PALLET_ID; use xcm::latest::prelude::*; use xcm_builder::{ @@ -48,11 +48,13 @@ use xcm_builder::{ AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, - IsConcrete, LocalMint, NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, - SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, StartsWith, + IsConcrete, LocalMint, MatchedConvertedConcreteId, NoChecking, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, + SingleAssetExchangeAdapter, SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, - WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, + WeightInfoBounds, WithComputedOrigin, WithLatestLocationConverter, WithUniqueTopic, + XcmFeeManagerFromComponents, }; use xcm_executor::{traits::ConvertLocation, XcmExecutor}; @@ -197,6 +199,29 @@ pub type PoolFungiblesTransactor = FungiblesAdapter< pub type AssetTransactors = (FungibleTransactor, FungiblesTransactor, ForeignFungiblesTransactor, PoolFungiblesTransactor); +/// Asset converter for pool assets. +/// Used to convert one asset to another, when there is a pool available between the two. +/// This type thus allows paying delivery fees with any asset as long as there is a pool between +/// said asset and the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + AssetConversion, + NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` doesn't include Relay token, so we handle it + // explicitly here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, /// ready for dispatching a transaction with Xcm's `Transact`. /// @@ -378,7 +403,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, SendXcmFeeToAccount, diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs index 8b8fc4b9dc..9c7decf477 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ }; use crate::ForeignAssetsInstance; use assets_common::{ - matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -41,7 +41,7 @@ use parachains_common::xcm_config::{ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_constants::system_parachain; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use system_parachains_constants::TREASURY_PALLET_ID; use xcm::latest::prelude::*; use xcm_builder::{ @@ -49,12 +49,13 @@ use xcm_builder::{ AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, - IsConcrete, LocalMint, NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, - SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, - SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + IsConcrete, LocalMint, MatchedConvertedConcreteId, NoChecking, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, + SingleAssetExchangeAdapter, SovereignPaidRemoteExporter, SovereignSignedViaLocation, + StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, + UsingComponents, WeightInfoBounds, WithComputedOrigin, WithLatestLocationConverter, + WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{traits::ConvertLocation, XcmExecutor}; @@ -196,6 +197,29 @@ pub type PoolFungiblesTransactor = FungiblesAdapter< pub type AssetTransactors = (FungibleTransactor, FungiblesTransactor, ForeignFungiblesTransactor, PoolFungiblesTransactor); +/// Asset converter for pool assets. +/// Used to convert one asset to another, when there is a pool available between the two. +/// This type thus allows paying delivery fees with any asset as long as there is a pool between +/// said asset and the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + AssetConversion, + NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` doesn't include Relay token, so we handle it + // explicitly here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, /// ready for dispatching a transaction with Xcm's `Transact`. /// @@ -443,7 +467,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, SendXcmFeeToAccount,