From 8ccb6b33c564da038de2af987d4e8d347f32e9c7 Mon Sep 17 00:00:00 2001
From: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Date: Fri, 2 Aug 2024 14:24:19 +0200
Subject: [PATCH] Add an adapter for configuring AssetExchanger (#5130)

Added a new adapter to xcm-builder, the `SingleAssetExchangeAdapter`.
This adapter makes it easy to use `pallet-asset-conversion` for
configuring the `AssetExchanger` XCM config item.

I also took the liberty of adding a new function to the `AssetExchange`
trait, with the following signature:

```rust
fn quote_exchange_price(give: &Assets, want: &Assets, maximal: bool) -> Option<Assets>;
```

The signature is meant to be fairly symmetric to that of
`exchange_asset`.
The way they interact can be seen in the doc comment for it in the
`AssetExchange` trait.

This is a breaking change but is needed for
https://github.com/paritytech/polkadot-sdk/pull/5131.
Another idea is to create a new trait for this but that would require
setting it in the XCM config which is also breaking.

Old PR: https://github.com/paritytech/polkadot-sdk/pull/4375.

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
---
 Cargo.lock                                    |   2 +
 cumulus/primitives/utility/src/lib.rs         |  21 +-
 polkadot/xcm/xcm-builder/Cargo.toml           |   7 +-
 .../xcm/xcm-builder/src/asset_exchange/mod.rs |  22 ++
 .../single_asset_adapter/adapter.rs           | 210 ++++++++++
 .../single_asset_adapter/mock.rs              | 370 ++++++++++++++++++
 .../single_asset_adapter/mod.rs               |  25 ++
 .../single_asset_adapter/tests.rs             | 233 +++++++++++
 polkadot/xcm/xcm-builder/src/lib.rs           |   3 +
 polkadot/xcm/xcm-builder/src/test_utils.rs    |   4 +
 polkadot/xcm/xcm-builder/src/tests/mock.rs    |  14 +
 .../xcm-executor/src/traits/asset_exchange.rs |  31 ++
 prdoc/pr_5130.prdoc                           |  40 ++
 substrate/frame/asset-conversion/src/lib.rs   |   2 +-
 14 files changed, 980 insertions(+), 4 deletions(-)
 create mode 100644 polkadot/xcm/xcm-builder/src/asset_exchange/mod.rs
 create mode 100644 polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs
 create mode 100644 polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs
 create mode 100644 polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mod.rs
 create mode 100644 polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/tests.rs
 create mode 100644 prdoc/pr_5130.prdoc

diff --git a/Cargo.lock b/Cargo.lock
index b89cdf7828c..8cca629e379 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21024,6 +21024,7 @@ dependencies = [
  "frame-system",
  "impl-trait-for-tuples",
  "log",
+ "pallet-asset-conversion",
  "pallet-assets",
  "pallet-balances",
  "pallet-salary",
@@ -21037,6 +21038,7 @@ dependencies = [
  "primitive-types",
  "scale-info",
  "sp-arithmetic",
+ "sp-core",
  "sp-io",
  "sp-runtime",
  "sp-weights",
diff --git a/cumulus/primitives/utility/src/lib.rs b/cumulus/primitives/utility/src/lib.rs
index 9d5bf4e231e..3ebcb44fa43 100644
--- a/cumulus/primitives/utility/src/lib.rs
+++ b/cumulus/primitives/utility/src/lib.rs
@@ -407,10 +407,22 @@ impl<
 		let first_asset: Asset =
 			payment.fungible.pop_first().ok_or(XcmError::AssetNotFound)?.into();
 		let (fungibles_asset, balance) = FungiblesAssetMatcher::matches_fungibles(&first_asset)
-			.map_err(|_| XcmError::AssetNotFound)?;
+			.map_err(|error| {
+				log::trace!(
+					target: "xcm::weight",
+					"SwapFirstAssetTrader::buy_weight asset {:?} didn't match. Error: {:?}",
+					first_asset,
+					error,
+				);
+				XcmError::AssetNotFound
+			})?;
 
 		let swap_asset = fungibles_asset.clone().into();
 		if Target::get().eq(&swap_asset) {
+			log::trace!(
+				target: "xcm::weight",
+				"SwapFirstAssetTrader::buy_weight Asset was same as Target, swap not needed.",
+			);
 			// current trader is not applicable.
 			return Err(XcmError::FeesNotMet)
 		}
@@ -424,7 +436,12 @@ impl<
 			credit_in,
 			fee,
 		)
-		.map_err(|(credit_in, _)| {
+		.map_err(|(credit_in, error)| {
+			log::trace!(
+				target: "xcm::weight",
+				"SwapFirstAssetTrader::buy_weight swap couldn't be done. Error was: {:?}",
+				error,
+			);
 			drop(credit_in);
 			XcmError::FeesNotMet
 		})?;
diff --git a/polkadot/xcm/xcm-builder/Cargo.toml b/polkadot/xcm/xcm-builder/Cargo.toml
index 7702e2f9be0..671f0181277 100644
--- a/polkadot/xcm/xcm-builder/Cargo.toml
+++ b/polkadot/xcm/xcm-builder/Cargo.toml
@@ -22,13 +22,15 @@ sp-weights = { workspace = true }
 frame-support = { workspace = true }
 frame-system = { workspace = true }
 pallet-transaction-payment = { workspace = true }
+pallet-asset-conversion = { workspace = true }
 log = { workspace = true }
 
 # Polkadot dependencies
 polkadot-parachain-primitives = { workspace = true }
 
 [dev-dependencies]
-primitive-types = { workspace = true, default-features = true }
+sp-core = { workspace = true, default-features = true }
+primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true }
 pallet-balances = { workspace = true, default-features = true }
 pallet-xcm = { workspace = true, default-features = true }
 pallet-salary = { workspace = true, default-features = true }
@@ -43,6 +45,7 @@ default = ["std"]
 runtime-benchmarks = [
 	"frame-support/runtime-benchmarks",
 	"frame-system/runtime-benchmarks",
+	"pallet-asset-conversion/runtime-benchmarks",
 	"pallet-assets/runtime-benchmarks",
 	"pallet-balances/runtime-benchmarks",
 	"pallet-salary/runtime-benchmarks",
@@ -59,8 +62,10 @@ std = [
 	"frame-support/std",
 	"frame-system/std",
 	"log/std",
+	"pallet-asset-conversion/std",
 	"pallet-transaction-payment/std",
 	"polkadot-parachain-primitives/std",
+	"primitive-types/std",
 	"scale-info/std",
 	"sp-arithmetic/std",
 	"sp-io/std",
diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/mod.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/mod.rs
new file mode 100644
index 00000000000..d42a443c9be
--- /dev/null
+++ b/polkadot/xcm/xcm-builder/src/asset_exchange/mod.rs
@@ -0,0 +1,22 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Adapters for the AssetExchanger config item.
+//!
+//! E.g. types that implement the [`xcm_executor::traits::AssetExchange`] trait.
+
+mod single_asset_adapter;
+pub use single_asset_adapter::SingleAssetExchangeAdapter;
diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs
new file mode 100644
index 00000000000..fa94ee5f1ca
--- /dev/null
+++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs
@@ -0,0 +1,210 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Single asset exchange adapter.
+
+extern crate alloc;
+use alloc::vec;
+use core::marker::PhantomData;
+use frame_support::{ensure, traits::tokens::fungibles};
+use pallet_asset_conversion::{QuotePrice, SwapCredit};
+use xcm::prelude::*;
+use xcm_executor::{
+	traits::{AssetExchange, MatchesFungibles},
+	AssetsInHolding,
+};
+
+/// An adapter from [`pallet_asset_conversion::SwapCredit`] and
+/// [`pallet_asset_conversion::QuotePrice`] to [`xcm_executor::traits::AssetExchange`].
+///
+/// This adapter takes just one fungible asset in `give` and allows only one fungible asset in
+/// `want`. If you need to handle more assets in either `give` or `want`, then you should use
+/// another type that implements [`xcm_executor::traits::AssetExchange`] or build your own.
+///
+/// This adapter also only works for fungible assets.
+///
+/// `exchange_asset` and `quote_exchange_price` will both return an error if there's
+/// more than one asset in `give` or `want`.
+pub struct SingleAssetExchangeAdapter<AssetConversion, Fungibles, Matcher, AccountId>(
+	PhantomData<(AssetConversion, Fungibles, Matcher, AccountId)>,
+);
+impl<AssetConversion, Fungibles, Matcher, AccountId> AssetExchange
+	for SingleAssetExchangeAdapter<AssetConversion, Fungibles, Matcher, AccountId>
+where
+	AssetConversion: SwapCredit<
+			AccountId,
+			Balance = u128,
+			AssetKind = Fungibles::AssetId,
+			Credit = fungibles::Credit<AccountId, Fungibles>,
+		> + QuotePrice<Balance = u128, AssetKind = Fungibles::AssetId>,
+	Fungibles: fungibles::Balanced<AccountId, Balance = u128>,
+	Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
+{
+	fn exchange_asset(
+		_: Option<&Location>,
+		give: AssetsInHolding,
+		want: &Assets,
+		maximal: bool,
+	) -> Result<AssetsInHolding, AssetsInHolding> {
+		let mut give_iter = give.fungible_assets_iter();
+		let give_asset = give_iter.next().ok_or_else(|| {
+			log::trace!(
+				target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
+				"No fungible asset was in `give`.",
+			);
+			give.clone()
+		})?;
+		ensure!(give_iter.next().is_none(), give.clone()); // We only support 1 asset in `give`.
+		ensure!(give.non_fungible_assets_iter().next().is_none(), give.clone()); // We don't allow non-fungible assets.
+		ensure!(want.len() == 1, give.clone()); // We only support 1 asset in `want`.
+		let want_asset = want.get(0).ok_or_else(|| give.clone())?;
+		let (give_asset_id, give_amount) =
+			Matcher::matches_fungibles(&give_asset).map_err(|error| {
+				log::trace!(
+					target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
+					"Could not map XCM asset give {:?} to FRAME asset. Error: {:?}",
+					give_asset,
+					error,
+				);
+				give.clone()
+			})?;
+		let (want_asset_id, want_amount) =
+			Matcher::matches_fungibles(&want_asset).map_err(|error| {
+				log::trace!(
+					target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
+					"Could not map XCM asset want {:?} to FRAME asset. Error: {:?}",
+					want_asset,
+					error,
+				);
+				give.clone()
+			})?;
+
+		// We have to do this to convert the XCM assets into credit the pool can use.
+		let swap_asset = give_asset_id.clone().into();
+		let credit_in = Fungibles::issue(give_asset_id, give_amount);
+
+		// Do the swap.
+		let (credit_out, maybe_credit_change) = if maximal {
+			// If `maximal`, then we swap exactly `credit_in` to get as much of `want_asset_id` as
+			// we can, with a minimum of `want_amount`.
+			let credit_out = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
+				vec![swap_asset, want_asset_id],
+				credit_in,
+				Some(want_amount),
+			)
+			.map_err(|(credit_in, error)| {
+				log::error!(
+					target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
+					"Could not perform the swap, error: {:?}.",
+					error
+				);
+				drop(credit_in);
+				give.clone()
+			})?;
+
+			// We don't have leftover assets if exchange was maximal.
+			(credit_out, None)
+		} else {
+			// If `minimal`, then we swap as little of `credit_in` as we can to get exactly
+			// `want_amount` of `want_asset_id`.
+			let (credit_out, credit_change) =
+				<AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
+					vec![swap_asset, want_asset_id],
+					credit_in,
+					want_amount,
+				)
+				.map_err(|(credit_in, error)| {
+					log::error!(
+						target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
+						"Could not perform the swap, error: {:?}.",
+						error
+					);
+					drop(credit_in);
+					give.clone()
+				})?;
+
+			(credit_out, Some(credit_change))
+		};
+
+		// We create an `AssetsInHolding` instance by putting in the resulting asset
+		// of the exchange.
+		let resulting_asset: Asset = (want_asset.id.clone(), credit_out.peek()).into();
+		let mut result: AssetsInHolding = resulting_asset.into();
+
+		// If we have some leftover assets from the exchange, also put them in the result.
+		if let Some(credit_change) = maybe_credit_change {
+			let leftover_asset: Asset = (give_asset.id.clone(), credit_change.peek()).into();
+			result.subsume(leftover_asset);
+		}
+
+		Ok(result.into())
+	}
+
+	fn quote_exchange_price(give: &Assets, want: &Assets, maximal: bool) -> Option<Assets> {
+		if give.len() != 1 || want.len() != 1 {
+			return None;
+		} // We only support 1 asset in `give` or `want`.
+		let give_asset = give.get(0)?;
+		let want_asset = want.get(0)?;
+		// We first match both XCM assets to the asset ID types `AssetConversion` can handle.
+		let (give_asset_id, give_amount) = Matcher::matches_fungibles(give_asset)
+			.map_err(|error| {
+				log::trace!(
+					target: "xcm::SingleAssetExchangeAdapter::quote_exchange_price",
+					"Could not map XCM asset {:?} to FRAME asset. Error: {:?}.",
+					give_asset,
+					error,
+				);
+				()
+			})
+			.ok()?;
+		let (want_asset_id, want_amount) = Matcher::matches_fungibles(want_asset)
+			.map_err(|error| {
+				log::trace!(
+					target: "xcm::SingleAssetExchangeAdapter::quote_exchange_price",
+					"Could not map XCM asset {:?} to FRAME asset. Error: {:?}.",
+					want_asset,
+					error,
+				);
+				()
+			})
+			.ok()?;
+		// We quote the price.
+		if maximal {
+			// The amount of `want` resulting from swapping `give`.
+			let resulting_want =
+				<AssetConversion as QuotePrice>::quote_price_exact_tokens_for_tokens(
+					give_asset_id,
+					want_asset_id,
+					give_amount,
+					true, // Include fee.
+				)?;
+
+			Some((want_asset.id.clone(), resulting_want).into())
+		} else {
+			// The `give` amount required to obtain `want`.
+			let necessary_give =
+				<AssetConversion as QuotePrice>::quote_price_tokens_for_exact_tokens(
+					give_asset_id,
+					want_asset_id,
+					want_amount,
+					true, // Include fee.
+				)?;
+
+			Some((give_asset.id.clone(), necessary_give).into())
+		}
+	}
+}
diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs
new file mode 100644
index 00000000000..4d9809e84f8
--- /dev/null
+++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs
@@ -0,0 +1,370 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Mock to test [`SingleAssetExchangeAdapter`].
+
+use core::marker::PhantomData;
+use frame_support::{
+	assert_ok, construct_runtime, derive_impl, ord_parameter_types, parameter_types,
+	traits::{
+		fungible::{self, NativeFromLeft, NativeOrWithId},
+		fungibles::Mutate,
+		tokens::imbalance::ResolveAssetTo,
+		AsEnsureOriginWithArg, Equals, Everything, Nothing, OriginTrait, PalletInfoAccess,
+	},
+	PalletId,
+};
+use sp_core::{ConstU128, ConstU32, Get};
+use sp_runtime::{
+	traits::{AccountIdConversion, IdentityLookup, MaybeEquivalence, TryConvert, TryConvertInto},
+	BuildStorage, Permill,
+};
+use xcm::prelude::*;
+use xcm_executor::{traits::ConvertLocation, XcmExecutor};
+
+use crate::{FungibleAdapter, IsConcrete, MatchedConvertedConcreteId, StartsWith};
+
+pub type Block = frame_system::mocking::MockBlock<Runtime>;
+pub type AccountId = u64;
+pub type Balance = u128;
+
+construct_runtime! {
+	pub struct Runtime {
+		System: frame_system,
+		Balances: pallet_balances,
+		AssetsPallet: pallet_assets::<Instance1>,
+		PoolAssets: pallet_assets::<Instance2>,
+		XcmPallet: pallet_xcm,
+		AssetConversion: pallet_asset_conversion,
+	}
+}
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
+impl frame_system::Config for Runtime {
+	type Block = Block;
+	type AccountId = AccountId;
+	type Lookup = IdentityLookup<AccountId>;
+	type AccountData = pallet_balances::AccountData<Balance>;
+}
+
+#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
+impl pallet_balances::Config for Runtime {
+	type Balance = Balance;
+	type AccountStore = System;
+	type ExistentialDeposit = ConstU128<1>;
+}
+
+pub type TrustBackedAssetsInstance = pallet_assets::Instance1;
+pub type PoolAssetsInstance = pallet_assets::Instance2;
+
+#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)]
+impl pallet_assets::Config<TrustBackedAssetsInstance> for Runtime {
+	type Currency = Balances;
+	type Balance = Balance;
+	type AssetDeposit = ConstU128<1>;
+	type AssetAccountDeposit = ConstU128<10>;
+	type MetadataDepositBase = ConstU128<1>;
+	type MetadataDepositPerByte = ConstU128<1>;
+	type ApprovalDeposit = ConstU128<1>;
+	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
+	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
+	type Freezer = ();
+	type CallbackHandle = ();
+}
+
+#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)]
+impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
+	type Currency = Balances;
+	type Balance = Balance;
+	type AssetDeposit = ConstU128<1>;
+	type AssetAccountDeposit = ConstU128<10>;
+	type MetadataDepositBase = ConstU128<1>;
+	type MetadataDepositPerByte = ConstU128<1>;
+	type ApprovalDeposit = ConstU128<1>;
+	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
+	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
+	type Freezer = ();
+	type CallbackHandle = ();
+}
+
+/// Union fungibles implementation for `Assets` and `Balances`.
+pub type NativeAndAssets =
+	fungible::UnionOf<Balances, AssetsPallet, NativeFromLeft, NativeOrWithId<u32>, AccountId>;
+
+parameter_types! {
+	pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon");
+	pub const Native: NativeOrWithId<u32> = NativeOrWithId::Native;
+	pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0);
+}
+
+ord_parameter_types! {
+	pub const AssetConversionOrigin: AccountId =
+		AccountIdConversion::<AccountId>::into_account_truncating(&AssetConversionPalletId::get());
+}
+
+pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter<
+	AssetConversionPalletId,
+	(NativeOrWithId<u32>, NativeOrWithId<u32>),
+>;
+
+impl pallet_asset_conversion::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type Balance = Balance;
+	type HigherPrecisionBalance = sp_core::U256;
+	type AssetKind = NativeOrWithId<u32>;
+	type Assets = NativeAndAssets;
+	type PoolId = (Self::AssetKind, Self::AssetKind);
+	type PoolLocator = pallet_asset_conversion::WithFirstAsset<
+		Native,
+		AccountId,
+		Self::AssetKind,
+		PoolIdToAccountId,
+	>;
+	type PoolAssetId = u32;
+	type PoolAssets = PoolAssets;
+	type PoolSetupFee = ConstU128<100>; // Asset class deposit fees are sufficient to prevent spam
+	type PoolSetupFeeAsset = Native;
+	type PoolSetupFeeTarget = ResolveAssetTo<AssetConversionOrigin, Self::Assets>;
+	type LiquidityWithdrawalFee = LiquidityWithdrawalFee;
+	type LPFee = ConstU32<3>;
+	type PalletId = AssetConversionPalletId;
+	type MaxSwapPathLength = ConstU32<3>;
+	type MintMinLiquidity = ConstU128<100>;
+	type WeightInfo = ();
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = ();
+}
+
+/// We only alias local accounts.
+pub type LocationToAccountId = AccountIndex64Aliases;
+
+parameter_types! {
+	pub HereLocation: Location = Here.into_location();
+	pub WeightPerInstruction: Weight = Weight::from_parts(1, 1);
+	pub MaxInstructions: u32 = 100;
+	pub UniversalLocation: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into();
+	pub TrustBackedAssetsPalletIndex: u8 = <AssetsPallet as PalletInfoAccess>::index() as u8;
+	pub TrustBackedAssetsPalletLocation: Location =	PalletInstance(TrustBackedAssetsPalletIndex::get()).into();
+}
+
+/// Adapter for the native token.
+pub type FungibleTransactor = FungibleAdapter<
+	// Use this implementation of the `fungible::*` traits.
+	// `Balances` is the name given to the balances pallet
+	Balances,
+	// This transactor deals with the native token.
+	IsConcrete<HereLocation>,
+	// How to convert an XCM Location into a local account id.
+	// This is also something that's configured in the XCM executor.
+	LocationToAccountId,
+	// The type for account ids, only needed because `fungible` is generic over it.
+	AccountId,
+	// Not tracking teleports.
+	(),
+>;
+
+pub type Weigher = crate::FixedWeightBounds<WeightPerInstruction, RuntimeCall, MaxInstructions>;
+
+pub struct LocationToAssetId;
+impl MaybeEquivalence<Location, NativeOrWithId<u32>> for LocationToAssetId {
+	fn convert(location: &Location) -> Option<NativeOrWithId<u32>> {
+		let pallet_instance = TrustBackedAssetsPalletIndex::get();
+		match location.unpack() {
+			(0, [PalletInstance(instance), GeneralIndex(index)])
+				if *instance == pallet_instance =>
+				Some(NativeOrWithId::WithId(*index as u32)),
+			(0, []) => Some(NativeOrWithId::Native),
+			_ => None,
+		}
+	}
+
+	fn convert_back(asset_id: &NativeOrWithId<u32>) -> Option<Location> {
+		let pallet_instance = TrustBackedAssetsPalletIndex::get();
+		Some(match asset_id {
+			NativeOrWithId::WithId(id) =>
+				Location::new(0, [PalletInstance(pallet_instance), GeneralIndex((*id).into())]),
+			NativeOrWithId::Native => Location::new(0, []),
+		})
+	}
+}
+
+pub type PoolAssetsExchanger = crate::SingleAssetExchangeAdapter<
+	AssetConversion,
+	NativeAndAssets,
+	MatchedConvertedConcreteId<
+		NativeOrWithId<u32>,
+		Balance,
+		(StartsWith<TrustBackedAssetsPalletLocation>, Equals<HereLocation>),
+		LocationToAssetId,
+		TryConvertInto,
+	>,
+	AccountId,
+>;
+
+pub struct XcmConfig;
+impl xcm_executor::Config for XcmConfig {
+	type RuntimeCall = RuntimeCall;
+	type XcmSender = ();
+	type AssetTransactor = FungibleTransactor;
+	type OriginConverter = ();
+	type IsReserve = ();
+	type IsTeleporter = ();
+	type UniversalLocation = UniversalLocation;
+	// This is not safe, you should use `crate::AllowTopLevelPaidExecutionFrom<T>` in a
+	// production chain
+	type Barrier = crate::AllowUnpaidExecutionFrom<Everything>;
+	type Weigher = Weigher;
+	type Trader = ();
+	type ResponseHandler = ();
+	type AssetTrap = ();
+	type AssetLocker = ();
+	type AssetExchanger = PoolAssetsExchanger;
+	type AssetClaims = ();
+	type SubscriptionService = ();
+	type PalletInstancesInfo = ();
+	type FeeManager = ();
+	type MaxAssetsIntoHolding = ConstU32<1>;
+	type MessageExporter = ();
+	type UniversalAliases = Nothing;
+	type CallDispatcher = RuntimeCall;
+	type SafeCallFilter = Everything;
+	type Aliasers = Nothing;
+	type TransactionalProcessor = crate::FrameTransactionalProcessor;
+	type HrmpNewChannelOpenRequestHandler = ();
+	type HrmpChannelAcceptedHandler = ();
+	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
+}
+
+/// Simple converter from a [`Location`] with an [`AccountIndex64`] junction and no parent to a
+/// `u64`.
+pub struct AccountIndex64Aliases;
+impl ConvertLocation<AccountId> for AccountIndex64Aliases {
+	fn convert_location(location: &Location) -> Option<AccountId> {
+		let index = match location.unpack() {
+			(0, [AccountIndex64 { index, network: None }]) => index,
+			_ => return None,
+		};
+		Some((*index).into())
+	}
+}
+
+/// `Convert` implementation to convert from some a `Signed` (system) `Origin` into an
+/// `AccountIndex64`.
+///
+/// Typically used when configuring `pallet-xcm` in tests to allow `u64` accounts to dispatch an XCM
+/// from an `AccountIndex64` origin.
+pub struct SignedToAccountIndex64<RuntimeOrigin, AccountId, Network>(
+	PhantomData<(RuntimeOrigin, AccountId, Network)>,
+);
+impl<RuntimeOrigin: OriginTrait + Clone, AccountId: Into<u64>, Network: Get<Option<NetworkId>>>
+	TryConvert<RuntimeOrigin, Location> for SignedToAccountIndex64<RuntimeOrigin, AccountId, Network>
+where
+	RuntimeOrigin::PalletsOrigin: From<frame_system::RawOrigin<AccountId>>
+		+ TryInto<frame_system::RawOrigin<AccountId>, Error = RuntimeOrigin::PalletsOrigin>,
+{
+	fn try_convert(o: RuntimeOrigin) -> Result<Location, RuntimeOrigin> {
+		o.try_with_caller(|caller| match caller.try_into() {
+			Ok(frame_system::RawOrigin::Signed(who)) =>
+				Ok(Junction::AccountIndex64 { network: Network::get(), index: who.into() }.into()),
+			Ok(other) => Err(other.into()),
+			Err(other) => Err(other),
+		})
+	}
+}
+
+parameter_types! {
+	pub const NoNetwork: Option<NetworkId> = None;
+}
+
+pub type LocalOriginToLocation = SignedToAccountIndex64<RuntimeOrigin, AccountId, NoNetwork>;
+
+impl pallet_xcm::Config for Runtime {
+	// We turn off sending for these tests
+	type SendXcmOrigin = crate::EnsureXcmOrigin<RuntimeOrigin, ()>;
+	type XcmRouter = ();
+	// Anyone can execute XCM programs
+	type ExecuteXcmOrigin = crate::EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
+	// We execute any type of program
+	type XcmExecuteFilter = Everything;
+	// How we execute programs
+	type XcmExecutor = XcmExecutor<XcmConfig>;
+	// We don't allow teleports
+	type XcmTeleportFilter = Nothing;
+	// We don't allow reserve transfers
+	type XcmReserveTransferFilter = Nothing;
+	// Same weigher executor uses to weigh XCM programs
+	type Weigher = Weigher;
+	// Same universal location
+	type UniversalLocation = UniversalLocation;
+	// No version discovery needed
+	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 0;
+	type AdvertisedXcmVersion = frame_support::traits::ConstU32<3>;
+	type AdminOrigin = frame_system::EnsureRoot<AccountId>;
+	// No locking
+	type TrustedLockers = ();
+	type MaxLockers = frame_support::traits::ConstU32<0>;
+	type MaxRemoteLockConsumers = frame_support::traits::ConstU32<0>;
+	type RemoteLockConsumerIdentifier = ();
+	// How to turn locations into accounts
+	type SovereignAccountOf = LocationToAccountId;
+	// A currency to pay for things and its matcher, we are using the relay token
+	type Currency = Balances;
+	type CurrencyMatcher = crate::IsConcrete<HereLocation>;
+	// Pallet benchmarks, no need for this recipe
+	type WeightInfo = pallet_xcm::TestWeightInfo;
+	// Runtime types
+	type RuntimeOrigin = RuntimeOrigin;
+	type RuntimeCall = RuntimeCall;
+	type RuntimeEvent = RuntimeEvent;
+}
+
+pub const INITIAL_BALANCE: Balance = 1_000_000_000;
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+	let mut t = frame_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
+
+	pallet_balances::GenesisConfig::<Runtime> {
+		balances: vec![(0, INITIAL_BALANCE), (1, INITIAL_BALANCE), (2, INITIAL_BALANCE)],
+	}
+	.assimilate_storage(&mut t)
+	.unwrap();
+
+	let owner = 0;
+
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| {
+		System::set_block_number(1);
+		assert_ok!(AssetsPallet::force_create(RuntimeOrigin::root(), 1, owner, false, 1,));
+		assert_ok!(AssetsPallet::mint_into(1, &owner, INITIAL_BALANCE,));
+		assert_ok!(AssetConversion::create_pool(
+			RuntimeOrigin::signed(owner),
+			Box::new(NativeOrWithId::Native),
+			Box::new(NativeOrWithId::WithId(1)),
+		));
+		assert_ok!(AssetConversion::add_liquidity(
+			RuntimeOrigin::signed(owner),
+			Box::new(NativeOrWithId::Native),
+			Box::new(NativeOrWithId::WithId(1)),
+			50_000_000,
+			100_000_000,
+			0,
+			0,
+			owner,
+		));
+	});
+	ext
+}
diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mod.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mod.rs
new file mode 100644
index 00000000000..2a47832923f
--- /dev/null
+++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mod.rs
@@ -0,0 +1,25 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! SingleAssetExchangeAdapter.
+
+mod adapter;
+pub use adapter::SingleAssetExchangeAdapter;
+
+#[cfg(test)]
+mod mock;
+#[cfg(test)]
+mod tests;
diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/tests.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/tests.rs
new file mode 100644
index 00000000000..83f57f32822
--- /dev/null
+++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/tests.rs
@@ -0,0 +1,233 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Tests for the [`SingleAssetExchangeAdapter`] type.
+
+use super::mock::*;
+use xcm::prelude::*;
+use xcm_executor::{traits::AssetExchange, AssetsInHolding};
+
+// ========== Happy path ==========
+
+/// Scenario:
+/// Account #3 wants to use the local liquidity pool between two custom assets,
+/// 1 and 2.
+#[test]
+fn maximal_exchange() {
+	new_test_ext().execute_with(|| {
+		let assets = PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 10_000_000).into()].into(),
+			&vec![(Here, 2_000_000).into()].into(),
+			true, // Maximal
+		)
+		.unwrap();
+		let amount = get_amount_from_first_fungible(&assets);
+		assert_eq!(amount, 4_533_054);
+	});
+}
+
+#[test]
+fn minimal_exchange() {
+	new_test_ext().execute_with(|| {
+		let assets = PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 10_000_000).into()].into(),
+			&vec![(Here, 2_000_000).into()].into(),
+			false, // Minimal
+		)
+		.unwrap();
+		let (first_amount, second_amount) = get_amount_from_fungibles(&assets);
+		assert_eq!(first_amount, 2_000_000);
+		assert_eq!(second_amount, 5_820_795);
+	});
+}
+
+#[test]
+fn maximal_quote() {
+	new_test_ext().execute_with(|| {
+		let assets = quote(
+			&([PalletInstance(2), GeneralIndex(1)], 10_000_000).into(),
+			&(Here, 2_000_000).into(),
+			true,
+		)
+		.unwrap();
+		let amount = get_amount_from_first_fungible(&assets.into());
+		// The amount of the native token resulting from swapping all `10_000_000` of the custom
+		// token.
+		assert_eq!(amount, 4_533_054);
+	});
+}
+
+#[test]
+fn minimal_quote() {
+	new_test_ext().execute_with(|| {
+		let assets = quote(
+			&([PalletInstance(2), GeneralIndex(1)], 10_000_000).into(),
+			&(Here, 2_000_000).into(),
+			false,
+		)
+		.unwrap();
+		let amount = get_amount_from_first_fungible(&assets.into());
+		// The amount of the custom token needed to get `2_000_000` of the native token.
+		assert_eq!(amount, 4_179_205);
+	});
+}
+
+// ========== Unhappy path ==========
+
+#[test]
+fn no_asset_in_give() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![].into(),
+			&vec![(Here, 2_000_000).into()].into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn more_than_one_asset_in_give() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 1).into(), (Here, 2).into()].into(),
+			&vec![(Here, 2_000_000).into()].into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn no_asset_in_want() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 10_000_000).into()].into(),
+			&vec![].into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn more_than_one_asset_in_want() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 10_000_000).into()].into(),
+			&vec![(Here, 2_000_000).into(), ([PalletInstance(2), GeneralIndex(1)], 1).into()]
+				.into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn give_asset_does_not_match() {
+	new_test_ext().execute_with(|| {
+		let nonexistent_asset_id = 1000;
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(nonexistent_asset_id)], 10_000_000).into()]
+				.into(),
+			&vec![(Here, 2_000_000).into()].into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn want_asset_does_not_match() {
+	new_test_ext().execute_with(|| {
+		let nonexistent_asset_id = 1000;
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![(Here, 2_000_000).into()].into(),
+			&vec![([PalletInstance(2), GeneralIndex(nonexistent_asset_id)], 10_000_000).into()]
+				.into(),
+			true
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn exchange_fails() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			vec![([PalletInstance(2), GeneralIndex(1)], 10_000_000).into()].into(),
+			// We're asking for too much of the native token...
+			&vec![(Here, 200_000_000).into()].into(),
+			false, // Minimal
+		)
+		.is_err());
+	});
+}
+
+#[test]
+fn non_fungible_asset_in_give() {
+	new_test_ext().execute_with(|| {
+		assert!(PoolAssetsExchanger::exchange_asset(
+			None,
+			// Using `u64` here will give us a non-fungible instead of a fungible.
+			vec![([PalletInstance(2), GeneralIndex(2)], 10_000_000u64).into()].into(),
+			&vec![(Here, 10_000_000).into()].into(),
+			false, // Minimal
+		)
+		.is_err());
+	});
+}
+
+// ========== Helper functions ==========
+
+fn get_amount_from_first_fungible(assets: &AssetsInHolding) -> u128 {
+	let mut fungibles_iter = assets.fungible_assets_iter();
+	let first_fungible = fungibles_iter.next().unwrap();
+	let Fungible(amount) = first_fungible.fun else {
+		unreachable!("Asset should be fungible");
+	};
+	amount
+}
+
+fn get_amount_from_fungibles(assets: &AssetsInHolding) -> (u128, u128) {
+	let mut fungibles_iter = assets.fungible_assets_iter();
+	let first_fungible = fungibles_iter.next().unwrap();
+	let Fungible(first_amount) = first_fungible.fun else {
+		unreachable!("Asset should be fungible");
+	};
+	let second_fungible = fungibles_iter.next().unwrap();
+	let Fungible(second_amount) = second_fungible.fun else {
+		unreachable!("Asset should be fungible");
+	};
+	(first_amount, second_amount)
+}
+
+fn quote(asset_1: &Asset, asset_2: &Asset, maximal: bool) -> Option<Assets> {
+	PoolAssetsExchanger::quote_exchange_price(
+		&asset_1.clone().into(),
+		&asset_2.clone().into(),
+		maximal,
+	)
+}
diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs
index 4cf83c9fc45..bec3bdcb05a 100644
--- a/polkadot/xcm/xcm-builder/src/lib.rs
+++ b/polkadot/xcm/xcm-builder/src/lib.rs
@@ -35,6 +35,9 @@ pub use asset_conversion::{
 	AsPrefixedGeneralIndex, ConvertedConcreteId, MatchedConvertedConcreteId,
 };
 
+mod asset_exchange;
+pub use asset_exchange::SingleAssetExchangeAdapter;
+
 mod barriers;
 pub use barriers::{
 	AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain,
diff --git a/polkadot/xcm/xcm-builder/src/test_utils.rs b/polkadot/xcm/xcm-builder/src/test_utils.rs
index 37a49a1b3dc..90afb2c9a3d 100644
--- a/polkadot/xcm/xcm-builder/src/test_utils.rs
+++ b/polkadot/xcm/xcm-builder/src/test_utils.rs
@@ -109,6 +109,10 @@ impl AssetExchange for TestAssetExchanger {
 	) -> Result<AssetsInHolding, AssetsInHolding> {
 		Ok(want.clone().into())
 	}
+
+	fn quote_exchange_price(give: &Assets, _want: &Assets, _maximal: bool) -> Option<Assets> {
+		Some(give.clone())
+	}
 }
 
 pub struct TestPalletsInfo;
diff --git a/polkadot/xcm/xcm-builder/src/tests/mock.rs b/polkadot/xcm/xcm-builder/src/tests/mock.rs
index ac43d217ff3..9f42aee87c9 100644
--- a/polkadot/xcm/xcm-builder/src/tests/mock.rs
+++ b/polkadot/xcm/xcm-builder/src/tests/mock.rs
@@ -695,6 +695,20 @@ impl AssetExchange for TestAssetExchange {
 		EXCHANGE_ASSETS.with(|l| l.replace(have));
 		Ok(get)
 	}
+
+	fn quote_exchange_price(give: &Assets, want: &Assets, maximal: bool) -> Option<Assets> {
+		let mut have = EXCHANGE_ASSETS.with(|l| l.borrow().clone());
+		if !have.contains_assets(want) {
+			return None;
+		}
+		let get = if maximal {
+			have.saturating_take(give.clone().into())
+		} else {
+			have.saturating_take(want.clone().into())
+		};
+		let result: Vec<Asset> = get.fungible_assets_iter().collect();
+		Some(result.into())
+	}
 }
 
 pub struct SiblingPrefix;
diff --git a/polkadot/xcm/xcm-executor/src/traits/asset_exchange.rs b/polkadot/xcm/xcm-executor/src/traits/asset_exchange.rs
index 432a7498ed4..f4b7135d420 100644
--- a/polkadot/xcm/xcm-executor/src/traits/asset_exchange.rs
+++ b/polkadot/xcm/xcm-executor/src/traits/asset_exchange.rs
@@ -37,6 +37,27 @@ pub trait AssetExchange {
 		want: &Assets,
 		maximal: bool,
 	) -> Result<AssetsInHolding, AssetsInHolding>;
+
+	/// Handler for quoting the exchange price of two asset collections.
+	///
+	/// It's useful before calling `exchange_asset`, to get some information on whether or not the
+	/// exchange will be successful.
+	///
+	/// Arguments:
+	/// - `give` The asset(s) that are going to be given.
+	/// - `want` The asset(s) that are wanted.
+	/// - `maximal`:
+	/// 	  - If `true`, then the return value is the resulting amount of `want` obtained by swapping
+	///      `give`.
+	///   - If `false`, then the return value is the required amount of `give` needed to get `want`.
+	///
+	/// The return value is `Assets` since it comprises both which assets and how much of them.
+	///
+	/// The relationship between this function and `exchange_asset` is the following:
+	/// - quote(give, want, maximal) = resulting_want -> exchange(give, resulting_want, maximal) ✅
+	/// - quote(give, want, minimal) = required_give -> exchange(required_give_amount, want,
+	///   minimal) ✅
+	fn quote_exchange_price(_give: &Assets, _want: &Assets, _maximal: bool) -> Option<Assets>;
 }
 
 #[impl_trait_for_tuples::impl_for_tuples(30)]
@@ -55,4 +76,14 @@ impl AssetExchange for Tuple {
 		)* );
 		Err(give)
 	}
+
+	fn quote_exchange_price(give: &Assets, want: &Assets, maximal: bool) -> Option<Assets> {
+		for_tuples!( #(
+			match Tuple::quote_exchange_price(give, want, maximal) {
+				Some(assets) => return Some(assets),
+				None => {}
+			}
+		)* );
+		None
+	}
 }
diff --git a/prdoc/pr_5130.prdoc b/prdoc/pr_5130.prdoc
new file mode 100644
index 00000000000..c6a00505bab
--- /dev/null
+++ b/prdoc/pr_5130.prdoc
@@ -0,0 +1,40 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Add SingleAssetExchangeAdapter
+
+doc:
+  - audience: Runtime Dev
+    description: |
+      SingleAssetExchangeAdapter is an adapter in xcm-builder that can be used
+      to configure the AssetExchanger in XCM to use pallet-asset-conversion,
+      or any other type that implements the `SwapCredit` and `QuotePrice` traits.
+      It can be configured as follows:
+      ```rust
+      pub type AssetExchanger = SingleAssetExchangeAdapter<
+        // pallet-assets-conversion, as named in `construct_runtime`.
+        AssetConversion,
+        // The fungibles implementation that brings together all assets in pools.
+        // This may be created using `fungible::UnionOf` to mix the native token
+        // with more tokens.
+        Fungibles,
+        // The matcher for making sure which assets should be handled by this exchanger.
+        Matcher,
+      >;
+      ```
+      It's called "single asset" since it will only allow exchanging one asset for another.
+      It will error out if more than one asset tries to be exchanged.
+
+      Also, a new method was added to the `xcm_executor::traits::AssetExchange` trait:
+      `quote_exchange_price`. This is used to get the exchange price between two asset collections.
+      If you were using the trait, you now need to also implement this new function.
+
+crates:
+  - name: staging-xcm-executor
+    bump: major
+  - name: staging-xcm-builder
+    bump: minor
+  - name: pallet-asset-conversion
+    bump: minor
+  - name: cumulus-primitives-utility
+    bump: minor
diff --git a/substrate/frame/asset-conversion/src/lib.rs b/substrate/frame/asset-conversion/src/lib.rs
index a9dc30375e5..d6671a45be5 100644
--- a/substrate/frame/asset-conversion/src/lib.rs
+++ b/substrate/frame/asset-conversion/src/lib.rs
@@ -435,7 +435,7 @@ pub mod pallet {
 		/// calls to render the liquidity withdrawable and rectify the exchange rate.
 		///
 		/// Once liquidity is added, someone may successfully call
-		/// [`Pallet::swap_exact_tokens_for_tokens`] successfully.
+		/// [`Pallet::swap_exact_tokens_for_tokens`].
 		#[pallet::call_index(1)]
 		#[pallet::weight(T::WeightInfo::add_liquidity())]
 		pub fn add_liquidity(
-- 
GitLab