From e7651cf41b2d775dedcc897e17ead16e200a4fff Mon Sep 17 00:00:00 2001
From: Adrian Catangiu <adrian@parity.io>
Date: Wed, 6 Dec 2023 13:18:12 +0200
Subject: [PATCH] pallet-xcm: add new flexible `transfer_assets()`
 call/extrinsic (#2388)

# Motivation (+testing)

### Enable easy `ForeignAssets` transfers using `pallet-xcm`

We had just previously added capabilities to teleport fees during
reserve-based transfers, but what about reserve-transferring fees when
needing to teleport some non-fee asset?

This PR aligns everything under either explicit reserve-transfer,
explicit teleport, or this new flexible `transfer_assets()` which can
mix and match as needed with fewer artificial constraints imposed to the
user.

This will enable, for example, a (non-system) parachain to teleport
their `ForeignAssets` assets to AssetHub while using DOT to pay fees.
(the assets are teleported - as foreign assets should from their owner
chain - while DOT used for fees can only be reserve-based transferred
between said parachain and AssetHub).

Added `xcm-emulator` tests for this scenario ^.

# Description

Reverts `(limited_)reserve_transfer_assets` to only allow reserve-based
transfers for all `assets` including fees.

Similarly `(limited_)teleport_assets` only allows teleports for all
`assets` including fees.

For complex combinations of asset transfers where assets and fees may
have different reserves or different reserve/teleport trust
configurations, users can use the newly added `transfer_assets()`
extrinsic which is more flexible in allowing more complex scenarios.

`assets` (excluding `fees`) must have same reserve location or otherwise
be teleportable to `dest`.
No limitations imposed on `fees`.

- for local reserve: transfer assets to sovereign account of destination
chain and forward a notification XCM to `dest` to mint and deposit
reserve-based assets to `beneficiary`.
- for destination reserve: burn local assets and forward a notification
to `dest` chain to withdraw the reserve assets from this chain's
sovereign account and deposit them to `beneficiary`.
- for remote reserve: burn local assets, forward XCM to reserve chain to
move reserves from this chain's SA to `dest` chain's SA, and forward
another XCM to `dest` to mint and deposit reserve-based assets to
`beneficiary`.
- for teleports: burn local assets and forward XCM to `dest` chain to
mint/teleport assets and deposit them to `beneficiary`.

## Review notes

Only around 500 lines are prod code (see `pallet_xcm/src/lib.rs`), the
rest of the PR is new tests and improving existing tests.

---------

Co-authored-by: command-bot <>
---
 Cargo.lock                                    |    2 -
 .../parachains/testing/penpal/src/lib.rs      |    2 +
 .../emulated/common/src/macros.rs             |   99 ++
 .../emulated/common/src/xcm_helpers.rs        |   14 +
 .../tests/assets/asset-hub-rococo/Cargo.toml  |    1 -
 .../assets/asset-hub-rococo/src/tests/mod.rs  |    8 +
 .../src/tests/reserve_transfer.rs             |   28 +-
 .../assets/asset-hub-rococo/src/tests/swap.rs |  157 +-
 .../asset-hub-rococo/src/tests/teleport.rs    |  337 +++++
 .../tests/assets/asset-hub-westend/Cargo.toml |    1 -
 .../assets/asset-hub-westend/src/tests/mod.rs |    8 +
 .../src/tests/reserve_transfer.rs             |   16 +-
 .../asset-hub-westend/src/tests/swap.rs       |  156 +-
 .../asset-hub-westend/src/tests/teleport.rs   |  337 +++++
 .../assets/asset-hub-rococo/src/lib.rs        |   49 +
 .../src/weights/pallet_xcm.rs                 |  154 +-
 .../assets/asset-hub-westend/src/lib.rs       |   49 +
 .../src/weights/pallet_xcm.rs                 |  154 +-
 .../bridge-hubs/bridge-hub-rococo/src/lib.rs  |   12 +
 .../src/weights/pallet_xcm.rs                 |  191 ++-
 .../bridge-hubs/bridge-hub-westend/src/lib.rs |   12 +
 .../src/weights/pallet_xcm.rs                 |  220 +--
 .../collectives-westend/src/lib.rs            |   12 +
 .../src/weights/pallet_xcm.rs                 |  117 +-
 .../contracts/contracts-rococo/src/lib.rs     |   12 +
 .../runtimes/testing/penpal/src/xcm_config.rs |   47 +-
 polkadot/runtime/rococo/src/lib.rs            |   14 +
 .../runtime/rococo/src/weights/pallet_xcm.rs  |    4 +
 polkadot/runtime/westend/src/lib.rs           |   15 +
 .../runtime/westend/src/weights/pallet_xcm.rs |    4 +
 polkadot/xcm/pallet-xcm/src/benchmarking.rs   |   71 +-
 polkadot/xcm/pallet-xcm/src/lib.rs            |  568 +++++--
 polkadot/xcm/pallet-xcm/src/mock.rs           |   45 +
 .../pallet-xcm/src/tests/assets_transfer.rs   | 1331 ++++++++++++++---
 polkadot/xcm/pallet-xcm/src/tests/mod.rs      |    2 +-
 prdoc/pr_2388.prdoc                           |   26 +
 substrate/frame/assets/src/benchmarking.rs    |    2 +-
 substrate/frame/assets/src/lib.rs             |    2 +-
 38 files changed, 3322 insertions(+), 957 deletions(-)
 create mode 100644 prdoc/pr_2388.prdoc

diff --git a/Cargo.lock b/Cargo.lock
index 0afacd1fc21..f2693730b91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -761,7 +761,6 @@ dependencies = [
  "pallet-xcm",
  "parachains-common",
  "parity-scale-codec",
- "penpal-runtime",
  "rococo-runtime",
  "rococo-system-emulated-network",
  "sp-runtime",
@@ -884,7 +883,6 @@ dependencies = [
  "pallet-xcm",
  "parachains-common",
  "parity-scale-codec",
- "penpal-runtime",
  "polkadot-runtime-common",
  "sp-runtime",
  "staging-xcm",
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 c76120adb79..62bafb5cb30 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
@@ -15,6 +15,7 @@
 
 mod genesis;
 pub use genesis::{genesis, ED, PARA_ID_A, PARA_ID_B};
+pub use penpal_runtime::xcm_config::{LocalTeleportableToAssetHub, XcmConfig};
 
 // Substrate
 use frame_support::traits::OnInitialize;
@@ -67,6 +68,7 @@ decl_test_parachains! {
 
 // Penpal implementation
 impl_accounts_helpers_for_parachain!(PenpalA);
+impl_accounts_helpers_for_parachain!(PenpalB);
 impl_assets_helpers_for_parachain!(PenpalA, Rococo);
 impl_assets_helpers_for_parachain!(PenpalB, Westend);
 impl_assert_events_helpers_for_parachain!(PenpalA);
diff --git a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
index 6ea3524ed4a..8718f1e83a0 100644
--- a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
+++ b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
@@ -120,3 +120,102 @@ macro_rules! test_parachain_is_trusted_teleporter {
 		}
 	};
 }
+
+#[macro_export]
+macro_rules! include_penpal_create_foreign_asset_on_asset_hub {
+	( $penpal:ident, $asset_hub:ident, $relay_ed:expr, $weight_to_fee:expr) => {
+		$crate::impls::paste::paste! {
+			pub fn penpal_create_foreign_asset_on_asset_hub(
+				asset_id_on_penpal: u32,
+				foreign_asset_at_asset_hub: MultiLocation,
+				ah_as_seen_by_penpal: MultiLocation,
+				is_sufficient: bool,
+				asset_owner: AccountId,
+				prefund_amount: u128,
+			) {
+				use frame_support::weights::WeightToFee;
+				let ah_check_account = $asset_hub::execute_with(|| {
+					<$asset_hub as [<$asset_hub Pallet>]>::PolkadotXcm::check_account()
+				});
+				let penpal_check_account =
+					$penpal::execute_with(|| <$penpal as [<$penpal Pallet>]>::PolkadotXcm::check_account());
+				let penpal_as_seen_by_ah = $asset_hub::sibling_location_of($penpal::para_id());
+
+				// prefund SA of Penpal on AssetHub with enough native tokens to pay for creating
+				// new foreign asset, also prefund CheckingAccount with ED, because teleported asset
+				// itself might not be sufficient and CheckingAccount cannot be created otherwise
+				let sov_penpal_on_ah = $asset_hub::sovereign_account_id_of(penpal_as_seen_by_ah);
+				$asset_hub::fund_accounts(vec![
+					(sov_penpal_on_ah.clone().into(), $relay_ed * 100_000_000_000),
+					(ah_check_account.clone().into(), $relay_ed * 1000),
+				]);
+
+				// prefund SA of AssetHub on Penpal with native asset
+				let sov_ah_on_penpal = $penpal::sovereign_account_id_of(ah_as_seen_by_penpal);
+				$penpal::fund_accounts(vec![
+					(sov_ah_on_penpal.into(), $relay_ed * 1_000_000_000),
+					(penpal_check_account.clone().into(), $relay_ed * 1000),
+				]);
+
+				// Force create asset on $penpal and prefund [<$penpal Sender>]
+				$penpal::force_create_and_mint_asset(
+					asset_id_on_penpal,
+					ASSET_MIN_BALANCE,
+					is_sufficient,
+					asset_owner,
+					None,
+					prefund_amount,
+				);
+
+				let require_weight_at_most = Weight::from_parts(1_100_000_000_000, 30_000);
+				// `OriginKind::Xcm` required by ForeignCreators pallet-assets origin filter
+				let origin_kind = OriginKind::Xcm;
+				let call_create_foreign_assets =
+					<$asset_hub as Chain>::RuntimeCall::ForeignAssets(pallet_assets::Call::<
+						<$asset_hub as Chain>::Runtime,
+						pallet_assets::Instance2,
+					>::create {
+						id: foreign_asset_at_asset_hub,
+						min_balance: ASSET_MIN_BALANCE,
+						admin: sov_penpal_on_ah.into(),
+					})
+					.encode();
+				let buy_execution_fee_amount = $weight_to_fee::weight_to_fee(
+					&Weight::from_parts(10_100_000_000_000, 300_000),
+				);
+				let buy_execution_fee = MultiAsset {
+					id: Concrete(MultiLocation { parents: 1, interior: Here }),
+					fun: Fungible(buy_execution_fee_amount),
+				};
+				let xcm = VersionedXcm::from(Xcm(vec![
+					WithdrawAsset { 0: vec![buy_execution_fee.clone()].into() },
+					BuyExecution { fees: buy_execution_fee.clone(), weight_limit: Unlimited },
+					Transact { require_weight_at_most, origin_kind, call: call_create_foreign_assets.into() },
+					ExpectTransactStatus(MaybeErrorCode::Success),
+					RefundSurplus,
+					DepositAsset { assets: All.into(), beneficiary: penpal_as_seen_by_ah },
+				]));
+				// Send XCM message from penpal => asset_hub
+				let sudo_penpal_origin = <$penpal as Chain>::RuntimeOrigin::root();
+				$penpal::execute_with(|| {
+					assert_ok!(<$penpal as [<$penpal Pallet>]>::PolkadotXcm::send(
+						sudo_penpal_origin.clone(),
+						bx!(ah_as_seen_by_penpal.into()),
+						bx!(xcm),
+					));
+					type RuntimeEvent = <$penpal as Chain>::RuntimeEvent;
+					assert_expected_events!(
+						$penpal,
+						vec![
+							RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { .. }) => {},
+						]
+					);
+				});
+				$asset_hub::execute_with(|| {
+					type ForeignAssets = <$asset_hub as [<$asset_hub Pallet>]>::ForeignAssets;
+					assert!(ForeignAssets::asset_exists(foreign_asset_at_asset_hub));
+				});
+			}
+		}
+	};
+}
diff --git a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs
index 47e92ed075f..70a9408c309 100644
--- a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs
+++ b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs
@@ -59,3 +59,17 @@ pub fn xcm_transact_unpaid_execution(
 		Transact { require_weight_at_most, origin_kind, call },
 	]))
 }
+
+/// Helper method to get the non-fee asset used in multiple assets transfer
+pub fn non_fee_asset(assets: &MultiAssets, fee_idx: usize) -> Option<(MultiLocation, u128)> {
+	let asset = assets.inner().into_iter().enumerate().find(|a| a.0 != fee_idx)?.1.clone();
+	let asset_id = match asset.id {
+		Concrete(id) => id,
+		_ => return None,
+	};
+	let asset_amount = match asset.fun {
+		Fungible(amount) => amount,
+		_ => return None,
+	};
+	Some((asset_id, asset_amount))
+}
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
index 16e2f736bdf..bc4e7cfe376 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
@@ -30,5 +30,4 @@ asset-test-utils = { path = "../../../../../runtimes/assets/test-utils" }
 parachains-common = { path = "../../../../../../parachains/common" }
 asset-hub-rococo-runtime = { path = "../../../../../runtimes/assets/asset-hub-rococo" }
 emulated-integration-tests-common = { path = "../../../common", default-features = false }
-penpal-runtime = { path = "../../../../../runtimes/testing/penpal" }
 rococo-system-emulated-network = { path = "../../../networks/rococo-system" }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
index b3841af0e6c..c9270934ddf 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
@@ -18,3 +18,11 @@ mod send;
 mod set_xcm_versions;
 mod swap;
 mod teleport;
+
+use crate::*;
+emulated_integration_tests_common::include_penpal_create_foreign_asset_on_asset_hub!(
+	PenpalA,
+	AssetHubRococo,
+	ROCOCO_ED,
+	parachains_common::rococo::fee::WeightToFee
+);
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 18483762ae1..e6142e29b7c 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
@@ -15,14 +15,12 @@
 
 use crate::*;
 use asset_hub_rococo_runtime::xcm_config::XcmConfig as AssetHubRococoXcmConfig;
-use penpal_runtime::xcm_config::XcmConfig as PenpalRococoXcmConfig;
 use rococo_runtime::xcm_config::XcmConfig as RococoXcmConfig;
+use rococo_system_emulated_network::penpal_emulated_chain::XcmConfig as PenpalRococoXcmConfig;
 
 fn relay_to_para_sender_assertions(t: RelayToParaTest) {
 	type RuntimeEvent = <Rococo as Chain>::RuntimeEvent;
-
 	Rococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(864_610_000, 8_799)));
-
 	assert_expected_events!(
 		Rococo,
 		vec![
@@ -42,12 +40,10 @@ fn relay_to_para_sender_assertions(t: RelayToParaTest) {
 
 fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) {
 	type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
-
 	AssetHubRococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(
 		864_610_000,
 		8_799,
 	)));
-
 	assert_expected_events!(
 		AssetHubRococo,
 		vec![
@@ -80,9 +76,7 @@ fn para_receiver_assertions<Test>(_: Test) {
 
 fn para_to_system_para_sender_assertions(t: ParaToSystemParaTest) {
 	type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
-
 	PenpalA::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(864_610_000, 8_799)));
-
 	assert_expected_events!(
 		PenpalA,
 		vec![
@@ -99,15 +93,13 @@ fn para_to_system_para_sender_assertions(t: ParaToSystemParaTest) {
 
 fn para_to_system_para_receiver_assertions(t: ParaToSystemParaTest) {
 	type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
-
 	let sov_penpal_on_ahr = AssetHubRococo::sovereign_account_id_of(
 		AssetHubRococo::sibling_location_of(PenpalA::para_id()),
 	);
-
 	assert_expected_events!(
 		AssetHubRococo,
 		vec![
-			// Amount to reserve transfer is transferred to Parachain's Sovereign account
+			// Amount to reserve transfer is withdrawn from Parachain's Sovereign account
 			RuntimeEvent::Balances(
 				pallet_balances::Event::Withdraw { who, amount }
 			) => {
@@ -124,12 +116,10 @@ fn para_to_system_para_receiver_assertions(t: ParaToSystemParaTest) {
 
 fn system_para_to_para_assets_sender_assertions(t: SystemParaToParaTest) {
 	type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
-
 	AssetHubRococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(
 		864_610_000,
 		8799,
 	)));
-
 	assert_expected_events!(
 		AssetHubRococo,
 		vec![
@@ -162,7 +152,7 @@ fn system_para_to_para_assets_receiver_assertions<Test>(_: Test) {
 	);
 }
 
-fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
+fn relay_to_para_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
 	<Rococo as RococoPallet>::XcmPallet::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -173,7 +163,7 @@ fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> Dispatch
 	)
 }
 
-fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
+fn system_para_to_para_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
 	<AssetHubRococo as AssetHubRococoPallet>::PolkadotXcm::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -184,7 +174,7 @@ fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest)
 	)
 }
 
-fn para_to_system_para_limited_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
+fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
 	<PenpalA as PenpalAPallet>::PolkadotXcm::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -285,7 +275,7 @@ fn reserve_transfer_native_asset_from_relay_to_para() {
 
 	test.set_assertion::<Rococo>(relay_to_para_sender_assertions);
 	test.set_assertion::<PenpalA>(para_receiver_assertions);
-	test.set_dispatchable::<Rococo>(relay_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<Rococo>(relay_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let delivery_fees = Rococo::execute_with(|| {
@@ -329,7 +319,7 @@ fn reserve_transfer_native_asset_from_system_para_to_para() {
 
 	test.set_assertion::<AssetHubRococo>(system_para_to_para_sender_assertions);
 	test.set_assertion::<PenpalA>(para_receiver_assertions);
-	test.set_dispatchable::<AssetHubRococo>(system_para_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<AssetHubRococo>(system_para_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
@@ -379,7 +369,7 @@ fn reserve_transfer_native_asset_from_para_to_system_para() {
 
 	test.set_assertion::<PenpalA>(para_to_system_para_sender_assertions);
 	test.set_assertion::<AssetHubRococo>(para_to_system_para_receiver_assertions);
-	test.set_dispatchable::<PenpalA>(para_to_system_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<PenpalA>(para_to_system_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
@@ -474,7 +464,7 @@ fn reserve_transfer_assets_from_system_para_to_para() {
 
 	test.set_assertion::<AssetHubRococo>(system_para_to_para_assets_sender_assertions);
 	test.set_assertion::<PenpalA>(system_para_to_para_assets_receiver_assertions);
-	test.set_dispatchable::<AssetHubRococo>(system_para_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<AssetHubRococo>(system_para_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/swap.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/swap.rs
index 9e691bf02f1..35a660ed3c4 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/swap.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/swap.rs
@@ -14,9 +14,10 @@
 // limitations under the License.
 
 use crate::*;
-use frame_support::{instances::Instance2, BoundedVec};
+use frame_support::BoundedVec;
 use parachains_common::rococo::currency::EXISTENTIAL_DEPOSIT;
-use sp_runtime::{DispatchError, ModuleError};
+use rococo_system_emulated_network::penpal_emulated_chain::LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub;
+use sp_runtime::ModuleError;
 
 #[test]
 fn swap_locally_on_chain_using_local_assets() {
@@ -112,114 +113,37 @@ fn swap_locally_on_chain_using_local_assets() {
 
 #[test]
 fn swap_locally_on_chain_using_foreign_assets() {
-	use frame_support::weights::WeightToFee;
-
 	let asset_native = Box::new(asset_hub_rococo_runtime::xcm_config::TokenLocation::get());
+	let ah_as_seen_by_penpal = PenpalA::sibling_location_of(AssetHubRococo::para_id());
+	let asset_location_on_penpal = PenpalLocalTeleportableToAssetHub::get();
+	let asset_id_on_penpal = match asset_location_on_penpal.last() {
+		Some(GeneralIndex(id)) => *id as u32,
+		_ => unreachable!(),
+	};
+	let asset_owner_on_penpal = PenpalASender::get();
+	let foreign_asset_at_asset_hub_rococo =
+		MultiLocation { parents: 1, interior: X1(Parachain(PenpalA::para_id().into())) }
+			.appended_with(asset_location_on_penpal)
+			.unwrap();
+
+	// 1. Create asset on penpal and, 2. Create foreign asset on asset_hub_rococo
+	super::penpal_create_foreign_asset_on_asset_hub(
+		asset_id_on_penpal,
+		foreign_asset_at_asset_hub_rococo,
+		ah_as_seen_by_penpal,
+		true,
+		asset_owner_on_penpal,
+		ASSET_MIN_BALANCE * 1_000_000,
+	);
 
-	let foreign_asset1_at_asset_hub_rococo = Box::new(MultiLocation {
-		parents: 1,
-		interior: X3(
-			Parachain(PenpalA::para_id().into()),
-			PalletInstance(ASSETS_PALLET_ID),
-			GeneralIndex(ASSET_ID.into()),
-		),
-	});
-
-	let assets_para_destination: VersionedMultiLocation =
-		MultiLocation { parents: 1, interior: X1(Parachain(AssetHubRococo::para_id().into())) }
-			.into();
-
-	let penpal_location =
-		MultiLocation { parents: 1, interior: X1(Parachain(PenpalA::para_id().into())) };
-
-	// 1. Create asset on penpal:
-	PenpalA::execute_with(|| {
-		assert_ok!(<PenpalA as PenpalAPallet>::Assets::create(
-			<PenpalA as Chain>::RuntimeOrigin::signed(PenpalASender::get()),
-			ASSET_ID.into(),
-			PenpalASender::get().into(),
-			1000,
-		));
-
-		assert!(<PenpalA as PenpalAPallet>::Assets::asset_exists(ASSET_ID));
-	});
-
-	// 2. Create foreign asset on asset_hub_rococo:
-
-	let require_weight_at_most = Weight::from_parts(1_100_000_000_000, 30_000);
-	let origin_kind = OriginKind::Xcm;
-	let sov_penpal_on_asset_hub_rococo = AssetHubRococo::sovereign_account_id_of(penpal_location);
-
+	let penpal_as_seen_by_ah = AssetHubRococo::sibling_location_of(PenpalA::para_id());
+	let sov_penpal_on_ahr = AssetHubRococo::sovereign_account_id_of(penpal_as_seen_by_ah);
 	AssetHubRococo::fund_accounts(vec![
 		(AssetHubRococoSender::get().into(), 5_000_000 * ROCOCO_ED), /* An account to swap dot
 		                                                              * for something else. */
-		(sov_penpal_on_asset_hub_rococo.clone().into(), 1000_000_000_000_000_000 * ROCOCO_ED),
 	]);
 
-	let sov_penpal_on_asset_hub_rococo_as_location: MultiLocation = MultiLocation {
-		parents: 0,
-		interior: X1(AccountId32Junction {
-			network: None,
-			id: sov_penpal_on_asset_hub_rococo.clone().into(),
-		}),
-	};
-
-	let call_foreign_assets_create =
-		<AssetHubRococo as Chain>::RuntimeCall::ForeignAssets(pallet_assets::Call::<
-			<AssetHubRococo as Chain>::Runtime,
-			Instance2,
-		>::create {
-			id: *foreign_asset1_at_asset_hub_rococo,
-			min_balance: 1000,
-			admin: sov_penpal_on_asset_hub_rococo.clone().into(),
-		})
-		.encode()
-		.into();
-
-	let buy_execution_fee_amount = parachains_common::rococo::fee::WeightToFee::weight_to_fee(
-		&Weight::from_parts(10_100_000_000_000, 300_000),
-	);
-	let buy_execution_fee = MultiAsset {
-		id: Concrete(MultiLocation { parents: 1, interior: Here }),
-		fun: Fungible(buy_execution_fee_amount),
-	};
-
-	let xcm = VersionedXcm::from(Xcm(vec![
-		WithdrawAsset { 0: vec![buy_execution_fee.clone()].into() },
-		BuyExecution { fees: buy_execution_fee.clone(), weight_limit: Unlimited },
-		Transact { require_weight_at_most, origin_kind, call: call_foreign_assets_create },
-		RefundSurplus,
-		DepositAsset {
-			assets: All.into(),
-			beneficiary: sov_penpal_on_asset_hub_rococo_as_location,
-		},
-	]));
-
-	// Send XCM message from penpal => asset_hub_rococo
-	let sudo_penpal_origin = <PenpalA as Chain>::RuntimeOrigin::root();
-	PenpalA::execute_with(|| {
-		assert_ok!(<PenpalA as PenpalAPallet>::PolkadotXcm::send(
-			sudo_penpal_origin.clone(),
-			bx!(assets_para_destination.clone()),
-			bx!(xcm),
-		));
-
-		type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
-
-		assert_expected_events!(
-			PenpalA,
-			vec![
-				RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { .. }) => {},
-			]
-		);
-	});
-
-	// Receive XCM message in Assets Parachain
 	AssetHubRococo::execute_with(|| {
-		assert!(<AssetHubRococo as AssetHubRococoPallet>::ForeignAssets::asset_exists(
-			*foreign_asset1_at_asset_hub_rococo
-		));
-
 		// 3: Mint foreign asset on asset_hub_rococo:
 		//
 		// (While it might be nice to use batch,
@@ -228,11 +152,9 @@ fn swap_locally_on_chain_using_foreign_assets() {
 		type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
 		// 3. Mint foreign asset (in reality this should be a teleport or some such)
 		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::ForeignAssets::mint(
-			<AssetHubRococo as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_rococo.clone().into()
-			),
-			*foreign_asset1_at_asset_hub_rococo,
-			sov_penpal_on_asset_hub_rococo.clone().into(),
+			<AssetHubRococo as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahr.clone().into()),
+			foreign_asset_at_asset_hub_rococo,
+			sov_penpal_on_ahr.clone().into(),
 			3_000_000_000_000,
 		));
 
@@ -243,11 +165,12 @@ fn swap_locally_on_chain_using_foreign_assets() {
 			]
 		);
 
+		let foreign_asset_at_asset_hub_rococo = Box::new(foreign_asset_at_asset_hub_rococo);
 		// 4. Create pool:
 		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::create_pool(
 			<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_rococo.clone(),
+			foreign_asset_at_asset_hub_rococo.clone(),
 		));
 
 		assert_expected_events!(
@@ -259,16 +182,14 @@ fn swap_locally_on_chain_using_foreign_assets() {
 
 		// 5. Add liquidity:
 		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::add_liquidity(
-			<AssetHubRococo as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_rococo.clone()
-			),
+			<AssetHubRococo as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahr.clone()),
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_rococo.clone(),
+			foreign_asset_at_asset_hub_rococo.clone(),
 			1_000_000_000_000,
 			2_000_000_000_000,
 			0,
 			0,
-			sov_penpal_on_asset_hub_rococo.clone().into()
+			sov_penpal_on_ahr.clone().into()
 		));
 
 		assert_expected_events!(
@@ -283,7 +204,7 @@ fn swap_locally_on_chain_using_foreign_assets() {
 		// 6. Swap!
 		let path = BoundedVec::<_, _>::truncate_from(vec![
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_rococo.clone(),
+			foreign_asset_at_asset_hub_rococo.clone(),
 		]);
 
 		assert_ok!(
@@ -309,15 +230,13 @@ fn swap_locally_on_chain_using_foreign_assets() {
 
 		// 7. Remove liquidity
 		assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::remove_liquidity(
-			<AssetHubRococo as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_rococo.clone()
-			),
+			<AssetHubRococo as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahr.clone()),
 			asset_native,
-			foreign_asset1_at_asset_hub_rococo,
+			foreign_asset_at_asset_hub_rococo,
 			1414213562273 - 2_000_000_000, // all but the 2 EDs can't be retrieved.
 			0,
 			0,
-			sov_penpal_on_asset_hub_rococo.clone().into(),
+			sov_penpal_on_ahr.clone().into(),
 		));
 	});
 }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs
index f8017f7a1c5..e64c02f5258 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs
@@ -15,7 +15,9 @@
 
 use crate::*;
 use asset_hub_rococo_runtime::xcm_config::XcmConfig as AssetHubRococoXcmConfig;
+use emulated_integration_tests_common::xcm_helpers::non_fee_asset;
 use rococo_runtime::xcm_config::XcmConfig as RococoXcmConfig;
+use rococo_system_emulated_network::penpal_emulated_chain::LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub;
 
 fn relay_origin_assertions(t: RelayToSystemParaTest) {
 	type RuntimeEvent = <Rococo as Chain>::RuntimeEvent;
@@ -110,6 +112,123 @@ fn para_dest_assertions(t: RelayToSystemParaTest) {
 	);
 }
 
+fn penpal_to_ah_foreign_assets_sender_assertions(t: ParaToSystemParaTest) {
+	type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
+	PenpalA::assert_xcm_pallet_attempted_complete(None);
+	let expected_asset_id = t.args.asset_id.unwrap();
+	let (_, expected_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		PenpalA,
+		vec![
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Withdraw { who, amount }
+			) => {
+				who: *who == t.sender.account_id,
+				amount: *amount == t.args.amount,
+			},
+			RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == t.sender.account_id,
+				balance: *balance == expected_asset_amount,
+			},
+		]
+	);
+}
+
+fn penpal_to_ah_foreign_assets_receiver_assertions(t: ParaToSystemParaTest) {
+	type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
+	let sov_penpal_on_ahr = AssetHubRococo::sovereign_account_id_of(
+		AssetHubRococo::sibling_location_of(PenpalA::para_id()),
+	);
+	let (expected_foreign_asset_id, expected_foreign_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		AssetHubRococo,
+		vec![
+			// native asset reserve transfer for paying fees, withdrawn from Penpal's sov account
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Withdraw { who, amount }
+			) => {
+				who: *who == sov_penpal_on_ahr.clone().into(),
+				amount: *amount == t.args.amount,
+			},
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { who, .. }) => {
+				who: *who == t.receiver.account_id,
+			},
+			RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, amount }) => {
+				asset_id: *asset_id == expected_foreign_asset_id,
+				owner: *owner == t.receiver.account_id,
+				amount: *amount == expected_foreign_asset_amount,
+			},
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { .. }) => {},
+			RuntimeEvent::MessageQueue(
+				pallet_message_queue::Event::Processed { success: true, .. }
+			) => {},
+		]
+	);
+}
+
+fn ah_to_penpal_foreign_assets_sender_assertions(t: SystemParaToParaTest) {
+	type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
+	AssetHubRococo::assert_xcm_pallet_attempted_complete(None);
+	let (expected_foreign_asset_id, expected_foreign_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		AssetHubRococo,
+		vec![
+			// native asset used for fees is transferred to Parachain's Sovereign account as reserve
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Transfer { from, to, amount }
+			) => {
+				from: *from == t.sender.account_id,
+				to: *to == AssetHubRococo::sovereign_account_id_of(
+					t.args.dest
+				),
+				amount: *amount == t.args.amount,
+			},
+			// foreign asset is burned locally as part of teleportation
+			RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_foreign_asset_id,
+				owner: *owner == t.sender.account_id,
+				balance: *balance == expected_foreign_asset_amount,
+			},
+		]
+	);
+}
+
+fn ah_to_penpal_foreign_assets_receiver_assertions(t: SystemParaToParaTest) {
+	type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
+	let expected_asset_id = t.args.asset_id.unwrap();
+	let (_, expected_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	let checking_account = <PenpalA as PenpalAPallet>::PolkadotXcm::check_account();
+	assert_expected_events!(
+		PenpalA,
+		vec![
+			// checking account burns local asset as part of incoming teleport
+			RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == checking_account,
+				balance: *balance == expected_asset_amount,
+			},
+			// local asset is teleported into account of receiver
+			RuntimeEvent::Assets(pallet_assets::Event::Issued { asset_id, owner, amount }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == t.receiver.account_id,
+				amount: *amount == expected_asset_amount,
+			},
+			// native asset for fee is deposited to receiver
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { who, .. }) => {
+				who: *who == t.receiver.account_id,
+			},
+			RuntimeEvent::MessageQueue(
+				pallet_message_queue::Event::Processed { success: true, .. }
+			) => {},
+		]
+	);
+}
+
 fn relay_limited_teleport_assets(t: RelayToSystemParaTest) -> DispatchResult {
 	<Rococo as RococoPallet>::XcmPallet::limited_teleport_assets(
 		t.signed_origin,
@@ -152,6 +271,28 @@ fn system_para_teleport_assets(t: SystemParaToRelayTest) -> DispatchResult {
 	)
 }
 
+fn para_to_system_para_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
+	<PenpalA as PenpalAPallet>::PolkadotXcm::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 system_para_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
+	<AssetHubRococo as AssetHubRococoPallet>::PolkadotXcm::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,
+	)
+}
+
 /// Limited Teleport of native asset from Relay Chain to the System Parachain should work
 #[test]
 fn limited_teleport_native_assets_from_relay_to_system_para_works() {
@@ -410,3 +551,199 @@ fn teleport_to_other_system_parachains_works() {
 		(native_asset, amount)
 	);
 }
+
+/// Bidirectional teleports of local Penpal assets to Asset Hub as foreign assets should work
+/// (using native reserve-based transfer for fees)
+#[test]
+fn bidirectional_teleport_foreign_assets_between_para_and_asset_hub() {
+	let ah_as_seen_by_penpal = PenpalA::sibling_location_of(AssetHubRococo::para_id());
+	let asset_location_on_penpal = PenpalLocalTeleportableToAssetHub::get();
+	let asset_id_on_penpal = match asset_location_on_penpal.last() {
+		Some(GeneralIndex(id)) => *id as u32,
+		_ => unreachable!(),
+	};
+	let asset_owner_on_penpal = PenpalASender::get();
+	let foreign_asset_at_asset_hub_rococo =
+		MultiLocation { parents: 1, interior: X1(Parachain(PenpalA::para_id().into())) }
+			.appended_with(asset_location_on_penpal)
+			.unwrap();
+	super::penpal_create_foreign_asset_on_asset_hub(
+		asset_id_on_penpal,
+		foreign_asset_at_asset_hub_rococo,
+		ah_as_seen_by_penpal,
+		false,
+		asset_owner_on_penpal,
+		ASSET_MIN_BALANCE * 1_000_000,
+	);
+	let penpal_to_ah_beneficiary_id = AssetHubRococoReceiver::get();
+
+	let fee_amount_to_send = ASSET_HUB_ROCOCO_ED * 10_000;
+	let asset_amount_to_send = ASSET_MIN_BALANCE * 1000;
+
+	let penpal_assets: MultiAssets = vec![
+		(Parent, fee_amount_to_send).into(),
+		(asset_location_on_penpal, asset_amount_to_send).into(),
+	]
+	.into();
+	let fee_asset_index = penpal_assets
+		.inner()
+		.iter()
+		.position(|r| r == &(Parent, fee_amount_to_send).into())
+		.unwrap() as u32;
+
+	// Penpal to AH test args
+	let penpal_to_ah_test_args = TestContext {
+		sender: PenpalASender::get(),
+		receiver: AssetHubRococoReceiver::get(),
+		args: para_test_args(
+			ah_as_seen_by_penpal,
+			penpal_to_ah_beneficiary_id,
+			asset_amount_to_send,
+			penpal_assets,
+			Some(asset_id_on_penpal),
+			fee_asset_index,
+		),
+	};
+	let mut penpal_to_ah = ParaToSystemParaTest::new(penpal_to_ah_test_args);
+
+	let penpal_sender_balance_before = penpal_to_ah.sender.balance;
+	let ah_receiver_balance_before = penpal_to_ah.receiver.balance;
+
+	let penpal_sender_assets_before = PenpalA::execute_with(|| {
+		type Assets = <PenpalA as PenpalAPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalASender::get())
+	});
+	let ah_receiver_assets_before = AssetHubRococo::execute_with(|| {
+		type Assets = <AssetHubRococo as AssetHubRococoPallet>::ForeignAssets;
+		<Assets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_rococo,
+			&AssetHubRococoReceiver::get(),
+		)
+	});
+
+	penpal_to_ah.set_assertion::<PenpalA>(penpal_to_ah_foreign_assets_sender_assertions);
+	penpal_to_ah.set_assertion::<AssetHubRococo>(penpal_to_ah_foreign_assets_receiver_assertions);
+	penpal_to_ah.set_dispatchable::<PenpalA>(para_to_system_para_transfer_assets);
+	penpal_to_ah.assert();
+
+	let penpal_sender_balance_after = penpal_to_ah.sender.balance;
+	let ah_receiver_balance_after = penpal_to_ah.receiver.balance;
+
+	let penpal_sender_assets_after = PenpalA::execute_with(|| {
+		type Assets = <PenpalA as PenpalAPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalASender::get())
+	});
+	let ah_receiver_assets_after = AssetHubRococo::execute_with(|| {
+		type Assets = <AssetHubRococo as AssetHubRococoPallet>::ForeignAssets;
+		<Assets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_rococo,
+			&AssetHubRococoReceiver::get(),
+		)
+	});
+
+	// Sender's balance is reduced
+	assert!(penpal_sender_balance_after < penpal_sender_balance_before);
+	// Receiver's balance is increased
+	assert!(ah_receiver_balance_after > ah_receiver_balance_before);
+	// Receiver's balance increased by `amount_to_send - delivery_fees - bought_execution`;
+	// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
+	// should be non-zero
+	assert!(ah_receiver_balance_after < ah_receiver_balance_before + fee_amount_to_send);
+
+	// Sender's balance is reduced by exact amount
+	assert_eq!(penpal_sender_assets_before - asset_amount_to_send, penpal_sender_assets_after);
+	// Receiver's balance is increased by exact amount
+	assert_eq!(ah_receiver_assets_after, ah_receiver_assets_before + asset_amount_to_send);
+
+	///////////////////////////////////////////////////////////////////////
+	// Now test transferring foreign assets back from AssetHub to Penpal //
+	///////////////////////////////////////////////////////////////////////
+
+	// Move funds on AH from AHReceiver to AHSender
+	AssetHubRococo::execute_with(|| {
+		type ForeignAssets = <AssetHubRococo as AssetHubRococoPallet>::ForeignAssets;
+		assert_ok!(ForeignAssets::transfer(
+			<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoReceiver::get()),
+			foreign_asset_at_asset_hub_rococo,
+			AssetHubRococoSender::get().into(),
+			asset_amount_to_send,
+		));
+	});
+
+	let ah_to_penpal_beneficiary_id = PenpalAReceiver::get();
+	let penpal_as_seen_by_ah = AssetHubRococo::sibling_location_of(PenpalA::para_id());
+	let ah_assets: MultiAssets = vec![
+		(Parent, fee_amount_to_send).into(),
+		(foreign_asset_at_asset_hub_rococo, asset_amount_to_send).into(),
+	]
+	.into();
+	let fee_asset_index = ah_assets
+		.inner()
+		.iter()
+		.position(|r| r == &(Parent, fee_amount_to_send).into())
+		.unwrap() as u32;
+
+	// AH to Penpal test args
+	let ah_to_penpal_test_args = TestContext {
+		sender: AssetHubRococoSender::get(),
+		receiver: PenpalAReceiver::get(),
+		args: para_test_args(
+			penpal_as_seen_by_ah,
+			ah_to_penpal_beneficiary_id,
+			asset_amount_to_send,
+			ah_assets,
+			Some(asset_id_on_penpal),
+			fee_asset_index,
+		),
+	};
+	let mut ah_to_penpal = SystemParaToParaTest::new(ah_to_penpal_test_args);
+
+	let ah_sender_balance_before = ah_to_penpal.sender.balance;
+	let penpal_receiver_balance_before = ah_to_penpal.receiver.balance;
+
+	let ah_sender_assets_before = AssetHubRococo::execute_with(|| {
+		type ForeignAssets = <AssetHubRococo as AssetHubRococoPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_rococo,
+			&AssetHubRococoSender::get(),
+		)
+	});
+	let penpal_receiver_assets_before = PenpalA::execute_with(|| {
+		type Assets = <PenpalA as PenpalAPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalAReceiver::get())
+	});
+
+	ah_to_penpal.set_assertion::<AssetHubRococo>(ah_to_penpal_foreign_assets_sender_assertions);
+	ah_to_penpal.set_assertion::<PenpalA>(ah_to_penpal_foreign_assets_receiver_assertions);
+	ah_to_penpal.set_dispatchable::<AssetHubRococo>(system_para_to_para_transfer_assets);
+	ah_to_penpal.assert();
+
+	let ah_sender_balance_after = ah_to_penpal.sender.balance;
+	let penpal_receiver_balance_after = ah_to_penpal.receiver.balance;
+
+	let ah_sender_assets_after = AssetHubRococo::execute_with(|| {
+		type ForeignAssets = <AssetHubRococo as AssetHubRococoPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_rococo,
+			&AssetHubRococoSender::get(),
+		)
+	});
+	let penpal_receiver_assets_after = PenpalA::execute_with(|| {
+		type Assets = <PenpalA as PenpalAPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalAReceiver::get())
+	});
+
+	// Sender's balance is reduced
+	assert!(ah_sender_balance_after < ah_sender_balance_before);
+	// Receiver's balance is increased
+	assert!(penpal_receiver_balance_after > penpal_receiver_balance_before);
+	// Receiver's balance increased by `amount_to_send - delivery_fees - bought_execution`;
+	// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
+	// should be non-zero
+	assert!(penpal_receiver_balance_after < penpal_receiver_balance_before + fee_amount_to_send);
+
+	// Sender's balance is reduced by exact amount
+	assert_eq!(ah_sender_assets_before - asset_amount_to_send, ah_sender_assets_after);
+	// Receiver's balance is increased by exact amount
+	assert_eq!(penpal_receiver_assets_after, penpal_receiver_assets_before + asset_amount_to_send);
+}
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
index 74e6660d4d4..cdaa65f02cb 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
@@ -38,5 +38,4 @@ asset-test-utils = { path = "../../../../../runtimes/assets/test-utils" }
 cumulus-pallet-dmp-queue = { default-features = false, path = "../../../../../../pallets/dmp-queue" }
 cumulus-pallet-parachain-system = { default-features = false, path = "../../../../../../pallets/parachain-system" }
 emulated-integration-tests-common = { path = "../../../common", default-features = false }
-penpal-runtime = { path = "../../../../../runtimes/testing/penpal" }
 westend-system-emulated-network = { path = "../../../networks/westend-system" }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
index 0c9de89c5f9..d2127b63048 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
@@ -19,3 +19,11 @@ mod set_xcm_versions;
 mod swap;
 mod teleport;
 mod treasury;
+
+use crate::*;
+emulated_integration_tests_common::include_penpal_create_foreign_asset_on_asset_hub!(
+	PenpalB,
+	AssetHubWestend,
+	WESTEND_ED,
+	parachains_common::westend::fee::WeightToFee
+);
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 1a69a4f3f71..7472445c4ba 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
@@ -15,8 +15,8 @@
 
 use crate::*;
 use asset_hub_westend_runtime::xcm_config::XcmConfig as AssetHubWestendXcmConfig;
-use penpal_runtime::xcm_config::XcmConfig as PenpalWestendXcmConfig;
 use westend_runtime::xcm_config::XcmConfig as WestendXcmConfig;
+use westend_system_emulated_network::penpal_emulated_chain::XcmConfig as PenpalWestendXcmConfig;
 
 fn relay_to_para_sender_assertions(t: RelayToParaTest) {
 	type RuntimeEvent = <Westend as Chain>::RuntimeEvent;
@@ -162,7 +162,7 @@ fn system_para_to_para_assets_receiver_assertions<Test>(_: Test) {
 	);
 }
 
-fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
+fn relay_to_para_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
 	<Westend as WestendPallet>::XcmPallet::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -173,7 +173,7 @@ fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> Dispatch
 	)
 }
 
-fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
+fn system_para_to_para_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
 	<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -184,7 +184,7 @@ fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest)
 	)
 }
 
-fn para_to_system_para_limited_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
+fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
 	<PenpalB as PenpalBPallet>::PolkadotXcm::limited_reserve_transfer_assets(
 		t.signed_origin,
 		bx!(t.args.dest.into()),
@@ -284,7 +284,7 @@ fn reserve_transfer_native_asset_from_relay_to_para() {
 
 	test.set_assertion::<Westend>(relay_to_para_sender_assertions);
 	test.set_assertion::<PenpalB>(para_receiver_assertions);
-	test.set_dispatchable::<Westend>(relay_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<Westend>(relay_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let delivery_fees = Westend::execute_with(|| {
@@ -328,7 +328,7 @@ fn reserve_transfer_native_asset_from_system_para_to_para() {
 
 	test.set_assertion::<AssetHubWestend>(system_para_to_para_sender_assertions);
 	test.set_assertion::<PenpalB>(para_receiver_assertions);
-	test.set_dispatchable::<AssetHubWestend>(system_para_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<AssetHubWestend>(system_para_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
@@ -379,7 +379,7 @@ fn reserve_transfer_native_asset_from_para_to_system_para() {
 
 	test.set_assertion::<PenpalB>(para_to_system_para_sender_assertions);
 	test.set_assertion::<AssetHubWestend>(para_to_system_para_receiver_assertions);
-	test.set_dispatchable::<PenpalB>(para_to_system_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<PenpalB>(para_to_system_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
@@ -474,7 +474,7 @@ fn reserve_transfer_assets_from_system_para_to_para() {
 
 	test.set_assertion::<AssetHubWestend>(system_para_to_para_assets_sender_assertions);
 	test.set_assertion::<PenpalB>(system_para_to_para_assets_receiver_assertions);
-	test.set_dispatchable::<AssetHubWestend>(system_para_to_para_limited_reserve_transfer_assets);
+	test.set_dispatchable::<AssetHubWestend>(system_para_to_para_reserve_transfer_assets);
 	test.assert();
 
 	let sender_balance_after = test.sender.balance;
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/swap.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/swap.rs
index aca60f68802..1fa77bd4565 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/swap.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/swap.rs
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 use crate::*;
+use westend_system_emulated_network::penpal_emulated_chain::LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub;
 
 #[test]
 fn swap_locally_on_chain_using_local_assets() {
@@ -107,113 +108,37 @@ fn swap_locally_on_chain_using_local_assets() {
 
 #[test]
 fn swap_locally_on_chain_using_foreign_assets() {
-	use frame_support::weights::WeightToFee;
-
 	let asset_native = Box::new(asset_hub_westend_runtime::xcm_config::WestendLocation::get());
-
-	let foreign_asset1_at_asset_hub_westend = Box::new(MultiLocation {
-		parents: 1,
-		interior: X3(
-			Parachain(PenpalB::para_id().into()),
-			PalletInstance(ASSETS_PALLET_ID),
-			GeneralIndex(ASSET_ID.into()),
-		),
-	});
-
-	let assets_para_destination: VersionedMultiLocation =
-		MultiLocation { parents: 1, interior: X1(Parachain(AssetHubWestend::para_id().into())) }
-			.into();
-
-	let penpal_location =
-		MultiLocation { parents: 1, interior: X1(Parachain(PenpalB::para_id().into())) };
-
-	// 1. Create asset on penpal:
-	PenpalB::execute_with(|| {
-		assert_ok!(<PenpalB as PenpalBPallet>::Assets::create(
-			<PenpalB as Chain>::RuntimeOrigin::signed(PenpalBSender::get()),
-			ASSET_ID.into(),
-			PenpalBSender::get().into(),
-			1000,
-		));
-
-		assert!(<PenpalB as PenpalBPallet>::Assets::asset_exists(ASSET_ID));
-	});
-
-	// 2. Create foreign asset on asset_hub_westend:
-
-	let require_weight_at_most = Weight::from_parts(1_100_000_000_000, 30_000);
-	let origin_kind = OriginKind::Xcm;
-	let sov_penpal_on_asset_hub_westend = AssetHubWestend::sovereign_account_id_of(penpal_location);
-
-	AssetHubWestend::fund_accounts(vec![
-		(AssetHubWestendSender::get().into(), 5_000_000 * WESTEND_ED),
-		(sov_penpal_on_asset_hub_westend.clone().into(), 1000_000_000_000_000_000 * WESTEND_ED),
-	]);
-
-	let sov_penpal_on_asset_hub_westend_as_location: MultiLocation = MultiLocation {
-		parents: 0,
-		interior: X1(AccountId32Junction {
-			network: None,
-			id: sov_penpal_on_asset_hub_westend.clone().into(),
-		}),
+	let ah_as_seen_by_penpal = PenpalB::sibling_location_of(AssetHubWestend::para_id());
+	let asset_location_on_penpal = PenpalLocalTeleportableToAssetHub::get();
+	let asset_id_on_penpal = match asset_location_on_penpal.last() {
+		Some(GeneralIndex(id)) => *id as u32,
+		_ => unreachable!(),
 	};
-
-	let call_foreign_assets_create =
-		<AssetHubWestend as Chain>::RuntimeCall::ForeignAssets(pallet_assets::Call::<
-			<AssetHubWestend as Chain>::Runtime,
-			Instance2,
-		>::create {
-			id: *foreign_asset1_at_asset_hub_westend,
-			min_balance: 1000,
-			admin: sov_penpal_on_asset_hub_westend.clone().into(),
-		})
-		.encode()
-		.into();
-
-	let buy_execution_fee_amount = parachains_common::westend::fee::WeightToFee::weight_to_fee(
-		&Weight::from_parts(10_100_000_000_000, 300_000),
+	let asset_owner_on_penpal = PenpalBSender::get();
+	let foreign_asset_at_asset_hub_westend =
+		MultiLocation { parents: 1, interior: X1(Parachain(PenpalB::para_id().into())) }
+			.appended_with(asset_location_on_penpal)
+			.unwrap();
+
+	// 1. Create asset on penpal and, 2. Create foreign asset on asset_hub_westend
+	super::penpal_create_foreign_asset_on_asset_hub(
+		asset_id_on_penpal,
+		foreign_asset_at_asset_hub_westend,
+		ah_as_seen_by_penpal,
+		true,
+		asset_owner_on_penpal,
+		ASSET_MIN_BALANCE * 1_000_000,
 	);
-	let buy_execution_fee = MultiAsset {
-		id: Concrete(MultiLocation { parents: 1, interior: Here }),
-		fun: Fungible(buy_execution_fee_amount),
-	};
-
-	let xcm = VersionedXcm::from(Xcm(vec![
-		WithdrawAsset { 0: vec![buy_execution_fee.clone()].into() },
-		BuyExecution { fees: buy_execution_fee.clone(), weight_limit: Unlimited },
-		Transact { require_weight_at_most, origin_kind, call: call_foreign_assets_create },
-		RefundSurplus,
-		DepositAsset {
-			assets: All.into(),
-			beneficiary: sov_penpal_on_asset_hub_westend_as_location,
-		},
-	]));
-
-	// Send XCM message from penpal => asset_hub_westend
-	let sudo_penpal_origin = <PenpalB as Chain>::RuntimeOrigin::root();
-	PenpalB::execute_with(|| {
-		assert_ok!(<PenpalB as PenpalBPallet>::PolkadotXcm::send(
-			sudo_penpal_origin.clone(),
-			bx!(assets_para_destination.clone()),
-			bx!(xcm),
-		));
-
-		type RuntimeEvent = <PenpalB as Chain>::RuntimeEvent;
 
-		assert_expected_events!(
-			PenpalB,
-			vec![
-				RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { .. }) => {},
-			]
-		);
-	});
+	let penpal_as_seen_by_ah = AssetHubWestend::sibling_location_of(PenpalB::para_id());
+	let sov_penpal_on_ahw = AssetHubWestend::sovereign_account_id_of(penpal_as_seen_by_ah);
+	AssetHubWestend::fund_accounts(vec![
+		(AssetHubWestendSender::get().into(), 5_000_000 * WESTEND_ED), /* An account to swap dot
+		                                                                * for something else. */
+	]);
 
-	// Receive XCM message in Assets Parachain in the next block.
 	AssetHubWestend::execute_with(|| {
-		assert!(<AssetHubWestend as AssetHubWestendPallet>::ForeignAssets::asset_exists(
-			*foreign_asset1_at_asset_hub_westend
-		));
-
 		// 3: Mint foreign asset on asset_hub_westend:
 		//
 		// (While it might be nice to use batch,
@@ -222,11 +147,9 @@ fn swap_locally_on_chain_using_foreign_assets() {
 		type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
 		// 3. Mint foreign asset (in reality this should be a teleport or some such)
 		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::ForeignAssets::mint(
-			<AssetHubWestend as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_westend.clone().into()
-			),
-			*foreign_asset1_at_asset_hub_westend,
-			sov_penpal_on_asset_hub_westend.clone().into(),
+			<AssetHubWestend as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahw.clone().into()),
+			foreign_asset_at_asset_hub_westend,
+			sov_penpal_on_ahw.clone().into(),
 			3_000_000_000_000,
 		));
 
@@ -237,11 +160,12 @@ fn swap_locally_on_chain_using_foreign_assets() {
 			]
 		);
 
+		let foreign_asset_at_asset_hub_westend = Box::new(foreign_asset_at_asset_hub_westend);
 		// 4. Create pool:
 		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::create_pool(
 			<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_westend.clone(),
+			foreign_asset_at_asset_hub_westend.clone(),
 		));
 
 		assert_expected_events!(
@@ -253,16 +177,14 @@ fn swap_locally_on_chain_using_foreign_assets() {
 
 		// 5. Add liquidity:
 		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::add_liquidity(
-			<AssetHubWestend as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_westend.clone()
-			),
+			<AssetHubWestend as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahw.clone()),
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_westend.clone(),
+			foreign_asset_at_asset_hub_westend.clone(),
 			1_000_000_000_000,
 			2_000_000_000_000,
 			0,
 			0,
-			sov_penpal_on_asset_hub_westend.clone().into()
+			sov_penpal_on_ahw.clone().into()
 		));
 
 		assert_expected_events!(
@@ -277,7 +199,7 @@ fn swap_locally_on_chain_using_foreign_assets() {
 		// 6. Swap!
 		let path = BoundedVec::<_, _>::truncate_from(vec![
 			asset_native.clone(),
-			foreign_asset1_at_asset_hub_westend.clone(),
+			foreign_asset_at_asset_hub_westend.clone(),
 		]);
 
 		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::swap_exact_tokens_for_tokens(
@@ -301,15 +223,13 @@ fn swap_locally_on_chain_using_foreign_assets() {
 
 		// 7. Remove liquidity
 		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::remove_liquidity(
-			<AssetHubWestend as Chain>::RuntimeOrigin::signed(
-				sov_penpal_on_asset_hub_westend.clone()
-			),
+			<AssetHubWestend as Chain>::RuntimeOrigin::signed(sov_penpal_on_ahw.clone()),
 			asset_native,
-			foreign_asset1_at_asset_hub_westend,
+			foreign_asset_at_asset_hub_westend,
 			1414213562273 - 2_000_000_000, // all but the 2 EDs can't be retrieved.
 			0,
 			0,
-			sov_penpal_on_asset_hub_westend.clone().into(),
+			sov_penpal_on_ahw.clone().into(),
 		));
 	});
 }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs
index 2c43bb9d801..2dd68ae3a83 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs
@@ -15,7 +15,9 @@
 
 use crate::*;
 use asset_hub_westend_runtime::xcm_config::XcmConfig as AssetHubWestendXcmConfig;
+use emulated_integration_tests_common::xcm_helpers::non_fee_asset;
 use westend_runtime::xcm_config::XcmConfig as WestendXcmConfig;
+use westend_system_emulated_network::penpal_emulated_chain::LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub;
 
 fn relay_origin_assertions(t: RelayToSystemParaTest) {
 	type RuntimeEvent = <Westend as Chain>::RuntimeEvent;
@@ -110,6 +112,123 @@ fn para_dest_assertions(t: RelayToSystemParaTest) {
 	);
 }
 
+fn penpal_to_ah_foreign_assets_sender_assertions(t: ParaToSystemParaTest) {
+	type RuntimeEvent = <PenpalB as Chain>::RuntimeEvent;
+	PenpalB::assert_xcm_pallet_attempted_complete(None);
+	let expected_asset_id = t.args.asset_id.unwrap();
+	let (_, expected_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		PenpalB,
+		vec![
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Withdraw { who, amount }
+			) => {
+				who: *who == t.sender.account_id,
+				amount: *amount == t.args.amount,
+			},
+			RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == t.sender.account_id,
+				balance: *balance == expected_asset_amount,
+			},
+		]
+	);
+}
+
+fn penpal_to_ah_foreign_assets_receiver_assertions(t: ParaToSystemParaTest) {
+	type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+	let sov_penpal_on_ahr = AssetHubWestend::sovereign_account_id_of(
+		AssetHubWestend::sibling_location_of(PenpalB::para_id()),
+	);
+	let (expected_foreign_asset_id, expected_foreign_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		AssetHubWestend,
+		vec![
+			// native asset reserve transfer for paying fees, withdrawn from Penpal's sov account
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Withdraw { who, amount }
+			) => {
+				who: *who == sov_penpal_on_ahr.clone().into(),
+				amount: *amount == t.args.amount,
+			},
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { who, .. }) => {
+				who: *who == t.receiver.account_id,
+			},
+			RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, amount }) => {
+				asset_id: *asset_id == expected_foreign_asset_id,
+				owner: *owner == t.receiver.account_id,
+				amount: *amount == expected_foreign_asset_amount,
+			},
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { .. }) => {},
+			RuntimeEvent::MessageQueue(
+				pallet_message_queue::Event::Processed { success: true, .. }
+			) => {},
+		]
+	);
+}
+
+fn ah_to_penpal_foreign_assets_sender_assertions(t: SystemParaToParaTest) {
+	type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+	AssetHubWestend::assert_xcm_pallet_attempted_complete(None);
+	let (expected_foreign_asset_id, expected_foreign_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	assert_expected_events!(
+		AssetHubWestend,
+		vec![
+			// native asset used for fees is transferred to Parachain's Sovereign account as reserve
+			RuntimeEvent::Balances(
+				pallet_balances::Event::Transfer { from, to, amount }
+			) => {
+				from: *from == t.sender.account_id,
+				to: *to == AssetHubWestend::sovereign_account_id_of(
+					t.args.dest
+				),
+				amount: *amount == t.args.amount,
+			},
+			// foreign asset is burned locally as part of teleportation
+			RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_foreign_asset_id,
+				owner: *owner == t.sender.account_id,
+				balance: *balance == expected_foreign_asset_amount,
+			},
+		]
+	);
+}
+
+fn ah_to_penpal_foreign_assets_receiver_assertions(t: SystemParaToParaTest) {
+	type RuntimeEvent = <PenpalB as Chain>::RuntimeEvent;
+	let expected_asset_id = t.args.asset_id.unwrap();
+	let (_, expected_asset_amount) =
+		non_fee_asset(&t.args.assets, t.args.fee_asset_item as usize).unwrap();
+	let checking_account = <PenpalB as PenpalBPallet>::PolkadotXcm::check_account();
+	assert_expected_events!(
+		PenpalB,
+		vec![
+			// checking account burns local asset as part of incoming teleport
+			RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == checking_account,
+				balance: *balance == expected_asset_amount,
+			},
+			// local asset is teleported into account of receiver
+			RuntimeEvent::Assets(pallet_assets::Event::Issued { asset_id, owner, amount }) => {
+				asset_id: *asset_id == expected_asset_id,
+				owner: *owner == t.receiver.account_id,
+				amount: *amount == expected_asset_amount,
+			},
+			// native asset for fee is deposited to receiver
+			RuntimeEvent::Balances(pallet_balances::Event::Deposit { who, .. }) => {
+				who: *who == t.receiver.account_id,
+			},
+			RuntimeEvent::MessageQueue(
+				pallet_message_queue::Event::Processed { success: true, .. }
+			) => {},
+		]
+	);
+}
+
 fn relay_limited_teleport_assets(t: RelayToSystemParaTest) -> DispatchResult {
 	<Westend as WestendPallet>::XcmPallet::limited_teleport_assets(
 		t.signed_origin,
@@ -152,6 +271,28 @@ fn system_para_teleport_assets(t: SystemParaToRelayTest) -> DispatchResult {
 	)
 }
 
+fn system_para_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
+	<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::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_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
+	<PenpalB as PenpalBPallet>::PolkadotXcm::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,
+	)
+}
+
 /// Limited Teleport of native asset from Relay Chain to the System Parachain should work
 #[test]
 fn limited_teleport_native_assets_from_relay_to_system_para_works() {
@@ -410,3 +551,199 @@ fn teleport_to_other_system_parachains_works() {
 		(native_asset, amount)
 	);
 }
+
+/// Bidirectional teleports of local Penpal assets to Asset Hub as foreign assets should work
+/// (using native reserve-based transfer for fees)
+#[test]
+fn bidirectional_teleport_foreign_assets_between_para_and_asset_hub() {
+	let ah_as_seen_by_penpal = PenpalB::sibling_location_of(AssetHubWestend::para_id());
+	let asset_location_on_penpal = PenpalLocalTeleportableToAssetHub::get();
+	let asset_id_on_penpal = match asset_location_on_penpal.last() {
+		Some(GeneralIndex(id)) => *id as u32,
+		_ => unreachable!(),
+	};
+	let asset_owner_on_penpal = PenpalBSender::get();
+	let foreign_asset_at_asset_hub_westend =
+		MultiLocation { parents: 1, interior: X1(Parachain(PenpalB::para_id().into())) }
+			.appended_with(asset_location_on_penpal)
+			.unwrap();
+	super::penpal_create_foreign_asset_on_asset_hub(
+		asset_id_on_penpal,
+		foreign_asset_at_asset_hub_westend,
+		ah_as_seen_by_penpal,
+		false,
+		asset_owner_on_penpal,
+		ASSET_MIN_BALANCE * 1_000_000,
+	);
+	let penpal_to_ah_beneficiary_id = AssetHubWestendReceiver::get();
+
+	let fee_amount_to_send = ASSET_HUB_WESTEND_ED * 1000;
+	let asset_amount_to_send = ASSET_MIN_BALANCE * 1000;
+
+	let penpal_assets: MultiAssets = vec![
+		(Parent, fee_amount_to_send).into(),
+		(asset_location_on_penpal, asset_amount_to_send).into(),
+	]
+	.into();
+	let fee_asset_index = penpal_assets
+		.inner()
+		.iter()
+		.position(|r| r == &(Parent, fee_amount_to_send).into())
+		.unwrap() as u32;
+
+	// Penpal to AH test args
+	let penpal_to_ah_test_args = TestContext {
+		sender: PenpalBSender::get(),
+		receiver: AssetHubWestendReceiver::get(),
+		args: para_test_args(
+			ah_as_seen_by_penpal,
+			penpal_to_ah_beneficiary_id,
+			asset_amount_to_send,
+			penpal_assets,
+			Some(asset_id_on_penpal),
+			fee_asset_index,
+		),
+	};
+	let mut penpal_to_ah = ParaToSystemParaTest::new(penpal_to_ah_test_args);
+
+	let penpal_sender_balance_before = penpal_to_ah.sender.balance;
+	let ah_receiver_balance_before = penpal_to_ah.receiver.balance;
+
+	let penpal_sender_assets_before = PenpalB::execute_with(|| {
+		type Assets = <PenpalB as PenpalBPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalBSender::get())
+	});
+	let ah_receiver_assets_before = AssetHubWestend::execute_with(|| {
+		type Assets = <AssetHubWestend as AssetHubWestendPallet>::ForeignAssets;
+		<Assets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_westend,
+			&AssetHubWestendReceiver::get(),
+		)
+	});
+
+	penpal_to_ah.set_assertion::<PenpalB>(penpal_to_ah_foreign_assets_sender_assertions);
+	penpal_to_ah.set_assertion::<AssetHubWestend>(penpal_to_ah_foreign_assets_receiver_assertions);
+	penpal_to_ah.set_dispatchable::<PenpalB>(para_to_system_para_transfer_assets);
+	penpal_to_ah.assert();
+
+	let penpal_sender_balance_after = penpal_to_ah.sender.balance;
+	let ah_receiver_balance_after = penpal_to_ah.receiver.balance;
+
+	let penpal_sender_assets_after = PenpalB::execute_with(|| {
+		type Assets = <PenpalB as PenpalBPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalBSender::get())
+	});
+	let ah_receiver_assets_after = AssetHubWestend::execute_with(|| {
+		type Assets = <AssetHubWestend as AssetHubWestendPallet>::ForeignAssets;
+		<Assets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_westend,
+			&AssetHubWestendReceiver::get(),
+		)
+	});
+
+	// Sender's balance is reduced
+	assert!(penpal_sender_balance_after < penpal_sender_balance_before);
+	// Receiver's balance is increased
+	assert!(ah_receiver_balance_after > ah_receiver_balance_before);
+	// Receiver's balance increased by `amount_to_send - delivery_fees - bought_execution`;
+	// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
+	// should be non-zero
+	assert!(ah_receiver_balance_after < ah_receiver_balance_before + fee_amount_to_send);
+
+	// Sender's balance is reduced by exact amount
+	assert_eq!(penpal_sender_assets_before - asset_amount_to_send, penpal_sender_assets_after);
+	// Receiver's balance is increased by exact amount
+	assert_eq!(ah_receiver_assets_after, ah_receiver_assets_before + asset_amount_to_send);
+
+	///////////////////////////////////////////////////////////////////////
+	// Now test transferring foreign assets back from AssetHub to Penpal //
+	///////////////////////////////////////////////////////////////////////
+
+	// Move funds on AH from AHReceiver to AHSender
+	AssetHubWestend::execute_with(|| {
+		type ForeignAssets = <AssetHubWestend as AssetHubWestendPallet>::ForeignAssets;
+		assert_ok!(ForeignAssets::transfer(
+			<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendReceiver::get()),
+			foreign_asset_at_asset_hub_westend,
+			AssetHubWestendSender::get().into(),
+			asset_amount_to_send,
+		));
+	});
+
+	let ah_to_penpal_beneficiary_id = PenpalBReceiver::get();
+	let penpal_as_seen_by_ah = AssetHubWestend::sibling_location_of(PenpalB::para_id());
+	let ah_assets: MultiAssets = vec![
+		(Parent, fee_amount_to_send).into(),
+		(foreign_asset_at_asset_hub_westend, asset_amount_to_send).into(),
+	]
+	.into();
+	let fee_asset_index = ah_assets
+		.inner()
+		.iter()
+		.position(|r| r == &(Parent, fee_amount_to_send).into())
+		.unwrap() as u32;
+
+	// AH to Penpal test args
+	let ah_to_penpal_test_args = TestContext {
+		sender: AssetHubWestendSender::get(),
+		receiver: PenpalBReceiver::get(),
+		args: para_test_args(
+			penpal_as_seen_by_ah,
+			ah_to_penpal_beneficiary_id,
+			asset_amount_to_send,
+			ah_assets,
+			Some(asset_id_on_penpal),
+			fee_asset_index,
+		),
+	};
+	let mut ah_to_penpal = SystemParaToParaTest::new(ah_to_penpal_test_args);
+
+	let ah_sender_balance_before = ah_to_penpal.sender.balance;
+	let penpal_receiver_balance_before = ah_to_penpal.receiver.balance;
+
+	let ah_sender_assets_before = AssetHubWestend::execute_with(|| {
+		type ForeignAssets = <AssetHubWestend as AssetHubWestendPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_westend,
+			&AssetHubWestendSender::get(),
+		)
+	});
+	let penpal_receiver_assets_before = PenpalB::execute_with(|| {
+		type Assets = <PenpalB as PenpalBPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalBReceiver::get())
+	});
+
+	ah_to_penpal.set_assertion::<AssetHubWestend>(ah_to_penpal_foreign_assets_sender_assertions);
+	ah_to_penpal.set_assertion::<PenpalB>(ah_to_penpal_foreign_assets_receiver_assertions);
+	ah_to_penpal.set_dispatchable::<AssetHubWestend>(system_para_to_para_transfer_assets);
+	ah_to_penpal.assert();
+
+	let ah_sender_balance_after = ah_to_penpal.sender.balance;
+	let penpal_receiver_balance_after = ah_to_penpal.receiver.balance;
+
+	let ah_sender_assets_after = AssetHubWestend::execute_with(|| {
+		type ForeignAssets = <AssetHubWestend as AssetHubWestendPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(
+			foreign_asset_at_asset_hub_westend,
+			&AssetHubWestendSender::get(),
+		)
+	});
+	let penpal_receiver_assets_after = PenpalB::execute_with(|| {
+		type Assets = <PenpalB as PenpalBPallet>::Assets;
+		<Assets as Inspect<_>>::balance(asset_id_on_penpal, &PenpalBReceiver::get())
+	});
+
+	// Sender's balance is reduced
+	assert!(ah_sender_balance_after < ah_sender_balance_before);
+	// Receiver's balance is increased
+	assert!(penpal_receiver_balance_after > penpal_receiver_balance_before);
+	// Receiver's balance increased by `amount_to_send - delivery_fees - bought_execution`;
+	// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
+	// should be non-zero
+	assert!(penpal_receiver_balance_after < penpal_receiver_balance_before + fee_amount_to_send);
+
+	// Sender's balance is reduced by exact amount
+	assert_eq!(ah_sender_assets_before - asset_amount_to_send, ah_sender_assets_after);
+	// Receiver's balance is increased by exact amount
+	assert_eq!(penpal_receiver_assets_after, penpal_receiver_assets_before + asset_amount_to_send);
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
index 46bca804a9c..86000a20b5b 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -1359,6 +1359,55 @@ impl_runtime_apis! {
 						ParentThen(Parachain(random_para_id).into()).into(),
 					))
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Transfer to Relay some local AH asset (local-reserve-transfer) while paying
+					// fees using teleported native token.
+					// (We don't care that Relay doesn't accept incoming unknown AH local asset)
+					let dest = Parent.into();
+
+					let fee_amount = EXISTENTIAL_DEPOSIT;
+					let fee_asset: MultiAsset = (MultiLocation::parent(), fee_amount).into();
+
+					let who = frame_benchmarking::whitelisted_caller();
+					// Give some multiple of the existential deposit
+					let balance = fee_amount + EXISTENTIAL_DEPOSIT * 1000;
+					let _ = <Balances as frame_support::traits::Currency<_>>::make_free_balance_be(
+						&who, balance,
+					);
+					// verify initial balance
+					assert_eq!(Balances::free_balance(&who), balance);
+
+					// set up local asset
+					let asset_amount = 10u128;
+					let initial_asset_amount = asset_amount * 10;
+					let (asset_id, _, _) = pallet_assets::benchmarking::create_default_minted_asset::<
+						Runtime,
+						pallet_assets::Instance1
+					>(true, initial_asset_amount);
+					let asset_location = MultiLocation::new(
+						0,
+						X2(PalletInstance(50), GeneralIndex(u32::from(asset_id).into()))
+					);
+					let transfer_asset: MultiAsset = (asset_location, asset_amount).into();
+
+					let assets: MultiAssets = vec![fee_asset.clone(), transfer_asset].into();
+					let fee_index = if assets.get(0).unwrap().eq(&fee_asset) { 0 } else { 1 };
+
+					// verify transferred successfully
+					let verify = Box::new(move || {
+						// verify native balance after transfer, decreased by transferred fee amount
+						// (plus transport fees)
+						assert!(Balances::free_balance(&who) <= balance - fee_amount);
+						// verify asset balance decreased by exactly transferred amount
+						assert_eq!(
+							Assets::balance(asset_id.into(), &who),
+							initial_asset_amount - asset_amount,
+						);
+					});
+					Some((assets, fee_index as u32, dest, verify))
+				}
 			}
 
 			impl XcmBridgeHubRouterConfig<ToWestendXcmRouterInstance> for Runtime {
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm.rs
index afe85fdaf28..f8820bbb58c 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_xcm.rs
@@ -17,9 +17,9 @@
 //! Autogenerated weights for `pallet_xcm`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-11-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! WORST CASE MAP SIZE: `1000000`
-//! HOSTNAME: `runner-yprdrvc7-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! HOSTNAME: `runner-r43aesjn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
 //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-rococo-dev")`, DB CACHE: 1024
 
 // Executed Command:
@@ -64,37 +64,95 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 24_498_000 picoseconds.
-		Weight::from_parts(25_385_000, 0)
+		// Minimum execution time: 25_003_000 picoseconds.
+		Weight::from_parts(25_800_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
-	/// Storage: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
-	/// Proof: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn teleport_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `39`
-		//  Estimated: `3504`
-		// Minimum execution time: 19_746_000 picoseconds.
-		Weight::from_parts(20_535_000, 0)
-			.saturating_add(Weight::from_parts(0, 3504))
-			.saturating_add(T::DbWeight::get().reads(2))
+		//  Measured:  `145`
+		//  Estimated: `3610`
+		// Minimum execution time: 88_832_000 picoseconds.
+		Weight::from_parts(90_491_000, 0)
+			.saturating_add(Weight::from_parts(0, 3610))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
 	}
-	/// Storage: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
-	/// Proof: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `XcmpQueue::DeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `XcmpQueue::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::RelevantMessagingState` (r:1 w:0)
+	/// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1)
+	/// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `XcmpQueue::OutboundXcmpMessages` (r:0 w:1)
+	/// Proof: `XcmpQueue::OutboundXcmpMessages` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn reserve_transfer_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `39`
-		//  Estimated: `3504`
-		// Minimum execution time: 15_059_000 picoseconds.
-		Weight::from_parts(15_386_000, 0)
-			.saturating_add(Weight::from_parts(0, 3504))
-			.saturating_add(T::DbWeight::get().reads(2))
+		//  Measured:  `400`
+		//  Estimated: `6196`
+		// Minimum execution time: 138_911_000 picoseconds.
+		Weight::from_parts(142_483_000, 0)
+			.saturating_add(Weight::from_parts(0, 6196))
+			.saturating_add(T::DbWeight::get().reads(9))
+			.saturating_add(T::DbWeight::get().writes(5))
+	}
+	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
+	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	fn transfer_assets() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `496`
+		//  Estimated: `6208`
+		// Minimum execution time: 146_932_000 picoseconds.
+		Weight::from_parts(153_200_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(12))
+			.saturating_add(T::DbWeight::get().writes(7))
 	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
@@ -112,8 +170,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 7_108_000 picoseconds.
-		Weight::from_parts(7_458_000, 0)
+		// Minimum execution time: 7_081_000 picoseconds.
+		Weight::from_parts(7_397_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -123,8 +181,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_205_000 picoseconds.
-		Weight::from_parts(2_360_000, 0)
+		// Minimum execution time: 2_007_000 picoseconds.
+		Weight::from_parts(2_183_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -150,8 +208,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 29_099_000 picoseconds.
-		Weight::from_parts(29_580_000, 0)
+		// Minimum execution time: 28_790_000 picoseconds.
+		Weight::from_parts(29_767_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(5))
@@ -176,8 +234,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `363`
 		//  Estimated: `3828`
-		// Minimum execution time: 31_161_000 picoseconds.
-		Weight::from_parts(31_933_000, 0)
+		// Minimum execution time: 30_951_000 picoseconds.
+		Weight::from_parts(31_804_000, 0)
 			.saturating_add(Weight::from_parts(0, 3828))
 			.saturating_add(T::DbWeight::get().reads(7))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -188,8 +246,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_158_000 picoseconds.
-		Weight::from_parts(2_316_000, 0)
+		// Minimum execution time: 2_164_000 picoseconds.
+		Weight::from_parts(2_311_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -199,8 +257,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `162`
 		//  Estimated: `11052`
-		// Minimum execution time: 16_934_000 picoseconds.
-		Weight::from_parts(17_655_000, 0)
+		// Minimum execution time: 16_906_000 picoseconds.
+		Weight::from_parts(17_612_000, 0)
 			.saturating_add(Weight::from_parts(0, 11052))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -211,8 +269,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `166`
 		//  Estimated: `11056`
-		// Minimum execution time: 17_658_000 picoseconds.
-		Weight::from_parts(17_973_000, 0)
+		// Minimum execution time: 17_443_000 picoseconds.
+		Weight::from_parts(18_032_000, 0)
 			.saturating_add(Weight::from_parts(0, 11056))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -223,8 +281,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `13538`
-		// Minimum execution time: 18_673_000 picoseconds.
-		Weight::from_parts(19_027_000, 0)
+		// Minimum execution time: 18_992_000 picoseconds.
+		Weight::from_parts(19_464_000, 0)
 			.saturating_add(Weight::from_parts(0, 13538))
 			.saturating_add(T::DbWeight::get().reads(5))
 	}
@@ -246,8 +304,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `212`
 		//  Estimated: `6152`
-		// Minimum execution time: 27_171_000 picoseconds.
-		Weight::from_parts(27_802_000, 0)
+		// Minimum execution time: 28_011_000 picoseconds.
+		Weight::from_parts(28_716_000, 0)
 			.saturating_add(Weight::from_parts(0, 6152))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(3))
@@ -258,8 +316,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `206`
 		//  Estimated: `8621`
-		// Minimum execution time: 9_423_000 picoseconds.
-		Weight::from_parts(9_636_000, 0)
+		// Minimum execution time: 9_533_000 picoseconds.
+		Weight::from_parts(9_856_000, 0)
 			.saturating_add(Weight::from_parts(0, 8621))
 			.saturating_add(T::DbWeight::get().reads(3))
 	}
@@ -269,8 +327,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `11063`
-		// Minimum execution time: 17_442_000 picoseconds.
-		Weight::from_parts(17_941_000, 0)
+		// Minimum execution time: 17_628_000 picoseconds.
+		Weight::from_parts(18_146_000, 0)
 			.saturating_add(Weight::from_parts(0, 11063))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -293,8 +351,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `215`
 		//  Estimated: `11105`
-		// Minimum execution time: 34_340_000 picoseconds.
-		Weight::from_parts(34_934_000, 0)
+		// Minimum execution time: 34_877_000 picoseconds.
+		Weight::from_parts(35_607_000, 0)
 			.saturating_add(Weight::from_parts(0, 11105))
 			.saturating_add(T::DbWeight::get().reads(10))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -307,8 +365,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `103`
 		//  Estimated: `1588`
-		// Minimum execution time: 5_496_000 picoseconds.
-		Weight::from_parts(5_652_000, 0)
+		// Minimum execution time: 5_370_000 picoseconds.
+		Weight::from_parts(5_616_000, 0)
 			.saturating_add(Weight::from_parts(0, 1588))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -319,8 +377,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `7740`
 		//  Estimated: `11205`
-		// Minimum execution time: 26_140_000 picoseconds.
-		Weight::from_parts(26_824_000, 0)
+		// Minimum execution time: 26_820_000 picoseconds.
+		Weight::from_parts(27_143_000, 0)
 			.saturating_add(Weight::from_parts(0, 11205))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
index dfe4990df96..6709a20b275 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -1431,6 +1431,55 @@ impl_runtime_apis! {
 						ParentThen(Parachain(random_para_id).into()).into(),
 					))
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Transfer to Relay some local AH asset (local-reserve-transfer) while paying
+					// fees using teleported native token.
+					// (We don't care that Relay doesn't accept incoming unknown AH local asset)
+					let dest = Parent.into();
+
+					let fee_amount = EXISTENTIAL_DEPOSIT;
+					let fee_asset: MultiAsset = (MultiLocation::parent(), fee_amount).into();
+
+					let who = frame_benchmarking::whitelisted_caller();
+					// Give some multiple of the existential deposit
+					let balance = fee_amount + EXISTENTIAL_DEPOSIT * 1000;
+					let _ = <Balances as frame_support::traits::Currency<_>>::make_free_balance_be(
+						&who, balance,
+					);
+					// verify initial balance
+					assert_eq!(Balances::free_balance(&who), balance);
+
+					// set up local asset
+					let asset_amount = 10u128;
+					let initial_asset_amount = asset_amount * 10;
+					let (asset_id, _, _) = pallet_assets::benchmarking::create_default_minted_asset::<
+						Runtime,
+						pallet_assets::Instance1
+					>(true, initial_asset_amount);
+					let asset_location = MultiLocation::new(
+						0,
+						X2(PalletInstance(50), GeneralIndex(u32::from(asset_id).into()))
+					);
+					let transfer_asset: MultiAsset = (asset_location, asset_amount).into();
+
+					let assets: MultiAssets = vec![fee_asset.clone(), transfer_asset].into();
+					let fee_index = if assets.get(0).unwrap().eq(&fee_asset) { 0 } else { 1 };
+
+					// verify transferred successfully
+					let verify = Box::new(move || {
+						// verify native balance after transfer, decreased by transferred fee amount
+						// (plus transport fees)
+						assert!(Balances::free_balance(&who) <= balance - fee_amount);
+						// verify asset balance decreased by exactly transferred amount
+						assert_eq!(
+							Assets::balance(asset_id.into(), &who),
+							initial_asset_amount - asset_amount,
+						);
+					});
+					Some((assets, fee_index as u32, dest, verify))
+				}
 			}
 
 			use pallet_xcm_bridge_hub_router::benchmarking::{
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs
index 340edafb0b0..504731f4a9e 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs
@@ -17,9 +17,9 @@
 //! Autogenerated weights for `pallet_xcm`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-11-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! WORST CASE MAP SIZE: `1000000`
-//! HOSTNAME: `runner-yprdrvc7-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! HOSTNAME: `runner-r43aesjn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
 //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-westend-dev")`, DB CACHE: 1024
 
 // Executed Command:
@@ -64,40 +64,102 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 25_534_000 picoseconds.
-		Weight::from_parts(26_413_000, 0)
+		// Minimum execution time: 25_482_000 picoseconds.
+		Weight::from_parts(26_622_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn teleport_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `0`
-		//  Estimated: `1489`
-		// Minimum execution time: 20_513_000 picoseconds.
-		Weight::from_parts(20_837_000, 0)
-			.saturating_add(Weight::from_parts(0, 1489))
-			.saturating_add(T::DbWeight::get().reads(1))
+		//  Measured:  `145`
+		//  Estimated: `3610`
+		// Minimum execution time: 87_319_000 picoseconds.
+		Weight::from_parts(89_764_000, 0)
+			.saturating_add(Weight::from_parts(0, 3610))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
 	}
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `XcmpQueue::DeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `XcmpQueue::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::RelevantMessagingState` (r:1 w:0)
+	/// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1)
+	/// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `XcmpQueue::OutboundXcmpMessages` (r:0 w:1)
+	/// Proof: `XcmpQueue::OutboundXcmpMessages` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn reserve_transfer_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `0`
-		//  Estimated: `1489`
-		// Minimum execution time: 14_977_000 picoseconds.
-		Weight::from_parts(15_207_000, 0)
-			.saturating_add(Weight::from_parts(0, 1489))
-			.saturating_add(T::DbWeight::get().reads(1))
+		//  Measured:  `367`
+		//  Estimated: `6196`
+		// Minimum execution time: 139_133_000 picoseconds.
+		Weight::from_parts(141_507_000, 0)
+			.saturating_add(Weight::from_parts(0, 6196))
+			.saturating_add(T::DbWeight::get().reads(9))
+			.saturating_add(T::DbWeight::get().writes(5))
+	}
+	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
+	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	fn transfer_assets() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `496`
+		//  Estimated: `6208`
+		// Minimum execution time: 144_241_000 picoseconds.
+		Weight::from_parts(149_709_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(12))
+			.saturating_add(T::DbWeight::get().writes(7))
 	}
 	fn execute() -> Weight {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 7_440_000 picoseconds.
-		Weight::from_parts(7_651_000, 0)
+		// Minimum execution time: 10_392_000 picoseconds.
+		Weight::from_parts(10_779_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 	}
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:0 w:1)
@@ -106,8 +168,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 7_253_000 picoseconds.
-		Weight::from_parts(7_584_000, 0)
+		// Minimum execution time: 7_088_000 picoseconds.
+		Weight::from_parts(7_257_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -117,8 +179,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_299_000 picoseconds.
-		Weight::from_parts(2_435_000, 0)
+		// Minimum execution time: 2_095_000 picoseconds.
+		Weight::from_parts(2_136_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -144,8 +206,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 29_440_000 picoseconds.
-		Weight::from_parts(30_675_000, 0)
+		// Minimum execution time: 28_728_000 picoseconds.
+		Weight::from_parts(29_349_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(5))
@@ -170,8 +232,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `363`
 		//  Estimated: `3828`
-		// Minimum execution time: 31_876_000 picoseconds.
-		Weight::from_parts(32_588_000, 0)
+		// Minimum execution time: 30_605_000 picoseconds.
+		Weight::from_parts(31_477_000, 0)
 			.saturating_add(Weight::from_parts(0, 3828))
 			.saturating_add(T::DbWeight::get().reads(7))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -182,8 +244,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_385_000 picoseconds.
-		Weight::from_parts(2_607_000, 0)
+		// Minimum execution time: 2_137_000 picoseconds.
+		Weight::from_parts(2_303_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -193,8 +255,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `162`
 		//  Estimated: `11052`
-		// Minimum execution time: 16_927_000 picoseconds.
-		Weight::from_parts(17_554_000, 0)
+		// Minimum execution time: 16_719_000 picoseconds.
+		Weight::from_parts(17_329_000, 0)
 			.saturating_add(Weight::from_parts(0, 11052))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -205,8 +267,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `166`
 		//  Estimated: `11056`
-		// Minimum execution time: 16_965_000 picoseconds.
-		Weight::from_parts(17_807_000, 0)
+		// Minimum execution time: 16_687_000 picoseconds.
+		Weight::from_parts(17_405_000, 0)
 			.saturating_add(Weight::from_parts(0, 11056))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -217,8 +279,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `13538`
-		// Minimum execution time: 18_763_000 picoseconds.
-		Weight::from_parts(19_359_000, 0)
+		// Minimum execution time: 18_751_000 picoseconds.
+		Weight::from_parts(19_130_000, 0)
 			.saturating_add(Weight::from_parts(0, 13538))
 			.saturating_add(T::DbWeight::get().reads(5))
 	}
@@ -240,8 +302,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `212`
 		//  Estimated: `6152`
-		// Minimum execution time: 27_371_000 picoseconds.
-		Weight::from_parts(28_185_000, 0)
+		// Minimum execution time: 27_189_000 picoseconds.
+		Weight::from_parts(27_760_000, 0)
 			.saturating_add(Weight::from_parts(0, 6152))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(3))
@@ -252,8 +314,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `206`
 		//  Estimated: `8621`
-		// Minimum execution time: 9_165_000 picoseconds.
-		Weight::from_parts(9_539_000, 0)
+		// Minimum execution time: 9_307_000 picoseconds.
+		Weight::from_parts(9_691_000, 0)
 			.saturating_add(Weight::from_parts(0, 8621))
 			.saturating_add(T::DbWeight::get().reads(3))
 	}
@@ -263,8 +325,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `11063`
-		// Minimum execution time: 17_384_000 picoseconds.
-		Weight::from_parts(17_777_000, 0)
+		// Minimum execution time: 17_607_000 picoseconds.
+		Weight::from_parts(18_090_000, 0)
 			.saturating_add(Weight::from_parts(0, 11063))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -287,8 +349,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `215`
 		//  Estimated: `11105`
-		// Minimum execution time: 34_260_000 picoseconds.
-		Weight::from_parts(35_428_000, 0)
+		// Minimum execution time: 34_322_000 picoseconds.
+		Weight::from_parts(35_754_000, 0)
 			.saturating_add(Weight::from_parts(0, 11105))
 			.saturating_add(T::DbWeight::get().reads(10))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -301,8 +363,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `103`
 		//  Estimated: `1588`
-		// Minimum execution time: 4_710_000 picoseconds.
-		Weight::from_parts(4_900_000, 0)
+		// Minimum execution time: 4_513_000 picoseconds.
+		Weight::from_parts(4_754_000, 0)
 			.saturating_add(Weight::from_parts(0, 1588))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -313,8 +375,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `7740`
 		//  Estimated: `11205`
-		// Minimum execution time: 26_843_000 picoseconds.
-		Weight::from_parts(27_404_000, 0)
+		// Minimum execution time: 27_860_000 picoseconds.
+		Weight::from_parts(28_279_000, 0)
 			.saturating_add(Weight::from_parts(0, 11205))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
index 01efb9d20cc..7dbf015de50 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs
@@ -823,6 +823,18 @@ impl_runtime_apis! {
 					// Reserve transfers are disabled on BH.
 					None
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// BH only supports teleports to system parachain.
+					// Relay/native token can be teleported between BH and Relay.
+					let native_location = Parent.into();
+					let dest = Parent.into();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 
 			use xcm::latest::prelude::*;
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs
index 5aa4999c624..5faded42aa8 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs
@@ -17,9 +17,9 @@
 //! Autogenerated weights for `pallet_xcm`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-11-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! WORST CASE MAP SIZE: `1000000`
-//! HOSTNAME: `runner-yprdrvc7-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! HOSTNAME: `runner-r43aesjn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
 //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: 1024
 
 // Executed Command:
@@ -62,26 +62,39 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn send() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `75`
-		//  Estimated: `3540`
-		// Minimum execution time: 24_179_000 picoseconds.
-		Weight::from_parts(24_684_000, 0)
-			.saturating_add(Weight::from_parts(0, 3540))
+		//  Measured:  `38`
+		//  Estimated: `3503`
+		// Minimum execution time: 23_683_000 picoseconds.
+		Weight::from_parts(24_199_000, 0)
+			.saturating_add(Weight::from_parts(0, 3503))
 			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
-	/// Storage: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
-	/// Proof: UNKNOWN KEY `0x48297505634037ef48c848c99c0b1f1b` (r:1 w:0)
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn teleport_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `38`
-		//  Estimated: `3503`
-		// Minimum execution time: 21_093_000 picoseconds.
-		Weight::from_parts(21_523_000, 0)
-			.saturating_add(Weight::from_parts(0, 3503))
-			.saturating_add(T::DbWeight::get().reads(2))
+		//  Measured:  `70`
+		//  Estimated: `3593`
+		// Minimum execution time: 89_524_000 picoseconds.
+		Weight::from_parts(91_401_000, 0)
+			.saturating_add(Weight::from_parts(0, 3593))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
 	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
@@ -93,6 +106,32 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		Weight::from_parts(18_446_744_073_709_551_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 	}
+	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
+	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	fn transfer_assets() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `70`
+		//  Estimated: `3593`
+		// Minimum execution time: 91_890_000 picoseconds.
+		Weight::from_parts(93_460_000, 0)
+			.saturating_add(Weight::from_parts(0, 3593))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
+	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn execute() -> Weight {
@@ -109,8 +148,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 6_938_000 picoseconds.
-		Weight::from_parts(7_243_000, 0)
+		// Minimum execution time: 7_152_000 picoseconds.
+		Weight::from_parts(7_355_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -120,8 +159,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_159_000 picoseconds.
-		Weight::from_parts(2_290_000, 0)
+		// Minimum execution time: 2_081_000 picoseconds.
+		Weight::from_parts(2_258_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -145,11 +184,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn force_subscribe_version_notify() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `75`
-		//  Estimated: `3540`
-		// Minimum execution time: 28_337_000 picoseconds.
-		Weight::from_parts(29_265_000, 0)
-			.saturating_add(Weight::from_parts(0, 3540))
+		//  Measured:  `38`
+		//  Estimated: `3503`
+		// Minimum execution time: 28_067_000 picoseconds.
+		Weight::from_parts(28_693_000, 0)
+			.saturating_add(Weight::from_parts(0, 3503))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(5))
 	}
@@ -171,11 +210,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn force_unsubscribe_version_notify() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `292`
-		//  Estimated: `3757`
-		// Minimum execution time: 30_599_000 picoseconds.
-		Weight::from_parts(31_272_000, 0)
-			.saturating_add(Weight::from_parts(0, 3757))
+		//  Measured:  `255`
+		//  Estimated: `3720`
+		// Minimum execution time: 30_420_000 picoseconds.
+		Weight::from_parts(31_373_000, 0)
+			.saturating_add(Weight::from_parts(0, 3720))
 			.saturating_add(T::DbWeight::get().reads(7))
 			.saturating_add(T::DbWeight::get().writes(4))
 	}
@@ -185,8 +224,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_132_000 picoseconds.
-		Weight::from_parts(2_280_000, 0)
+		// Minimum execution time: 2_087_000 picoseconds.
+		Weight::from_parts(2_243_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -194,11 +233,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_supported_version() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `187`
-		//  Estimated: `11077`
-		// Minimum execution time: 18_262_000 picoseconds.
-		Weight::from_parts(18_640_000, 0)
-			.saturating_add(Weight::from_parts(0, 11077))
+		//  Measured:  `95`
+		//  Estimated: `10985`
+		// Minimum execution time: 15_142_000 picoseconds.
+		Weight::from_parts(15_598_000, 0)
+			.saturating_add(Weight::from_parts(0, 10985))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -206,11 +245,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifiers` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_version_notifiers() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `191`
-		//  Estimated: `11081`
-		// Minimum execution time: 18_512_000 picoseconds.
-		Weight::from_parts(18_888_000, 0)
-			.saturating_add(Weight::from_parts(0, 11081))
+		//  Measured:  `99`
+		//  Estimated: `10989`
+		// Minimum execution time: 15_041_000 picoseconds.
+		Weight::from_parts(15_493_000, 0)
+			.saturating_add(Weight::from_parts(0, 10989))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -218,11 +257,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn already_notified_target() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `198`
-		//  Estimated: `13563`
-		// Minimum execution time: 19_362_000 picoseconds.
-		Weight::from_parts(20_056_000, 0)
-			.saturating_add(Weight::from_parts(0, 13563))
+		//  Measured:  `106`
+		//  Estimated: `13471`
+		// Minimum execution time: 16_624_000 picoseconds.
+		Weight::from_parts(17_031_000, 0)
+			.saturating_add(Weight::from_parts(0, 13471))
 			.saturating_add(T::DbWeight::get().reads(5))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:2 w:1)
@@ -241,11 +280,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn notify_current_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `142`
-		//  Estimated: `6082`
-		// Minimum execution time: 27_318_000 picoseconds.
-		Weight::from_parts(28_075_000, 0)
-			.saturating_add(Weight::from_parts(0, 6082))
+		//  Measured:  `106`
+		//  Estimated: `6046`
+		// Minimum execution time: 26_398_000 picoseconds.
+		Weight::from_parts(26_847_000, 0)
+			.saturating_add(Weight::from_parts(0, 6046))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(3))
 	}
@@ -253,22 +292,22 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn notify_target_migration_fail() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `172`
-		//  Estimated: `8587`
-		// Minimum execution time: 9_930_000 picoseconds.
-		Weight::from_parts(10_192_000, 0)
-			.saturating_add(Weight::from_parts(0, 8587))
+		//  Measured:  `136`
+		//  Estimated: `8551`
+		// Minimum execution time: 8_741_000 picoseconds.
+		Weight::from_parts(8_954_000, 0)
+			.saturating_add(Weight::from_parts(0, 8551))
 			.saturating_add(T::DbWeight::get().reads(3))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:4 w:2)
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_version_notify_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `198`
-		//  Estimated: `11088`
-		// Minimum execution time: 18_305_000 picoseconds.
-		Weight::from_parts(18_738_000, 0)
-			.saturating_add(Weight::from_parts(0, 11088))
+		//  Measured:  `106`
+		//  Estimated: `10996`
+		// Minimum execution time: 15_306_000 picoseconds.
+		Weight::from_parts(15_760_000, 0)
+			.saturating_add(Weight::from_parts(0, 10996))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -288,11 +327,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn migrate_and_notify_old_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `204`
-		//  Estimated: `11094`
-		// Minimum execution time: 34_559_000 picoseconds.
-		Weight::from_parts(35_241_000, 0)
-			.saturating_add(Weight::from_parts(0, 11094))
+		//  Measured:  `112`
+		//  Estimated: `11002`
+		// Minimum execution time: 33_127_000 picoseconds.
+		Weight::from_parts(33_938_000, 0)
+			.saturating_add(Weight::from_parts(0, 11002))
 			.saturating_add(T::DbWeight::get().reads(10))
 			.saturating_add(T::DbWeight::get().writes(4))
 	}
@@ -302,11 +341,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn new_query() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `69`
-		//  Estimated: `1554`
-		// Minimum execution time: 4_512_000 picoseconds.
-		Weight::from_parts(4_671_000, 0)
-			.saturating_add(Weight::from_parts(0, 1554))
+		//  Measured:  `32`
+		//  Estimated: `1517`
+		// Minimum execution time: 4_290_000 picoseconds.
+		Weight::from_parts(4_450_000, 0)
+			.saturating_add(Weight::from_parts(0, 1517))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -314,11 +353,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn take_response() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `7706`
-		//  Estimated: `11171`
-		// Minimum execution time: 26_473_000 picoseconds.
-		Weight::from_parts(26_960_000, 0)
-			.saturating_add(Weight::from_parts(0, 11171))
+		//  Measured:  `7669`
+		//  Estimated: `11134`
+		// Minimum execution time: 26_408_000 picoseconds.
+		Weight::from_parts(26_900_000, 0)
+			.saturating_add(Weight::from_parts(0, 11134))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs
index 39ce4e04ed1..e77184351ed 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs
@@ -813,6 +813,18 @@ impl_runtime_apis! {
 					// Reserve transfers are disabled on BH.
 					None
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// BH only supports teleports to system parachain.
+					// Relay/native token can be teleported between BH and Relay.
+					let native_location = Parent.into();
+					let dest = Parent.into();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 
 			use xcm::latest::prelude::*;
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_xcm.rs
index 9f17d327024..83e4260e771 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_xcm.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_xcm.rs
@@ -17,27 +17,25 @@
 //! Autogenerated weights for `pallet_xcm`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! WORST CASE MAP SIZE: `1000000`
-//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
-//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: 1024
+//! HOSTNAME: `runner-r43aesjn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-westend-dev")`, DB CACHE: 1024
 
 // Executed Command:
-// ./target/production/polkadot-parachain
+// target/production/polkadot-parachain
 // benchmark
 // pallet
-// --chain=bridge-hub-rococo-dev
-// --wasm-execution=compiled
-// --pallet=pallet_xcm
-// --no-storage-info
-// --no-median-slopes
-// --no-min-squares
-// --extrinsic=*
 // --steps=50
 // --repeat=20
-// --json
-// --header=./file_header.txt
-// --output=./parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/
+// --extrinsic=*
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json
+// --pallet=pallet_xcm
+// --chain=bridge-hub-westend-dev
+// --header=./cumulus/file_header.txt
+// --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/
 
 #![cfg_attr(rustfmt, rustfmt_skip)]
 #![allow(unused_parens)]
@@ -50,6 +48,8 @@ use core::marker::PhantomData;
 /// Weight functions for `pallet_xcm`.
 pub struct WeightInfo<T>(PhantomData<T>);
 impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
@@ -62,24 +62,39 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn send() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `75`
-		//  Estimated: `3540`
-		// Minimum execution time: 29_724_000 picoseconds.
-		Weight::from_parts(30_440_000, 0)
-			.saturating_add(Weight::from_parts(0, 3540))
-			.saturating_add(T::DbWeight::get().reads(5))
+		//  Measured:  `38`
+		//  Estimated: `3503`
+		// Minimum execution time: 23_219_000 picoseconds.
+		Weight::from_parts(23_818_000, 0)
+			.saturating_add(Weight::from_parts(0, 3503))
+			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn teleport_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `32`
-		//  Estimated: `1489`
-		// Minimum execution time: 26_779_000 picoseconds.
-		Weight::from_parts(27_249_000, 0)
-			.saturating_add(Weight::from_parts(0, 1489))
-			.saturating_add(T::DbWeight::get().reads(1))
+		//  Measured:  `70`
+		//  Estimated: `3593`
+		// Minimum execution time: 90_120_000 picoseconds.
+		Weight::from_parts(92_545_000, 0)
+			.saturating_add(Weight::from_parts(0, 3593))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
 	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
@@ -91,6 +106,32 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		Weight::from_parts(18_446_744_073_709_551_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 	}
+	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
+	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	fn transfer_assets() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `70`
+		//  Estimated: `3593`
+		// Minimum execution time: 91_339_000 picoseconds.
+		Weight::from_parts(93_204_000, 0)
+			.saturating_add(Weight::from_parts(0, 3593))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
+	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn execute() -> Weight {
@@ -107,8 +148,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 9_170_000 picoseconds.
-		Weight::from_parts(9_629_000, 0)
+		// Minimum execution time: 6_976_000 picoseconds.
+		Weight::from_parts(7_284_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -118,8 +159,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_769_000 picoseconds.
-		Weight::from_parts(2_933_000, 0)
+		// Minimum execution time: 2_044_000 picoseconds.
+		Weight::from_parts(2_223_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -127,6 +168,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifiers` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::QueryCounter` (r:1 w:1)
 	/// Proof: `PolkadotXcm::QueryCounter` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
@@ -141,16 +184,18 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn force_subscribe_version_notify() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `75`
-		//  Estimated: `3540`
-		// Minimum execution time: 34_547_000 picoseconds.
-		Weight::from_parts(35_653_000, 0)
-			.saturating_add(Weight::from_parts(0, 3540))
-			.saturating_add(T::DbWeight::get().reads(7))
+		//  Measured:  `38`
+		//  Estimated: `3503`
+		// Minimum execution time: 27_778_000 picoseconds.
+		Weight::from_parts(28_318_000, 0)
+			.saturating_add(Weight::from_parts(0, 3503))
+			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(5))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifiers` (r:1 w:1)
 	/// Proof: `PolkadotXcm::VersionNotifiers` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
@@ -165,12 +210,12 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn force_unsubscribe_version_notify() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `292`
-		//  Estimated: `3757`
-		// Minimum execution time: 36_274_000 picoseconds.
-		Weight::from_parts(37_281_000, 0)
-			.saturating_add(Weight::from_parts(0, 3757))
-			.saturating_add(T::DbWeight::get().reads(6))
+		//  Measured:  `255`
+		//  Estimated: `3720`
+		// Minimum execution time: 30_446_000 picoseconds.
+		Weight::from_parts(31_925_000, 0)
+			.saturating_add(Weight::from_parts(0, 3720))
+			.saturating_add(T::DbWeight::get().reads(7))
 			.saturating_add(T::DbWeight::get().writes(4))
 	}
 	/// Storage: `PolkadotXcm::XcmExecutionSuspended` (r:0 w:1)
@@ -179,8 +224,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_749_000 picoseconds.
-		Weight::from_parts(2_917_000, 0)
+		// Minimum execution time: 2_037_000 picoseconds.
+		Weight::from_parts(2_211_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -188,11 +233,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_supported_version() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `187`
-		//  Estimated: `11077`
-		// Minimum execution time: 17_649_000 picoseconds.
-		Weight::from_parts(17_964_000, 0)
-			.saturating_add(Weight::from_parts(0, 11077))
+		//  Measured:  `95`
+		//  Estimated: `10985`
+		// Minimum execution time: 15_620_000 picoseconds.
+		Weight::from_parts(15_984_000, 0)
+			.saturating_add(Weight::from_parts(0, 10985))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -200,11 +245,11 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifiers` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_version_notifiers() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `191`
-		//  Estimated: `11081`
-		// Minimum execution time: 17_551_000 picoseconds.
-		Weight::from_parts(18_176_000, 0)
-			.saturating_add(Weight::from_parts(0, 11081))
+		//  Measured:  `99`
+		//  Estimated: `10989`
+		// Minimum execution time: 15_689_000 picoseconds.
+		Weight::from_parts(16_093_000, 0)
+			.saturating_add(Weight::from_parts(0, 10989))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
@@ -212,15 +257,17 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn already_notified_target() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `198`
-		//  Estimated: `13563`
-		// Minimum execution time: 19_261_000 picoseconds.
-		Weight::from_parts(19_714_000, 0)
-			.saturating_add(Weight::from_parts(0, 13563))
+		//  Measured:  `106`
+		//  Estimated: `13471`
+		// Minimum execution time: 16_946_000 picoseconds.
+		Weight::from_parts(17_192_000, 0)
+			.saturating_add(Weight::from_parts(0, 13471))
 			.saturating_add(T::DbWeight::get().reads(5))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:2 w:1)
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
@@ -233,39 +280,41 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn notify_current_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `142`
-		//  Estimated: `6082`
-		// Minimum execution time: 31_630_000 picoseconds.
-		Weight::from_parts(32_340_000, 0)
-			.saturating_add(Weight::from_parts(0, 6082))
-			.saturating_add(T::DbWeight::get().reads(7))
+		//  Measured:  `106`
+		//  Estimated: `6046`
+		// Minimum execution time: 27_164_000 picoseconds.
+		Weight::from_parts(27_760_000, 0)
+			.saturating_add(Weight::from_parts(0, 6046))
+			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(3))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:3 w:0)
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn notify_target_migration_fail() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `172`
-		//  Estimated: `8587`
-		// Minimum execution time: 9_218_000 picoseconds.
-		Weight::from_parts(9_558_000, 0)
-			.saturating_add(Weight::from_parts(0, 8587))
+		//  Measured:  `136`
+		//  Estimated: `8551`
+		// Minimum execution time: 8_689_000 picoseconds.
+		Weight::from_parts(8_874_000, 0)
+			.saturating_add(Weight::from_parts(0, 8551))
 			.saturating_add(T::DbWeight::get().reads(3))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:4 w:2)
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn migrate_version_notify_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `198`
-		//  Estimated: `11088`
-		// Minimum execution time: 18_133_000 picoseconds.
-		Weight::from_parts(18_663_000, 0)
-			.saturating_add(Weight::from_parts(0, 11088))
+		//  Measured:  `106`
+		//  Estimated: `10996`
+		// Minimum execution time: 15_944_000 picoseconds.
+		Weight::from_parts(16_381_000, 0)
+			.saturating_add(Weight::from_parts(0, 10996))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
 	/// Storage: `PolkadotXcm::VersionNotifyTargets` (r:4 w:2)
 	/// Proof: `PolkadotXcm::VersionNotifyTargets` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
 	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
@@ -278,12 +327,12 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn migrate_and_notify_old_targets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `204`
-		//  Estimated: `11094`
-		// Minimum execution time: 38_878_000 picoseconds.
-		Weight::from_parts(39_779_000, 0)
-			.saturating_add(Weight::from_parts(0, 11094))
-			.saturating_add(T::DbWeight::get().reads(9))
+		//  Measured:  `112`
+		//  Estimated: `11002`
+		// Minimum execution time: 33_826_000 picoseconds.
+		Weight::from_parts(34_784_000, 0)
+			.saturating_add(Weight::from_parts(0, 11002))
+			.saturating_add(T::DbWeight::get().reads(10))
 			.saturating_add(T::DbWeight::get().writes(4))
 	}
 	/// Storage: `PolkadotXcm::QueryCounter` (r:1 w:1)
@@ -294,8 +343,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `32`
 		//  Estimated: `1517`
-		// Minimum execution time: 4_142_000 picoseconds.
-		Weight::from_parts(4_308_000, 0)
+		// Minimum execution time: 4_257_000 picoseconds.
+		Weight::from_parts(4_383_000, 0)
 			.saturating_add(Weight::from_parts(0, 1517))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -306,11 +355,10 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `7669`
 		//  Estimated: `11134`
-		// Minimum execution time: 25_814_000 picoseconds.
-		Weight::from_parts(26_213_000, 0)
+		// Minimum execution time: 26_924_000 picoseconds.
+		Weight::from_parts(27_455_000, 0)
 			.saturating_add(Weight::from_parts(0, 11134))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
 }
-
diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs
index 842ddbe54f2..d6f8d32628e 100644
--- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs
@@ -969,6 +969,18 @@ impl_runtime_apis! {
 					// Reserve transfers are disabled on Collectives.
 					None
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Collectives only supports teleports to system parachain.
+					// Relay/native token can be teleported between Collectives and Relay.
+					let native_location = Parent.into();
+					let dest = Parent.into();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 
 			let whitelist: Vec<TrackedStorageKey> = vec![
diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_xcm.rs
index a3b42cb86c4..50dfbffde01 100644
--- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_xcm.rs
+++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_xcm.rs
@@ -17,9 +17,9 @@
 //! Autogenerated weights for `pallet_xcm`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-11-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! WORST CASE MAP SIZE: `1000000`
-//! HOSTNAME: `runner-yprdrvc7-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! HOSTNAME: `runner-r43aesjn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
 //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("collectives-westend-dev")`, DB CACHE: 1024
 
 // Executed Command:
@@ -64,22 +64,37 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 25_746_000 picoseconds.
-		Weight::from_parts(26_349_000, 0)
+		// Minimum execution time: 24_540_000 picoseconds.
+		Weight::from_parts(25_439_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
 	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
 	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
 	fn teleport_assets() -> Weight {
 		// Proof Size summary in bytes:
-		//  Measured:  `69`
-		//  Estimated: `1489`
-		// Minimum execution time: 22_660_000 picoseconds.
-		Weight::from_parts(23_173_000, 0)
-			.saturating_add(Weight::from_parts(0, 1489))
-			.saturating_add(T::DbWeight::get().reads(1))
+		//  Measured:  `214`
+		//  Estimated: `3679`
+		// Minimum execution time: 86_614_000 picoseconds.
+		Weight::from_parts(88_884_000, 0)
+			.saturating_add(Weight::from_parts(0, 3679))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
 	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
@@ -91,6 +106,32 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		Weight::from_parts(18_446_744_073_709_551_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 	}
+	/// Storage: `ParachainInfo::ParachainId` (r:1 w:0)
+	/// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::UpwardDeliveryFeeFactor` (r:1 w:0)
+	/// Proof: `ParachainSystem::UpwardDeliveryFeeFactor` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1)
+	/// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0)
+	/// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `ParachainSystem::HostConfiguration` (r:1 w:0)
+	/// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `ParachainSystem::PendingUpwardMessages` (r:1 w:1)
+	/// Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	fn transfer_assets() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `214`
+		//  Estimated: `3679`
+		// Minimum execution time: 87_915_000 picoseconds.
+		Weight::from_parts(90_219_000, 0)
+			.saturating_add(Weight::from_parts(0, 3679))
+			.saturating_add(T::DbWeight::get().reads(8))
+			.saturating_add(T::DbWeight::get().writes(3))
+	}
 	/// Storage: `Benchmark::Override` (r:0 w:0)
 	/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	fn execute() -> Weight {
@@ -107,8 +148,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 7_321_000 picoseconds.
-		Weight::from_parts(7_542_000, 0)
+		// Minimum execution time: 6_872_000 picoseconds.
+		Weight::from_parts(7_110_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -118,8 +159,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_232_000 picoseconds.
-		Weight::from_parts(2_395_000, 0)
+		// Minimum execution time: 2_009_000 picoseconds.
+		Weight::from_parts(2_163_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -145,8 +186,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `145`
 		//  Estimated: `3610`
-		// Minimum execution time: 29_006_000 picoseconds.
-		Weight::from_parts(29_777_000, 0)
+		// Minimum execution time: 28_858_000 picoseconds.
+		Weight::from_parts(29_355_000, 0)
 			.saturating_add(Weight::from_parts(0, 3610))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(5))
@@ -171,8 +212,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `363`
 		//  Estimated: `3828`
-		// Minimum execution time: 31_245_000 picoseconds.
-		Weight::from_parts(32_125_000, 0)
+		// Minimum execution time: 30_598_000 picoseconds.
+		Weight::from_parts(31_168_000, 0)
 			.saturating_add(Weight::from_parts(0, 3828))
 			.saturating_add(T::DbWeight::get().reads(7))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -183,8 +224,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `0`
 		//  Estimated: `0`
-		// Minimum execution time: 2_255_000 picoseconds.
-		Weight::from_parts(2_399_000, 0)
+		// Minimum execution time: 2_090_000 picoseconds.
+		Weight::from_parts(2_253_000, 0)
 			.saturating_add(Weight::from_parts(0, 0))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
@@ -194,8 +235,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `162`
 		//  Estimated: `11052`
-		// Minimum execution time: 16_521_000 picoseconds.
-		Weight::from_parts(17_001_000, 0)
+		// Minimum execution time: 16_133_000 picoseconds.
+		Weight::from_parts(16_433_000, 0)
 			.saturating_add(Weight::from_parts(0, 11052))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -206,8 +247,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `166`
 		//  Estimated: `11056`
-		// Minimum execution time: 16_486_000 picoseconds.
-		Weight::from_parts(16_729_000, 0)
+		// Minimum execution time: 16_012_000 picoseconds.
+		Weight::from_parts(16_449_000, 0)
 			.saturating_add(Weight::from_parts(0, 11056))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -218,8 +259,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `13538`
-		// Minimum execution time: 18_037_000 picoseconds.
-		Weight::from_parts(18_310_000, 0)
+		// Minimum execution time: 17_922_000 picoseconds.
+		Weight::from_parts(18_426_000, 0)
 			.saturating_add(Weight::from_parts(0, 13538))
 			.saturating_add(T::DbWeight::get().reads(5))
 	}
@@ -241,8 +282,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `212`
 		//  Estimated: `6152`
-		// Minimum execution time: 27_901_000 picoseconds.
-		Weight::from_parts(28_566_000, 0)
+		// Minimum execution time: 27_280_000 picoseconds.
+		Weight::from_parts(28_026_000, 0)
 			.saturating_add(Weight::from_parts(0, 6152))
 			.saturating_add(T::DbWeight::get().reads(8))
 			.saturating_add(T::DbWeight::get().writes(3))
@@ -253,8 +294,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `206`
 		//  Estimated: `8621`
-		// Minimum execution time: 9_299_000 picoseconds.
-		Weight::from_parts(9_547_000, 0)
+		// Minimum execution time: 9_387_000 picoseconds.
+		Weight::from_parts(9_644_000, 0)
 			.saturating_add(Weight::from_parts(0, 8621))
 			.saturating_add(T::DbWeight::get().reads(3))
 	}
@@ -264,8 +305,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `173`
 		//  Estimated: `11063`
-		// Minimum execution time: 16_768_000 picoseconds.
-		Weight::from_parts(17_215_000, 0)
+		// Minimum execution time: 16_649_000 picoseconds.
+		Weight::from_parts(17_025_000, 0)
 			.saturating_add(Weight::from_parts(0, 11063))
 			.saturating_add(T::DbWeight::get().reads(4))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -288,8 +329,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `215`
 		//  Estimated: `11105`
-		// Minimum execution time: 35_134_000 picoseconds.
-		Weight::from_parts(35_883_000, 0)
+		// Minimum execution time: 34_355_000 picoseconds.
+		Weight::from_parts(35_295_000, 0)
 			.saturating_add(Weight::from_parts(0, 11105))
 			.saturating_add(T::DbWeight::get().reads(10))
 			.saturating_add(T::DbWeight::get().writes(4))
@@ -302,8 +343,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `103`
 		//  Estimated: `1588`
-		// Minimum execution time: 4_562_000 picoseconds.
-		Weight::from_parts(4_802_000, 0)
+		// Minimum execution time: 4_527_000 picoseconds.
+		Weight::from_parts(4_699_000, 0)
 			.saturating_add(Weight::from_parts(0, 1588))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(2))
@@ -314,8 +355,8 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
 		// Proof Size summary in bytes:
 		//  Measured:  `7740`
 		//  Estimated: `11205`
-		// Minimum execution time: 26_865_000 picoseconds.
-		Weight::from_parts(27_400_000, 0)
+		// Minimum execution time: 27_011_000 picoseconds.
+		Weight::from_parts(27_398_000, 0)
 			.saturating_add(Weight::from_parts(0, 11205))
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
diff --git a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/lib.rs b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/lib.rs
index 81cc8558a2b..4ab3014cace 100644
--- a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/lib.rs
@@ -723,6 +723,18 @@ impl_runtime_apis! {
 					// Reserve transfers are disabled on Contracts-System-Para.
 					None
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Contracts-System-Para only supports teleports to system parachain.
+					// Relay/native token can be teleported between Contracts-System-Para and Relay.
+					let native_location = Parent.into();
+					let dest = Parent.into();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 
 			let whitelist: Vec<TrackedStorageKey> = vec![
diff --git a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
index 74d9a0b071d..21f421f8285 100644
--- a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
@@ -94,12 +94,24 @@ pub type FungiblesTransactor = FungiblesAdapter<
 	// Use this fungibles implementation:
 	Assets,
 	// Use this currency when it is a fungible asset matching the given location or name:
-	ConvertedConcreteId<
-		AssetIdPalletAssets,
-		Balance,
-		AsPrefixedGeneralIndex<SystemAssetHubAssetsPalletLocation, AssetIdPalletAssets, JustTry>,
-		JustTry,
-	>,
+	(
+		ConvertedConcreteId<
+			AssetIdPalletAssets,
+			Balance,
+			AsPrefixedGeneralIndex<AssetsPalletLocation, AssetIdPalletAssets, JustTry>,
+			JustTry,
+		>,
+		ConvertedConcreteId<
+			AssetIdPalletAssets,
+			Balance,
+			AsPrefixedGeneralIndex<
+				SystemAssetHubAssetsPalletLocation,
+				AssetIdPalletAssets,
+				JustTry,
+			>,
+			JustTry,
+		>,
+	),
 	// Convert an XCM MultiLocation into a local account id:
 	LocationToAccountId,
 	// Our chain's account ID type (we can't get away without mentioning it explicitly):
@@ -237,6 +249,8 @@ where
 	}
 }
 
+// This asset can be added to AH as ForeignAsset and teleported between Penpal and AH
+pub const TELEPORTABLE_ASSET_ID: u32 = 2;
 parameter_types! {
 	/// The location that this chain recognizes as the Relay network's Asset Hub.
 	pub SystemAssetHubLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(1000)));
@@ -244,11 +258,30 @@ parameter_types! {
 	// the Relay Chain's Asset Hub's Assets pallet index
 	pub SystemAssetHubAssetsPalletLocation: MultiLocation =
 		MultiLocation::new(1, X2(Parachain(1000), PalletInstance(50)));
+	pub AssetsPalletLocation: MultiLocation =
+		MultiLocation::new(0, X1(PalletInstance(50)));
 	pub CheckingAccount: AccountId = PolkadotXcm::check_account();
+	pub LocalTeleportableToAssetHub: MultiLocation = MultiLocation::new(
+		0,
+		X2(PalletInstance(50), GeneralIndex(TELEPORTABLE_ASSET_ID.into()))
+	);
+}
+
+/// Accepts asset with ID `AssetLocation` and is coming from `Origin` chain.
+pub struct AssetFromChain<AssetLocation, Origin>(PhantomData<(AssetLocation, Origin)>);
+impl<AssetLocation: Get<MultiLocation>, Origin: Get<MultiLocation>>
+	ContainsPair<MultiAsset, MultiLocation> for AssetFromChain<AssetLocation, Origin>
+{
+	fn contains(asset: &MultiAsset, origin: &MultiLocation) -> bool {
+		log::trace!(target: "xcm::contains", "AssetFromChain asset: {:?}, origin: {:?}", asset, origin);
+		*origin == Origin::get() && matches!(asset.id, Concrete(id) if id == AssetLocation::get())
+	}
 }
 
 pub type Reserves =
 	(NativeAsset, AssetsFrom<SystemAssetHubLocation>, NativeAssetFrom<SystemAssetHubLocation>);
+pub type TrustedTeleporters =
+	(AssetFromChain<LocalTeleportableToAssetHub, SystemAssetHubLocation>,);
 
 pub struct XcmConfig;
 impl xcm_executor::Config for XcmConfig {
@@ -259,7 +292,7 @@ impl xcm_executor::Config for XcmConfig {
 	type OriginConverter = XcmOriginToTransactDispatchOrigin;
 	type IsReserve = Reserves;
 	// no teleport trust established with other chains
-	type IsTeleporter = NativeAsset;
+	type IsTeleporter = TrustedTeleporters;
 	type UniversalLocation = UniversalLocation;
 	type Barrier = Barrier;
 	type Weigher = FixedWeightBounds<UnitWeightCost, RuntimeCall, MaxInstructions>;
diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs
index 21d4088df19..26670cdcc23 100644
--- a/polkadot/runtime/rococo/src/lib.rs
+++ b/polkadot/runtime/rococo/src/lib.rs
@@ -2261,6 +2261,20 @@ sp_api::impl_runtime_apis! {
 						Parachain(43211234).into(),
 					))
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Relay supports only native token, either reserve transfer it to non-system parachains,
+					// or teleport it to system parachain. Use the teleport case for benchmarking as it's
+					// slightly heavier.
+					// Relay/native token can be teleported to/from AH.
+					let native_location = Here.into();
+					let dest = crate::xcm_config::AssetHub::get();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 			impl pallet_xcm_benchmarks::Config for Runtime {
 				type XcmConfig = XcmConfig;
diff --git a/polkadot/runtime/rococo/src/weights/pallet_xcm.rs b/polkadot/runtime/rococo/src/weights/pallet_xcm.rs
index aafded3f731..177407ef708 100644
--- a/polkadot/runtime/rococo/src/weights/pallet_xcm.rs
+++ b/polkadot/runtime/rococo/src/weights/pallet_xcm.rs
@@ -48,6 +48,10 @@ use core::marker::PhantomData;
 /// Weight functions for `pallet_xcm`.
 pub struct WeightInfo<T>(PhantomData<T>);
 impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
+	fn transfer_assets() -> Weight {
+		// TODO: run benchmarks
+		Weight::zero()
+	}
 	/// Storage: `Dmp::DeliveryFeeFactor` (r:1 w:0)
 	/// Proof: `Dmp::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `XcmPallet::SupportedVersion` (r:1 w:0)
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index afc2e5ea3da..04723ef0d9b 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -2295,6 +2295,21 @@ sp_api::impl_runtime_apis! {
 						crate::Junction::Parachain(43211234).into(),
 					))
 				}
+
+				fn set_up_complex_asset_transfer(
+				) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+					// Relay supports only native token, either reserve transfer it to non-system parachains,
+					// or teleport it to system parachain. Use the teleport case for benchmarking as it's
+					// slightly heavier.
+
+					// Relay/native token can be teleported to/from AH.
+					let native_location = Here.into();
+					let dest = crate::xcm_config::AssetHub::get();
+					pallet_xcm::benchmarking::helpers::native_teleport_as_asset_transfer::<Runtime>(
+						native_location,
+						dest
+					)
+				}
 			}
 			impl frame_system_benchmarking::Config for Runtime {}
 			impl pallet_nomination_pools_benchmarking::Config for Runtime {}
diff --git a/polkadot/runtime/westend/src/weights/pallet_xcm.rs b/polkadot/runtime/westend/src/weights/pallet_xcm.rs
index cca4bdbd91e..493acd0f9e7 100644
--- a/polkadot/runtime/westend/src/weights/pallet_xcm.rs
+++ b/polkadot/runtime/westend/src/weights/pallet_xcm.rs
@@ -50,6 +50,10 @@ use core::marker::PhantomData;
 /// Weight functions for `pallet_xcm`.
 pub struct WeightInfo<T>(PhantomData<T>);
 impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
+	fn transfer_assets() -> Weight {
+		// TODO: run benchmarks
+		Weight::zero()
+	}
 	/// Storage: `Dmp::DeliveryFeeFactor` (r:1 w:0)
 	/// Proof: `Dmp::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`)
 	/// Storage: `XcmPallet::SupportedVersion` (r:1 w:0)
diff --git a/polkadot/xcm/pallet-xcm/src/benchmarking.rs b/polkadot/xcm/pallet-xcm/src/benchmarking.rs
index f0289512399..28a198f40a0 100644
--- a/polkadot/xcm/pallet-xcm/src/benchmarking.rs
+++ b/polkadot/xcm/pallet-xcm/src/benchmarking.rs
@@ -44,7 +44,7 @@ pub trait Config: crate::Config {
 	///
 	/// Implementation should also make sure `dest` is reachable/connected.
 	///
-	/// If `None`, the benchmarks that depend on this will be skipped.
+	/// If `None`, the benchmarks that depend on this will default to `Weight::MAX`.
 	fn teleportable_asset_and_dest() -> Option<(MultiAsset, MultiLocation)> {
 		None
 	}
@@ -54,10 +54,27 @@ pub trait Config: crate::Config {
 	///
 	/// Implementation should also make sure `dest` is reachable/connected.
 	///
-	/// If `None`, the benchmarks that depend on this will be skipped.
+	/// If `None`, the benchmarks that depend on this will default to `Weight::MAX`.
 	fn reserve_transferable_asset_and_dest() -> Option<(MultiAsset, MultiLocation)> {
 		None
 	}
+
+	/// Sets up a complex transfer (usually consisting of a teleport and reserve-based transfer), so
+	/// that runtime can properly benchmark `transfer_assets()` extrinsic. Should return a tuple
+	/// `(MultiAsset, u32, MultiLocation, dyn FnOnce())` representing the assets to transfer, the
+	/// `u32` index of the asset to be used for fees, the destination chain for the transfer, and a
+	/// `verify()` closure to verify the intended transfer side-effects.
+	///
+	/// Implementation should make sure the provided assets can be transacted by the runtime, there
+	/// are enough balances in the involved accounts, and that `dest` is reachable/connected.
+	///
+	/// Used only in benchmarks.
+	///
+	/// If `None`, the benchmarks that depend on this will default to `Weight::MAX`.
+	fn set_up_complex_asset_transfer(
+	) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+		None
+	}
 }
 
 benchmarks! {
@@ -158,6 +175,23 @@ benchmarks! {
 		assert!(pallet_balances::Pallet::<T>::free_balance(&caller) <= balance - transferred_amount);
 	}
 
+	transfer_assets {
+		let (assets, fee_index, destination, verify) = T::set_up_complex_asset_transfer().ok_or(
+			BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
+		)?;
+		let caller: T::AccountId = whitelisted_caller();
+		let send_origin = RawOrigin::Signed(caller.clone());
+		let recipient = [0u8; 32];
+		let versioned_dest: VersionedMultiLocation = destination.into();
+		let versioned_beneficiary: VersionedMultiLocation =
+			AccountId32 { network: None, id: recipient.into() }.into();
+		let versioned_assets: VersionedMultiAssets = assets.into();
+	}: _<RuntimeOrigin<T>>(send_origin.into(), Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0, WeightLimit::Unlimited)
+	verify {
+		// run provided verification function
+		verify();
+	}
+
 	execute {
 		let execute_origin =
 			T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
@@ -302,3 +336,36 @@ benchmarks! {
 		crate::mock::Test
 	);
 }
+
+pub mod helpers {
+	use super::*;
+	pub fn native_teleport_as_asset_transfer<T>(
+		native_asset_location: MultiLocation,
+		destination: MultiLocation,
+	) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)>
+	where
+		T: Config + pallet_balances::Config,
+		u128: From<<T as pallet_balances::Config>::Balance>,
+	{
+		// Relay/native token can be teleported to/from AH.
+		let amount = T::ExistentialDeposit::get() * 100u32.into();
+		let assets: MultiAssets =
+			MultiAsset { fun: Fungible(amount.into()), id: Concrete(native_asset_location) }.into();
+		let fee_index = 0u32;
+
+		// Give some multiple of transferred amount
+		let balance = amount * 10u32.into();
+		let who = whitelisted_caller();
+		let _ =
+			<pallet_balances::Pallet::<T> as frame_support::traits::Currency<_>>::make_free_balance_be(&who, balance);
+		// verify initial balance
+		assert_eq!(pallet_balances::Pallet::<T>::free_balance(&who), balance);
+
+		// verify transferred successfully
+		let verify = Box::new(move || {
+			// verify balance after transfer, decreased by transferred amount (and delivery fees)
+			assert!(pallet_balances::Pallet::<T>::free_balance(&who) <= balance - amount);
+		});
+		Some((assets, fee_index, destination, verify))
+	}
+}
diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs
index 74a24b132da..ab4403f6caa 100644
--- a/polkadot/xcm/pallet-xcm/src/lib.rs
+++ b/polkadot/xcm/pallet-xcm/src/lib.rs
@@ -66,6 +66,7 @@ pub trait WeightInfo {
 	fn send() -> Weight;
 	fn teleport_assets() -> Weight;
 	fn reserve_transfer_assets() -> Weight;
+	fn transfer_assets() -> Weight;
 	fn execute() -> Weight;
 	fn force_xcm_version() -> Weight;
 	fn force_default_xcm_version() -> Weight;
@@ -98,6 +99,10 @@ impl WeightInfo for TestWeightInfo {
 		Weight::from_parts(100_000_000, 0)
 	}
 
+	fn transfer_assets() -> Weight {
+		Weight::from_parts(100_000_000, 0)
+	}
+
 	fn execute() -> Weight {
 		Weight::from_parts(100_000_000, 0)
 	}
@@ -905,8 +910,8 @@ pub mod pallet {
 		///   from relay to parachain.
 		/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
 		///   generally be an `AccountId32` value.
-		/// - `assets`: The assets to be withdrawn. The first item should be the currency used to to
-		///   pay the fee on the `dest` side. May not be empty.
+		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
+		///   fee on the `dest` chain.
 		/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
 		///   fees.
 		#[pallet::call_index(1)]
@@ -937,8 +942,19 @@ pub mod pallet {
 			Self::do_teleport_assets(origin, dest, beneficiary, assets, fee_asset_item, Unlimited)
 		}
 
-		/// Transfer some assets from the local chain to the sovereign account of a destination
-		/// chain and forward a notification XCM.
+		/// Transfer some assets from the local chain to the destination chain through their local,
+		/// destination or remote reserve.
+		///
+		/// `assets` must have same reserve location and may not be teleportable to `dest`.
+		///  - `assets` have local reserve: transfer assets to sovereign account of destination
+		///    chain and forward a notification XCM to `dest` to mint and deposit reserve-based
+		///    assets to `beneficiary`.
+		///  - `assets` have destination reserve: burn local assets and forward a notification to
+		///    `dest` chain to withdraw the reserve assets from this chain's sovereign account and
+		///    deposit them to `beneficiary`.
+		///  - `assets` have remote reserve: burn local assets, forward XCM to reserve chain to move
+		///    reserves from this chain's SA to `dest` chain's SA, and forward another XCM to `dest`
+		///    to mint and deposit reserve-based assets to `beneficiary`.
 		///
 		/// **This function is deprecated: Use `limited_reserve_transfer_assets` instead.**
 		///
@@ -953,7 +969,7 @@ pub mod pallet {
 		/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
 		///   generally be an `AccountId32` value.
 		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
-		///   fee on the `dest` side.
+		///   fee on the `dest` (and possibly reserve) chains.
 		/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
 		///   fees.
 		#[pallet::call_index(2)]
@@ -1105,8 +1121,19 @@ pub mod pallet {
 			})
 		}
 
-		/// Transfer some assets from the local chain to the sovereign account of a destination
-		/// chain and forward a notification XCM.
+		/// Transfer some assets from the local chain to the destination chain through their local,
+		/// destination or remote reserve.
+		///
+		/// `assets` must have same reserve location and may not be teleportable to `dest`.
+		///  - `assets` have local reserve: transfer assets to sovereign account of destination
+		///    chain and forward a notification XCM to `dest` to mint and deposit reserve-based
+		///    assets to `beneficiary`.
+		///  - `assets` have destination reserve: burn local assets and forward a notification to
+		///    `dest` chain to withdraw the reserve assets from this chain's sovereign account and
+		///    deposit them to `beneficiary`.
+		///  - `assets` have remote reserve: burn local assets, forward XCM to reserve chain to move
+		///    reserves from this chain's SA to `dest` chain's SA, and forward another XCM to `dest`
+		///    to mint and deposit reserve-based assets to `beneficiary`.
 		///
 		/// Fee payment on the destination side is made from the asset in the `assets` vector of
 		/// index `fee_asset_item`, up to enough to pay for `weight_limit` of weight. If more weight
@@ -1120,7 +1147,7 @@ pub mod pallet {
 		/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
 		///   generally be an `AccountId32` value.
 		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
-		///   fee on the `dest` side.
+		///   fee on the `dest` (and possibly reserve) chains.
 		/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
 		///   fees.
 		/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
@@ -1173,8 +1200,8 @@ pub mod pallet {
 		///   from relay to parachain.
 		/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
 		///   generally be an `AccountId32` value.
-		/// - `assets`: The assets to be withdrawn. The first item should be the currency used to to
-		///   pay the fee on the `dest` side. May not be empty.
+		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
+		///   fee on the `dest` chain.
 		/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
 		///   fees.
 		/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
@@ -1225,12 +1252,162 @@ pub mod pallet {
 			XcmExecutionSuspended::<T>::set(suspended);
 			Ok(())
 		}
+
+		/// Transfer some assets from the local chain to the destination chain through their local,
+		/// destination or remote reserve, or through teleports.
+		///
+		/// Fee payment on the destination side is made from the asset in the `assets` vector of
+		/// index `fee_asset_item` (hence referred to as `fees`), up to enough to pay for
+		/// `weight_limit` of weight. If more weight is needed than `weight_limit`, then the
+		/// operation will fail and the assets sent may be at risk.
+		///
+		/// `assets` (excluding `fees`) must have same reserve location or otherwise be teleportable
+		/// to `dest`, no limitations imposed on `fees`.
+		///  - for local reserve: transfer assets to sovereign account of destination chain and
+		///    forward a notification XCM to `dest` to mint and deposit reserve-based assets to
+		///    `beneficiary`.
+		///  - for destination reserve: burn local assets and forward a notification to `dest` chain
+		///    to withdraw the reserve assets from this chain's sovereign account and deposit them
+		///    to `beneficiary`.
+		///  - for remote reserve: burn local assets, forward XCM to reserve chain to move reserves
+		///    from this chain's SA to `dest` chain's SA, and forward another XCM to `dest` to mint
+		///    and deposit reserve-based assets to `beneficiary`.
+		///  - for teleports: burn local assets and forward XCM to `dest` chain to mint/teleport
+		///    assets and deposit them to `beneficiary`.
+		///
+		/// - `origin`: Must be capable of withdrawing the `assets` and executing XCM.
+		/// - `dest`: Destination context for the assets. Will typically be `X2(Parent,
+		///   Parachain(..))` to send from parachain to parachain, or `X1(Parachain(..))` to send
+		///   from relay to parachain.
+		/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
+		///   generally be an `AccountId32` value.
+		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
+		///   fee on the `dest` (and possibly reserve) chains.
+		/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
+		///   fees.
+		/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
+		#[pallet::call_index(11)]
+		#[pallet::weight({
+			let maybe_assets: Result<MultiAssets, ()> = (*assets.clone()).try_into();
+			let maybe_dest: Result<MultiLocation, ()> = (*dest.clone()).try_into();
+			match (maybe_assets, maybe_dest) {
+				(Ok(assets), Ok(dest)) => {
+					use sp_std::vec;
+					// heaviest version of locally executed XCM program: equivalent in weight to withdrawing fees,
+					// burning them, transferring rest of assets to SA, reanchoring them, extending XCM program,
+					// and sending onward XCM
+					let mut message = Xcm(vec![
+						SetFeesMode { jit_withdraw: true },
+						WithdrawAsset(assets.clone()),
+						BurnAsset(assets.clone()),
+						TransferReserveAsset { assets, dest, xcm: Xcm(vec![]) }
+					]);
+					T::Weigher::weight(&mut message).map_or(Weight::MAX, |w| T::WeightInfo::transfer_assets().saturating_add(w))
+				}
+				_ => Weight::MAX,
+			}
+		})]
+		pub fn transfer_assets(
+			origin: OriginFor<T>,
+			dest: Box<VersionedMultiLocation>,
+			beneficiary: Box<VersionedMultiLocation>,
+			assets: Box<VersionedMultiAssets>,
+			fee_asset_item: u32,
+			weight_limit: WeightLimit,
+		) -> DispatchResult {
+			let origin = T::ExecuteXcmOrigin::ensure_origin(origin)?;
+			let dest = (*dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			let beneficiary: MultiLocation =
+				(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			let assets: MultiAssets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			log::debug!(
+				target: "xcm::pallet_xcm::transfer_assets",
+				"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, fee-idx {:?}, weight_limit {:?}",
+				origin, dest, beneficiary, assets, fee_asset_item, weight_limit,
+			);
+
+			ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
+			let mut assets = assets.into_inner();
+			let fee_asset_item = fee_asset_item as usize;
+			let fees = assets.get(fee_asset_item as usize).ok_or(Error::<T>::Empty)?.clone();
+			// Find transfer types for fee and non-fee assets.
+			let (fees_transfer_type, assets_transfer_type) =
+				Self::find_fee_and_assets_transfer_types(&assets, fee_asset_item, &dest)?;
+
+			// local and remote XCM programs to potentially handle fees separately
+			let fees = if fees_transfer_type == assets_transfer_type {
+				// no need for custom fees instructions, fees are batched with assets
+				FeesHandling::Batched { fees }
+			} else {
+				// Disallow _remote reserves_ unless assets & fees have same remote reserve (covered
+				// by branch above). The reason for this is that we'd need to send XCMs to separate
+				// chains with no guarantee of delivery order on final destination; therefore we
+				// cannot guarantee to have fees in place on final destination chain to pay for
+				// assets transfer.
+				ensure!(
+					!matches!(assets_transfer_type, TransferType::RemoteReserve(_)),
+					Error::<T>::InvalidAssetUnsupportedReserve
+				);
+				let weight_limit = weight_limit.clone();
+				// remove `fees` from `assets` and build separate fees transfer instructions to be
+				// added to assets transfers XCM programs
+				let fees = assets.remove(fee_asset_item);
+				let (local_xcm, remote_xcm) = match fees_transfer_type {
+					TransferType::LocalReserve =>
+						Self::local_reserve_fees_instructions(origin, dest, fees, weight_limit)?,
+					TransferType::DestinationReserve =>
+						Self::destination_reserve_fees_instructions(
+							origin,
+							dest,
+							fees,
+							weight_limit,
+						)?,
+					TransferType::Teleport =>
+						Self::teleport_fees_instructions(origin, dest, fees, weight_limit)?,
+					TransferType::RemoteReserve(_) =>
+						return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
+				};
+				FeesHandling::Separate { local_xcm, remote_xcm }
+			};
+
+			Self::build_and_execute_xcm_transfer_type(
+				origin,
+				dest,
+				beneficiary,
+				assets,
+				assets_transfer_type,
+				fees,
+				weight_limit,
+			)
+		}
 	}
 }
 
 /// The maximum number of distinct assets allowed to be transferred in a single helper extrinsic.
 const MAX_ASSETS_FOR_TRANSFER: usize = 2;
 
+/// Specify how assets used for fees are handled during asset transfers.
+#[derive(Clone, PartialEq)]
+enum FeesHandling<T: Config> {
+	/// `fees` asset can be batch-transferred with rest of assets using same XCM instructions.
+	Batched { fees: MultiAsset },
+	/// fees cannot be batched, they are handled separately using XCM programs here.
+	Separate { local_xcm: Xcm<<T as Config>::RuntimeCall>, remote_xcm: Xcm<()> },
+}
+
+impl<T: Config> sp_std::fmt::Debug for FeesHandling<T> {
+	fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+		match self {
+			Self::Batched { fees } => write!(f, "FeesHandling::Batched({:?})", fees),
+			Self::Separate { local_xcm, remote_xcm } => write!(
+				f,
+				"FeesHandling::Separate(local: {:?}, remote: {:?})",
+				local_xcm, remote_xcm
+			),
+		}
+	}
+}
+
 impl<T: Config> QueryHandler for Pallet<T> {
 	type QueryId = u64;
 	type BlockNumber = BlockNumberFor<T>;
@@ -1292,31 +1469,45 @@ impl<T: Config> QueryHandler for Pallet<T> {
 }
 
 impl<T: Config> Pallet<T> {
-	/// Validate `assets` to be reserve-transferred and return their reserve location.
-	fn validate_assets_and_find_reserve(
+	/// Find `TransferType`s for `assets` and fee identified through `fee_asset_item`, when
+	/// transferring to `dest`.
+	///
+	/// Validate `assets` to all have same `TransferType`.
+	fn find_fee_and_assets_transfer_types(
 		assets: &[MultiAsset],
+		fee_asset_item: usize,
 		dest: &MultiLocation,
-	) -> Result<TransferType, Error<T>> {
-		let mut reserve = None;
-		for asset in assets.iter() {
+	) -> Result<(TransferType, TransferType), Error<T>> {
+		let mut fees_transfer_type = None;
+		let mut assets_transfer_type = None;
+		for (idx, asset) in assets.iter().enumerate() {
 			if let Fungible(x) = asset.fun {
 				// If fungible asset, ensure non-zero amount.
 				ensure!(!x.is_zero(), Error::<T>::Empty);
 			}
 			let transfer_type =
 				T::XcmExecutor::determine_for(&asset, dest).map_err(Error::<T>::from)?;
-			// Ensure asset is not teleportable to `dest`.
-			ensure!(transfer_type != TransferType::Teleport, Error::<T>::Filtered);
-			if let Some(reserve) = reserve.as_ref() {
-				// Ensure transfer for multiple assets uses same reserve location (only fee may have
-				// different reserve location)
-				ensure!(reserve == &transfer_type, Error::<T>::TooManyReserves);
+			if idx == fee_asset_item {
+				fees_transfer_type = Some(transfer_type);
 			} else {
-				// asset reserve identified
-				reserve = Some(transfer_type);
+				if let Some(existing) = assets_transfer_type.as_ref() {
+					// Ensure transfer for multiple assets uses same transfer type (only fee may
+					// have different transfer type/path)
+					ensure!(existing == &transfer_type, Error::<T>::TooManyReserves);
+				} else {
+					// asset reserve identified
+					assets_transfer_type = Some(transfer_type);
+				}
 			}
 		}
-		reserve.ok_or(Error::<T>::Empty)
+		// single asset also marked as fee item
+		if assets.len() == 1 {
+			assets_transfer_type = fees_transfer_type
+		}
+		Ok((
+			fees_transfer_type.ok_or(Error::<T>::Empty)?,
+			assets_transfer_type.ok_or(Error::<T>::Empty)?,
+		))
 	}
 
 	fn do_reserve_transfer_assets(
@@ -1332,7 +1523,7 @@ impl<T: Config> Pallet<T> {
 		let beneficiary: MultiLocation =
 			(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
 		let assets: MultiAssets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
-		log::trace!(
+		log::debug!(
 			target: "xcm::pallet_xcm::do_reserve_transfer_assets",
 			"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, fee-idx {:?}",
 			origin_location, dest, beneficiary, assets, fee_asset_item,
@@ -1341,64 +1532,26 @@ impl<T: Config> Pallet<T> {
 		ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
 		let value = (origin_location, assets.into_inner());
 		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
-		let (origin_location, mut assets) = value;
+		let (origin, assets) = value;
 
-		if fee_asset_item as usize >= assets.len() {
-			return Err(Error::<T>::Empty.into())
-		}
-		let fees = assets.swap_remove(fee_asset_item as usize);
-		let fees_transfer_type =
-			T::XcmExecutor::determine_for(&fees, &dest).map_err(Error::<T>::from)?;
-		let assets_transfer_type = if assets.is_empty() {
-			// Single asset to transfer (one used for fees where transfer type is determined above).
-			ensure!(fees_transfer_type != TransferType::Teleport, Error::<T>::Filtered);
-			fees_transfer_type
-		} else {
-			// Find reserve for non-fee assets.
-			Self::validate_assets_and_find_reserve(&assets, &dest)?
-		};
+		let fee_asset_item = fee_asset_item as usize;
+		let fees = assets.get(fee_asset_item as usize).ok_or(Error::<T>::Empty)?.clone();
 
-		// local and remote XCM programs to potentially handle fees separately
-		let separate_fees_instructions: Option<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>)>;
-		if fees_transfer_type == assets_transfer_type {
-			// Same reserve location (fees not teleportable), we can batch together fees and assets
-			// in same reserve-based-transfer.
-			assets.push(fees.clone());
-			// no need for custom fees instructions, fees are batched with assets
-			separate_fees_instructions = None;
-		} else {
-			// Disallow _remote reserves_ unless assets & fees have same remote reserve (covered by
-			// branch above). The reason for this is that we'd need to send XCMs to separate chains
-			// with no guarantee of delivery order on final destination; therefore we cannot
-			// guarantee to have fees in place on final destination chain to pay for assets
-			// transfer.
-			ensure!(
-				!matches!(assets_transfer_type, TransferType::RemoteReserve(_)),
-				Error::<T>::InvalidAssetUnsupportedReserve
-			);
-			let fees = fees.clone();
-			let weight_limit = weight_limit.clone();
-			// build fees transfer instructions to be added to assets transfers XCM programs
-			separate_fees_instructions = Some(match fees_transfer_type {
-				TransferType::LocalReserve =>
-					Self::local_reserve_fees_instructions(dest, fees, weight_limit)?,
-				TransferType::DestinationReserve =>
-					Self::destination_reserve_fees_instructions(dest, fees, weight_limit)?,
-				TransferType::Teleport =>
-					Self::teleport_fees_instructions(origin_location, dest, fees, weight_limit)?,
-				TransferType::RemoteReserve(_) =>
-					return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
-			});
-		};
+		// Find transfer types for fee and non-fee assets.
+		let (fees_transfer_type, assets_transfer_type) =
+			Self::find_fee_and_assets_transfer_types(&assets, fee_asset_item, &dest)?;
+		// Ensure assets (and fees according to check below) are not teleportable to `dest`.
+		ensure!(assets_transfer_type != TransferType::Teleport, Error::<T>::Filtered);
+		// Ensure all assets (including fees) have same reserve location.
+		ensure!(assets_transfer_type == fees_transfer_type, Error::<T>::TooManyReserves);
 
 		Self::build_and_execute_xcm_transfer_type(
-			origin_location,
+			origin,
 			dest,
 			beneficiary,
 			assets,
 			assets_transfer_type,
-			fees,
-			separate_fees_instructions,
+			FeesHandling::Batched { fees },
 			weight_limit,
 		)
 	}
@@ -1416,6 +1569,11 @@ impl<T: Config> Pallet<T> {
 		let beneficiary: MultiLocation =
 			(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
 		let assets: MultiAssets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
+		log::debug!(
+			target: "xcm::pallet_xcm::do_teleport_assets",
+			"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, fee-idx {:?}, weight_limit {:?}",
+			origin_location, dest, beneficiary, assets, fee_asset_item, weight_limit,
+		);
 
 		ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
 		let value = (origin_location, assets.into_inner());
@@ -1424,7 +1582,7 @@ impl<T: Config> Pallet<T> {
 		for asset in assets.iter() {
 			let transfer_type =
 				T::XcmExecutor::determine_for(asset, &dest).map_err(Error::<T>::from)?;
-			ensure!(matches!(transfer_type, TransferType::Teleport), Error::<T>::Filtered);
+			ensure!(transfer_type == TransferType::Teleport, Error::<T>::Filtered);
 		}
 		let fees = assets.get(fee_asset_item as usize).ok_or(Error::<T>::Empty)?.clone();
 
@@ -1434,8 +1592,7 @@ impl<T: Config> Pallet<T> {
 			beneficiary,
 			assets,
 			TransferType::Teleport,
-			fees,
-			None,
+			FeesHandling::Batched { fees },
 			weight_limit,
 		)
 	}
@@ -1446,54 +1603,65 @@ impl<T: Config> Pallet<T> {
 		beneficiary: MultiLocation,
 		assets: Vec<MultiAsset>,
 		transfer_type: TransferType,
-		fees: MultiAsset,
-		separate_fees_instructions: Option<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>)>,
+		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
 	) -> DispatchResult {
-		log::trace!(
+		log::debug!(
 			target: "xcm::pallet_xcm::build_and_execute_xcm_transfer_type",
 			"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, transfer_type {:?}, \
-			fees {:?}, fees_xcm: {:?}, weight_limit: {:?}",
-			origin, dest, beneficiary, assets, transfer_type, fees, separate_fees_instructions, weight_limit,
+			fees_handling {:?}, weight_limit: {:?}",
+			origin, dest, beneficiary, assets, transfer_type, fees, weight_limit,
 		);
 		let (mut local_xcm, remote_xcm) = match transfer_type {
 			TransferType::LocalReserve => {
 				let (local, remote) = Self::local_reserve_transfer_programs(
+					origin,
 					dest,
 					beneficiary,
 					assets,
 					fees,
-					separate_fees_instructions,
 					weight_limit,
 				)?;
 				(local, Some(remote))
 			},
 			TransferType::DestinationReserve => {
 				let (local, remote) = Self::destination_reserve_transfer_programs(
+					origin,
 					dest,
 					beneficiary,
 					assets,
 					fees,
-					separate_fees_instructions,
 					weight_limit,
 				)?;
 				(local, Some(remote))
 			},
-			TransferType::RemoteReserve(reserve) => (
-				Self::remote_reserve_transfer_program(
+			TransferType::RemoteReserve(reserve) => {
+				let fees = match fees {
+					FeesHandling::Batched { fees } => fees,
+					_ => return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
+				};
+				let local = Self::remote_reserve_transfer_program(
+					origin,
 					reserve,
 					dest,
 					beneficiary,
 					assets,
 					fees,
 					weight_limit,
-				)?,
-				None,
-			),
-			TransferType::Teleport => (
-				Self::teleport_assets_program(dest, beneficiary, assets, fees, weight_limit)?,
-				None,
-			),
+				)?;
+				(local, None)
+			},
+			TransferType::Teleport => {
+				let (local, remote) = Self::teleport_assets_program(
+					origin,
+					dest,
+					beneficiary,
+					assets,
+					fees,
+					weight_limit,
+				)?;
+				(local, Some(remote))
+			},
 		};
 		let weight =
 			T::Weigher::weight(&mut local_xcm).map_err(|()| Error::<T>::UnweighableMessage)?;
@@ -1529,11 +1697,45 @@ impl<T: Config> Pallet<T> {
 		Ok(())
 	}
 
+	fn add_fees_to_xcm(
+		dest: MultiLocation,
+		fees: FeesHandling<T>,
+		weight_limit: WeightLimit,
+		local: &mut Xcm<<T as Config>::RuntimeCall>,
+		remote: &mut Xcm<()>,
+	) -> Result<(), Error<T>> {
+		match fees {
+			FeesHandling::Batched { fees } => {
+				let context = T::UniversalLocation::get();
+				// no custom fees instructions, they are batched together with `assets` transfer;
+				// BuyExecution happens after receiving all `assets`
+				let reanchored_fees =
+					fees.reanchored(&dest, context).map_err(|_| Error::<T>::CannotReanchor)?;
+				// buy execution using `fees` batched together with above `reanchored_assets`
+				remote.inner_mut().push(BuyExecution { fees: reanchored_fees, weight_limit });
+			},
+			FeesHandling::Separate { local_xcm: mut local_fees, remote_xcm: mut remote_fees } => {
+				// fees are handled by separate XCM instructions, prepend fees instructions (for
+				// remote XCM they have to be prepended instead of appended to pass barriers).
+				sp_std::mem::swap(local, &mut local_fees);
+				sp_std::mem::swap(remote, &mut remote_fees);
+				// these are now swapped so fees actually go first
+				local.inner_mut().append(&mut local_fees.into_inner());
+				remote.inner_mut().append(&mut remote_fees.into_inner());
+			},
+		}
+		Ok(())
+	}
+
 	fn local_reserve_fees_instructions(
+		origin: MultiLocation,
 		dest: MultiLocation,
 		fees: MultiAsset,
 		weight_limit: WeightLimit,
 	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>), Error<T>> {
+		let value = (origin, vec![fees.clone()]);
+		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
+
 		let context = T::UniversalLocation::get();
 		let reanchored_fees = fees
 			.clone()
@@ -1554,16 +1756,20 @@ impl<T: Config> Pallet<T> {
 	}
 
 	fn local_reserve_transfer_programs(
+		origin: MultiLocation,
 		dest: MultiLocation,
 		beneficiary: MultiLocation,
 		assets: Vec<MultiAsset>,
-		fees: MultiAsset,
-		separate_fees_instructions: Option<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>)>,
+		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
 	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>), Error<T>> {
+		let value = (origin, assets);
+		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
+		let (_, assets) = value;
+
 		// max assets is `assets` (+ potentially separately handled fee)
 		let max_assets =
-			assets.len() as u32 + separate_fees_instructions.as_ref().map(|_| 1).unwrap_or(0);
+			assets.len() as u32 + if matches!(&fees, FeesHandling::Batched { .. }) { 0 } else { 1 };
 		let assets: MultiAssets = assets.into();
 		let context = T::UniversalLocation::get();
 		let mut reanchored_assets = assets.clone();
@@ -1571,45 +1777,37 @@ impl<T: Config> Pallet<T> {
 			.reanchor(&dest, context)
 			.map_err(|_| Error::<T>::CannotReanchor)?;
 
-		// fees are either handled through dedicated instructions, or batched together with assets
-		let fees_already_handled = separate_fees_instructions.is_some();
-		let (fees_local_xcm, fees_remote_xcm) = separate_fees_instructions
-			.map(|(local, remote)| (local.into_inner(), remote.into_inner()))
-			.unwrap_or_default();
-
-		// start off with any necessary local fees specific instructions
-		let mut local_execute_xcm = fees_local_xcm;
-		// move `assets` to `dest`s local sovereign account
-		local_execute_xcm.push(TransferAsset { assets, beneficiary: dest });
-
-		// on destination chain, start off with custom fee instructions
-		let mut xcm_on_dest = fees_remote_xcm;
-		// continue with rest of assets
-		xcm_on_dest.extend_from_slice(&[
+		// XCM instructions to be executed on local chain
+		let mut local_execute_xcm = Xcm(vec![
+			// locally move `assets` to `dest`s local sovereign account
+			TransferAsset { assets, beneficiary: dest },
+		]);
+		// XCM instructions to be executed on destination chain
+		let mut xcm_on_dest = Xcm(vec![
 			// let (dest) chain know assets are in its SA on reserve
 			ReserveAssetDeposited(reanchored_assets),
 			// following instructions are not exec'ed on behalf of origin chain anymore
 			ClearOrigin,
 		]);
-		if !fees_already_handled {
-			// no custom fees instructions, they are batched together with `assets` transfer;
-			// BuyExecution happens after receiving all `assets`
-			let reanchored_fees =
-				fees.reanchored(&dest, context).map_err(|_| Error::<T>::CannotReanchor)?;
-			// buy execution using `fees` batched together with above `reanchored_assets`
-			xcm_on_dest.push(BuyExecution { fees: reanchored_fees, weight_limit });
-		}
+		// handle fees
+		Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
 		// deposit all remaining assets in holding to `beneficiary` location
-		xcm_on_dest.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
+		xcm_on_dest
+			.inner_mut()
+			.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
 
-		Ok((Xcm(local_execute_xcm), Xcm(xcm_on_dest)))
+		Ok((local_execute_xcm, xcm_on_dest))
 	}
 
 	fn destination_reserve_fees_instructions(
+		origin: MultiLocation,
 		dest: MultiLocation,
 		fees: MultiAsset,
 		weight_limit: WeightLimit,
 	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>), Error<T>> {
+		let value = (origin, vec![fees.clone()]);
+		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
+
 		let context = T::UniversalLocation::get();
 		let reanchored_fees = fees
 			.clone()
@@ -1633,16 +1831,20 @@ impl<T: Config> Pallet<T> {
 	}
 
 	fn destination_reserve_transfer_programs(
+		origin: MultiLocation,
 		dest: MultiLocation,
 		beneficiary: MultiLocation,
 		assets: Vec<MultiAsset>,
-		fees: MultiAsset,
-		separate_fees_instructions: Option<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>)>,
+		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
 	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>), Error<T>> {
+		let value = (origin, assets);
+		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
+		let (_, assets) = value;
+
 		// max assets is `assets` (+ potentially separately handled fee)
 		let max_assets =
-			assets.len() as u32 + separate_fees_instructions.as_ref().map(|_| 1).unwrap_or(0);
+			assets.len() as u32 + if matches!(&fees, FeesHandling::Batched { .. }) { 0 } else { 1 };
 		let assets: MultiAssets = assets.into();
 		let context = T::UniversalLocation::get();
 		let mut reanchored_assets = assets.clone();
@@ -1650,47 +1852,33 @@ impl<T: Config> Pallet<T> {
 			.reanchor(&dest, context)
 			.map_err(|_| Error::<T>::CannotReanchor)?;
 
-		// fees are either handled through dedicated instructions, or batched together with assets
-		let fees_already_handled = separate_fees_instructions.is_some();
-		let (fees_local_xcm, fees_remote_xcm) = separate_fees_instructions
-			.map(|(local, remote)| (local.into_inner(), remote.into_inner()))
-			.unwrap_or_default();
-
-		// start off with any necessary local fees specific instructions
-		let mut local_execute_xcm = fees_local_xcm;
-		// continue with rest of assets
-		local_execute_xcm.extend_from_slice(&[
+		// XCM instructions to be executed on local chain
+		let mut local_execute_xcm = Xcm(vec![
 			// withdraw reserve-based assets
 			WithdrawAsset(assets.clone()),
 			// burn reserve-based assets
 			BurnAsset(assets),
 		]);
-
-		// on destination chain, start off with custom fee instructions
-		let mut xcm_on_dest = fees_remote_xcm;
-		// continue with rest of assets
-		xcm_on_dest.extend_from_slice(&[
+		// XCM instructions to be executed on destination chain
+		let mut xcm_on_dest = Xcm(vec![
 			// withdraw `assets` from origin chain's sovereign account
 			WithdrawAsset(reanchored_assets),
 			// following instructions are not exec'ed on behalf of origin chain anymore
 			ClearOrigin,
 		]);
-		if !fees_already_handled {
-			// no custom fees instructions, they are batched together with `assets` transfer;
-			// BuyExecution happens after receiving all `assets`
-			let reanchored_fees =
-				fees.reanchored(&dest, context).map_err(|_| Error::<T>::CannotReanchor)?;
-			// buy execution using `fees` batched together with above `reanchored_assets`
-			xcm_on_dest.push(BuyExecution { fees: reanchored_fees, weight_limit });
-		}
+		// handle fees
+		Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
 		// deposit all remaining assets in holding to `beneficiary` location
-		xcm_on_dest.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
+		xcm_on_dest
+			.inner_mut()
+			.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
 
-		Ok((Xcm(local_execute_xcm), Xcm(xcm_on_dest)))
+		Ok((local_execute_xcm, xcm_on_dest))
 	}
 
 	// function assumes fees and assets have the same remote reserve
 	fn remote_reserve_transfer_program(
+		origin: MultiLocation,
 		reserve: MultiLocation,
 		dest: MultiLocation,
 		beneficiary: MultiLocation,
@@ -1698,6 +1886,10 @@ impl<T: Config> Pallet<T> {
 		fees: MultiAsset,
 		weight_limit: WeightLimit,
 	) -> Result<Xcm<<T as Config>::RuntimeCall>, Error<T>> {
+		let value = (origin, assets);
+		ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
+		let (_, assets) = value;
+
 		let max_assets = assets.len() as u32;
 		let context = T::UniversalLocation::get();
 		// we spend up to half of fees for execution on reserve and other half for execution on
@@ -1760,6 +1952,8 @@ impl<T: Config> Pallet<T> {
 			&dummy_context,
 		)
 		.map_err(|_| Error::<T>::CannotCheckOutTeleport)?;
+		// safe to do this here, we're in a transactional call that will be reverted on any
+		// errors down the line
 		<T::XcmExecutor as XcmAssetTransfers>::AssetTransactor::check_out(
 			&dest,
 			&fees,
@@ -1783,24 +1977,74 @@ impl<T: Config> Pallet<T> {
 	}
 
 	fn teleport_assets_program(
+		origin: MultiLocation,
 		dest: MultiLocation,
 		beneficiary: MultiLocation,
 		assets: Vec<MultiAsset>,
-		mut fees: MultiAsset,
+		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
-	) -> Result<Xcm<<T as Config>::RuntimeCall>, Error<T>> {
+	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Xcm<()>), Error<T>> {
+		let value = (origin, assets);
+		ensure!(T::XcmTeleportFilter::contains(&value), Error::<T>::Filtered);
+		let (_, assets) = value;
+
+		// max assets is `assets` (+ potentially separately handled fee)
+		let max_assets =
+			assets.len() as u32 + if matches!(&fees, FeesHandling::Batched { .. }) { 0 } else { 1 };
 		let context = T::UniversalLocation::get();
-		fees.reanchor(&dest, context).map_err(|_| Error::<T>::CannotReanchor)?;
-		let max_assets = assets.len() as u32;
-		let xcm_on_dest = Xcm(vec![
-			BuyExecution { fees, weight_limit },
-			DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
+		let assets: MultiAssets = assets.into();
+		let mut reanchored_assets = assets.clone();
+		reanchored_assets
+			.reanchor(&dest, context)
+			.map_err(|_| Error::<T>::CannotReanchor)?;
+
+		// XcmContext irrelevant in teleports checks
+		let dummy_context =
+			XcmContext { origin: None, message_id: Default::default(), topic: None };
+		for asset in assets.inner() {
+			// We should check that the asset can actually be teleported out (for this to
+			// be in error, there would need to be an accounting violation by ourselves,
+			// so it's unlikely, but we don't want to allow that kind of bug to leak into
+			// a trusted chain.
+			<T::XcmExecutor as XcmAssetTransfers>::AssetTransactor::can_check_out(
+				&dest,
+				asset,
+				&dummy_context,
+			)
+			.map_err(|_| Error::<T>::CannotCheckOutTeleport)?;
+		}
+		for asset in assets.inner() {
+			// safe to do this here, we're in a transactional call that will be reverted on any
+			// errors down the line
+			<T::XcmExecutor as XcmAssetTransfers>::AssetTransactor::check_out(
+				&dest,
+				asset,
+				&dummy_context,
+			);
+		}
+
+		// XCM instructions to be executed on local chain
+		let mut local_execute_xcm = Xcm(vec![
+			// withdraw assets to be teleported
+			WithdrawAsset(assets.clone()),
+			// burn assets on local chain
+			BurnAsset(assets),
 		]);
-		Ok(Xcm(vec![
-			WithdrawAsset(assets.into()),
-			SetFeesMode { jit_withdraw: true },
-			InitiateTeleport { assets: Wild(AllCounted(max_assets)), dest, xcm: xcm_on_dest },
-		]))
+		// XCM instructions to be executed on destination chain
+		let mut xcm_on_dest = Xcm(vec![
+			// teleport `assets` in from origin chain
+			ReceiveTeleportedAsset(reanchored_assets),
+			// following instructions are not exec'ed on behalf of origin chain anymore
+			ClearOrigin,
+		]);
+		// handle fees
+		Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
+		// deposit all remaining assets in holding to `beneficiary` location
+		xcm_on_dest
+			.inner_mut()
+			.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
+
+		Ok((local_execute_xcm, xcm_on_dest))
 	}
 
 	/// Halve `fees` fungible amount.
diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs
index 606d51bb8bc..9a734d0f276 100644
--- a/polkadot/xcm/pallet-xcm/src/mock.rs
+++ b/polkadot/xcm/pallet-xcm/src/mock.rs
@@ -579,6 +579,51 @@ impl super::benchmarking::Config for Test {
 			Parachain(OTHER_PARA_ID).into(),
 		))
 	}
+
+	fn set_up_complex_asset_transfer(
+	) -> Option<(MultiAssets, u32, MultiLocation, Box<dyn FnOnce()>)> {
+		use crate::tests::assets_transfer::{into_multiassets_checked, set_up_foreign_asset};
+		// Transfer native asset (local reserve) to `USDT_PARA_ID`. Using teleport-trusted USDT for
+		// fees.
+
+		let asset_amount = 10u128;
+		let fee_amount = 2u128;
+
+		// create sufficient foreign asset USDT
+		let usdt_initial_local_amount = fee_amount * 10;
+		let (usdt_chain, _, usdt_id_multilocation) =
+			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, true);
+
+		// native assets transfer destination is USDT chain (teleport trust only for USDT)
+		let dest = usdt_chain;
+		let (assets, fee_index, _, _) = into_multiassets_checked(
+			// USDT for fees (is sufficient on local chain too) - teleported
+			(usdt_id_multilocation, fee_amount).into(),
+			// native asset to transfer (not used for fees) - local reserve
+			(MultiLocation::here(), asset_amount).into(),
+		);
+
+		let existential_deposit = ExistentialDeposit::get();
+		let caller = frame_benchmarking::whitelisted_caller();
+		// Give some multiple of the existential deposit
+		let balance = asset_amount + existential_deposit * 1000;
+		let _ = <Balances as frame_support::traits::Currency<_>>::make_free_balance_be(
+			&caller, balance,
+		);
+		// verify initial balance
+		assert_eq!(Balances::free_balance(&caller), balance);
+
+		// verify transferred successfully
+		let verify = Box::new(move || {
+			// verify balance after transfer, decreased by transferred amount
+			assert_eq!(Balances::free_balance(&caller), balance - asset_amount);
+			assert_eq!(
+				Assets::balance(usdt_id_multilocation, &caller),
+				usdt_initial_local_amount - fee_amount
+			);
+		});
+		Some((assets, fee_index as u32, dest, verify))
+	}
 }
 
 pub(crate) fn last_event() -> RuntimeEvent {
diff --git a/polkadot/xcm/pallet-xcm/src/tests/assets_transfer.rs b/polkadot/xcm/pallet-xcm/src/tests/assets_transfer.rs
index d1b298765e2..fb0bef26ebd 100644
--- a/polkadot/xcm/pallet-xcm/src/tests/assets_transfer.rs
+++ b/polkadot/xcm/pallet-xcm/src/tests/assets_transfer.rs
@@ -19,6 +19,7 @@
 use crate::{
 	mock::*,
 	tests::{ALICE, BOB, FEE_AMOUNT, INITIAL_BALANCE, SEND_AMOUNT},
+	DispatchResult, OriginFor,
 };
 use frame_support::{
 	assert_ok,
@@ -32,6 +33,7 @@ use xcm_executor::traits::ConvertLocation;
 
 // Helper function to deduplicate testing different teleport types.
 fn do_test_and_verify_teleport_assets<Call: FnOnce()>(
+	origin_location: MultiLocation,
 	expected_beneficiary: MultiLocation,
 	call: Call,
 	expected_weight_limit: WeightLimit,
@@ -40,8 +42,9 @@ fn do_test_and_verify_teleport_assets<Call: FnOnce()>(
 		(ALICE, INITIAL_BALANCE),
 		(ParaId::from(OTHER_PARA_ID).into_account_truncating(), INITIAL_BALANCE),
 	];
+	let dest = RelayLocation::get().into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		let weight = BaseXcmWeight::get() * 3;
+		let weight = BaseXcmWeight::get() * 2;
 		assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE);
 		// call extrinsic
 		call();
@@ -49,7 +52,7 @@ fn do_test_and_verify_teleport_assets<Call: FnOnce()>(
 		assert_eq!(
 			sent_xcm(),
 			vec![(
-				RelayLocation::get().into(),
+				dest,
 				Xcm(vec![
 					ReceiveTeleportedAsset((Here, SEND_AMOUNT).into()),
 					ClearOrigin,
@@ -63,10 +66,23 @@ fn do_test_and_verify_teleport_assets<Call: FnOnce()>(
 		);
 		let versioned_sent = VersionedXcm::from(sent_xcm().into_iter().next().unwrap().1);
 		let _check_v2_ok: xcm::v2::Xcm<()> = versioned_sent.try_into().unwrap();
+
+		let mut last_events = last_events(3).into_iter();
 		assert_eq!(
-			last_event(),
+			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(weight) })
 		);
+		assert_eq!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
+				paying: origin_location,
+				fees: MultiAssets::new(),
+			})
+		);
+		assert!(matches!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::Sent { .. })
+		));
 	});
 }
 
@@ -76,8 +92,10 @@ fn do_test_and_verify_teleport_assets<Call: FnOnce()>(
 /// local effects.
 #[test]
 fn teleport_assets_works() {
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	let beneficiary: MultiLocation = AccountId32 { network: None, id: BOB.into() }.into();
 	do_test_and_verify_teleport_assets(
+		origin_location,
 		beneficiary,
 		|| {
 			assert_ok!(XcmPallet::teleport_assets(
@@ -98,10 +116,12 @@ fn teleport_assets_works() {
 /// local effects.
 #[test]
 fn limited_teleport_assets_works() {
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	let beneficiary: MultiLocation = AccountId32 { network: None, id: BOB.into() }.into();
 	let weight_limit = WeightLimit::Limited(Weight::from_parts(5000, 5000));
 	let expected_weight_limit = weight_limit.clone();
 	do_test_and_verify_teleport_assets(
+		origin_location,
 		beneficiary,
 		|| {
 			assert_ok!(XcmPallet::limited_teleport_assets(
@@ -160,7 +180,7 @@ fn reserve_transfer_assets_with_paid_router_works() {
 		let xcm_router_fee_amount = Para3000PaymentAmount::get();
 		let weight = BaseXcmWeight::get();
 		let dest: MultiLocation =
-			Junction::AccountId32 { network: None, id: user_account.clone().into() }.into();
+			AccountId32 { network: None, id: user_account.clone().into() }.into();
 		assert_eq!(Balances::total_balance(&user_account), INITIAL_BALANCE);
 		assert_ok!(XcmPallet::reserve_transfer_assets(
 			RuntimeOrigin::signed(user_account.clone()),
@@ -221,7 +241,7 @@ fn reserve_transfer_assets_with_paid_router_works() {
 	});
 }
 
-fn set_up_foreign_asset(
+pub(crate) fn set_up_foreign_asset(
 	reserve_para_id: u32,
 	inner_junction: Option<Junction>,
 	initial_amount: u128,
@@ -238,7 +258,7 @@ fn set_up_foreign_asset(
 		reserve_location
 	};
 
-	// create sufficient (to be used as fees as well) foreign asset (0 total issuance)
+	// create sufficient (to be used as fees as well) foreign asset
 	assert_ok!(Assets::force_create(
 		RuntimeOrigin::root(),
 		foreign_asset_id_multilocation,
@@ -260,7 +280,7 @@ fn set_up_foreign_asset(
 
 // Helper function that provides correct `fee_index` after `sort()` done by
 // `vec![MultiAsset, MultiAsset].into()`.
-fn into_multiassets_checked(
+pub(crate) fn into_multiassets_checked(
 	fee_asset: MultiAsset,
 	transfer_asset: MultiAsset,
 ) -> (MultiAssets, usize, MultiAsset, MultiAsset) {
@@ -269,29 +289,31 @@ fn into_multiassets_checked(
 	(assets, fee_index, fee_asset, transfer_asset)
 }
 
-/// Test `limited_reserve_transfer_assets` with local asset reserve and local fee reserve.
+/// Test `tested_call` with local asset reserve and local fee reserve.
 ///
 /// Transferring native asset (local reserve) to some `OTHER_PARA_ID` (no teleport trust).
 /// Using native asset for fees as well.
 ///
-/// ```nocompile
-///    Here (source)                               OTHER_PARA_ID (destination)
-///    |  `assets` reserve
-///    |  `fees` reserve
-///    |
-///    |  1. execute `TransferReserveAsset(assets_and_fees_batched_together)`
-///    |     \--> sends `ReserveAssetDeposited(both), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
-#[test]
-fn limited_reserve_transfer_assets_with_local_asset_reserve_and_local_fee_reserve_works() {
+/// Verifies `expected_result`
+fn local_asset_reserve_and_local_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![
 		(ALICE, INITIAL_BALANCE),
 		(ParaId::from(OTHER_PARA_ID).into_account_truncating(), INITIAL_BALANCE),
 	];
-
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	let weight_limit = WeightLimit::Limited(Weight::from_parts(5000, 5000));
 	let expected_weight_limit = weight_limit.clone();
 	let expected_beneficiary = beneficiary;
@@ -301,14 +323,19 @@ fn limited_reserve_transfer_assets_with_local_asset_reserve_and_local_fee_reserv
 		let weight = BaseXcmWeight::get();
 		assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE);
 		// call extrinsic
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new((Here, SEND_AMOUNT).into()),
 			0,
 			weight_limit,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
 		// Alice spent amount
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - SEND_AMOUNT);
 		// Destination account (parachain account) has amount
@@ -337,7 +364,7 @@ fn limited_reserve_transfer_assets_with_local_asset_reserve_and_local_fee_reserv
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: expected_beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -348,33 +375,66 @@ fn limited_reserve_transfer_assets_with_local_asset_reserve_and_local_fee_reserv
 	});
 }
 
-/// Test `reserve_transfer_assets` with destination asset reserve and local fee reserve.
+/// Test `transfer_assets` with local asset reserve and local fee reserve works.
+#[test]
+fn transfer_assets_with_local_asset_reserve_and_local_fee_reserve_works() {
+	let expected_result = Ok(());
+	local_asset_reserve_and_local_fee_reserve_call(XcmPallet::transfer_assets, expected_result);
+}
+
+/// Test `limited_reserve_transfer_assets` with local asset reserve and local fee reserve works.
+#[test]
+fn reserve_transfer_assets_with_local_asset_reserve_and_local_fee_reserve_works() {
+	let expected_result = Ok(());
+	local_asset_reserve_and_local_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with local asset reserve and local fee reserve disallowed.
+#[test]
+fn teleport_assets_with_local_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	local_asset_reserve_and_local_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with destination asset reserve and local fee reserve.
 ///
 /// Transferring foreign asset (`FOREIGN_ASSET_RESERVE_PARA_ID` reserve) to
 /// `FOREIGN_ASSET_RESERVE_PARA_ID` (no teleport trust).
 /// Using native asset (local reserve) for fees.
 ///
-/// ```nocompile
-///    Here (source)                               FOREIGN_ASSET_RESERVE_PARA_ID (destination)
-///    |  `fees` reserve                          `assets` reserve
-///    |
-///    |  1. execute `TransferReserveAsset(fees)`
-///    |     \-> sends `ReserveAssetDeposited(fees), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    |  2. execute `InitiateReserveWithdraw(assets)`
-///    |     \--> sends `WithdrawAsset(assets), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
-///
 /// Asserts that the sender's balance is decreased and the beneficiary's balance
 /// is increased. Verifies the correct message is sent and event is emitted.
-#[test]
-fn reserve_transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_works() {
+///
+/// Verifies `expected_result`.
+fn destination_asset_reserve_and_local_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let weight = BaseXcmWeight::get() * 3;
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (reserve_location, reserve_sovereign_account, foreign_asset_id_multilocation) =
 			set_up_foreign_asset(
@@ -404,14 +464,19 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
 			fee_index as u32,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
 
 		let mut last_events = last_events(3).into_iter();
 		assert_eq!(
@@ -454,7 +519,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -465,17 +530,67 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_
 	});
 }
 
-/// Test `reserve_transfer_assets` with remote asset reserve and local fee reserve.
+/// Test `transfer_assets` with destination asset reserve and local fee reserve.
+#[test]
+fn transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_works() {
+	let expected_result = Ok(());
+	destination_asset_reserve_and_local_fee_reserve_call(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with destination asset reserve and local fee reserve
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_destination_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	destination_asset_reserve_and_local_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with destination asset reserve and local fee reserve
+/// disallowed.
+#[test]
+fn teleport_assets_with_destination_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	destination_asset_reserve_and_local_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with remote asset reserve and local fee reserve is disallowed.
 ///
 /// Transferring foreign asset (reserve on `FOREIGN_ASSET_RESERVE_PARA_ID`) to `OTHER_PARA_ID`.
 /// Using native (local reserve) as fee should be disallowed.
-#[test]
-fn reserve_transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disallowed() {
+fn remote_asset_reserve_and_local_fee_reserve_call_disallowed<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (_, _, foreign_asset_id_multilocation) = set_up_foreign_asset(
 			FOREIGN_ASSET_RESERVE_PARA_ID,
@@ -500,7 +615,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disal
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// try the transfer
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
@@ -508,14 +623,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disal
 			fee_index as u32,
 			Unlimited,
 		);
-		assert_eq!(
-			result,
-			Err(DispatchError::Module(ModuleError {
-				index: 4,
-				error: [22, 0, 0, 0],
-				message: Some("InvalidAssetUnsupportedReserve")
-			}))
-		);
+		assert_eq!(result, expected_result);
 
 		// Alice transferred nothing
 		assert_eq!(Assets::balance(foreign_asset_id_multilocation, ALICE), foreign_initial_amount);
@@ -529,28 +637,76 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disal
 	});
 }
 
-/// Test `reserve_transfer_assets` with local asset reserve and destination fee reserve.
+/// Test `transfer_assets` with remote asset reserve and local fee reserve is disallowed.
+#[test]
+fn transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [22, 0, 0, 0],
+		message: Some("InvalidAssetUnsupportedReserve"),
+	}));
+	remote_asset_reserve_and_local_fee_reserve_call_disallowed(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with remote asset reserve and local fee reserve is
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_remote_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	remote_asset_reserve_and_local_fee_reserve_call_disallowed(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with remote asset reserve and local fee reserve is disallowed.
+#[test]
+fn teleport_assets_with_remote_asset_reserve_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	remote_asset_reserve_and_local_fee_reserve_call_disallowed(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with local asset reserve and destination fee reserve.
 ///
 /// Transferring native asset (local reserve) to `USDC_RESERVE_PARA_ID` (no teleport trust). Using
 /// foreign asset (`USDC_RESERVE_PARA_ID` reserve) for fees.
 ///
-/// ```nocompile
-///    Here (source)                               USDC_RESERVE_PARA_ID (destination)
-///    |  `assets` reserve                         `fees` reserve
-///    |
-///    |  1. execute `InitiateReserveWithdraw(fees)`
-///    |     \--> sends `WithdrawAsset(fees), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    |  2. execute `TransferReserveAsset(assets)`
-///    |     \-> sends `ReserveAssetDeposited(assets), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
-#[test]
-fn reserve_transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_works() {
+/// Asserts that the sender's balance is decreased and the beneficiary's balance
+/// is increased. Verifies the correct message is sent and event is emitted.
+///
+/// Verifies `expected_result`.
+fn local_asset_reserve_and_destination_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 142;
 		let (usdc_reserve_location, usdc_chain_sovereign_account, usdc_id_multilocation) =
 			set_up_foreign_asset(
@@ -580,14 +736,20 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
 			fee_index as u32,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
 		let weight = BaseXcmWeight::get() * 3;
 		let mut last_events = last_events(3).into_iter();
 		assert_eq!(
@@ -597,7 +759,7 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -642,22 +804,64 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_
 	});
 }
 
-/// Test `reserve_transfer_assets` with destination asset reserve and destination fee reserve.
-///
-/// ```nocompile
-///    Here (source)                               FOREIGN_ASSET_RESERVE_PARA_ID (destination)
-///    |                                           `fees` reserve
-///    |                                           `assets` reserve
-///    |
-///    |  1. execute `InitiateReserveWithdraw(assets_and_fees_batched_together)`
-///    |     \--> sends `WithdrawAsset(batch), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
+/// Test `transfer_assets` with local asset reserve and destination fee reserve.
 #[test]
-fn reserve_transfer_assets_with_destination_asset_reserve_and_destination_fee_reserve_works() {
+fn transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_works() {
+	let expected_result = Ok(());
+	local_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with local asset reserve and destination fee reserve
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_local_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	local_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with local asset reserve and destination fee reserve disallowed.
+#[test]
+fn teleport_assets_with_local_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	local_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with destination asset reserve and destination fee reserve.
+///
+/// Verifies `expected_result`
+fn destination_asset_reserve_and_destination_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
 		// we'll send just this foreign asset back to its reserve location and use it for fees as
 		// well
@@ -684,14 +888,19 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_destination_fee_re
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
 			fee_index,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
 
 		let weight = BaseXcmWeight::get() * 2;
 		let mut last_events = last_events(3).into_iter();
@@ -702,7 +911,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_destination_fee_re
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -743,18 +952,63 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_destination_fee_re
 	});
 }
 
-/// Test `reserve_transfer_assets` with remote asset reserve and destination fee reserve is
+/// Test `transfer_assets` with destination asset reserve and destination fee reserve.
+#[test]
+fn transfer_assets_with_destination_asset_reserve_and_destination_fee_reserve_works() {
+	let expected_result = Ok(());
+	destination_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with destination asset reserve and destination fee
+/// reserve.
+#[test]
+fn reserve_transfer_assets_with_destination_asset_reserve_and_destination_fee_reserve_works() {
+	let expected_result = Ok(());
+	destination_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with destination asset reserve and destination fee reserve
 /// disallowed.
+#[test]
+fn teleport_assets_with_destination_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	destination_asset_reserve_and_destination_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `transfer_assets` with remote asset reserve and destination fee reserve is disallowed.
 ///
 /// Transferring foreign asset (reserve on `FOREIGN_ASSET_RESERVE_PARA_ID`) to
 /// `USDC_RESERVE_PARA_ID`. Using USDC (destination reserve) as fee.
-#[test]
-fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve_disallowed() {
+fn remote_asset_reserve_and_destination_fee_reserve_call_disallowed<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 42;
 		let (usdc_chain, _, usdc_id_multilocation) = set_up_foreign_asset(
 			USDC_RESERVE_PARA_ID,
@@ -763,7 +1017,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve
 			true,
 		);
 
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (_, _, foreign_asset_id_multilocation) = set_up_foreign_asset(
 			FOREIGN_ASSET_RESERVE_PARA_ID,
@@ -789,7 +1043,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
@@ -797,14 +1051,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve
 			fee_index as u32,
 			Unlimited,
 		);
-		assert_eq!(
-			result,
-			Err(DispatchError::Module(ModuleError {
-				index: 4,
-				error: [22, 0, 0, 0],
-				message: Some("InvalidAssetUnsupportedReserve")
-			}))
-		);
+		assert_eq!(result, expected_result);
 
 		// Alice native asset untouched
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
@@ -819,17 +1066,71 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve
 	});
 }
 
-/// Test `reserve_transfer_assets` with local asset reserve and remote fee reserve is disallowed.
+/// Test `transfer_assets` with remote asset reserve and destination fee reserve is disallowed.
+#[test]
+fn transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [22, 0, 0, 0],
+		message: Some("InvalidAssetUnsupportedReserve"),
+	}));
+	remote_asset_reserve_and_destination_fee_reserve_call_disallowed(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with remote asset reserve and destination fee reserve is
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_remote_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	remote_asset_reserve_and_destination_fee_reserve_call_disallowed(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with remote asset reserve and destination fee reserve is
+/// disallowed.
+#[test]
+fn teleport_assets_with_remote_asset_reserve_and_destination_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	remote_asset_reserve_and_destination_fee_reserve_call_disallowed(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with local asset reserve and remote fee reserve is disallowed.
 ///
 /// Transferring native asset (local reserve) to `OTHER_PARA_ID` (no teleport trust). Using foreign
 /// asset (`USDC_RESERVE_PARA_ID` remote reserve) for fees.
-#[test]
-fn reserve_transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disallowed() {
+fn local_asset_reserve_and_remote_fee_reserve_call_disallowed<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 142;
 		let (_, usdc_chain_sovereign_account, usdc_id_multilocation) = set_up_foreign_asset(
 			USDC_RESERVE_PARA_ID,
@@ -854,7 +1155,7 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disal
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
@@ -862,14 +1163,7 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disal
 			fee_index as u32,
 			Unlimited,
 		);
-		assert_eq!(
-			result,
-			Err(DispatchError::Module(ModuleError {
-				index: 4,
-				error: [22, 0, 0, 0],
-				message: Some("InvalidAssetUnsupportedReserve")
-			}))
-		);
+		assert_eq!(result, expected_result);
 		assert_eq!(Assets::balance(usdc_id_multilocation, ALICE), usdc_initial_local_amount);
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 		// Sovereign account of reserve parachain is unchanged
@@ -882,18 +1176,70 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disal
 	});
 }
 
-/// Test `reserve_transfer_assets` with destination asset reserve and remote fee reserve is
+/// Test `transfer_assets` with local asset reserve and remote fee reserve is disallowed.
+#[test]
+fn transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [22, 0, 0, 0],
+		message: Some("InvalidAssetUnsupportedReserve"),
+	}));
+	local_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with local asset reserve and remote fee reserve is
 /// disallowed.
+#[test]
+fn reserve_transfer_assets_with_local_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	local_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with local asset reserve and remote fee reserve is disallowed.
+#[test]
+fn teleport_assets_with_local_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	local_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with destination asset reserve and remote fee reserve is disallowed.
 ///
 /// Transferring native asset (local reserve) to `OTHER_PARA_ID` (no teleport trust). Using foreign
 /// asset (`USDC_RESERVE_PARA_ID` remote reserve) for fees.
-#[test]
-fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve_disallowed() {
+fn destination_asset_reserve_and_remote_fee_reserve_call_disallowed<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 42;
 		let (_, usdc_chain_sovereign_account, usdc_id_multilocation) = set_up_foreign_asset(
 			USDC_RESERVE_PARA_ID,
@@ -902,7 +1248,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve
 			true,
 		);
 
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (reserve_location, foreign_sovereign_account, foreign_asset_id_multilocation) =
 			set_up_foreign_asset(
@@ -928,7 +1274,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
@@ -936,14 +1282,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve
 			fee_index as u32,
 			Unlimited,
 		);
-		assert_eq!(
-			result,
-			Err(DispatchError::Module(ModuleError {
-				index: 4,
-				error: [22, 0, 0, 0],
-				message: Some("InvalidAssetUnsupportedReserve")
-			}))
-		);
+		assert_eq!(result, expected_result);
 		// Alice native asset untouched
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 		assert_eq!(Assets::balance(usdc_id_multilocation, ALICE), usdc_initial_local_amount);
@@ -961,7 +1300,51 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve
 	});
 }
 
-/// Test `reserve_transfer_assets` with remote asset reserve and (same) remote fee reserve.
+/// Test `transfer_assets` with destination asset reserve and remote fee reserve is disallowed.
+#[test]
+fn transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [22, 0, 0, 0],
+		message: Some("InvalidAssetUnsupportedReserve"),
+	}));
+	destination_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with destination asset reserve and remote fee reserve is
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	destination_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with destination asset reserve and remote fee reserve is
+/// disallowed.
+#[test]
+fn teleport_assets_with_destination_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	destination_asset_reserve_and_remote_fee_reserve_call_disallowed(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with remote asset reserve and (same) remote fee reserve.
 ///
 /// Transferring native asset (local reserve) to `OTHER_PARA_ID` (no teleport trust). Using foreign
 /// asset (`USDC_RESERVE_PARA_ID` remote reserve) for fees.
@@ -976,13 +1359,25 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_remote_fee_reserve
 ///    |     -----------------> `C` executes `DepositReserveAsset(both)` dest `B`
 ///    |                             --------------------------> `DepositAsset(both)`
 /// ```
-#[test]
-fn reserve_transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_works() {
+///
+/// Verifies `expected_result`
+fn remote_asset_reserve_and_remote_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 142;
 		let (usdc_chain, usdc_chain_sovereign_account, usdc_id_multilocation) =
 			set_up_foreign_asset(
@@ -996,12 +1391,12 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_work
 		let dest = RelayLocation::get().pushed_with_interior(Parachain(OTHER_PARA_ID)).unwrap();
 
 		let assets: MultiAssets = vec![(usdc_id_multilocation, SEND_AMOUNT).into()].into();
-		let fee_index = 0;
+		let fee_index = 0u32;
 
 		// reanchor according to test-case
 		let context = UniversalLocation::get();
 		let expected_dest_on_reserve = dest.reanchored(&usdc_chain, context).unwrap();
-		let fees = assets.get(fee_index).unwrap().clone();
+		let fees = assets.get(fee_index as usize).unwrap().clone();
 		let (fees_half_1, fees_half_2) = XcmPallet::halve_fees(fees).unwrap();
 		let mut expected_assets_on_reserve = assets.clone();
 		expected_assets_on_reserve.reanchor(&usdc_chain, context).unwrap();
@@ -1013,14 +1408,20 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_work
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
-			fee_index as u32,
+			fee_index,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
 		assert!(matches!(
 			last_event(),
 			RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(_) })
@@ -1067,28 +1468,62 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_work
 	});
 }
 
-/// Test `reserve_transfer_assets` with local asset reserve and teleported fee.
+/// Test `transfer_assets` with remote asset reserve and (same) remote fee reserve.
+#[test]
+fn transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_works() {
+	let expected_result = Ok(());
+	remote_asset_reserve_and_remote_fee_reserve_call(XcmPallet::transfer_assets, expected_result);
+}
+
+/// Test `limited_reserve_transfer_assets` with remote asset reserve and (same) remote fee reserve.
+#[test]
+fn reserve_transfer_assets_with_remote_asset_reserve_and_remote_fee_reserve_works() {
+	let expected_result = Ok(());
+	remote_asset_reserve_and_remote_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with remote asset reserve and (same) remote fee reserve
+/// disallowed.
+#[test]
+fn teleport_assets_with_remote_asset_reserve_and_remote_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	remote_asset_reserve_and_remote_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with local asset reserve and teleported fee.
 ///
 /// Transferring native asset (local reserve) to `USDT_PARA_ID`. Using teleport-trusted USDT for
 /// fees.
 ///
-/// ```nocompile
-///    Here (source)                               USDT_PARA_ID (destination)
-///    |  `assets` reserve                         `fees` teleport-trust
-///    |
-///    |  1. execute `InitiateTeleport(fees)`
-///    |     \--> sends `ReceiveTeleportedAsset(fees), .., DepositAsset(fees)`
-///    |  2. execute `TransferReserveAsset(assets)`
-///    |     \-> sends `ReserveAssetDeposited(assets), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
-#[test]
-fn reserve_transfer_assets_with_local_asset_reserve_and_teleported_fee_works() {
+/// Verifies `expected_result`
+fn local_asset_reserve_and_teleported_fee_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDT (0 total issuance)
+		// create sufficient foreign asset USDT
 		let usdt_initial_local_amount = 42;
 		let (usdt_chain, usdt_chain_sovereign_account, usdt_id_multilocation) =
 			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, true);
@@ -1113,14 +1548,20 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_teleported_fee_works() {
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
 			fee_index as u32,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
 		let weight = BaseXcmWeight::get() * 3;
 		let mut last_events = last_events(3).into_iter();
 		assert_eq!(
@@ -1130,7 +1571,7 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_teleported_fee_works() {
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -1173,34 +1614,70 @@ fn reserve_transfer_assets_with_local_asset_reserve_and_teleported_fee_works() {
 	});
 }
 
-/// Test `reserve_transfer_assets` with destination asset reserve and teleported fee.
+/// Test `transfer_assets` with local asset reserve and teleported fee.
+#[test]
+fn transfer_assets_with_local_asset_reserve_and_teleported_fee_works() {
+	let expected_result = Ok(());
+	local_asset_reserve_and_teleported_fee_call(XcmPallet::transfer_assets, expected_result);
+}
+
+/// Test `limited_reserve_transfer_assets` with local asset reserve and teleported fee disallowed.
+#[test]
+fn reserve_transfer_assets_with_local_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	local_asset_reserve_and_teleported_fee_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with local asset reserve and teleported fee disallowed.
+#[test]
+fn teleport_assets_with_local_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	local_asset_reserve_and_teleported_fee_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with destination asset reserve and teleported fee.
 ///
 /// Transferring foreign asset (destination reserve) to `FOREIGN_ASSET_RESERVE_PARA_ID`. Using
 /// teleport-trusted USDT for fees.
 ///
-/// ```nocompile
-///    Here (source)                               FOREIGN_ASSET_RESERVE_PARA_ID (destination)
-///    |                                           `fees` (USDT) teleport-trust
-///    |                                           `assets` reserve
-///    |
-///    |  1. execute `InitiateTeleport(fees)`
-///    |     \--> sends `ReceiveTeleportedAsset(fees), .., DepositAsset(fees)`
-///    |  2. execute `InitiateReserveWithdraw(assets)`
-///    |     \--> sends `WithdrawAsset(asset), ClearOrigin, BuyExecution(fees), DepositAsset`
-///    \------------------------------------------>
-/// ```
-#[test]
-fn reserve_transfer_assets_with_destination_asset_reserve_and_teleported_fee_works() {
+/// Verifies `expected_result`
+fn destination_asset_reserve_and_teleported_fee_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDT (0 total issuance)
+		// create sufficient foreign asset USDT
 		let usdt_initial_local_amount = 42;
 		let (_, usdt_chain_sovereign_account, usdt_id_multilocation) =
 			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, true);
 
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (reserve_location, foreign_sovereign_account, foreign_asset_id_multilocation) =
 			set_up_foreign_asset(
@@ -1231,14 +1708,20 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_teleported_fee_wor
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
 		// do the transfer
-		assert_ok!(XcmPallet::limited_reserve_transfer_assets(
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
 			Box::new(assets.into()),
 			fee_index as u32,
 			Unlimited,
-		));
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
 		let weight = BaseXcmWeight::get() * 4;
 		let mut last_events = last_events(3).into_iter();
 		assert_eq!(
@@ -1248,7 +1731,7 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_teleported_fee_wor
 		assert_eq!(
 			last_events.next().unwrap(),
 			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
-				paying: beneficiary,
+				paying: origin_location,
 				fees: MultiAssets::new(),
 			})
 		);
@@ -1303,22 +1786,68 @@ fn reserve_transfer_assets_with_destination_asset_reserve_and_teleported_fee_wor
 	});
 }
 
-/// Test `reserve_transfer_assets` with remote asset reserve and teleported fee is disallowed.
+/// Test `transfer_assets` with destination asset reserve and teleported fee.
+#[test]
+fn transfer_assets_with_destination_asset_reserve_and_teleported_fee_works() {
+	let expected_result = Ok(());
+	destination_asset_reserve_and_teleported_fee_call(XcmPallet::transfer_assets, expected_result);
+}
+
+/// Test `limited_reserve_transfer_assets` with destination asset reserve and teleported fee
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_destination_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	destination_asset_reserve_and_teleported_fee_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with destination asset reserve and teleported fee disallowed.
+#[test]
+fn teleport_assets_with_destination_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	destination_asset_reserve_and_teleported_fee_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with remote asset reserve and teleported fee is disallowed.
 ///
 /// Transferring foreign asset (reserve on `FOREIGN_ASSET_RESERVE_PARA_ID`) to `USDT_PARA_ID`.
 /// Using teleport-trusted USDT for fees.
-#[test]
-fn reserve_transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallowed() {
+fn remote_asset_reserve_and_teleported_fee_reserve_call_disallowed<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDT (0 total issuance)
+		// create sufficient foreign asset USDT
 		let usdt_initial_local_amount = 42;
 		let (usdt_chain, usdt_chain_sovereign_account, usdt_id_multilocation) =
 			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, true);
 
-		// create non-sufficient foreign asset BLA (0 total issuance)
+		// create non-sufficient foreign asset BLA
 		let foreign_initial_amount = 142;
 		let (_, reserve_sovereign_account, foreign_asset_id_multilocation) = set_up_foreign_asset(
 			FOREIGN_ASSET_RESERVE_PARA_ID,
@@ -1341,8 +1870,8 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallow
 		assert_eq!(Assets::balance(usdt_id_multilocation, ALICE), usdt_initial_local_amount);
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 
-		// do the transfer
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		// try the transfer
+		let result = tested_call(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(dest.into()),
 			Box::new(beneficiary.into()),
@@ -1350,14 +1879,7 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallow
 			fee_index as u32,
 			Unlimited,
 		);
-		assert_eq!(
-			result,
-			Err(DispatchError::Module(ModuleError {
-				index: 4,
-				error: [22, 0, 0, 0],
-				message: Some("InvalidAssetUnsupportedReserve")
-			}))
-		);
+		assert_eq!(result, expected_result);
 		// Alice native asset untouched
 		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
 		assert_eq!(Assets::balance(usdt_id_multilocation, ALICE), usdt_initial_local_amount);
@@ -1375,17 +1897,59 @@ fn reserve_transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallow
 	});
 }
 
+/// Test `transfer_assets` with remote asset reserve and teleported fee is disallowed.
+#[test]
+fn transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [22, 0, 0, 0],
+		message: Some("InvalidAssetUnsupportedReserve"),
+	}));
+	remote_asset_reserve_and_teleported_fee_reserve_call_disallowed(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with remote asset reserve and teleported fee is
+/// disallowed.
+#[test]
+fn reserve_transfer_assets_with_remote_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [23, 0, 0, 0],
+		message: Some("TooManyReserves"),
+	}));
+	remote_asset_reserve_and_teleported_fee_reserve_call_disallowed(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with remote asset reserve and teleported fee is disallowed.
+#[test]
+fn teleport_assets_with_remote_asset_reserve_and_teleported_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	remote_asset_reserve_and_teleported_fee_reserve_call_disallowed(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
 /// Test `reserve_transfer_assets` single asset which is teleportable - should fail.
 ///
 /// Attempting to reserve-transfer teleport-trusted USDT to `USDT_PARA_ID` should fail.
 #[test]
-fn reserve_transfer_assets_with_teleportable_asset_fails() {
+fn reserve_transfer_assets_with_teleportable_asset_disallowed() {
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDT (0 total issuance)
+		// create sufficient foreign asset USDT
 		let usdt_initial_local_amount = 42;
 		let (usdt_chain, usdt_chain_sovereign_account, usdt_id_multilocation) =
 			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, true);
@@ -1428,9 +1992,9 @@ fn reserve_transfer_assets_with_teleportable_asset_fails() {
 	});
 }
 
-/// Test `reserve_transfer_assets` with teleportable fee that is filtered - should fail.
+/// Test `transfer_assets` with teleportable fee that is filtered - should fail.
 #[test]
-fn reserve_transfer_assets_with_filtered_teleported_fee_disallowed() {
+fn transfer_assets_with_filtered_teleported_fee_disallowed() {
 	let beneficiary: MultiLocation = AccountId32 { network: None, id: BOB.into() }.into();
 	new_test_ext_with_balances(vec![(ALICE, INITIAL_BALANCE)]).execute_with(|| {
 		let (assets, fee_index, _, _) = into_multiassets_checked(
@@ -1439,7 +2003,7 @@ fn reserve_transfer_assets_with_filtered_teleported_fee_disallowed() {
 			// native asset to transfer (not used for fees) - local reserve
 			(MultiLocation::here(), SEND_AMOUNT).into(),
 		);
-		let result = XcmPallet::limited_reserve_transfer_assets(
+		let result = XcmPallet::transfer_assets(
 			RuntimeOrigin::signed(ALICE),
 			Box::new(FilteredTeleportLocation::get().into()),
 			Box::new(beneficiary.into()),
@@ -1466,10 +2030,9 @@ fn reserve_transfer_assets_with_filtered_teleported_fee_disallowed() {
 #[test]
 fn intermediary_error_reverts_side_effects() {
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
-	let beneficiary: MultiLocation =
-		Junction::AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
 	new_test_ext_with_balances(balances).execute_with(|| {
-		// create sufficient foreign asset USDC (0 total issuance)
+		// create sufficient foreign asset USDC
 		let usdc_initial_local_amount = 142;
 		let (_, usdc_chain_sovereign_account, usdc_id_multilocation) = set_up_foreign_asset(
 			USDC_RESERVE_PARA_ID,
@@ -1515,3 +2078,329 @@ fn intermediary_error_reverts_side_effects() {
 		assert_eq!(sent_xcm(), vec![]);
 	});
 }
+
+/// Test `tested_call` with teleportable asset and local fee reserve.
+///
+/// Transferring USDT to `USDT_PARA_ID` (teleport trust). Using native asset (local reserve) for
+/// fees.
+///
+/// Verifies `expected_result`
+fn teleport_asset_using_local_fee_reserve_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
+	let weight = BaseXcmWeight::get() * 3;
+	let balances = vec![(ALICE, INITIAL_BALANCE)];
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	new_test_ext_with_balances(balances).execute_with(|| {
+		// create non-sufficient foreign asset USDT
+		let usdt_initial_local_amount = 42;
+		let (usdt_chain, usdt_chain_sovereign_account, usdt_id_multilocation) =
+			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, false);
+
+		// transfer destination is reserve location (no teleport trust)
+		let dest = usdt_chain;
+
+		let (assets, fee_index, fee_asset, xfer_asset) = into_multiassets_checked(
+			// native asset for fee - local reserve
+			(MultiLocation::here(), FEE_AMOUNT).into(),
+			// USDT to transfer - destination reserve
+			(usdt_id_multilocation, SEND_AMOUNT).into(),
+		);
+
+		// reanchor according to test-case
+		let context = UniversalLocation::get();
+		let expected_fee = fee_asset.reanchored(&dest, context).unwrap();
+		let expected_asset = xfer_asset.reanchored(&dest, context).unwrap();
+
+		// balances checks before
+		assert_eq!(Assets::balance(usdt_id_multilocation, ALICE), usdt_initial_local_amount);
+		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
+
+		// do the transfer
+		let result = tested_call(
+			RuntimeOrigin::signed(ALICE),
+			Box::new(dest.into()),
+			Box::new(beneficiary.into()),
+			Box::new(assets.into()),
+			fee_index as u32,
+			Unlimited,
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
+		let mut last_events = last_events(3).into_iter();
+		assert_eq!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(weight) })
+		);
+
+		// Alice spent (transferred) amount
+		assert_eq!(
+			Assets::balance(usdt_id_multilocation, ALICE),
+			usdt_initial_local_amount - SEND_AMOUNT
+		);
+		// Alice used native asset for fees
+		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - FEE_AMOUNT);
+		// Destination account (parachain account) added native reserve to balances
+		assert_eq!(Balances::free_balance(usdt_chain_sovereign_account.clone()), FEE_AMOUNT);
+		assert_eq!(Assets::balance(usdt_id_multilocation, usdt_chain_sovereign_account), 0);
+		// Verify total and active issuance of foreign BLA have decreased (burned on
+		// reserve-withdraw)
+		let expected_issuance = usdt_initial_local_amount - SEND_AMOUNT;
+		assert_eq!(Assets::total_issuance(usdt_id_multilocation), expected_issuance);
+		assert_eq!(Assets::active_issuance(usdt_id_multilocation), expected_issuance);
+
+		// Verify sent XCM program
+		assert_eq!(
+			sent_xcm(),
+			vec![(
+				dest,
+				// `fees` are being sent through local-reserve transfer because fee reserve is
+				// local chain; `assets` are burned on source and withdrawn from SA here
+				Xcm(vec![
+					ReserveAssetDeposited(expected_fee.clone().into()),
+					buy_limited_execution(expected_fee, Unlimited),
+					ReceiveTeleportedAsset(expected_asset.into()),
+					ClearOrigin,
+					DepositAsset { assets: AllCounted(2).into(), beneficiary },
+				])
+			)]
+		);
+		assert_eq!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
+				paying: origin_location,
+				fees: MultiAssets::new(),
+			})
+		);
+		assert!(matches!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::Sent { .. })
+		));
+	});
+}
+
+/// Test `transfer_assets` with teleportable asset and local fee reserve.
+#[test]
+fn transfer_assets_with_teleportable_asset_and_local_fee_reserve_works() {
+	let expected_result = Ok(());
+	teleport_asset_using_local_fee_reserve_call(XcmPallet::transfer_assets, expected_result);
+}
+
+/// Test `limited_reserve_transfer_assets` with teleportable asset and local fee reserve disallowed.
+#[test]
+fn reserve_transfer_assets_with_teleportable_asset_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	teleport_asset_using_local_fee_reserve_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with teleportable asset and local fee reserve disallowed.
+#[test]
+fn teleport_assets_with_teleportable_asset_and_local_fee_reserve_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	teleport_asset_using_local_fee_reserve_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
+
+/// Test `tested_call` with teleported asset reserve and destination fee.
+///
+/// Transferring USDT to `FOREIGN_ASSET_RESERVE_PARA_ID` (teleport trust). Using foreign asset
+/// (destination reserve) for fees.
+///
+/// Verifies `expected_result`
+fn teleported_asset_using_destination_reserve_fee_call<Call>(
+	tested_call: Call,
+	expected_result: DispatchResult,
+) where
+	Call: FnOnce(
+		OriginFor<Test>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiLocation>,
+		Box<VersionedMultiAssets>,
+		u32,
+		WeightLimit,
+	) -> DispatchResult,
+{
+	let balances = vec![(ALICE, INITIAL_BALANCE)];
+	let origin_location: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	let beneficiary: MultiLocation = AccountId32 { network: None, id: ALICE.into() }.into();
+	new_test_ext_with_balances(balances).execute_with(|| {
+		// create sufficient foreign asset BLA to be used for fees
+		let foreign_initial_amount = 142;
+		let (reserve_location, foreign_sovereign_account, foreign_asset_id_multilocation) =
+			set_up_foreign_asset(
+				FOREIGN_ASSET_RESERVE_PARA_ID,
+				Some(FOREIGN_ASSET_INNER_JUNCTION),
+				foreign_initial_amount,
+				true,
+			);
+
+		// create non-sufficient foreign asset USDT
+		let usdt_initial_local_amount = 42;
+		let (_, usdt_chain_sovereign_account, usdt_id_multilocation) =
+			set_up_foreign_asset(USDT_PARA_ID, None, usdt_initial_local_amount, false);
+
+		// transfer destination is BLA reserve location
+		let dest = reserve_location;
+		let dest_sovereign_account = foreign_sovereign_account;
+
+		let (assets, fee_index, fee_asset, xfer_asset) = into_multiassets_checked(
+			// foreign asset BLA used for fees - destination reserve
+			(foreign_asset_id_multilocation, FEE_AMOUNT).into(),
+			// USDT to transfer - teleported
+			(usdt_id_multilocation, SEND_AMOUNT).into(),
+		);
+
+		// reanchor according to test-case
+		let context = UniversalLocation::get();
+		let expected_fee = fee_asset.reanchored(&dest, context).unwrap();
+		let expected_asset = xfer_asset.reanchored(&dest, context).unwrap();
+
+		// balances checks before
+		assert_eq!(Assets::balance(usdt_id_multilocation, ALICE), usdt_initial_local_amount);
+		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
+
+		// do the transfer
+		let result = tested_call(
+			RuntimeOrigin::signed(ALICE),
+			Box::new(dest.into()),
+			Box::new(beneficiary.into()),
+			Box::new(assets.into()),
+			fee_index as u32,
+			Unlimited,
+		);
+		assert_eq!(result, expected_result);
+		if expected_result.is_err() {
+			// short-circuit here for tests where we expect failure
+			return
+		}
+
+		let weight = BaseXcmWeight::get() * 4;
+		let mut last_events = last_events(3).into_iter();
+		assert_eq!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(weight) })
+		);
+		assert_eq!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::FeesPaid {
+				paying: origin_location,
+				fees: MultiAssets::new(),
+			})
+		);
+		assert!(matches!(
+			last_events.next().unwrap(),
+			RuntimeEvent::XcmPallet(crate::Event::Sent { .. })
+		));
+		// Alice native asset untouched
+		assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE);
+		// Alice spent USDT for fees
+		assert_eq!(
+			Assets::balance(usdt_id_multilocation, ALICE),
+			usdt_initial_local_amount - SEND_AMOUNT
+		);
+		// Alice transferred BLA
+		assert_eq!(
+			Assets::balance(foreign_asset_id_multilocation, ALICE),
+			foreign_initial_amount - FEE_AMOUNT
+		);
+		// Verify balances of USDT reserve parachain
+		assert_eq!(Balances::free_balance(usdt_chain_sovereign_account.clone()), 0);
+		assert_eq!(Assets::balance(usdt_id_multilocation, usdt_chain_sovereign_account), 0);
+		// Verify balances of transferred-asset reserve parachain
+		assert_eq!(Balances::free_balance(dest_sovereign_account.clone()), 0);
+		assert_eq!(Assets::balance(foreign_asset_id_multilocation, dest_sovereign_account), 0);
+		// Verify total and active issuance of USDT have decreased (teleported)
+		let expected_usdt_issuance = usdt_initial_local_amount - SEND_AMOUNT;
+		assert_eq!(Assets::total_issuance(usdt_id_multilocation), expected_usdt_issuance);
+		assert_eq!(Assets::active_issuance(usdt_id_multilocation), expected_usdt_issuance);
+		// Verify total and active issuance of foreign BLA asset have decreased (burned on
+		// reserve-withdraw)
+		let expected_bla_issuance = foreign_initial_amount - FEE_AMOUNT;
+		assert_eq!(Assets::total_issuance(foreign_asset_id_multilocation), expected_bla_issuance);
+		assert_eq!(Assets::active_issuance(foreign_asset_id_multilocation), expected_bla_issuance);
+
+		// Verify sent XCM program
+		assert_eq!(
+			sent_xcm(),
+			vec![(
+				dest,
+				Xcm(vec![
+					// fees are withdrawn from origin's local SA
+					WithdrawAsset(expected_fee.clone().into()),
+					buy_limited_execution(expected_fee, Unlimited),
+					// assets are teleported to destination chain
+					ReceiveTeleportedAsset(expected_asset.into()),
+					ClearOrigin,
+					DepositAsset { assets: AllCounted(2).into(), beneficiary },
+				])
+			)]
+		);
+	});
+}
+
+/// Test `transfer_assets` with teleported asset reserve and destination fee.
+#[test]
+fn transfer_teleported_assets_using_destination_reserve_fee_works() {
+	let expected_result = Ok(());
+	teleported_asset_using_destination_reserve_fee_call(
+		XcmPallet::transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_reserve_transfer_assets` with teleported asset reserve and destination fee
+/// disallowed.
+#[test]
+fn reserve_transfer_teleported_assets_using_destination_reserve_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	teleported_asset_using_destination_reserve_fee_call(
+		XcmPallet::limited_reserve_transfer_assets,
+		expected_result,
+	);
+}
+
+/// Test `limited_teleport_assets` with teleported asset reserve and destination fee disallowed.
+#[test]
+fn teleport_assets_using_destination_reserve_fee_disallowed() {
+	let expected_result = Err(DispatchError::Module(ModuleError {
+		index: 4,
+		error: [2, 0, 0, 0],
+		message: Some("Filtered"),
+	}));
+	teleported_asset_using_destination_reserve_fee_call(
+		XcmPallet::limited_teleport_assets,
+		expected_result,
+	);
+}
diff --git a/polkadot/xcm/pallet-xcm/src/tests/mod.rs b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
index 056c7dcc196..5829eb6edec 100644
--- a/polkadot/xcm/pallet-xcm/src/tests/mod.rs
+++ b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
@@ -16,7 +16,7 @@
 
 #![cfg(test)]
 
-mod assets_transfer;
+pub(crate) mod assets_transfer;
 
 use crate::{
 	mock::*, AssetTraps, CurrentMigration, Error, LatestVersionedMultiLocation, Queries,
diff --git a/prdoc/pr_2388.prdoc b/prdoc/pr_2388.prdoc
new file mode 100644
index 00000000000..fa560197aff
--- /dev/null
+++ b/prdoc/pr_2388.prdoc
@@ -0,0 +1,26 @@
+# Schema: Parity PR Documentation Schema (prdoc)
+# See doc at https://github.com/paritytech/prdoc
+
+title: Add new flexible `pallet_xcm::transfer_assets()` call/extrinsic
+
+doc:
+  - audience: Builder
+    description: |
+      For complex combinations of asset transfers where assets and fees may have different reserves or
+      different reserve/teleport trust configurations, users can use the newly added `transfer_assets()`
+      extrinsic which is more flexible in allowing more complex scenarios.
+      The new extrinsic enables, for example, a (non-system) parachain to teleport their `ForeignAssets`
+      assets to `AssetHub` while using (reserve-based) `DOT` to pay fees.
+      notes:
+        - Now `(limited_)reserve_transfer_assets()` only allow reserve-based transfers for all assets
+          including fees, similarly `(limited_)teleport_assets()` only allows teleports for all assets
+          including fees.
+
+migrations:
+  db: []
+
+  runtime: []
+
+crates: pallet-xcm
+
+host_functions: []
diff --git a/substrate/frame/assets/src/benchmarking.rs b/substrate/frame/assets/src/benchmarking.rs
index c9b0825542d..f8495a1c8f2 100644
--- a/substrate/frame/assets/src/benchmarking.rs
+++ b/substrate/frame/assets/src/benchmarking.rs
@@ -54,7 +54,7 @@ fn create_default_asset<T: Config<I>, I: 'static>(
 	(asset_id, caller, caller_lookup)
 }
 
-fn create_default_minted_asset<T: Config<I>, I: 'static>(
+pub fn create_default_minted_asset<T: Config<I>, I: 'static>(
 	is_sufficient: bool,
 	amount: T::Balance,
 ) -> (T::AssetIdParameter, T::AccountId, AccountIdLookupOf<T>) {
diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs
index 79e4fe30018..13aee138ad3 100644
--- a/substrate/frame/assets/src/lib.rs
+++ b/substrate/frame/assets/src/lib.rs
@@ -141,7 +141,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 #[cfg(feature = "runtime-benchmarks")]
-mod benchmarking;
+pub mod benchmarking;
 pub mod migration;
 #[cfg(test)]
 pub mod mock;
-- 
GitLab