From 98dc667aefcb88367a8004b1be793e140abb9c7a Mon Sep 17 00:00:00 2001
From: Adrian Catangiu <adrian@parity.io>
Date: Wed, 15 May 2024 11:18:48 +0300
Subject: [PATCH] backport pallet-xcm::transfer_assets_using_type_and_then()
 (#3695) (#4462)

Add `transfer_assets_using_type_and_then()` for transferring assets from
 local chain to destination chain using explicit XCM transfer types:
- `TransferType::LocalReserve`: transfer assets to sovereign account of
destination chain and forward a notification XCM to `dest` to mint and
deposit reserve-based assets to `beneficiary`.
- `TransferType::DestinationReserve`: 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`.
- `TransferType::RemoteReserve(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`. Typically the remote `reserve` is
Asset Hub.
- `TransferType::Teleport`: burn local assets and forward XCM to `dest`
chain to mint/teleport assets and deposit them to `beneficiary`.

By default, an asset's reserve is its origin chain. But sometimes we may
want to explicitly use another chain as reserve (as long as allowed by
runtime `IsReserve` filter).

This is very helpful for transferring assets with multiple configured
reserves (such as Asset Hub ForeignAssets), when the transfer strictly
depends on the used reserve.

E.g. For transferring Foreign Assets over a bridge, Asset Hub must be
used as the reserve location.

ERC20-tokenX is registered on AssetHub as a ForeignAsset by the
Polkadot<>Ethereum bridge (Snowbridge). Its asset_id is something like
`(parents:2, (GlobalConsensus(Ethereum), Address(tokenX_contract)))`.
Its _original_ reserve is Ethereum (only we can't use Ethereum as a
reserve in local transfers); but, since tokenX is also registered on
AssetHub as a ForeignAsset, we can use AssetHub as a reserve.

With this PR we can transfer tokenX from ParaA to ParaB while using
AssetHub as a reserve.

AssetA created on ParaA but also registered as foreign asset on Asset
Hub. Can use AssetHub as a reserve.

And all of the above can be done while still controlling transfer type
for `fees` so mixing assets in same transfer is supported.

Provides the caller with the ability to specify custom XCM that be
executed on `dest` chain as the last step of the transfer, thus allowing
custom usecases for the transferred assets. E.g. some are
used/swapped/etc there, while some are sent further to yet another
chain.

This allows usecases such as:

https://forum.polkadot.network/t/managing-sas-on-multiple-reserve-chains-for-same-asset/7538/4

Signed-off-by: Adrian Catangiu <adrian@parity.io>
---
 polkadot/xcm/pallet-xcm/Cargo.toml            |   2 +-
 polkadot/xcm/pallet-xcm/src/lib.rs            | 396 ++++++++++++------
 polkadot/xcm/xcm-executor/Cargo.toml          |   2 +-
 .../xcm-executor/src/traits/asset_transfer.rs |   6 +-
 4 files changed, 277 insertions(+), 129 deletions(-)

diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml
index 83197282b5f6..70b33aa559b4 100644
--- a/polkadot/xcm/pallet-xcm/Cargo.toml
+++ b/polkadot/xcm/pallet-xcm/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-xcm"
-version = "8.0.4"
+version = "8.0.5"
 description = "A pallet for handling XCM programs."
 authors.workspace = true
 edition.workspace = true
diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs
index e183307dfc37..37ba3188e573 100644
--- a/polkadot/xcm/pallet-xcm/src/lib.rs
+++ b/polkadot/xcm/pallet-xcm/src/lib.rs
@@ -45,7 +45,7 @@ use sp_runtime::{
 		AccountIdConversion, BadOrigin, BlakeTwo256, BlockNumberProvider, Dispatchable, Hash,
 		Saturating, Zero,
 	},
-	RuntimeDebug,
+	Either, RuntimeDebug,
 };
 use sp_std::{boxed::Box, marker::PhantomData, prelude::*, result::Result, vec};
 use xcm::{latest::QueryResponseInfo, prelude::*};
@@ -1230,64 +1230,20 @@ pub mod pallet {
 			);
 
 			ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
-			let mut assets = assets.into_inner();
+			let 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.clone(),
-						dest.clone(),
-						fees,
-						weight_limit,
-					)?,
-					TransferType::DestinationReserve =>
-						Self::destination_reserve_fees_instructions(
-							origin.clone(),
-							dest.clone(),
-							fees,
-							weight_limit,
-						)?,
-					TransferType::Teleport => Self::teleport_fees_instructions(
-						origin.clone(),
-						dest.clone(),
-						fees,
-						weight_limit,
-					)?,
-					TransferType::RemoteReserve(_) =>
-						return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
-				};
-				FeesHandling::Separate { local_xcm, remote_xcm }
-			};
-
-			Self::build_and_execute_xcm_transfer_type(
+			Self::do_transfer_assets(
 				origin,
 				dest,
-				beneficiary,
+				Either::Left(beneficiary),
 				assets,
 				assets_transfer_type,
-				fees,
+				fee_asset_item,
+				fees_transfer_type,
 				weight_limit,
 			)
 		}
@@ -1333,6 +1289,97 @@ pub mod pallet {
 			})?;
 			Ok(())
 		}
+
+		/// Transfer assets from the local chain to the destination chain using explicit transfer
+		/// types for assets and fees.
+		///
+		/// `assets` must have same reserve location or may be teleportable to `dest`. Caller must
+		/// provide the `assets_transfer_type` to be used for `assets`:
+		///  - `TransferType::LocalReserve`: transfer assets to sovereign account of destination
+		///    chain and forward a notification XCM to `dest` to mint and deposit reserve-based
+		///    assets to `beneficiary`.
+		///  - `TransferType::DestinationReserve`: 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`.
+		///  - `TransferType::RemoteReserve(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`. Typically
+		///    the remote `reserve` is Asset Hub.
+		///  - `TransferType::Teleport`: burn local assets and forward XCM to `dest` chain to
+		///    mint/teleport assets and deposit them to `beneficiary`.
+		///
+		/// On the destination chain, as well as any intermediary hops, `BuyExecution` is used to
+		/// buy execution using transferred `assets` identified by `remote_fees_id`.
+		/// Make sure enough of the specified `remote_fees_id` asset is included in the given list
+		/// of `assets`. `remote_fees_id` should be enough to pay for `weight_limit`. If more weight
+		/// is needed than `weight_limit`, then the operation will fail and the sent assets may be
+		/// at risk.
+		///
+		/// `remote_fees_id` may use different transfer type than rest of `assets` and can be
+		/// specified through `fees_transfer_type`.
+		///
+		/// The caller needs to specify what should happen to the transferred assets once they reach
+		/// the `dest` chain. This is done through the `custom_xcm_on_dest` parameter, which
+		/// contains the instructions to execute on `dest` as a final step.
+		///   This is usually as simple as:
+		///   `Xcm(vec![DepositAsset { assets: Wild(AllCounted(assets.len())), beneficiary }])`,
+		///   but could be something more exotic like sending the `assets` even further.
+		///
+		/// - `origin`: Must be capable of withdrawing the `assets` and executing XCM.
+		/// - `dest`: Destination context for the assets. Will typically be `[Parent,
+		///   Parachain(..)]` to send from parachain to parachain, or `[Parachain(..)]` to send from
+		///   relay to parachain, or `(parents: 2, (GlobalConsensus(..), ..))` to send from
+		///   parachain across a bridge to another ecosystem destination.
+		/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
+		///   fee on the `dest` (and possibly reserve) chains.
+		/// - `assets_transfer_type`: The XCM `TransferType` used to transfer the `assets`.
+		/// - `remote_fees_id`: One of the included `assets` to be be used to pay fees.
+		/// - `fees_transfer_type`: The XCM `TransferType` used to transfer the `fees` assets.
+		/// - `custom_xcm_on_dest`: The XCM to be executed on `dest` chain as the last step of the
+		///   transfer, which also determines what happens to the assets on the destination chain.
+		/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
+		#[pallet::call_index(13)]
+		#[pallet::weight(T::WeightInfo::transfer_assets())]
+		pub fn transfer_assets_using_type_and_then(
+			origin: OriginFor<T>,
+			dest: Box<VersionedLocation>,
+			assets: Box<VersionedAssets>,
+			assets_transfer_type: Box<TransferType>,
+			remote_fees_id: Box<VersionedAssetId>,
+			fees_transfer_type: Box<TransferType>,
+			custom_xcm_on_dest: Box<VersionedXcm<()>>,
+			weight_limit: WeightLimit,
+		) -> DispatchResult {
+			let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?;
+			let dest: Location = (*dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			let assets: Assets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			let fees_id: AssetId =
+				(*remote_fees_id).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			let remote_xcm: Xcm<()> =
+				(*custom_xcm_on_dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
+			log::debug!(
+				target: "xcm::pallet_xcm::transfer_assets_using_type_and_then",
+				"origin {origin_location:?}, dest {dest:?}, assets {assets:?} through {assets_transfer_type:?}, \
+				remote_fees_id {fees_id:?} through {fees_transfer_type:?}, \
+				custom_xcm_on_dest {remote_xcm:?}, weight-limit {weight_limit:?}",
+			);
+
+			let assets = assets.into_inner();
+			ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
+
+			let fee_asset_index =
+				assets.iter().position(|a| a.id == fees_id).ok_or(Error::<T>::FeesNotMet)?;
+			Self::do_transfer_assets(
+				origin_location,
+				dest,
+				Either::Right(remote_xcm),
+				assets,
+				*assets_transfer_type,
+				fee_asset_index,
+				*fees_transfer_type,
+				weight_limit,
+			)
+		}
 	}
 }
 
@@ -1498,15 +1545,16 @@ impl<T: Config> Pallet<T> {
 		// 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,
-			dest,
-			beneficiary,
+		let (local_xcm, remote_xcm) = Self::build_xcm_transfer_type(
+			origin.clone(),
+			dest.clone(),
+			Either::Left(beneficiary),
 			assets,
 			assets_transfer_type,
 			FeesHandling::Batched { fees },
 			weight_limit,
-		)
+		)?;
+		Self::execute_xcm_transfer(origin, dest, local_xcm, remote_xcm)
 	}
 
 	fn do_teleport_assets(
@@ -1539,83 +1587,158 @@ impl<T: Config> Pallet<T> {
 		}
 		let fees = assets.get(fee_asset_item as usize).ok_or(Error::<T>::Empty)?.clone();
 
-		Self::build_and_execute_xcm_transfer_type(
-			origin_location,
-			dest,
-			beneficiary,
+		let (local_xcm, remote_xcm) = Self::build_xcm_transfer_type(
+			origin_location.clone(),
+			dest.clone(),
+			Either::Left(beneficiary),
 			assets,
 			TransferType::Teleport,
 			FeesHandling::Batched { fees },
 			weight_limit,
-		)
+		)?;
+		Self::execute_xcm_transfer(origin_location, dest, local_xcm, remote_xcm)
 	}
 
-	fn build_and_execute_xcm_transfer_type(
+	fn do_transfer_assets(
 		origin: Location,
 		dest: Location,
-		beneficiary: Location,
-		assets: Vec<Asset>,
-		transfer_type: TransferType,
-		fees: FeesHandling<T>,
+		beneficiary: Either<Location, Xcm<()>>,
+		mut assets: Vec<Asset>,
+		assets_transfer_type: TransferType,
+		fee_asset_index: usize,
+		fees_transfer_type: TransferType,
 		weight_limit: WeightLimit,
 	) -> DispatchResult {
-		log::debug!(
-			target: "xcm::pallet_xcm::build_and_execute_xcm_transfer_type",
-			"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, transfer_type {:?}, \
-			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(
+		// local and remote XCM programs to potentially handle fees separately
+		let fees = if fees_transfer_type == assets_transfer_type {
+			let fees = assets.get(fee_asset_index).ok_or(Error::<T>::Empty)?.clone();
+			// 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_index);
+			let (local_xcm, remote_xcm) = match fees_transfer_type {
+				TransferType::LocalReserve => Self::local_reserve_fees_instructions(
 					origin.clone(),
 					dest.clone(),
-					beneficiary,
-					assets,
 					fees,
 					weight_limit,
-				)?;
-				(local, Some(remote))
-			},
-			TransferType::DestinationReserve => {
-				let (local, remote) = Self::destination_reserve_transfer_programs(
+				)?,
+				TransferType::DestinationReserve => Self::destination_reserve_fees_instructions(
 					origin.clone(),
 					dest.clone(),
-					beneficiary,
-					assets,
 					fees,
 					weight_limit,
-				)?;
-				(local, Some(remote))
-			},
+				)?,
+				TransferType::Teleport => Self::teleport_fees_instructions(
+					origin.clone(),
+					dest.clone(),
+					fees,
+					weight_limit,
+				)?,
+				TransferType::RemoteReserve(_) =>
+					return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
+			};
+			FeesHandling::Separate { local_xcm, remote_xcm }
+		};
+
+		let (local_xcm, remote_xcm) = Self::build_xcm_transfer_type(
+			origin.clone(),
+			dest.clone(),
+			beneficiary,
+			assets,
+			assets_transfer_type,
+			fees,
+			weight_limit,
+		)?;
+		Self::execute_xcm_transfer(origin, dest, local_xcm, remote_xcm)
+	}
+
+	fn build_xcm_transfer_type(
+		origin: Location,
+		dest: Location,
+		beneficiary: Either<Location, Xcm<()>>,
+		assets: Vec<Asset>,
+		transfer_type: TransferType,
+		fees: FeesHandling<T>,
+		weight_limit: WeightLimit,
+	) -> Result<(Xcm<<T as Config>::RuntimeCall>, Option<Xcm<()>>), Error<T>> {
+		log::debug!(
+			target: "xcm::pallet_xcm::build_xcm_transfer_type",
+			"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, transfer_type {:?}, \
+			fees_handling {:?}, weight_limit: {:?}",
+			origin, dest, beneficiary, assets, transfer_type, fees, weight_limit,
+		);
+		match transfer_type {
+			TransferType::LocalReserve => Self::local_reserve_transfer_programs(
+				origin.clone(),
+				dest.clone(),
+				beneficiary,
+				assets,
+				fees,
+				weight_limit,
+			)
+			.map(|(local, remote)| (local, Some(remote))),
+			TransferType::DestinationReserve => Self::destination_reserve_transfer_programs(
+				origin.clone(),
+				dest.clone(),
+				beneficiary,
+				assets,
+				fees,
+				weight_limit,
+			)
+			.map(|(local, remote)| (local, Some(remote))),
 			TransferType::RemoteReserve(reserve) => {
 				let fees = match fees {
 					FeesHandling::Batched { fees } => fees,
 					_ => return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
 				};
-				let local = Self::remote_reserve_transfer_program(
+				Self::remote_reserve_transfer_program(
 					origin.clone(),
-					reserve,
-					dest.clone(),
+					reserve.try_into().map_err(|()| Error::<T>::BadVersion)?,
 					beneficiary,
-					assets,
-					fees,
-					weight_limit,
-				)?;
-				(local, None)
-			},
-			TransferType::Teleport => {
-				let (local, remote) = Self::teleport_assets_program(
-					origin.clone(),
 					dest.clone(),
-					beneficiary,
 					assets,
 					fees,
 					weight_limit,
-				)?;
-				(local, Some(remote))
+				)
+				.map(|local| (local, None))
 			},
-		};
+			TransferType::Teleport => Self::teleport_assets_program(
+				origin.clone(),
+				dest.clone(),
+				beneficiary,
+				assets,
+				fees,
+				weight_limit,
+			)
+			.map(|(local, remote)| (local, Some(remote))),
+		}
+	}
+
+	fn execute_xcm_transfer(
+		origin: Location,
+		dest: Location,
+		mut local_xcm: Xcm<<T as Config>::RuntimeCall>,
+		remote_xcm: Option<Xcm<()>>,
+	) -> DispatchResult {
+		log::debug!(
+			target: "xcm::pallet_xcm::execute_xcm_transfer",
+			"origin {:?}, dest {:?}, local_xcm {:?}, remote_xcm {:?}",
+			origin, dest, local_xcm, remote_xcm,
+		);
+
 		let weight =
 			T::Weigher::weight(&mut local_xcm).map_err(|()| Error::<T>::UnweighableMessage)?;
 		let mut hash = local_xcm.using_encoded(sp_io::hashing::blake2_256);
@@ -1629,7 +1752,7 @@ impl<T: Config> Pallet<T> {
 		Self::deposit_event(Event::Attempted { outcome: outcome.clone() });
 		outcome.ensure_complete().map_err(|error| {
 			log::error!(
-				target: "xcm::pallet_xcm::build_and_execute_xcm_transfer_type",
+				target: "xcm::pallet_xcm::execute_xcm_transfer",
 				"XCM execution failed with error {:?}", error
 			);
 			Error::<T>::LocalExecutionIncomplete
@@ -1641,7 +1764,7 @@ impl<T: Config> Pallet<T> {
 			if origin != Here.into_location() {
 				Self::charge_fees(origin.clone(), price).map_err(|error| {
 					log::error!(
-						target: "xcm::pallet_xcm::build_and_execute_xcm_transfer_type",
+						target: "xcm::pallet_xcm::execute_xcm_transfer",
 						"Unable to charge fee with error {:?}", error
 					);
 					Error::<T>::FeesNotMet
@@ -1716,7 +1839,7 @@ impl<T: Config> Pallet<T> {
 	fn local_reserve_transfer_programs(
 		origin: Location,
 		dest: Location,
-		beneficiary: Location,
+		beneficiary: Either<Location, Xcm<()>>,
 		assets: Vec<Asset>,
 		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
@@ -1749,10 +1872,16 @@ impl<T: Config> Pallet<T> {
 		]);
 		// 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 });
+
+		// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
+		let custom_remote_xcm = match beneficiary {
+			Either::Right(custom_xcm) => custom_xcm,
+			Either::Left(beneficiary) => {
+				// deposit all remaining assets in holding to `beneficiary` location
+				Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
+			},
+		};
+		xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
 
 		Ok((local_execute_xcm, xcm_on_dest))
 	}
@@ -1791,7 +1920,7 @@ impl<T: Config> Pallet<T> {
 	fn destination_reserve_transfer_programs(
 		origin: Location,
 		dest: Location,
-		beneficiary: Location,
+		beneficiary: Either<Location, Xcm<()>>,
 		assets: Vec<Asset>,
 		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
@@ -1826,10 +1955,16 @@ impl<T: Config> Pallet<T> {
 		]);
 		// 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 });
+
+		// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
+		let custom_remote_xcm = match beneficiary {
+			Either::Right(custom_xcm) => custom_xcm,
+			Either::Left(beneficiary) => {
+				// deposit all remaining assets in holding to `beneficiary` location
+				Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
+			},
+		};
+		xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
 
 		Ok((local_execute_xcm, xcm_on_dest))
 	}
@@ -1838,8 +1973,8 @@ impl<T: Config> Pallet<T> {
 	fn remote_reserve_transfer_program(
 		origin: Location,
 		reserve: Location,
+		beneficiary: Either<Location, Xcm<()>>,
 		dest: Location,
-		beneficiary: Location,
 		assets: Vec<Asset>,
 		fees: Asset,
 		weight_limit: WeightLimit,
@@ -1864,10 +1999,17 @@ impl<T: Config> Pallet<T> {
 		// identifies `dest` as seen by `reserve`
 		let dest = dest.reanchored(&reserve, &context).map_err(|_| Error::<T>::CannotReanchor)?;
 		// xcm to be executed at dest
-		let xcm_on_dest = Xcm(vec![
-			BuyExecution { fees: dest_fees, weight_limit: weight_limit.clone() },
-			DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
-		]);
+		let mut xcm_on_dest =
+			Xcm(vec![BuyExecution { fees: dest_fees, weight_limit: weight_limit.clone() }]);
+		// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
+		let custom_xcm_on_dest = match beneficiary {
+			Either::Right(custom_xcm) => custom_xcm,
+			Either::Left(beneficiary) => {
+				// deposit all remaining assets in holding to `beneficiary` location
+				Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
+			},
+		};
+		xcm_on_dest.0.extend(custom_xcm_on_dest.into_iter());
 		// xcm to be executed on reserve
 		let xcm_on_reserve = Xcm(vec![
 			BuyExecution { fees: reserve_fees, weight_limit },
@@ -1939,7 +2081,7 @@ impl<T: Config> Pallet<T> {
 	fn teleport_assets_program(
 		origin: Location,
 		dest: Location,
-		beneficiary: Location,
+		beneficiary: Either<Location, Xcm<()>>,
 		assets: Vec<Asset>,
 		fees: FeesHandling<T>,
 		weight_limit: WeightLimit,
@@ -1999,10 +2141,16 @@ impl<T: Config> Pallet<T> {
 		]);
 		// 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 });
+
+		// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
+		let custom_remote_xcm = match beneficiary {
+			Either::Right(custom_xcm) => custom_xcm,
+			Either::Left(beneficiary) => {
+				// deposit all remaining assets in holding to `beneficiary` location
+				Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
+			},
+		};
+		xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
 
 		Ok((local_execute_xcm, xcm_on_dest))
 	}
diff --git a/polkadot/xcm/xcm-executor/Cargo.toml b/polkadot/xcm/xcm-executor/Cargo.toml
index 8de7ae47d833..dba3fc332bd5 100644
--- a/polkadot/xcm/xcm-executor/Cargo.toml
+++ b/polkadot/xcm/xcm-executor/Cargo.toml
@@ -4,7 +4,7 @@ description = "An abstract and configurable XCM message executor."
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
-version = "8.0.1"
+version = "8.0.2"
 
 [lints]
 workspace = true
diff --git a/polkadot/xcm/xcm-executor/src/traits/asset_transfer.rs b/polkadot/xcm/xcm-executor/src/traits/asset_transfer.rs
index 5da3d1da37c8..6d72eaf680fd 100644
--- a/polkadot/xcm/xcm-executor/src/traits/asset_transfer.rs
+++ b/polkadot/xcm/xcm-executor/src/traits/asset_transfer.rs
@@ -30,7 +30,7 @@ pub enum Error {
 }
 
 /// Specify which type of asset transfer is required for a particular `(asset, dest)` combination.
-#[derive(Clone, PartialEq, Debug)]
+#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo)]
 pub enum TransferType {
 	/// should teleport `asset` to `dest`
 	Teleport,
@@ -39,7 +39,7 @@ pub enum TransferType {
 	/// should reserve-transfer `asset` to `dest`, using `dest` as reserve
 	DestinationReserve,
 	/// should reserve-transfer `asset` to `dest`, using remote chain `Location` as reserve
-	RemoteReserve(Location),
+	RemoteReserve(VersionedLocation),
 }
 
 /// A trait for identifying asset transfer type based on `IsTeleporter` and `IsReserve`
@@ -77,7 +77,7 @@ pub trait XcmAssetTransfers {
 			Ok(TransferType::LocalReserve)
 		} else if Self::IsReserve::contains(asset, &asset_location) {
 			// remote location that is recognized as reserve location for asset
-			Ok(TransferType::RemoteReserve(asset_location))
+			Ok(TransferType::RemoteReserve(asset_location.into()))
 		} else {
 			// remote location that is not configured either as teleporter or reserve => cannot
 			// determine asset reserve