diff --git a/Cargo.lock b/Cargo.lock index 9d8f4cef1420..c2cbb0f6d4f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,6 +1080,7 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-asset-conversion", + "pallet-assets", "pallet-xcm", "parachains-common", "parity-scale-codec", @@ -12567,6 +12568,7 @@ dependencies = [ "frame-try-runtime", "hex-literal", "log", + "pallet-asset-conversion", "pallet-asset-tx-payment", "pallet-assets", "pallet-aura", @@ -12585,6 +12587,7 @@ dependencies = [ "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", + "primitive-types", "scale-info", "smallvec", "sp-api", diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs index d20e059f9fea..2876474e094c 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs @@ -27,6 +27,7 @@ use parachains_common::{AccountId, Balance}; pub const PARA_ID: u32 = 1000; pub const ED: Balance = testnet_parachains_constants::westend::currency::EXISTENTIAL_DEPOSIT; +pub const USDT_ED: Balance = 70_000; parameter_types! { pub AssetHubWestendAssetOwner: AccountId = get_account_id_from_seed::("Alice"); @@ -67,7 +68,7 @@ pub fn genesis() -> Storage { assets: asset_hub_westend_runtime::AssetsConfig { assets: vec![ (RESERVABLE_ASSET_ID, AssetHubWestendAssetOwner::get(), false, ED), - (USDT_ID, AssetHubWestendAssetOwner::get(), true, ED), + (USDT_ID, AssetHubWestendAssetOwner::get(), true, USDT_ED), ], ..Default::default() }, diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs index 38c94b34aa2e..2c34b7e96f5e 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs @@ -27,6 +27,7 @@ use penpal_runtime::xcm_config::{LocalReservableFromAssetHub, RelayLocation, Usd pub const PARA_ID_A: u32 = 2000; pub const PARA_ID_B: u32 = 2001; pub const ED: Balance = penpal_runtime::EXISTENTIAL_DEPOSIT; +pub const USDT_ED: Balance = 70_000; parameter_types! { pub PenpalSudoAccount: AccountId = get_account_id_from_seed::("Alice"); @@ -81,8 +82,8 @@ pub fn genesis(para_id: u32) -> Storage { (RelayLocation::get(), PenpalAssetOwner::get(), true, ED), // Sufficient AssetHub asset representation (LocalReservableFromAssetHub::get(), PenpalAssetOwner::get(), true, ED), - // USDT from Asset Hub - (UsdtFromAssetHub::get(), PenpalAssetOwner::get(), true, ED), + // USDT from AssetHub + (UsdtFromAssetHub::get(), PenpalAssetOwner::get(), true, USDT_ED), ], ..Default::default() }, diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs index 91793d33f304..92dfa30f2e83 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs @@ -53,6 +53,7 @@ decl_test_parachains! { PolkadotXcm: penpal_runtime::PolkadotXcm, Assets: penpal_runtime::Assets, ForeignAssets: penpal_runtime::ForeignAssets, + AssetConversion: penpal_runtime::AssetConversion, Balances: penpal_runtime::Balances, } }, @@ -76,6 +77,7 @@ decl_test_parachains! { PolkadotXcm: penpal_runtime::PolkadotXcm, Assets: penpal_runtime::Assets, ForeignAssets: penpal_runtime::ForeignAssets, + AssetConversion: penpal_runtime::AssetConversion, Balances: penpal_runtime::Balances, } }, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs index 87a090bf1ae6..f4fe1478f3ed 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs @@ -66,6 +66,7 @@ mod imports { CustomizableAssetFromSystemAssetHub as PenpalCustomizableAssetFromSystemAssetHub, LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub, LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub, + UsdtFromAssetHub as PenpalUsdtFromAssetHub, }, PenpalAParaPallet as PenpalAPallet, PenpalAssetOwner, PenpalBParaPallet as PenpalBPallet, ED as PENPAL_ED, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs index 70dde03d75a2..faff5f7660c2 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs @@ -60,10 +60,10 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { AssetHubRococo::assert_xcm_pallet_attempted_complete(None); let sov_acc_of_dest = AssetHubRococo::sovereign_account_id_of(t.args.dest.clone()); - for (idx, asset) in t.args.assets.into_inner().into_iter().enumerate() { + for asset in t.args.assets.into_inner().into_iter() { let expected_id = asset.id.0.clone().try_into().unwrap(); let asset_amount = if let Fungible(a) = asset.fun { Some(a) } else { None }.unwrap(); - if idx == t.args.fee_asset_item as usize { + if asset.id == AssetId(Location::new(1, [])) { assert_expected_events!( AssetHubRococo, vec![ @@ -77,6 +77,23 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { }, ] ); + } else if matches!( + asset.id.0.unpack(), + (0, [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(_)]) + ) { + assert_expected_events!( + AssetHubRococo, + vec![ + // Amount of trust-backed asset is transferred to Parachain's Sovereign account + RuntimeEvent::Assets( + pallet_assets::Event::Transferred { from, to, amount, .. }, + ) => { + from: *from == t.sender.account_id, + to: *to == sov_acc_of_dest, + amount: *amount == asset_amount, + }, + ] + ); } else { assert_expected_events!( AssetHubRococo, @@ -388,6 +405,38 @@ pub fn para_to_para_through_hop_sender_assertions(t: Test::RuntimeEvent; + let sov_penpal_a_on_ah = AssetHubRococo::sovereign_account_id_of( + AssetHubRococo::sibling_location_of(PenpalA::para_id()), + ); + let sov_penpal_b_on_ah = AssetHubRococo::sovereign_account_id_of( + AssetHubRococo::sibling_location_of(PenpalB::para_id()), + ); + + assert_expected_events!( + AssetHubRococo, + vec![ + // Withdrawn from sender parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Burned { owner, balance, .. } + ) => { + owner: *owner == sov_penpal_a_on_ah, + balance: *balance == t.args.amount, + }, + // Deposited to receiver parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Deposited { who, .. } + ) => { + who: *who == sov_penpal_b_on_ah, + }, + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); +} + fn para_to_para_relay_hop_assertions(t: ParaToParaThroughRelayTest) { type RuntimeEvent = ::RuntimeEvent; let sov_penpal_a_on_rococo = @@ -469,6 +518,19 @@ fn system_para_to_para_reserve_transfer_assets(t: SystemParaToParaTest) -> Dispa ) } +fn para_to_para_through_asset_hub_limited_reserve_transfer_assets( + t: ParaToParaThroughAHTest, +) -> DispatchResult { + ::PolkadotXcm::limited_reserve_transfer_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + t.args.weight_limit, + ) +} + fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult { ::PolkadotXcm::limited_reserve_transfer_assets( t.signed_origin, @@ -1136,3 +1198,353 @@ fn reserve_transfer_native_asset_from_para_to_para_through_relay() { // Receiver's balance is increased assert!(receiver_assets_after > receiver_assets_before); } + +// ============================================================================ +// ==== Reserve Transfers USDT - AssetHub->Parachain - pay fees using pool ==== +// ============================================================================ +#[test] +fn reserve_transfer_usdt_from_asset_hub_to_para() { + let usdt_id = 1984u32; + let penpal_location = AssetHubRococo::sibling_location_of(PenpalA::para_id()); + let penpal_sov_account = AssetHubRococo::sovereign_account_id_of(penpal_location.clone()); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubRococo::fund_accounts(vec![(penpal_sov_account.clone().into(), ASSET_HUB_ROCOCO_ED)]); + + let sender = AssetHubRococoSender::get(); + let receiver = PenpalAReceiver::get(); + let asset_amount_to_send = 1_000_000_000_000; + + AssetHubRococo::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &AssetHubRococoSender::get(), + asset_amount_to_send + 10_000_000_000_000, // Make sure it has enough. + )); + }); + + let relay_asset_penpal_pov = RelayLocation::get(); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // Setup the pool between `relay_asset_penpal_pov` and `usdt_from_asset_hub` on PenpalA. + // So we can swap the custom asset that comes from AssetHubRococo for native asset to pay for + // fees. + PenpalA::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalASender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov), + Box::new(usdt_from_asset_hub.clone()), + // `usdt_from_asset_hub` is worth a third of `relay_asset_penpal_pov` + 1_000_000_000_000, + 3_000_000_000_000, + 0, + 0, + PenpalASender::get().into() + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let assets: Assets = vec![( + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(usdt_id.into())], + asset_amount_to_send, + ) + .into()] + .into(); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + penpal_location, + receiver.clone(), + asset_amount_to_send, + assets, + None, + 0, + ), + }; + let mut test = SystemParaToParaTest::new(test_args); + + let sender_initial_balance = AssetHubRococo::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_initial_native_balance = AssetHubRococo::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_initial_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + + test.set_assertion::(system_para_to_para_sender_assertions); + test.set_assertion::(system_para_to_para_receiver_assertions); + test.set_dispatchable::(system_para_to_para_reserve_transfer_assets); + test.assert(); + + let sender_after_balance = AssetHubRococo::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_after_native_balance = AssetHubRococo::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_after_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we allow payment with different assets locally, this should be the same, since + // they aren't used for fees. + assert!(sender_after_native_balance < sender_initial_native_balance); + // Sender account's balance decreases. + assert_eq!(sender_after_balance, sender_initial_balance - asset_amount_to_send); + // Receiver account's balance increases. + assert!(receiver_after_balance > receiver_initial_balance); + assert!(receiver_after_balance < receiver_initial_balance + asset_amount_to_send); +} + +// =================================================================================== +// == Reserve Transfers USDT - Parachain->AssetHub->Parachain - pay fees using pool == +// =================================================================================== +// +// Transfer USDT From Penpal A to Penpal B with AssetHub as the reserve, while paying fees using +// USDT by making use of existing USDT pools on AssetHub and destination. +#[test] +fn reserve_transfer_usdt_from_para_to_para_through_asset_hub() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let sender = PenpalASender::get(); + let asset_amount_to_send: Balance = ROCOCO_ED * 10000; + let fee_amount_to_send: Balance = ROCOCO_ED * 10000; + let sender_chain_as_seen_by_asset_hub = AssetHubRococo::sibling_location_of(PenpalA::para_id()); + let sov_of_sender_on_asset_hub = + AssetHubRococo::sovereign_account_id_of(sender_chain_as_seen_by_asset_hub); + let receiver_as_seen_by_asset_hub = AssetHubRococo::sibling_location_of(PenpalB::para_id()); + let sov_of_receiver_on_asset_hub = + AssetHubRococo::sovereign_account_id_of(receiver_as_seen_by_asset_hub); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubRococo::fund_accounts(vec![ + (sov_of_sender_on_asset_hub.clone().into(), ASSET_HUB_ROCOCO_ED), + (sov_of_receiver_on_asset_hub.clone().into(), ASSET_HUB_ROCOCO_ED), + ]); + + // Give USDT to sov account of sender. + let usdt_id = 1984; + AssetHubRococo::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &sov_of_sender_on_asset_hub.clone().into(), + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // We create a pool between WND and USDT in AssetHub. + let native_asset: Location = Parent.into(); + let usdt = Location::new( + 0, + [Junction::PalletInstance(ASSETS_PALLET_ID), Junction::GeneralIndex(usdt_id.into())], + ); + + // set up pool with USDT <> native pair + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::Assets::mint( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + usdt_id.into(), + AssetHubRococoSender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + Box::new(native_asset.clone()), + Box::new(usdt.clone()), + )); + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + Box::new(native_asset), + Box::new(usdt), + 1_000_000_000_000, + 2_000_000_000_000, // usdt is worth half of `native_asset` + 0, + 0, + AssetHubRococoSender::get().into() + )); + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // We also need a pool between WND and USDT on PenpalB. + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let relay_asset = RelayLocation::get(); + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalBReceiver::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset), + Box::new(usdt_from_asset_hub.clone()), + 1_000_000_000_000, + 2_000_000_000_000, // `usdt_from_asset_hub` is worth half of `relay_asset` + 0, + 0, + PenpalBReceiver::get().into() + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + PenpalA::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type ForeignAssets = ::ForeignAssets; + assert_ok!(>::mint_into( + usdt_from_asset_hub.clone(), + &sender, + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // Prepare assets to transfer. + let assets: Assets = + (usdt_from_asset_hub.clone(), asset_amount_to_send + fee_amount_to_send).into(); + // Just to be very specific we're not including anything other than USDT. + assert_eq!(assets.len(), 1); + + // Give the sender enough Relay tokens to pay for local delivery fees. + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we support local delivery fee payment in other assets, we don't need this. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + sender.clone(), + 10_000_000_000_000, // Large estimate to make sure it works. + ); + + // Init values for Parachain Destination + let receiver = PenpalBReceiver::get(); + + // Init Test + let fee_asset_index = 0; + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination, + receiver.clone(), + asset_amount_to_send, + assets, + None, + fee_asset_index, + ), + }; + let mut test = ParaToParaThroughAHTest::new(test_args); + + // Query initial balances + let sender_assets_before = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_before = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + test.set_assertion::(para_to_para_through_hop_sender_assertions); + test.set_assertion::(para_to_para_asset_hub_hop_assertions); + test.set_assertion::(para_to_para_through_hop_receiver_assertions); + test.set_dispatchable::( + para_to_para_through_asset_hub_limited_reserve_transfer_assets, + ); + test.assert(); + + // Query final balances + let sender_assets_after = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_after = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // Sender's balance is reduced by amount + assert!(sender_assets_after < sender_assets_before - asset_amount_to_send); + // Receiver's balance is increased + assert!(receiver_assets_after > receiver_assets_before); +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs index a887ee6a532a..f568fb4101db 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs @@ -64,6 +64,7 @@ mod imports { CustomizableAssetFromSystemAssetHub as PenpalCustomizableAssetFromSystemAssetHub, LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub, LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub, + UsdtFromAssetHub as PenpalUsdtFromAssetHub, }, PenpalAParaPallet as PenpalAPallet, PenpalAssetOwner, PenpalBParaPallet as PenpalBPallet, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs index 49dfe8d58394..975bacea7b4f 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs @@ -613,10 +613,10 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() { >::balance(roc_at_westend_parachains, &receiver) }); - // Sender's balance is reduced by amount sent plus delivery fees + // Sender's balance is reduced by amount sent. assert!(sender_wnds_after < sender_wnds_before - wnd_to_send); assert_eq!(sender_rocs_after, sender_rocs_before - roc_to_send); - // Sovereign accounts on reserve are changed accordingly + // Sovereign accounts on reserve are changed accordingly. assert_eq!( wnds_in_sender_reserve_on_ah_after, wnds_in_sender_reserve_on_ah_before - wnd_to_send @@ -630,7 +630,7 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() { rocs_in_receiver_reserve_on_ah_after, rocs_in_receiver_reserve_on_ah_before + roc_to_send ); - // Receiver's balance is increased + // Receiver's balance is increased by amount sent minus delivery fees. assert!(receiver_wnds_after > receiver_wnds_before); assert_eq!(receiver_rocs_after, receiver_rocs_before + roc_to_send); } diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs index 59f63d380590..53b6939298da 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs @@ -60,10 +60,10 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { AssetHubWestend::assert_xcm_pallet_attempted_complete(None); let sov_acc_of_dest = AssetHubWestend::sovereign_account_id_of(t.args.dest.clone()); - for (idx, asset) in t.args.assets.into_inner().into_iter().enumerate() { + for asset in t.args.assets.into_inner().into_iter() { let expected_id = asset.id.0.clone().try_into().unwrap(); let asset_amount = if let Fungible(a) = asset.fun { Some(a) } else { None }.unwrap(); - if idx == t.args.fee_asset_item as usize { + if asset.id == AssetId(Location::new(1, [])) { assert_expected_events!( AssetHubWestend, vec![ @@ -77,6 +77,23 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { }, ] ); + } else if matches!( + asset.id.0.unpack(), + (0, [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(_)]) + ) { + assert_expected_events!( + AssetHubWestend, + vec![ + // Amount of trust-backed asset is transferred to Parachain's Sovereign account + RuntimeEvent::Assets( + pallet_assets::Event::Transferred { from, to, amount, .. }, + ) => { + from: *from == t.sender.account_id, + to: *to == sov_acc_of_dest, + amount: *amount == asset_amount, + }, + ] + ); } else { assert_expected_events!( AssetHubWestend, @@ -418,6 +435,38 @@ fn para_to_para_relay_hop_assertions(t: ParaToParaThroughRelayTest) { ); } +fn para_to_para_asset_hub_hop_assertions(t: ParaToParaThroughAHTest) { + type RuntimeEvent = ::RuntimeEvent; + let sov_penpal_a_on_ah = AssetHubWestend::sovereign_account_id_of( + AssetHubWestend::sibling_location_of(PenpalA::para_id()), + ); + let sov_penpal_b_on_ah = AssetHubWestend::sovereign_account_id_of( + AssetHubWestend::sibling_location_of(PenpalB::para_id()), + ); + + assert_expected_events!( + AssetHubWestend, + vec![ + // Withdrawn from sender parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Burned { owner, balance, .. } + ) => { + owner: *owner == sov_penpal_a_on_ah, + balance: *balance == t.args.amount, + }, + // Deposited to receiver parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Deposited { who, .. } + ) => { + who: *who == sov_penpal_b_on_ah, + }, + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); +} + pub fn para_to_para_through_hop_receiver_assertions(t: Test) { type RuntimeEvent = ::RuntimeEvent; @@ -493,6 +542,19 @@ fn para_to_para_through_relay_limited_reserve_transfer_assets( ) } +fn para_to_para_through_asset_hub_limited_reserve_transfer_assets( + t: ParaToParaThroughAHTest, +) -> DispatchResult { + ::PolkadotXcm::limited_reserve_transfer_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + t.args.weight_limit, + ) +} + /// Reserve Transfers of native asset from Relay Chain to the Asset Hub shouldn't work #[test] fn reserve_transfer_native_asset_from_relay_to_asset_hub_fails() { @@ -1133,8 +1195,360 @@ fn reserve_transfer_native_asset_from_para_to_para_through_relay() { >::balance(relay_native_asset_location, &receiver) }); - // Sender's balance is reduced by amount sent plus delivery fees + // Sender's balance is reduced by amount sent plus delivery fees. assert!(sender_assets_after < sender_assets_before - amount_to_send); + // Receiver's balance is increased by `amount_to_send` minus delivery fees. + assert!(receiver_assets_after > receiver_assets_before); + assert!(receiver_assets_after < receiver_assets_before + amount_to_send); +} + +// ============================================================================ +// ==== Reserve Transfers USDT - AssetHub->Parachain - pay fees using pool ==== +// ============================================================================ +#[test] +fn reserve_transfer_usdt_from_asset_hub_to_para() { + let usdt_id = 1984u32; + let penpal_location = AssetHubWestend::sibling_location_of(PenpalA::para_id()); + let penpal_sov_account = AssetHubWestend::sovereign_account_id_of(penpal_location.clone()); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubWestend::fund_accounts(vec![(penpal_sov_account.clone().into(), ASSET_HUB_WESTEND_ED)]); + + let sender = AssetHubWestendSender::get(); + let receiver = PenpalAReceiver::get(); + let asset_amount_to_send = 1_000_000_000_000; + + AssetHubWestend::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &AssetHubWestendSender::get(), + asset_amount_to_send + 10_000_000_000_000, // Make sure it has enough. + )); + }); + + let relay_asset_penpal_pov = RelayLocation::get(); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // Setup the pool between `relay_asset_penpal_pov` and `usdt_from_asset_hub` on PenpalA. + // So we can swap the custom asset that comes from AssetHubWestend for native asset to pay for + // fees. + PenpalA::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalASender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov), + Box::new(usdt_from_asset_hub.clone()), + // `usdt_from_asset_hub` is worth a third of `relay_asset_penpal_pov` + 1_000_000_000_000, + 3_000_000_000_000, + 0, + 0, + PenpalASender::get().into() + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let assets: Assets = vec![( + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(usdt_id.into())], + asset_amount_to_send, + ) + .into()] + .into(); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + penpal_location, + receiver.clone(), + asset_amount_to_send, + assets, + None, + 0, + ), + }; + let mut test = SystemParaToParaTest::new(test_args); + + let sender_initial_balance = AssetHubWestend::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_initial_native_balance = AssetHubWestend::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_initial_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + + test.set_assertion::(system_para_to_para_sender_assertions); + test.set_assertion::(system_para_to_para_receiver_assertions); + test.set_dispatchable::(system_para_to_para_reserve_transfer_assets); + test.assert(); + + let sender_after_balance = AssetHubWestend::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_after_native_balance = AssetHubWestend::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_after_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we allow payment with different assets locally, this should be the same, since + // they aren't used for fees. + assert!(sender_after_native_balance < sender_initial_native_balance); + // Sender account's balance decreases. + assert_eq!(sender_after_balance, sender_initial_balance - asset_amount_to_send); + // Receiver account's balance increases. + assert!(receiver_after_balance > receiver_initial_balance); + assert!(receiver_after_balance < receiver_initial_balance + asset_amount_to_send); +} + +// =================================================================================== +// == Reserve Transfers USDT - Parachain->AssetHub->Parachain - pay fees using pool == +// =================================================================================== +// +// Transfer USDT From Penpal A to Penpal B with AssetHub as the reserve, while paying fees using +// USDT by making use of existing USDT pools on AssetHub and destination. +#[test] +fn reserve_transfer_usdt_from_para_to_para_through_asset_hub() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let sender = PenpalASender::get(); + let asset_amount_to_send: Balance = WESTEND_ED * 10000; + let fee_amount_to_send: Balance = WESTEND_ED * 10000; + let sender_chain_as_seen_by_asset_hub = + AssetHubWestend::sibling_location_of(PenpalA::para_id()); + let sov_of_sender_on_asset_hub = + AssetHubWestend::sovereign_account_id_of(sender_chain_as_seen_by_asset_hub); + let receiver_as_seen_by_asset_hub = AssetHubWestend::sibling_location_of(PenpalB::para_id()); + let sov_of_receiver_on_asset_hub = + AssetHubWestend::sovereign_account_id_of(receiver_as_seen_by_asset_hub); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubWestend::fund_accounts(vec![ + (sov_of_sender_on_asset_hub.clone().into(), ASSET_HUB_WESTEND_ED), + (sov_of_receiver_on_asset_hub.clone().into(), ASSET_HUB_WESTEND_ED), + ]); + + // Give USDT to sov account of sender. + let usdt_id = 1984; + AssetHubWestend::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &sov_of_sender_on_asset_hub.clone().into(), + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // We create a pool between WND and USDT in AssetHub. + let native_asset: Location = Parent.into(); + let usdt = Location::new( + 0, + [Junction::PalletInstance(ASSETS_PALLET_ID), Junction::GeneralIndex(usdt_id.into())], + ); + + // set up pool with USDT <> native pair + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::Assets::mint( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + usdt_id.into(), + AssetHubWestendSender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + Box::new(native_asset.clone()), + Box::new(usdt.clone()), + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + Box::new(native_asset), + Box::new(usdt), + 1_000_000_000_000, + 2_000_000_000_000, // usdt is worth half of `native_asset` + 0, + 0, + AssetHubWestendSender::get().into() + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // We also need a pool between WND and USDT on PenpalB. + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let relay_asset = RelayLocation::get(); + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalBReceiver::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset), + Box::new(usdt_from_asset_hub.clone()), + 1_000_000_000_000, + 2_000_000_000_000, // `usdt_from_asset_hub` is worth half of `relay_asset` + 0, + 0, + PenpalBReceiver::get().into() + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + PenpalA::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type ForeignAssets = ::ForeignAssets; + assert_ok!(>::mint_into( + usdt_from_asset_hub.clone(), + &sender, + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // Prepare assets to transfer. + let assets: Assets = + (usdt_from_asset_hub.clone(), asset_amount_to_send + fee_amount_to_send).into(); + // Just to be very specific we're not including anything other than USDT. + assert_eq!(assets.len(), 1); + + // Give the sender enough Relay tokens to pay for local delivery fees. + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we support local delivery fee payment in other assets, we don't need this. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + sender.clone(), + 10_000_000_000_000, // Large estimate to make sure it works. + ); + + // Init values for Parachain Destination + let receiver = PenpalBReceiver::get(); + + // Init Test + let fee_asset_index = 0; + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination, + receiver.clone(), + asset_amount_to_send, + assets, + None, + fee_asset_index, + ), + }; + let mut test = ParaToParaThroughAHTest::new(test_args); + + // Query initial balances + let sender_assets_before = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_before = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + test.set_assertion::(para_to_para_through_hop_sender_assertions); + test.set_assertion::(para_to_para_asset_hub_hop_assertions); + test.set_assertion::(para_to_para_through_hop_receiver_assertions); + test.set_dispatchable::( + para_to_para_through_asset_hub_limited_reserve_transfer_assets, + ); + test.assert(); + + // Query final balances + let sender_assets_after = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_after = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // Sender's balance is reduced by amount + assert!(sender_assets_after < sender_assets_before - asset_amount_to_send); // Receiver's balance is increased assert!(receiver_assets_after > receiver_assets_before); } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index 2d1914e059bf..f263baf4bef8 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ XcmpQueue, }; use assets_common::{ - matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -43,7 +43,7 @@ use parachains_common::{ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use testnet_parachains_constants::rococo::snowbridge::{ EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, }; @@ -54,12 +54,13 @@ use xcm_builder::{ DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, + MatchedConvertedConcreteId, NetworkExportTableItem, NoChecking, NonFungiblesAdapter, + ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, + WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -326,6 +327,28 @@ pub type TrustedTeleporters = ( IsForeignConcreteAsset>>, ); +/// 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 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< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -407,7 +430,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/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index d61381d3f50b..bc5d07f552b0 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ XcmpQueue, }; use assets_common::{ - matching::{FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -43,7 +43,7 @@ use parachains_common::{ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use xcm::latest::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, @@ -51,12 +51,13 @@ use xcm_builder::{ DenyReserveTransferToRelayChain, DenyThenTry, DescribeFamily, DescribePalletTerminal, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, + MatchedConvertedConcreteId, NetworkExportTableItem, NoChecking, NonFungiblesAdapter, + ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, + WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -350,6 +351,28 @@ pub type TrustedTeleporters = ( IsForeignConcreteAsset>>, ); +/// 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 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< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -430,7 +453,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/cumulus/parachains/runtimes/assets/common/Cargo.toml b/cumulus/parachains/runtimes/assets/common/Cargo.toml index c6740269339d..fb66f0de2322 100644 --- a/cumulus/parachains/runtimes/assets/common/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/common/Cargo.toml @@ -19,6 +19,7 @@ impl-trait-for-tuples = { workspace = true } frame-support = { workspace = true } sp-api = { workspace = true } sp-runtime = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-conversion = { workspace = true } # Polkadot @@ -42,6 +43,7 @@ std = [ "frame-support/std", "log/std", "pallet-asset-conversion/std", + "pallet-assets/std", "pallet-xcm/std", "parachains-common/std", "scale-info/std", @@ -56,6 +58,7 @@ runtime-benchmarks = [ "cumulus-primitives-core/runtime-benchmarks", "frame-support/runtime-benchmarks", "pallet-asset-conversion/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", diff --git a/cumulus/parachains/runtimes/assets/common/src/lib.rs b/cumulus/parachains/runtimes/assets/common/src/lib.rs index 4bb593f98929..deda5fa4ab9c 100644 --- a/cumulus/parachains/runtimes/assets/common/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/common/src/lib.rs @@ -29,7 +29,7 @@ use crate::matching::{LocalLocationPattern, ParentLocation}; use frame_support::traits::{Equals, EverythingBut}; use parachains_common::{AssetIdForTrustBackedAssets, CollectionId, ItemId}; use sp_runtime::traits::TryConvertInto; -use xcm::latest::Location; +use xcm::prelude::*; use xcm_builder::{ AsPrefixedGeneralIndex, MatchedConvertedConcreteId, StartsWith, WithLatestLocationConverter, }; @@ -138,7 +138,6 @@ pub type PoolAssetsConvertedConcreteId = mod tests { use super::*; use sp_runtime::traits::MaybeEquivalence; - use xcm::prelude::*; use xcm_builder::{StartsWithExplicitGlobalConsensus, WithLatestLocationConverter}; use xcm_executor::traits::{Error as MatchError, MatchesFungibles}; diff --git a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml index e16629302be6..96338b645581 100644 --- a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml +++ b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml @@ -42,6 +42,7 @@ pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-asset-tx-payment = { workspace = true } pallet-assets = { workspace = true } +pallet-asset-conversion = { workspace = true } sp-api = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } @@ -79,6 +80,8 @@ parachain-info = { workspace = true } parachains-common = { workspace = true } assets-common = { workspace = true } +primitive-types = { version = "0.12.1", default-features = false, features = ["codec", "num-traits", "scale-info"] } + [features] default = ["std"] std = [ @@ -99,6 +102,7 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "log/std", + "pallet-asset-conversion/std", "pallet-asset-tx-payment/std", "pallet-assets/std", "pallet-aura/std", @@ -117,6 +121,7 @@ std = [ "polkadot-parachain-primitives/std", "polkadot-primitives/std", "polkadot-runtime-common/std", + "primitive-types/std", "scale-info/std", "sp-api/std", "sp-block-builder/std", @@ -149,6 +154,7 @@ runtime-benchmarks = [ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "hex-literal", + "pallet-asset-conversion/runtime-benchmarks", "pallet-asset-tx-payment/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -176,6 +182,7 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "frame-try-runtime/try-runtime", + "pallet-asset-conversion/try-runtime", "pallet-asset-tx-payment/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index bf39c02a3f59..7d19c0ed8d85 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -35,6 +35,10 @@ pub mod xcm_config; extern crate alloc; use alloc::{vec, vec::Vec}; +use assets_common::{ + local_and_foreign_assets::{LocalFromLeft, TargetFromLeft}, + AssetIdForTrustBackedAssetsConvert, +}; use codec::Encode; use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; @@ -42,10 +46,13 @@ use frame_support::{ construct_runtime, derive_impl, dispatch::DispatchClass, genesis_builder_helper::{build_state, get_preset}, + ord_parameter_types, pallet_prelude::Weight, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, Everything, TransformOrigin, + tokens::{fungible, fungibles, imbalance::ResolveAssetTo}, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Everything, + TransformOrigin, }, weights::{ constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial, WeightToFee as _, @@ -55,7 +62,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureSigned, + EnsureRoot, EnsureSigned, EnsureSignedBy, }; use parachains_common::{ impls::{AssetsToBlockAuthor, NonZeroIssuance}, @@ -67,7 +74,7 @@ pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, Dispatchable}, + traits::{AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, Dispatchable}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; @@ -442,7 +449,9 @@ parameter_types! { // pub type AssetsForceOrigin = // EnsureOneOf, EnsureXcm>>; -impl pallet_assets::Config for Runtime { +pub type TrustBackedAssetsInstance = pallet_assets::Instance1; + +impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type AssetId = AssetId; @@ -500,6 +509,106 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; } +parameter_types! { + pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); + pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); +} + +ord_parameter_types! { + pub const AssetConversionOrigin: sp_runtime::AccountId32 = + AccountIdConversion::::into_account_truncating(&AssetConversionPalletId::get()); +} + +pub type AssetsForceOrigin = EnsureRoot; + +pub type PoolAssetsInstance = pallet_assets::Instance3; +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = + AsEnsureOriginWithArg>; + type ForceOrigin = AssetsForceOrigin; + type AssetDeposit = ConstU128<0>; + type AssetAccountDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type ApprovalDeposit = ConstU128<0>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = pallet_assets::weights::SubstrateWeight; + type CallbackHandle = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +/// Union fungibles implementation for `Assets` and `ForeignAssets`. +pub type LocalAndForeignAssets = fungibles::UnionOf< + Assets, + ForeignAssets, + LocalFromLeft< + AssetIdForTrustBackedAssetsConvert< + xcm_config::TrustBackedAssetsPalletLocation, + xcm::latest::Location, + >, + parachains_common::AssetIdForTrustBackedAssets, + xcm::latest::Location, + >, + xcm::latest::Location, + AccountId, +>; + +/// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`. +pub type NativeAndAssets = fungible::UnionOf< + Balances, + LocalAndForeignAssets, + TargetFromLeft, + xcm::latest::Location, + AccountId, +>; + +pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter< + AssetConversionPalletId, + (xcm::latest::Location, xcm::latest::Location), +>; + +impl pallet_asset_conversion::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type HigherPrecisionBalance = sp_core::U256; + type AssetKind = xcm::latest::Location; + type Assets = NativeAndAssets; + type PoolId = (Self::AssetKind, Self::AssetKind); + type PoolLocator = pallet_asset_conversion::WithFirstAsset< + xcm_config::RelayLocation, + AccountId, + Self::AssetKind, + PoolIdToAccountId, + >; + type PoolAssetId = u32; + type PoolAssets = PoolAssets; + type PoolSetupFee = ConstU128<0>; // Asset class deposit fees are sufficient to prevent spam + type PoolSetupFeeAsset = xcm_config::RelayLocation; + type PoolSetupFeeTarget = ResolveAssetTo; + type LiquidityWithdrawalFee = LiquidityWithdrawalFee; + type LPFee = ConstU32<3>; + type PalletId = AssetConversionPalletId; + type MaxSwapPathLength = ConstU32<3>; + type MintMinLiquidity = ConstU128<100>; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = assets_common::benchmarks::AssetPairFactory< + xcm_config::RelayLocation, + parachain_info::Pallet, + xcm_config::TrustBackedAssetsPalletIndex, + xcm::latest::Location, + >; +} + parameter_types! { pub const ReservedXcmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); @@ -642,9 +751,9 @@ impl pallet_asset_tx_payment::Config for Runtime { Balances, Runtime, ConvertInto, - pallet_assets::Instance1, + TrustBackedAssetsInstance, >, - AssetsToBlockAuthor, + AssetsToBlockAuthor, >; } @@ -685,6 +794,8 @@ construct_runtime!( // The main stage. Assets: pallet_assets:: = 50, ForeignAssets: pallet_assets:: = 51, + PoolAssets: pallet_assets:: = 52, + AssetConversion: pallet_asset_conversion = 53, Sudo: pallet_sudo = 255, } diff --git a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs index d0c421bccaf8..99aadb33b840 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs @@ -24,15 +24,19 @@ //! soon. use super::{ AccountId, AllPalletsWithSystem, AssetId as AssetIdPalletAssets, Assets, Authorship, Balance, - Balances, ForeignAssets, ForeignAssetsInstance, NonZeroIssuance, ParachainInfo, - ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, - XcmpQueue, + Balances, CollatorSelection, ForeignAssets, ForeignAssetsInstance, NonZeroIssuance, + ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + WeightToFee, XcmpQueue, }; use crate::{BaseDeliveryFee, FeeAssetId, TransactionByteFee}; +use assets_common::TrustBackedAssetsAsLocation; use core::marker::PhantomData; use frame_support::{ parameter_types, - traits::{ConstU32, Contains, ContainsPair, Everything, EverythingBut, Get, Nothing}, + traits::{ + tokens::imbalance::ResolveAssetTo, ConstU32, Contains, ContainsPair, Everything, + EverythingBut, Get, Nothing, PalletInfoAccess, + }, weights::Weight, }; use frame_system::EnsureRoot; @@ -49,15 +53,15 @@ use xcm_builder::{ FungibleAdapter, FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, - SignedToAccountId32, SovereignSignedViaLocation, StartsWith, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignSignedViaLocation, StartsWith, + TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{traits::JustTry, XcmExecutor}; parameter_types! { pub const RelayLocation: Location = Location::parent(); - // Local native currency which is stored in `pallet_balances`` + // Local native currency which is stored in `pallet_balances` pub const PenpalNativeCurrency: Location = Location::here(); // The Penpal runtime is utilized for testing with various environment setups. // This storage item allows us to customize the `NetworkId` where Penpal is deployed. @@ -70,6 +74,10 @@ parameter_types! { Parachain(ParachainInfo::parachain_id().into()) ].into(); pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating(); + pub StakingPot: AccountId = CollatorSelection::account_id(); + pub TrustBackedAssetsPalletIndex: u8 = ::index() as u8; + pub TrustBackedAssetsPalletLocation: Location = + PalletInstance(TrustBackedAssetsPalletIndex::get()).into(); } /// Type for specifying how a `Location` can be converted into an `AccountId`. This is used @@ -265,6 +273,8 @@ pub const TELEPORTABLE_ASSET_ID: u32 = 2; pub const ASSETS_PALLET_ID: u8 = 50; pub const ASSET_HUB_ID: u32 = 1000; +pub const USDT_ASSET_ID: u128 = 1984; + parameter_types! { /// The location that this chain recognizes as the Relay network's Asset Hub. pub SystemAssetHubLocation: Location = Location::new(1, [Parachain(ASSET_HUB_ID)]); @@ -284,7 +294,7 @@ parameter_types! { ); pub UsdtFromAssetHub: Location = Location::new( 1, - [Parachain(ASSET_HUB_ID), PalletInstance(ASSETS_PALLET_ID), GeneralIndex(1984)] + [Parachain(ASSET_HUB_ID), PalletInstance(ASSETS_PALLET_ID), GeneralIndex(USDT_ASSET_ID)], ); /// The Penpal runtime is utilized for testing with various environment setups. @@ -316,6 +326,28 @@ pub type TrustedReserves = ( pub type TrustedTeleporters = (AssetFromChain,); +/// `AssetId`/`Balance` converter for `TrustBackedAssets`. +pub type TrustBackedAssetsConvertedConcreteId = + assets_common::TrustBackedAssetsConvertedConcreteId; + +/// Asset converter for pool assets. +/// Used to convert assets in pools to the asset required for fee payment. +/// The pool must be between the first asset and the one required for fee payment. +/// This type allows paying fees with any asset in a pool with the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation< + TrustBackedAssetsPalletLocation, + Balance, + xcm::latest::Location, + >, + ForeignAssetsConvertedConcreteId, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -331,18 +363,21 @@ impl xcm_executor::Config for XcmConfig { type Weigher = FixedWeightBounds; type Trader = ( UsingComponents>, - // This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated - // `pallet_assets` instance - `ForeignAssets`. - cumulus_primitives_utility::TakeFirstAssetTrader< + cumulus_primitives_utility::SwapFirstAssetTrader< + RelayLocation, + crate::AssetConversion, + WeightToFee, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation< + TrustBackedAssetsPalletLocation, + Balance, + xcm::latest::Location, + >, + ForeignAssetsConvertedConcreteId, + ), + ResolveAssetTo, AccountId, - ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger, - ForeignAssetsConvertedConcreteId, - ForeignAssets, - cumulus_primitives_utility::XcmFeesTo32ByteAccount< - ForeignFungiblesTransactor, - AccountId, - XcmAssetFeesReceiver, - >, >, ); type ResponseHandler = PolkadotXcm; @@ -352,7 +387,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< (), SendXcmFeeToAccount, diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs index fa94ee5f1caa..3108068686f9 100644 --- a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs +++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs @@ -136,7 +136,7 @@ where give.clone() })?; - (credit_out, Some(credit_change)) + (credit_out, if credit_change.peek() > 0 { Some(credit_change) } else { None }) }; // We create an `AssetsInHolding` instance by putting in the resulting asset diff --git a/polkadot/xcm/xcm-executor/src/config.rs b/polkadot/xcm/xcm-executor/src/config.rs index 63b113bc250f..5bcbbd3466e8 100644 --- a/polkadot/xcm/xcm-executor/src/config.rs +++ b/polkadot/xcm/xcm-executor/src/config.rs @@ -33,6 +33,10 @@ pub trait Config { type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo; /// How to send an onward XCM message. + /// + /// The sender is tasked with returning the assets it needs to pay for delivery fees. + /// Only one asset should be returned as delivery fees, any other will be ignored by + /// the executor. type XcmSender: SendXcm; /// How to withdraw and deposit an asset. @@ -74,6 +78,9 @@ pub trait Config { type AssetLocker: AssetLock; /// Handler for exchanging assets. + /// + /// This is used in the executor to swap the asset wanted for fees with the asset needed for + /// delivery fees. type AssetExchanger: AssetExchange; /// The handler for when there is an instruction to claim assets. diff --git a/polkadot/xcm/xcm-executor/src/lib.rs b/polkadot/xcm/xcm-executor/src/lib.rs index 74561e931e7e..a8110ca3d19f 100644 --- a/polkadot/xcm/xcm-executor/src/lib.rs +++ b/polkadot/xcm/xcm-executor/src/lib.rs @@ -83,6 +83,9 @@ pub struct XcmExecutor { appendix_weight: Weight, transact_status: MaybeErrorCode, fees_mode: FeesMode, + /// Asset provided in last `BuyExecution` instruction (if any) in current XCM program. Same + /// asset type will be used for paying any potential delivery fees incurred by the program. + asset_used_for_fees: Option, _config: PhantomData, } @@ -269,7 +272,7 @@ impl ExecuteXcm for XcmExecutor XcmExecutor { appendix_weight: Weight::zero(), transact_status: Default::default(), fees_mode: FeesMode { jit_withdraw: false }, + asset_used_for_fees: None, _config: PhantomData, } } @@ -469,31 +473,112 @@ impl XcmExecutor { Ok(()) } - fn take_fee(&mut self, fee: Assets, reason: FeeReason) -> XcmResult { + fn take_fee(&mut self, fees: Assets, reason: FeeReason) -> XcmResult { if Config::FeeManager::is_waived(self.origin_ref(), reason.clone()) { return Ok(()) } tracing::trace!( target: "xcm::fees", - ?fee, + ?fees, origin_ref = ?self.origin_ref(), fees_mode = ?self.fees_mode, ?reason, "Taking fees", ); - let paid = if self.fees_mode.jit_withdraw { + // We only ever use the first asset from `fees`. + let asset_needed_for_fees = match fees.get(0) { + Some(fee) => fee, + None => return Ok(()), // No delivery fees need to be paid. + }; + // If `BuyExecution` was called, we use that asset for delivery fees as well. + let asset_to_pay_for_fees = + self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone()); + tracing::trace!(target: "xcm::fees", ?asset_to_pay_for_fees); + // We withdraw or take from holding the asset the user wants to use for fee payment. + let withdrawn_fee_asset = if self.fees_mode.jit_withdraw { let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?; - for asset in fee.inner() { - Config::AssetTransactor::withdraw_asset(&asset, origin, Some(&self.context))?; - } - fee + Config::AssetTransactor::withdraw_asset( + &asset_to_pay_for_fees, + origin, + Some(&self.context), + )?; + tracing::trace!(target: "xcm::fees", ?asset_needed_for_fees); + asset_to_pay_for_fees.clone().into() + } else { + let assets_taken_from_holding_to_pay_delivery_fees = self + .holding + .try_take(asset_to_pay_for_fees.clone().into()) + .map_err(|_| XcmError::NotHoldingFees)?; + tracing::trace!(target: "xcm::fees", ?assets_taken_from_holding_to_pay_delivery_fees); + let mut iter = assets_taken_from_holding_to_pay_delivery_fees.fungible_assets_iter(); + let asset = iter.next().ok_or(XcmError::NotHoldingFees)?; + asset.into() + }; + // We perform the swap, if needed, to pay fees. + let paid = if asset_to_pay_for_fees.id != asset_needed_for_fees.id { + let swapped_asset: Assets = Config::AssetExchanger::exchange_asset( + self.origin_ref(), + withdrawn_fee_asset, + &asset_needed_for_fees.clone().into(), + false, + ) + .map_err(|given_assets| { + tracing::error!( + target: "xcm::fees", + ?given_assets, + "Swap was deemed necessary but couldn't be done", + ); + XcmError::FeesNotMet + })? + .into(); + swapped_asset } else { - self.holding.try_take(fee.into()).map_err(|_| XcmError::NotHoldingFees)?.into() + // If the asset wanted to pay for fees is the one that was needed, + // we don't need to do any swap. + // We just use the assets withdrawn or taken from holding. + withdrawn_fee_asset.into() }; Config::FeeManager::handle_fee(paid, Some(&self.context), reason); Ok(()) } + /// Calculates the amount of `self.asset_used_for_fees` required to swap for + /// `asset_needed_for_fees`. + /// + /// The calculation is done by `Config::AssetExchanger`. + /// If `self.asset_used_for_fees` is not set, it will just return `asset_needed_for_fees`. + fn calculate_asset_for_delivery_fees(&self, asset_needed_for_fees: Asset) -> Asset { + if let Some(asset_wanted_for_fees) = &self.asset_used_for_fees { + if *asset_wanted_for_fees != asset_needed_for_fees.id { + match Config::AssetExchanger::quote_exchange_price( + &(asset_wanted_for_fees.clone(), Fungible(0)).into(), + &asset_needed_for_fees.clone().into(), + false, // Minimal. + ) { + Some(necessary_assets) => + // We only use the first asset for fees. + // If this is not enough to swap for the fee asset then it will error later down + // the line. + necessary_assets.get(0).unwrap_or(&asset_needed_for_fees.clone()).clone(), + // If we can't convert, then we return the original asset. + // It will error later in any case. + None => { + tracing::trace!( + target: "xcm::calculate_asset_for_delivery_fees", + ?asset_wanted_for_fees, + "Could not convert fees", + ); + asset_needed_for_fees.clone() + }, + } + } else { + asset_needed_for_fees + } + } else { + asset_needed_for_fees + } + } + /// Calculates what `local_querier` would be from the perspective of `destination`. fn to_querier( local_querier: Option, @@ -878,11 +963,23 @@ impl XcmExecutor { message_to_weigh.extend(xcm.0.clone().into_iter()); let (_, fee) = validate_send::(dest.clone(), Xcm(message_to_weigh))?; - // set aside fee to be charged by XcmSender - let transport_fee = self.holding.saturating_take(fee.into()); - - // now take assets to deposit (excluding transport_fee) + let maybe_delivery_fee = fee.get(0).map(|asset_needed_for_fees| { + tracing::trace!( + target: "xcm::DepositReserveAsset", + "Asset provided to pay for fees {:?}, asset required for delivery fees: {:?}", + self.asset_used_for_fees, asset_needed_for_fees, + ); + let asset_to_pay_for_fees = + self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone()); + // set aside fee to be charged by XcmSender + let delivery_fee = + self.holding.saturating_take(asset_to_pay_for_fees.into()); + tracing::trace!(target: "xcm::DepositReserveAsset", ?delivery_fee); + delivery_fee + }); + // now take assets to deposit (after having taken delivery fees) let deposited = self.holding.saturating_take(assets); + tracing::trace!(target: "xcm::DepositReserveAsset", ?deposited, "Assets except delivery fee"); self.deposit_assets_with_retry(&deposited, &dest)?; // Note that we pass `None` as `maybe_failed_bin` and drop any assets which // cannot be reanchored because we have already called `deposit_asset` on all @@ -890,8 +987,10 @@ impl XcmExecutor { let assets = Self::reanchored(deposited, &dest, None); let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin]; message.extend(xcm.0.into_iter()); - // put back transport_fee in holding register to be charged by XcmSender - self.holding.subsume_assets(transport_fee); + // put back delivery_fee in holding register to be charged by XcmSender + if let Some(delivery_fee) = maybe_delivery_fee { + self.holding.subsume_assets(delivery_fee); + } self.send(dest, Xcm(message), FeeReason::DepositReserveAsset)?; Ok(()) }); @@ -969,6 +1068,10 @@ impl XcmExecutor { // should be executed. let Some(weight) = Option::::from(weight_limit) else { return Ok(()) }; let old_holding = self.holding.clone(); + // Save the asset being used for execution fees, so we later know what should be + // used for delivery fees. + self.asset_used_for_fees = Some(fees.id.clone()); + tracing::trace!(target: "xcm::executor::BuyExecution", asset_used_for_fees = ?self.asset_used_for_fees); // pay for `weight` using up to `fees` of the holding register. let max_fee = self.holding.try_take(fees.into()).map_err(|_| XcmError::NotHoldingFees)?; diff --git a/prdoc/pr_5131.prdoc b/prdoc/pr_5131.prdoc new file mode 100644 index 000000000000..db1003ab4033 --- /dev/null +++ b/prdoc/pr_5131.prdoc @@ -0,0 +1,42 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Swap for paying delivery fees in different assets + +doc: + - audience: Runtime User + description: | + If the `AssetExchanger` is configured on a runtime, the XCM executor is now able to swap assets + to pay for delivery fees. + This was already possible for execution fees via the `SwapFirstAssetTrader`. + A runtime where this will be possible is Asset Hub. + That means reserve asset transfers from Parachain A to Parachain B passing through Asset Hub no + longer need to have any DOT to pay for fees on AssetHub. + They can have any asset in a pool with DOT on Asset Hub, for example USDT or USDC. + - audience: Runtime Dev + description: | + Using the `AssetExchanger` XCM config item, the executor now swaps fees to use for delivery fees, + if possible. + If you want your runtime to support this, you need to configure this new item. + Thankfully, `xcm-builder` now has a new adapter for this, which lets you use `pallet-asset-conversion` + or any type that implements the `SwapCredit` and `QuotePrice` traits. + It's called `SingleAssetExchangeAdapter`, you can read more about it in its rust docs. + This item is already configured in Asset Hub. + + IMPORTANT: The executor now only takes the first asset for delivery fees. If you have configured a custom router + that returns more than one asset for delivery fees, then only the first one will be taken into account. + This is most likely not what you want. + +crates: + - name: staging-xcm-executor + bump: minor + - name: asset-hub-westend-runtime + bump: minor + - name: asset-hub-rococo-runtime + bump: minor + - name: staging-xcm-builder + bump: patch + - name: assets-common + bump: patch + - name: penpal-runtime + bump: minor