referrerpolicy=no-referrer-when-downgrade

staging_xcm_builder/
fungibles_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//! Adapters to work with [`frame_support::traits::fungibles`] through XCM.
18
19use alloc::boxed::Box;
20use core::{fmt::Debug, marker::PhantomData};
21use frame_support::{
22	defensive_assert,
23	traits::{
24		tokens::{
25			fungibles,
26			imbalance::{ImbalanceAccounting, UnsafeManualAccounting},
27			Fortitude::Polite,
28			Precision::Exact,
29			Preservation::Expendable,
30			Provenance::Minted,
31		},
32		Contains, Get,
33	},
34};
35use xcm::latest::prelude::*;
36use xcm_executor::{
37	traits::{ConvertLocation, Error as MatchError, MatchesFungibles, TransactAsset},
38	AssetsInHolding,
39};
40
41/// `TransactAsset` implementation to convert a `fungibles` implementation to become usable in XCM.
42pub struct FungiblesTransferAdapter<Assets, Matcher, AccountIdConverter, AccountId>(
43	PhantomData<(Assets, Matcher, AccountIdConverter, AccountId)>,
44);
45impl<
46		Assets: fungibles::Mutate<AccountId>,
47		Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
48		AccountIdConverter: ConvertLocation<AccountId>,
49		AccountId: Eq + Clone + Debug, /* can't get away without it since Currency is generic
50		                                * over it. */
51	> TransactAsset for FungiblesTransferAdapter<Assets, Matcher, AccountIdConverter, AccountId>
52{
53	fn internal_transfer_asset(
54		what: &Asset,
55		from: &Location,
56		to: &Location,
57		_context: &XcmContext,
58	) -> Result<Asset, XcmError> {
59		tracing::trace!(
60			target: "xcm::fungibles_adapter",
61			?what, ?from, ?to,
62			"internal_transfer_asset"
63		);
64		// Check we handle this asset.
65		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
66		let source = AccountIdConverter::convert_location(from)
67			.ok_or(MatchError::AccountIdConversionFailed)?;
68		let dest = AccountIdConverter::convert_location(to)
69			.ok_or(MatchError::AccountIdConversionFailed)?;
70		Assets::transfer(asset_id.clone(), &source, &dest, amount, Expendable).map_err(|e| {
71			tracing::debug!(target: "xcm::fungibles_adapter", error = ?e, ?asset_id, ?source, ?dest, ?amount, "Failed internal transfer asset");
72			XcmError::FailedToTransactAsset(e.into())
73		})?;
74		Ok(what.clone())
75	}
76}
77
78/// The location which is allowed to mint a particular asset.
79#[derive(Copy, Clone, Eq, PartialEq)]
80pub enum MintLocation {
81	/// This chain is allowed to mint the asset. When we track teleports of the asset we ensure
82	/// that no more of the asset returns back to the chain than has been sent out.
83	Local,
84	/// This chain is not allowed to mint the asset. When we track teleports of the asset we ensure
85	/// that no more of the asset is sent out from the chain than has been previously received.
86	NonLocal,
87}
88
89/// Simple trait to indicate whether an asset is subject to having its teleportation into and out of
90/// this chain recorded and if so in what `MintLocation`.
91///
92/// The overall purpose of asset-checking is to ensure either no more assets are teleported into a
93/// chain than the outstanding balance of assets which were previously teleported out (as in the
94/// case of locally-minted assets); or that no more assets are teleported out of a chain than the
95/// outstanding balance of assets which have previously been teleported in (as in the case of chains
96/// where the `asset` is not minted locally).
97pub trait AssetChecking<AssetId> {
98	/// Return the teleportation asset-checking policy for the given `asset`. `None` implies no
99	/// checking. Otherwise the policy detailed by the inner `MintLocation` should be respected by
100	/// teleportation.
101	fn asset_checking(asset: &AssetId) -> Option<MintLocation>;
102}
103
104/// Implementation of `AssetChecking` which subjects no assets to having their teleportations
105/// recorded.
106pub struct NoChecking;
107impl<AssetId> AssetChecking<AssetId> for NoChecking {
108	fn asset_checking(_: &AssetId) -> Option<MintLocation> {
109		None
110	}
111}
112
113/// Implementation of `AssetChecking` which subjects a given set of assets `T` to having their
114/// teleportations recorded with a `MintLocation::Local`.
115pub struct LocalMint<T>(core::marker::PhantomData<T>);
116impl<AssetId, T: Contains<AssetId>> AssetChecking<AssetId> for LocalMint<T> {
117	fn asset_checking(asset: &AssetId) -> Option<MintLocation> {
118		match T::contains(asset) {
119			true => Some(MintLocation::Local),
120			false => None,
121		}
122	}
123}
124
125/// Implementation of `AssetChecking` which subjects a given set of assets `T` to having their
126/// teleportations recorded with a `MintLocation::NonLocal`.
127pub struct NonLocalMint<T>(core::marker::PhantomData<T>);
128impl<AssetId, T: Contains<AssetId>> AssetChecking<AssetId> for NonLocalMint<T> {
129	fn asset_checking(asset: &AssetId) -> Option<MintLocation> {
130		match T::contains(asset) {
131			true => Some(MintLocation::NonLocal),
132			false => None,
133		}
134	}
135}
136
137/// Implementation of `AssetChecking` which subjects a given set of assets `L` to having their
138/// teleportations recorded with a `MintLocation::Local` and a second set of assets `R` to having
139/// their teleportations recorded with a `MintLocation::NonLocal`.
140pub struct DualMint<L, R>(core::marker::PhantomData<(L, R)>);
141impl<AssetId, L: Contains<AssetId>, R: Contains<AssetId>> AssetChecking<AssetId>
142	for DualMint<L, R>
143{
144	fn asset_checking(asset: &AssetId) -> Option<MintLocation> {
145		if L::contains(asset) {
146			Some(MintLocation::Local)
147		} else if R::contains(asset) {
148			Some(MintLocation::NonLocal)
149		} else {
150			None
151		}
152	}
153}
154
155pub struct FungiblesMutateAdapter<
156	Assets,
157	Matcher,
158	AccountIdConverter,
159	AccountId,
160	CheckAsset,
161	CheckingAccount,
162>(PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)>);
163
164impl<
165		Assets: fungibles::Mutate<AccountId>,
166		Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
167		AccountIdConverter: ConvertLocation<AccountId>,
168		AccountId: Eq + Clone + Debug, /* can't get away without it since Currency is generic
169		                                * over it. */
170		CheckAsset: AssetChecking<Assets::AssetId>,
171		CheckingAccount: Get<AccountId>,
172	>
173	FungiblesMutateAdapter<Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount>
174{
175	fn can_accrue_checked(asset_id: Assets::AssetId, amount: Assets::Balance) -> XcmResult {
176		let checking_account = CheckingAccount::get();
177		Assets::can_deposit(asset_id, &checking_account, amount, Minted)
178			.into_result()
179			.map_err(|error| {
180				tracing::debug!(
181					target: "xcm::fungibles_adapter", ?error, ?checking_account, ?amount,
182					"Failed to check if asset can be accrued"
183				);
184				XcmError::NotDepositable
185			})
186	}
187	fn can_reduce_checked(asset_id: Assets::AssetId, amount: Assets::Balance) -> XcmResult {
188		let checking_account = CheckingAccount::get();
189		Assets::can_withdraw(asset_id, &checking_account, amount)
190			.into_result(false)
191			.map_err(|error| {
192				tracing::debug!(
193					target: "xcm::fungibles_adapter", ?error, ?checking_account, ?amount,
194					"Failed to check if asset can be reduced"
195				);
196				XcmError::NotWithdrawable
197			})
198			.map(|_| ())
199	}
200	fn accrue_checked(asset_id: Assets::AssetId, amount: Assets::Balance) {
201		let checking_account = CheckingAccount::get();
202		let ok = Assets::mint_into(asset_id, &checking_account, amount).is_ok();
203		debug_assert!(ok, "`can_accrue_checked` must have returned `true` immediately prior; qed");
204	}
205	fn reduce_checked(asset_id: Assets::AssetId, amount: Assets::Balance) {
206		let checking_account = CheckingAccount::get();
207		let ok = Assets::burn_from(asset_id, &checking_account, amount, Expendable, Exact, Polite)
208			.is_ok();
209		debug_assert!(ok, "`can_reduce_checked` must have returned `true` immediately prior; qed");
210	}
211}
212
213impl<
214		Assets: fungibles::Inspect<AccountId, AssetId: 'static, Balance: 'static>
215			+ fungibles::Mutate<AccountId>
216			+ fungibles::Balanced<AccountId, OnDropCredit: 'static, OnDropDebt: 'static>
217			+ 'static,
218		Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
219		AccountIdConverter: ConvertLocation<AccountId>,
220		AccountId: Eq + Clone + Debug, /* can't get away without it since Currency is generic
221		                                * over it. */
222		CheckAsset: AssetChecking<Assets::AssetId>,
223		CheckingAccount: Get<AccountId>,
224	> TransactAsset
225	for FungiblesMutateAdapter<
226		Assets,
227		Matcher,
228		AccountIdConverter,
229		AccountId,
230		CheckAsset,
231		CheckingAccount,
232	>
233where
234	fungibles::Imbalance<
235		<Assets as fungibles::Inspect<AccountId>>::AssetId,
236		<Assets as fungibles::Inspect<AccountId>>::Balance,
237		<Assets as fungibles::Balanced<AccountId>>::OnDropCredit,
238		<Assets as fungibles::Balanced<AccountId>>::OnDropDebt,
239	>: ImbalanceAccounting<u128>,
240{
241	fn can_check_in(origin: &Location, what: &Asset, _context: &XcmContext) -> XcmResult {
242		tracing::trace!(
243			target: "xcm::fungibles_adapter",
244			?origin, ?what,
245			"can_check_in"
246		);
247		// Check we handle this asset.
248		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
249		match CheckAsset::asset_checking(&asset_id) {
250			// We track this asset's teleports to ensure no more come in than have gone out.
251			Some(MintLocation::Local) => Self::can_reduce_checked(asset_id, amount),
252			// We track this asset's teleports to ensure no more go out than have come in.
253			Some(MintLocation::NonLocal) => Self::can_accrue_checked(asset_id, amount),
254			_ => Ok(()),
255		}
256	}
257
258	fn check_in(origin: &Location, what: &Asset, _context: &XcmContext) {
259		tracing::trace!(
260			target: "xcm::fungibles_adapter",
261			?origin, ?what,
262			"check_in"
263		);
264		if let Ok((asset_id, amount)) = Matcher::matches_fungibles(what) {
265			match CheckAsset::asset_checking(&asset_id) {
266				// We track this asset's teleports to ensure no more come in than have gone out.
267				Some(MintLocation::Local) => Self::reduce_checked(asset_id, amount),
268				// We track this asset's teleports to ensure no more go out than have come in.
269				Some(MintLocation::NonLocal) => Self::accrue_checked(asset_id, amount),
270				_ => (),
271			}
272		}
273	}
274
275	fn can_check_out(origin: &Location, what: &Asset, _context: &XcmContext) -> XcmResult {
276		tracing::trace!(
277			target: "xcm::fungibles_adapter",
278			?origin, ?what,
279			"can_check_out"
280		);
281		// Check we handle this asset.
282		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
283		match CheckAsset::asset_checking(&asset_id) {
284			// We track this asset's teleports to ensure no more come in than have gone out.
285			Some(MintLocation::Local) => Self::can_accrue_checked(asset_id, amount),
286			// We track this asset's teleports to ensure no more go out than have come in.
287			Some(MintLocation::NonLocal) => Self::can_reduce_checked(asset_id, amount),
288			_ => Ok(()),
289		}
290	}
291
292	fn check_out(dest: &Location, what: &Asset, _context: &XcmContext) {
293		tracing::trace!(
294			target: "xcm::fungibles_adapter",
295			?dest, ?what,
296			"check_out"
297		);
298		if let Ok((asset_id, amount)) = Matcher::matches_fungibles(what) {
299			match CheckAsset::asset_checking(&asset_id) {
300				// We track this asset's teleports to ensure no more come in than have gone out.
301				Some(MintLocation::Local) => Self::accrue_checked(asset_id, amount),
302				// We track this asset's teleports to ensure no more go out than have come in.
303				Some(MintLocation::NonLocal) => Self::reduce_checked(asset_id, amount),
304				_ => (),
305			}
306		}
307	}
308
309	fn deposit_asset(
310		mut what: AssetsInHolding,
311		who: &Location,
312		_context: Option<&XcmContext>,
313	) -> Result<(), (AssetsInHolding, XcmError)> {
314		tracing::trace!(
315			target: "xcm::fungibles_adapter",
316			?what, ?who,
317			"deposit_asset"
318		);
319		defensive_assert!(what.len() == 1, "Trying to deposit more than one asset!");
320		// Check we handle this asset.
321		let maybe = what.fungible_assets_iter().next().and_then(|asset| {
322			Matcher::matches_fungibles(&asset)
323				.map(|(fungibles_id, amount)| (asset.id, fungibles_id, amount))
324				.ok()
325		});
326		let Some((asset_id, fungibles_id, amount)) = maybe else {
327			return Err((what, MatchError::AssetNotHandled.into()));
328		};
329		let Some(who) = AccountIdConverter::convert_location(who) else {
330			return Err((what, MatchError::AccountIdConversionFailed.into()));
331		};
332		let Some(imbalance) = what.fungible.remove(&asset_id) else {
333			return Err((what, MatchError::AssetNotHandled.into()));
334		};
335		// "manually" build the concrete credit and move the imbalance there.
336		let mut credit = fungibles::Credit::<AccountId, Assets>::zero(fungibles_id);
337		credit.saturating_subsume(imbalance);
338
339		Assets::resolve(&who, credit).map_err(|unspent| {
340			tracing::debug!(target: "xcm::fungibles_adapter", ?asset_id, ?who, ?amount, "Failed to deposit asset");
341			(
342				AssetsInHolding::new_from_fungible_credit(asset_id, Box::new(unspent)),
343				XcmError::FailedToTransactAsset("")
344			)
345		})?;
346		Ok(())
347	}
348
349	fn withdraw_asset(
350		what: &Asset,
351		who: &Location,
352		_maybe_context: Option<&XcmContext>,
353	) -> Result<AssetsInHolding, XcmError> {
354		tracing::trace!(
355			target: "xcm::fungibles_adapter",
356			?what, ?who,
357			"withdraw_asset"
358		);
359		// Check we handle this asset.
360		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
361		let who = AccountIdConverter::convert_location(who)
362			.ok_or(MatchError::AccountIdConversionFailed)?;
363		let credit = Assets::withdraw(asset_id, &who, amount, Exact, Expendable, Polite).map_err(|error| {
364			tracing::debug!(target: "xcm::fungibles_adapter", ?error, ?who, ?amount, "Failed to withdraw asset");
365			XcmError::FailedToTransactAsset(error.into())
366		})?;
367		Ok(AssetsInHolding::new_from_fungible_credit(what.id.clone(), Box::new(credit)))
368	}
369
370	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
371		tracing::trace!(
372			target: "xcm::fungibles_adapter",
373			?what, ?context,
374			"mint_asset",
375		);
376		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
377		let credit = Assets::issue(asset_id, amount);
378		Ok(AssetsInHolding::new_from_fungible_credit(what.id.clone(), Box::new(credit)))
379	}
380}
381
382pub struct FungiblesAdapter<
383	Assets,
384	Matcher,
385	AccountIdConverter,
386	AccountId,
387	CheckAsset,
388	CheckingAccount,
389>(PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)>);
390impl<
391		Assets: fungibles::Inspect<AccountId, AssetId: 'static, Balance: 'static>
392			+ fungibles::Mutate<AccountId>
393			+ fungibles::Balanced<AccountId, OnDropCredit: 'static, OnDropDebt: 'static>
394			+ 'static,
395		Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
396		AccountIdConverter: ConvertLocation<AccountId>,
397		AccountId: Eq + Clone + Debug, /* can't get away without it since Currency is generic
398		                                * over it. */
399		CheckAsset: AssetChecking<Assets::AssetId>,
400		CheckingAccount: Get<AccountId>,
401	> TransactAsset
402	for FungiblesAdapter<Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount>
403where
404	fungibles::Imbalance<
405		<Assets as fungibles::Inspect<AccountId>>::AssetId,
406		<Assets as fungibles::Inspect<AccountId>>::Balance,
407		<Assets as fungibles::Balanced<AccountId>>::OnDropCredit,
408		<Assets as fungibles::Balanced<AccountId>>::OnDropDebt,
409	>: ImbalanceAccounting<u128>,
410{
411	fn can_check_in(origin: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
412		FungiblesMutateAdapter::<
413			Assets,
414			Matcher,
415			AccountIdConverter,
416			AccountId,
417			CheckAsset,
418			CheckingAccount,
419		>::can_check_in(origin, what, context)
420	}
421
422	fn check_in(origin: &Location, what: &Asset, context: &XcmContext) {
423		FungiblesMutateAdapter::<
424			Assets,
425			Matcher,
426			AccountIdConverter,
427			AccountId,
428			CheckAsset,
429			CheckingAccount,
430		>::check_in(origin, what, context)
431	}
432
433	fn can_check_out(dest: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
434		FungiblesMutateAdapter::<
435			Assets,
436			Matcher,
437			AccountIdConverter,
438			AccountId,
439			CheckAsset,
440			CheckingAccount,
441		>::can_check_out(dest, what, context)
442	}
443
444	fn check_out(dest: &Location, what: &Asset, context: &XcmContext) {
445		FungiblesMutateAdapter::<
446			Assets,
447			Matcher,
448			AccountIdConverter,
449			AccountId,
450			CheckAsset,
451			CheckingAccount,
452		>::check_out(dest, what, context)
453	}
454
455	fn deposit_asset(
456		what: AssetsInHolding,
457		who: &Location,
458		context: Option<&XcmContext>,
459	) -> Result<(), (AssetsInHolding, XcmError)> {
460		FungiblesMutateAdapter::<
461			Assets,
462			Matcher,
463			AccountIdConverter,
464			AccountId,
465			CheckAsset,
466			CheckingAccount,
467		>::deposit_asset(what, who, context)
468	}
469
470	fn withdraw_asset(
471		what: &Asset,
472		who: &Location,
473		context: Option<&XcmContext>,
474	) -> Result<AssetsInHolding, XcmError> {
475		FungiblesMutateAdapter::<
476			Assets,
477			Matcher,
478			AccountIdConverter,
479			AccountId,
480			CheckAsset,
481			CheckingAccount,
482		>::withdraw_asset(what, who, context)
483	}
484
485	fn internal_transfer_asset(
486		what: &Asset,
487		from: &Location,
488		to: &Location,
489		context: &XcmContext,
490	) -> Result<Asset, XcmError> {
491		FungiblesTransferAdapter::<Assets, Matcher, AccountIdConverter, AccountId>::internal_transfer_asset(
492			what, from, to, context
493		)
494	}
495
496	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
497		FungiblesMutateAdapter::<
498			Assets,
499			Matcher,
500			AccountIdConverter,
501			AccountId,
502			CheckAsset,
503			CheckingAccount,
504		>::mint_asset(what, context)
505	}
506}