referrerpolicy=no-referrer-when-downgrade

staging_xcm_builder/asset_exchange/single_asset_adapter/
adapter.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Single asset exchange adapter.
18
19extern crate alloc;
20use alloc::{boxed::Box, vec, vec::Vec};
21use core::marker::PhantomData;
22use frame_support::{
23	ensure,
24	traits::tokens::{fungibles, imbalance::UnsafeManualAccounting},
25};
26use pallet_asset_conversion::{QuotePrice, SwapCredit};
27use xcm::prelude::*;
28use xcm_executor::{
29	traits::{AssetExchange, MatchesFungibles},
30	AssetsInHolding,
31};
32
33/// An adapter from [`pallet_asset_conversion::SwapCredit`] and
34/// [`pallet_asset_conversion::QuotePrice`] to [`xcm_executor::traits::AssetExchange`].
35///
36/// This adapter takes just one fungible asset in `give` and allows only one fungible asset in
37/// `want`. If you need to handle more assets in either `give` or `want`, then you should use
38/// another type that implements [`xcm_executor::traits::AssetExchange`] or build your own.
39///
40/// This adapter also only works for fungible assets.
41///
42/// `exchange_asset` and `quote_exchange_price` will both return an error if there's
43/// more than one asset in `give` or `want`.
44pub struct SingleAssetExchangeAdapter<AssetConversion, Fungibles, Matcher, AccountId>(
45	PhantomData<(AssetConversion, Fungibles, Matcher, AccountId)>,
46);
47impl<AssetConversion, Fungibles, Matcher, AccountId> AssetExchange
48	for SingleAssetExchangeAdapter<AssetConversion, Fungibles, Matcher, AccountId>
49where
50	AssetConversion: SwapCredit<
51			AccountId,
52			Balance = u128,
53			AssetKind = Fungibles::AssetId,
54			Credit = fungibles::Credit<AccountId, Fungibles>,
55		> + QuotePrice<Balance = u128, AssetKind = Fungibles::AssetId>,
56	Fungibles: fungibles::Inspect<AccountId, AssetId: 'static>
57		+ fungibles::Balanced<AccountId, Balance = u128, OnDropCredit: 'static, OnDropDebt: 'static>,
58	Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
59{
60	fn exchange_asset(
61		_: Option<&Location>,
62		mut give: AssetsInHolding,
63		want: &Assets,
64		maximal: bool,
65	) -> Result<AssetsInHolding, AssetsInHolding> {
66		// We only support 1 asset in `want`.
67		ensure!(want.len() == 1, give);
68		let Some(want_asset) = want.get(0) else { return Err(give) };
69		// We don't allow non-fungible assets.
70		ensure!(give.non_fungible_assets_iter().next().is_none(), give);
71		let mut give_assets: Vec<Asset> = give.fungible_assets_iter().collect();
72		// We only support 1 asset in `give`.
73		ensure!(give_assets.len() == 1, give);
74		let Some(give_asset) = give_assets.pop() else {
75			tracing::trace!(
76				target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
77				?give, "No fungible asset was in `give`.",
78			);
79			return Err(give);
80		};
81
82		let Ok((give_asset_id, _)) = Matcher::matches_fungibles(&give_asset) else {
83			tracing::trace!(
84				target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
85				?give_asset,
86				"Could not map XCM asset give to FRAME asset.",
87			);
88			return Err(give);
89		};
90		let Ok((want_asset_id, want_amount)) = Matcher::matches_fungibles(&want_asset) else {
91			tracing::trace!(
92				target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
93				?want_asset,
94				"Could not map XCM asset want to FRAME asset."
95			);
96			return Err(give);
97		};
98
99		// We have to do this to convert the XCM assets into credit the pool can use.
100		let swap_asset = give_asset_id.clone().into();
101		let Some(imbalance) = give.fungible.remove(&give_asset.id) else { return Err(give) };
102		// "manually" build the concrete credit and move the imbalance there.
103		let mut credit_in = fungibles::Credit::<AccountId, Fungibles>::zero(give_asset_id);
104		credit_in.saturating_subsume(imbalance);
105
106		// Do the swap.
107		let (credit_out, maybe_credit_change) = if maximal {
108			// If `maximal`, then we swap exactly `credit_in` to get as much of `want_asset_id` as
109			// we can, with a minimum of `want_amount`.
110			let credit_out = match <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
111				vec![swap_asset, want_asset_id],
112				credit_in,
113				Some(want_amount),
114			) {
115				Ok(inner) => inner,
116				Err((credit_in, error)) => {
117					tracing::debug!(
118						target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
119						?error,
120						"Could not perform the swap"
121					);
122					// put back the taken credit
123					let taken = AssetsInHolding::new_from_fungible_credit(
124						give_asset.id.clone(),
125						Box::new(credit_in),
126					);
127					give.subsume_assets(taken);
128					return Err(give);
129				},
130			};
131			// We don't have leftover assets if exchange was maximal.
132			(credit_out, None)
133		} else {
134			// If `minimal`, then we swap as little of `credit_in` as we can to get exactly
135			// `want_amount` of `want_asset_id`.
136			let (credit_out, credit_change) =
137				match <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
138					vec![swap_asset, want_asset_id],
139					credit_in,
140					want_amount,
141				) {
142					Ok(inner) => inner,
143					Err((credit_in, error)) => {
144						tracing::debug!(
145							target: "xcm::SingleAssetExchangeAdapter::exchange_asset",
146							?error,
147							"Could not perform the swap",
148						);
149						// put back the taken credit
150						let taken = AssetsInHolding::new_from_fungible_credit(
151							give_asset.id.clone(),
152							Box::new(credit_in),
153						);
154						give.subsume_assets(taken);
155						return Err(give);
156					},
157				};
158			(credit_out, if credit_change.peek() > 0 { Some(credit_change) } else { None })
159		};
160
161		// We create an `AssetsInHolding` instance by putting in the resulting credit of the
162		// exchange.
163		let mut result =
164			AssetsInHolding::new_from_fungible_credit(want_asset.id.clone(), Box::new(credit_out));
165
166		// If we have some leftover assets from the exchange, also put them in the result.
167		if let Some(credit_change) = maybe_credit_change.filter(|credit| credit.peek() > 0) {
168			let leftover =
169				AssetsInHolding::new_from_fungible_credit(give_asset.id, Box::new(credit_change));
170			result.subsume_assets(leftover);
171		}
172
173		Ok(result)
174	}
175
176	fn quote_exchange_price(give: &Assets, want: &Assets, maximal: bool) -> Option<Assets> {
177		if give.len() != 1 || want.len() != 1 {
178			return None;
179		} // We only support 1 asset in `give` or `want`.
180		let give_asset = give.get(0)?;
181		let want_asset = want.get(0)?;
182		// We first match both XCM assets to the asset ID types `AssetConversion` can handle.
183		let (give_asset_id, give_amount) = Matcher::matches_fungibles(give_asset)
184			.map_err(|error| {
185				tracing::trace!(
186					target: "xcm::SingleAssetExchangeAdapter::quote_exchange_price",
187					?give_asset,
188					?error,
189					"Could not map XCM asset to FRAME asset."
190				);
191				()
192			})
193			.ok()?;
194		let (want_asset_id, want_amount) = Matcher::matches_fungibles(want_asset)
195			.map_err(|error| {
196				tracing::trace!(
197					target: "xcm::SingleAssetExchangeAdapter::quote_exchange_price",
198					?want_asset,
199					?error,
200					"Could not map XCM asset to FRAME asset"
201				);
202				()
203			})
204			.ok()?;
205		// We quote the price.
206		if maximal {
207			// The amount of `want` resulting from swapping `give`.
208			let resulting_want =
209				<AssetConversion as QuotePrice>::quote_price_exact_tokens_for_tokens(
210					give_asset_id,
211					want_asset_id,
212					give_amount,
213					true, // Include fee.
214				)?;
215
216			Some((want_asset.id.clone(), resulting_want).into())
217		} else {
218			// The `give` amount required to obtain `want`.
219			let necessary_give =
220				<AssetConversion as QuotePrice>::quote_price_tokens_for_exact_tokens(
221					give_asset_id,
222					want_asset_id,
223					want_amount,
224					true, // Include fee.
225				)?;
226
227			Some((give_asset.id.clone(), necessary_give).into())
228		}
229	}
230}