From 5291412e159d3b99c64d5f7f969dbde39d715769 Mon Sep 17 00:00:00 2001
From: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Date: Mon, 2 Sep 2024 12:47:13 +0200
Subject: [PATCH] Swaps for XCM delivery fees (#5131)

# Context

Fees can already be paid in other assets locally thanks to the Trader
implementations we have.
This doesn't work when sending messages because delivery fees go through
a different mechanism altogether.
The idea is to fix this leveraging the `AssetExchanger` config item
that's able to turn the asset the user wants to pay fees in into the
asset the router expects for delivery fees.

# Main addition

An adapter was needed to use `pallet-asset-conversion` for exchanging
assets in XCM.
This was created in
https://github.com/paritytech/polkadot-sdk/pull/5130.

The XCM executor was modified to use `AssetExchanger` (when available)
to swap assets to pay for delivery fees.

## Limitations

We can only pay for delivery fees in different assets in intermediate
hops. We can't pay in different assets locally. The first hop will
always need the native token of the chain (or whatever is specified in
the `XcmRouter`).
This is a byproduct of using the `BuyExecution` instruction to know
which asset should be used for delivery fee payment.
Since this instruction is not present when executing an XCM locally, we
are left with this limitation.
To illustrate this limitation, I'll show two scenarios. All chains
involved have pools.

### Scenario 1

Parachain A --> Parachain B

Here, parachain A can use any asset in a pool with its native asset to
pay for local execution fees.
However, as of now we can't use those for local delivery fees.
This means transfers from A to B need some amount of A's native token to
pay for delivery fees.

### Scenario 2

Parachain A --> Parachain C --> Parachain B

Here, Parachain C's remote delivery fees can be paid with any asset in a
pool with its native asset.
This allows a reserve asset transfer between A and B with C as the
reserve to only need A's native token at the starting hop.
After that, it could all be pool assets.

## Future work

The fact that delivery fees go through a totally different mechanism
results in a lot of bugs and pain points.
Unfortunately, this is not so easy to solve in a backwards compatible
manner.
Delivery fees will be integrated into the language in future XCM
versions, following
https://github.com/polkadot-fellows/xcm-format/pull/53.

Old PR: https://github.com/paritytech/polkadot-sdk/pull/4375.
---
 Cargo.lock                                    |   3 +
 .../assets/asset-hub-westend/src/genesis.rs   |   3 +-
 .../parachains/testing/penpal/src/genesis.rs  |   5 +-
 .../parachains/testing/penpal/src/lib.rs      |   2 +
 .../tests/assets/asset-hub-rococo/src/lib.rs  |   1 +
 .../src/tests/reserve_transfer.rs             | 416 ++++++++++++++++-
 .../tests/assets/asset-hub-westend/src/lib.rs |   1 +
 .../src/tests/hybrid_transfers.rs             |   6 +-
 .../src/tests/reserve_transfer.rs             | 420 +++++++++++++++++-
 .../assets/asset-hub-rococo/src/xcm_config.rs |  39 +-
 .../asset-hub-westend/src/xcm_config.rs       |  39 +-
 .../runtimes/assets/common/Cargo.toml         |   3 +
 .../runtimes/assets/common/src/lib.rs         |   3 +-
 .../runtimes/testing/penpal/Cargo.toml        |   7 +
 .../runtimes/testing/penpal/src/lib.rs        | 123 ++++-
 .../runtimes/testing/penpal/src/xcm_config.rs |  75 +++-
 .../single_asset_adapter/adapter.rs           |   2 +-
 polkadot/xcm/xcm-executor/src/config.rs       |   7 +
 polkadot/xcm/xcm-executor/src/lib.rs          | 133 +++++-
 prdoc/pr_5131.prdoc                           |  42 ++
 20 files changed, 1259 insertions(+), 71 deletions(-)
 create mode 100644 prdoc/pr_5131.prdoc

diff --git a/Cargo.lock b/Cargo.lock
index 9d8f4cef142..c2cbb0f6d4f 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 d20e059f9fe..2876474e094 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::<sr25519::Public>("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 38c94b34aa2..2c34b7e96f5 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::<sr25519::Public>("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 91793d33f30..92dfa30f2e8 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 87a090bf1ae..f4fe1478f3e 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 70dde03d75a..faff5f7660c 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<Hop: Clone>(t: Test<PenpalA, P
 	}
 }
 
+fn para_to_para_asset_hub_hop_assertions(t: ParaToParaThroughAHTest) {
+	type RuntimeEvent = <AssetHubRococo as Chain>::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 = <Rococo as Chain>::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 {
+	<PenpalA as PenpalAPallet>::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 {
 	<PenpalA as PenpalAPallet>::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 = <AssetHubRococo as AssetHubRococoPallet>::Assets;
+		assert_ok!(<Assets as Mutate<_>>::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 = <PenpalA as Chain>::RuntimeEvent;
+
+		assert_ok!(<PenpalA as PenpalAPallet>::ForeignAssets::mint(
+			<PenpalA as Chain>::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!(<PenpalA as PenpalAPallet>::AssetConversion::create_pool(
+			<PenpalA as Chain>::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!(<PenpalA as PenpalAPallet>::AssetConversion::add_liquidity(
+			<PenpalA as Chain>::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 = <AssetHubRococo as AssetHubRococoPallet>::Assets;
+		<Assets as Inspect<_>>::balance(usdt_id, &sender)
+	});
+	let sender_initial_native_balance = AssetHubRococo::execute_with(|| {
+		type Balances = <AssetHubRococo as AssetHubRococoPallet>::Balances;
+		Balances::free_balance(&sender)
+	});
+	let receiver_initial_balance = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &receiver)
+	});
+
+	test.set_assertion::<AssetHubRococo>(system_para_to_para_sender_assertions);
+	test.set_assertion::<PenpalA>(system_para_to_para_receiver_assertions);
+	test.set_dispatchable::<AssetHubRococo>(system_para_to_para_reserve_transfer_assets);
+	test.assert();
+
+	let sender_after_balance = AssetHubRococo::execute_with(|| {
+		type Assets = <AssetHubRococo as AssetHubRococoPallet>::Assets;
+		<Assets as Inspect<_>>::balance(usdt_id, &sender)
+	});
+	let sender_after_native_balance = AssetHubRococo::execute_with(|| {
+		type Balances = <AssetHubRococo as AssetHubRococoPallet>::Balances;
+		Balances::free_balance(&sender)
+	});
+	let receiver_after_balance = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::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 = <AssetHubRococo as AssetHubRococoPallet>::Assets;
+		assert_ok!(<Assets as Mutate<_>>::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 = <AssetHubRococo as Chain>::RuntimeEvent;
+
+		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::Assets::mint(
+			<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
+			usdt_id.into(),
+			AssetHubRococoSender::get().into(),
+			10_000_000_000_000, // For it to have more than enough.
+		));
+
+		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::create_pool(
+			<AssetHubRococo as Chain>::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!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::add_liquidity(
+			<AssetHubRococo as Chain>::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 = <PenpalB as Chain>::RuntimeEvent;
+		let relay_asset = RelayLocation::get();
+
+		assert_ok!(<PenpalB as PenpalBPallet>::ForeignAssets::mint(
+			<PenpalB as Chain>::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!(<PenpalB as PenpalBPallet>::AssetConversion::create_pool(
+			<PenpalB as Chain>::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!(<PenpalB as PenpalBPallet>::AssetConversion::add_liquidity(
+			<PenpalB as Chain>::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 = <PenpalA as PenpalAPallet>::ForeignAssets;
+		assert_ok!(<ForeignAssets as Mutate<_>>::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(
+		<PenpalA as Chain>::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 = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &sender)
+	});
+	let receiver_assets_before = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &receiver)
+	});
+	test.set_assertion::<PenpalA>(para_to_para_through_hop_sender_assertions);
+	test.set_assertion::<AssetHubRococo>(para_to_para_asset_hub_hop_assertions);
+	test.set_assertion::<PenpalB>(para_to_para_through_hop_receiver_assertions);
+	test.set_dispatchable::<PenpalA>(
+		para_to_para_through_asset_hub_limited_reserve_transfer_assets,
+	);
+	test.assert();
+
+	// Query final balances
+	let sender_assets_after = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &sender)
+	});
+	let receiver_assets_after = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::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 a887ee6a532..f568fb4101d 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 49dfe8d5839..975bacea7b4 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() {
 		<ForeignAssets as Inspect<_>>::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 59f63d38059..53b6939298d 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 = <AssetHubWestend as Chain>::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<Hop: Clone>(t: Test<PenpalA, PenpalB, Hop>) {
 	type RuntimeEvent = <PenpalB as Chain>::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 {
+	<PenpalA as PenpalAPallet>::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() {
 		<ForeignAssets as Inspect<_>>::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 = <AssetHubWestend as AssetHubWestendPallet>::Assets;
+		assert_ok!(<Assets as Mutate<_>>::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 = <PenpalA as Chain>::RuntimeEvent;
+
+		assert_ok!(<PenpalA as PenpalAPallet>::ForeignAssets::mint(
+			<PenpalA as Chain>::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!(<PenpalA as PenpalAPallet>::AssetConversion::create_pool(
+			<PenpalA as Chain>::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!(<PenpalA as PenpalAPallet>::AssetConversion::add_liquidity(
+			<PenpalA as Chain>::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 = <AssetHubWestend as AssetHubWestendPallet>::Assets;
+		<Assets as Inspect<_>>::balance(usdt_id, &sender)
+	});
+	let sender_initial_native_balance = AssetHubWestend::execute_with(|| {
+		type Balances = <AssetHubWestend as AssetHubWestendPallet>::Balances;
+		Balances::free_balance(&sender)
+	});
+	let receiver_initial_balance = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &receiver)
+	});
+
+	test.set_assertion::<AssetHubWestend>(system_para_to_para_sender_assertions);
+	test.set_assertion::<PenpalA>(system_para_to_para_receiver_assertions);
+	test.set_dispatchable::<AssetHubWestend>(system_para_to_para_reserve_transfer_assets);
+	test.assert();
+
+	let sender_after_balance = AssetHubWestend::execute_with(|| {
+		type Assets = <AssetHubWestend as AssetHubWestendPallet>::Assets;
+		<Assets as Inspect<_>>::balance(usdt_id, &sender)
+	});
+	let sender_after_native_balance = AssetHubWestend::execute_with(|| {
+		type Balances = <AssetHubWestend as AssetHubWestendPallet>::Balances;
+		Balances::free_balance(&sender)
+	});
+	let receiver_after_balance = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::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 = <AssetHubWestend as AssetHubWestendPallet>::Assets;
+		assert_ok!(<Assets as Mutate<_>>::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 = <AssetHubWestend as Chain>::RuntimeEvent;
+
+		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::Assets::mint(
+			<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
+			usdt_id.into(),
+			AssetHubWestendSender::get().into(),
+			10_000_000_000_000, // For it to have more than enough.
+		));
+
+		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::create_pool(
+			<AssetHubWestend as Chain>::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!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::add_liquidity(
+			<AssetHubWestend as Chain>::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 = <PenpalB as Chain>::RuntimeEvent;
+		let relay_asset = RelayLocation::get();
+
+		assert_ok!(<PenpalB as PenpalBPallet>::ForeignAssets::mint(
+			<PenpalB as Chain>::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!(<PenpalB as PenpalBPallet>::AssetConversion::create_pool(
+			<PenpalB as Chain>::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!(<PenpalB as PenpalBPallet>::AssetConversion::add_liquidity(
+			<PenpalB as Chain>::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 = <PenpalA as PenpalAPallet>::ForeignAssets;
+		assert_ok!(<ForeignAssets as Mutate<_>>::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(
+		<PenpalA as Chain>::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 = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &sender)
+	});
+	let receiver_assets_before = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &receiver)
+	});
+	test.set_assertion::<PenpalA>(para_to_para_through_hop_sender_assertions);
+	test.set_assertion::<AssetHubWestend>(para_to_para_asset_hub_hop_assertions);
+	test.set_assertion::<PenpalB>(para_to_para_through_hop_receiver_assertions);
+	test.set_dispatchable::<PenpalA>(
+		para_to_para_through_asset_hub_limited_reserve_transfer_assets,
+	);
+	test.assert();
+
+	// Query final balances
+	let sender_assets_after = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(usdt_from_asset_hub.clone(), &sender)
+	});
+	let receiver_assets_after = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::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 2d1914e059b..f263baf4bef 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<FromSiblingParachain<parachain_info::Pallet<Runtime>>>,
 );
 
+/// 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<TrustBackedAssetsPalletLocation, Balance, xcm::v4::Location>,
+		ForeignAssetsConvertedConcreteId,
+		// `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here.
+		MatchedConvertedConcreteId<
+			xcm::v4::Location,
+			Balance,
+			Equals<ParentLocation>,
+			WithLatestLocationConverter<xcm::v4::Location>,
+			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<Self::AssetTransactor, TreasuryAccount>,
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 d61381d3f50..bc5d07f552b 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<FromSiblingParachain<parachain_info::Pallet<Runtime>>>,
 );
 
+/// 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<TrustBackedAssetsPalletLocation, Balance, xcm::v4::Location>,
+		ForeignAssetsConvertedConcreteId,
+		// `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here.
+		MatchedConvertedConcreteId<
+			xcm::v4::Location,
+			Balance,
+			Equals<ParentLocation>,
+			WithLatestLocationConverter<xcm::v4::Location>,
+			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<Self::AssetTransactor, TreasuryAccount>,
diff --git a/cumulus/parachains/runtimes/assets/common/Cargo.toml b/cumulus/parachains/runtimes/assets/common/Cargo.toml
index c6740269339..fb66f0de232 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 4bb593f9892..deda5fa4ab9 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<PoolAssetsPalletLocation, Balance> =
 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 e16629302be..96338b64558 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 bf39c02a3f5..7d19c0ed8d8 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<EnsureRoot<AccountId>, EnsureXcm<IsMajorityOfBody<KsmLocation, ExecutiveBody>>>;
 
-impl pallet_assets::Config<pallet_assets::Instance1> for Runtime {
+pub type TrustBackedAssetsInstance = pallet_assets::Instance1;
+
+impl pallet_assets::Config<TrustBackedAssetsInstance> for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type Balance = Balance;
 	type AssetId = AssetId;
@@ -500,6 +509,106 @@ impl pallet_assets::Config<ForeignAssetsInstance> 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::<sp_runtime::AccountId32>::into_account_truncating(&AssetConversionPalletId::get());
+}
+
+pub type AssetsForceOrigin = EnsureRoot<AccountId>;
+
+pub type PoolAssetsInstance = pallet_assets::Instance3;
+impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type Balance = Balance;
+	type RemoveItemsLimit = ConstU32<1000>;
+	type AssetId = u32;
+	type AssetIdParameter = u32;
+	type Currency = Balances;
+	type CreateOrigin =
+		AsEnsureOriginWithArg<EnsureSignedBy<AssetConversionOrigin, sp_runtime::AccountId32>>;
+	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<Runtime>;
+	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_config::RelayLocation, xcm::latest::Location>,
+	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<AssetConversionOrigin, Self::Assets>;
+	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<Runtime>,
+		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<Runtime, pallet_assets::Instance1>,
+		AssetsToBlockAuthor<Runtime, TrustBackedAssetsInstance>,
 	>;
 }
 
@@ -685,6 +794,8 @@ construct_runtime!(
 		// The main stage.
 		Assets: pallet_assets::<Instance1> = 50,
 		ForeignAssets: pallet_assets::<Instance2> = 51,
+		PoolAssets: pallet_assets::<Instance3> = 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 d0c421bccaf..99aadb33b84 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 = <Assets as PalletInfoAccess>::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<LocalTeleportableToAssetHub, SystemAssetHubLocation>,);
 
+/// `AssetId`/`Balance` converter for `TrustBackedAssets`.
+pub type TrustBackedAssetsConvertedConcreteId =
+	assets_common::TrustBackedAssetsConvertedConcreteId<AssetsPalletLocation, Balance>;
+
+/// 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<UnitWeightCost, RuntimeCall, MaxInstructions>;
 	type Trader = (
 		UsingComponents<WeightToFee, RelayLocation, AccountId, Balances, ToAuthor<Runtime>>,
-		// 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<StakingPot, crate::NativeAndAssets>,
 			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<Self::AssetTransactor, TreasuryAccount>,
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 fa94ee5f1ca..3108068686f 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 63b113bc250..5bcbbd3466e 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<PostInfo = PostDispatchInfo> + 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 74561e931e7..a8110ca3d19 100644
--- a/polkadot/xcm/xcm-executor/src/lib.rs
+++ b/polkadot/xcm/xcm-executor/src/lib.rs
@@ -83,6 +83,9 @@ pub struct XcmExecutor<Config: config::Config> {
 	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<AssetId>,
 	_config: PhantomData<Config>,
 }
 
@@ -269,7 +272,7 @@ impl<Config: config::Config> ExecuteXcm<Config::RuntimeCall> for XcmExecutor<Con
 			for asset in fees.inner() {
 				Config::AssetTransactor::withdraw_asset(&asset, &origin, None)?;
 			}
-			Config::FeeManager::handle_fee(fees, None, FeeReason::ChargeFees);
+			Config::FeeManager::handle_fee(fees.into(), None, FeeReason::ChargeFees);
 		}
 		Ok(())
 	}
@@ -319,6 +322,7 @@ impl<Config: config::Config> XcmExecutor<Config> {
 			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<Config: config::Config> XcmExecutor<Config> {
 		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<Location>,
@@ -878,11 +963,23 @@ impl<Config: config::Config> XcmExecutor<Config> {
 					message_to_weigh.extend(xcm.0.clone().into_iter());
 					let (_, fee) =
 						validate_send::<Config::XcmSender>(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<Config: config::Config> XcmExecutor<Config> {
 					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<Config: config::Config> XcmExecutor<Config> {
 				// should be executed.
 				let Some(weight) = Option::<Weight>::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 00000000000..db1003ab403
--- /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
-- 
GitLab