referrerpolicy=no-referrer-when-downgrade

cumulus_primitives_utility/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: Apache-2.0
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// 	http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17//! Helper datatypes for cumulus. This includes the [`ParentAsUmp`] routing type which will route
18//! messages into an [`UpwardMessageSender`] if the destination is `Parent`.
19
20#![cfg_attr(not(feature = "std"), no_std)]
21
22extern crate alloc;
23
24use alloc::{boxed::Box, vec, vec::Vec};
25use codec::Encode;
26use core::marker::PhantomData;
27use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
28use frame_support::{
29	defensive,
30	traits::{
31		tokens::{fungibles, imbalance::UnsafeManualAccounting},
32		Get, OnUnbalanced as OnUnbalancedT,
33	},
34	weights::{Weight, WeightToFee as WeightToFeeT},
35};
36use pallet_asset_conversion::{QuotePrice, SwapCredit as SwapCreditT};
37use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery;
38use sp_runtime::traits::Zero;
39use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion};
40use xcm_builder::InspectMessageQueues;
41use xcm_executor::{
42	traits::{MatchesFungibles, WeightTrader},
43	AssetsInHolding,
44};
45
46#[cfg(test)]
47mod tests;
48
49#[cfg(test)]
50mod test_helpers {
51	pub use xcm_executor::test_helpers::mock_asset_to_holding as asset_to_holding;
52}
53
54/// Xcm router which recognises the `Parent` destination and handles it by sending the message into
55/// the given UMP `UpwardMessageSender` implementation. Thus this essentially adapts an
56/// `UpwardMessageSender` trait impl into a `SendXcm` trait impl.
57///
58/// NOTE: This is a pretty dumb "just send it" router; we will probably want to introduce queuing
59/// to UMP eventually and when we do, the pallet which implements the queuing will be responsible
60/// for the `SendXcm` implementation.
61pub struct ParentAsUmp<T, W, P>(PhantomData<(T, W, P)>);
62
63impl<T, W, P> SendXcm for ParentAsUmp<T, W, P>
64where
65	T: UpwardMessageSender,
66	W: WrapVersion,
67	P: PriceForMessageDelivery<Id = ()>,
68{
69	type Ticket = Vec<u8>;
70
71	fn validate(dest: &mut Option<Location>, msg: &mut Option<Xcm<()>>) -> SendResult<Vec<u8>> {
72		let d = dest.take().ok_or(SendError::MissingArgument)?;
73
74		if d.contains_parents_only(1) {
75			// An upward message for the relay chain.
76			let xcm = msg.take().ok_or(SendError::MissingArgument)?;
77			let price = P::price_for_delivery((), &xcm);
78			let versioned_xcm =
79				W::wrap_version(&d, xcm).map_err(|()| SendError::DestinationUnsupported)?;
80			versioned_xcm
81				.check_is_decodable()
82				.map_err(|()| SendError::ExceedsMaxMessageSize)?;
83			let data = versioned_xcm.encode();
84
85			// Pre-check with our message sender if everything else is okay.
86			T::can_send_upward_message(&data).map_err(Self::map_upward_sender_err)?;
87
88			Ok((data, price))
89		} else {
90			// Anything else is unhandled. This includes a message that is not meant for us.
91			// We need to make sure that dest/msg is not consumed here.
92			*dest = Some(d);
93			Err(SendError::NotApplicable)
94		}
95	}
96
97	fn deliver(data: Vec<u8>) -> Result<XcmHash, SendError> {
98		let (_, hash) = T::send_upward_message(data).map_err(Self::map_upward_sender_err)?;
99		Ok(hash)
100	}
101
102	#[cfg(feature = "runtime-benchmarks")]
103	fn ensure_successful_delivery(location: Option<Location>) {
104		if location.as_ref().map_or(false, |l| l.contains_parents_only(1)) {
105			T::ensure_successful_delivery();
106		}
107	}
108}
109
110impl<T, W, P> ParentAsUmp<T, W, P> {
111	fn map_upward_sender_err(message_send_error: MessageSendError) -> SendError {
112		match message_send_error {
113			MessageSendError::TooBig => SendError::ExceedsMaxMessageSize,
114			e => SendError::Transport(e.into()),
115		}
116	}
117}
118
119impl<T: UpwardMessageSender + InspectMessageQueues, W, P> InspectMessageQueues
120	for ParentAsUmp<T, W, P>
121{
122	fn clear_messages() {
123		T::clear_messages();
124	}
125
126	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
127		T::get_messages()
128	}
129}
130
131/// Charges for execution in the first asset of those selected for fee payment
132/// Only succeeds for Concrete Fungible Assets
133/// First tries to convert the this Asset into a local assetId
134/// Then charges for this assetId as described by FeeCharger
135/// Weight, paid balance, local asset Id and the location is stored for
136/// later refund purposes
137/// Important: Errors if the Trader is being called twice by 2 BuyExecution instructions
138/// Alternatively we could just return payment in the aforementioned case
139pub struct TakeFirstAssetTrader<
140	AccountId: Eq,
141	FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
142	Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
143	Fungibles: fungibles::Balanced<AccountId>,
144	OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
145> {
146	/// Accumulated fee paid for XCM execution.
147	outstanding_credit: Option<fungibles::Credit<AccountId, Fungibles>>,
148	/// The amount of weight bought minus the weight already refunded
149	weight_outstanding: Weight,
150	_phantom_data: PhantomData<(AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced)>,
151}
152
153impl<
154		AccountId: Eq,
155		FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
156		Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
157		Fungibles: fungibles::Inspect<AccountId, Balance = u128, AssetId: Into<Location> + 'static>
158			+ fungibles::Balanced<AccountId, OnDropCredit: 'static, OnDropDebt: 'static>,
159		OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
160	> WeightTrader for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced>
161{
162	fn new() -> Self {
163		Self {
164			outstanding_credit: None,
165			weight_outstanding: Weight::zero(),
166			_phantom_data: PhantomData,
167		}
168	}
169	// We take first asset
170	// Check whether we can convert fee to asset_fee (is_sufficient, min_deposit)
171	// If everything goes well, we charge.
172	fn buy_weight(
173		&mut self,
174		weight: Weight,
175		mut payment: AssetsInHolding,
176		context: &XcmContext,
177	) -> Result<AssetsInHolding, (AssetsInHolding, XcmError)> {
178		log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}, context: {:?}", weight, payment, context);
179
180		// Make sure we don't enter twice
181		if self.outstanding_credit.is_some() {
182			return Err((payment, XcmError::NotWithdrawable));
183		}
184
185		// We take the very first asset from payment
186		let Some(used) = payment.fungible_assets_iter().next() else {
187			return Err((payment, XcmError::AssetNotFound));
188		};
189
190		// Get the local asset id in which we can pay for fees
191		let Ok((fungibles_asset_id, _)) = Matcher::matches_fungibles(&used) else {
192			return Err((payment, XcmError::AssetNotFound));
193		};
194
195		// Calculate how much we should charge in the asset_id for such amount of weight
196		// Require at least a payment of minimum_balance
197		// Necessary for fully collateral-backed assets
198		let required_amount: u128 =
199			match FeeCharger::charge_weight_in_fungibles(fungibles_asset_id.clone(), weight)
200				.map(|amount| amount.max(Fungibles::minimum_balance(fungibles_asset_id.clone())))
201			{
202				Ok(a) => a,
203				Err(_) => return Err((payment, XcmError::Overflow)),
204			};
205
206		// Convert to the same kind of asset, with the required fungible balance
207		let required = used.id.into_asset(required_amount.into());
208
209		// Subtract required from payment.
210		// Note: `payment` may contain multiple assets, but we only take from the first fungible
211		// asset that was matched above. Any remaining assets stay in `payment` and are returned.
212		let Some(imbalance) = payment
213			.try_take(required.into())
214			.ok()
215			.and_then(|taken| taken.fungible.into_iter().next().map(|(_, v)| v))
216		else {
217			return Err((payment, XcmError::TooExpensive));
218		};
219		// "manually" build the concrete credit and move the imbalance there.
220		let mut credit = fungibles::Credit::<AccountId, Fungibles>::zero(fungibles_asset_id);
221		credit.saturating_subsume(imbalance);
222
223		// Record weight and credit.
224		self.outstanding_credit = Some(credit);
225		self.weight_outstanding = weight;
226
227		// return the unused payment
228		Ok(payment)
229	}
230
231	/// Refunds unused weight back to holding.
232	///
233	/// Note: This is a best-effort refund. The actual refunded amount may differ from the
234	/// weight-equivalent amount due to existential deposit (ED) constraints. Specifically:
235	/// - If refunding the full amount would leave less than ED in outstanding credit, we only
236	///   refund enough to keep ED for the drop handler.
237	/// - This ensures collateral-backed assets always have sufficient balance for proper cleanup.
238	fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option<AssetsInHolding> {
239		log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::refund_weight weight: {:?}, context: {:?}", weight, context);
240		if weight.is_zero() {
241			return None;
242		}
243		let outstanding_credit = self.outstanding_credit.as_mut()?;
244		let id = outstanding_credit.asset();
245		let fun = Fungible(outstanding_credit.peek());
246		let asset = (id.clone(), fun).into();
247
248		// Get the local asset id in which we can refund fees.
249		let (fungibles_asset_id, _) = Matcher::matches_fungibles(&asset).ok()?;
250		let minimum_balance = Fungibles::minimum_balance(fungibles_asset_id.clone());
251
252		// Calculate how much to refund based on unused weight.
253		// This read should have already been cached in buy_weight.
254		let refund_credit = FeeCharger::charge_weight_in_fungibles(fungibles_asset_id, weight)
255			.ok()
256			.map(|refund_balance| {
257				// Ensure at least minimum_balance remains for the drop handler.
258				// This is necessary for fully collateral-backed assets.
259				if outstanding_credit.peek().saturating_sub(refund_balance) >= minimum_balance {
260					outstanding_credit.extract(refund_balance)
261				} else {
262					// If refunding would leave less than ED, we refund ED to ensure the
263					// OnUnbalanced handler receives at least ED when this trader is dropped.
264					// This prevents dust amounts that can't be properly handled.
265					outstanding_credit.extract(minimum_balance)
266				}
267			})?;
268		// Subtract the refunded weight from existing weight.
269		self.weight_outstanding = self.weight_outstanding.saturating_sub(weight);
270
271		// Only return refund if non-zero.
272		if refund_credit.peek() != Zero::zero() {
273			Some(AssetsInHolding::new_from_fungible_credit(asset.id, Box::new(refund_credit)))
274		} else {
275			None
276		}
277	}
278
279	fn quote_weight(
280		&mut self,
281		weight: Weight,
282		given_id: AssetId,
283		context: &XcmContext,
284	) -> Result<Asset, XcmError> {
285		log::trace!(
286			target: "xcm::weight",
287			"TakeFirstAssetTrader::quote_weight weight: {:?}, given_id: {:?}, context: {:?}",
288			weight, given_id, context
289		);
290		if weight.is_zero() {
291			return Err(XcmError::NoDeal);
292		}
293
294		let give_matcher: Asset = (given_id.clone(), 1).into();
295		// Get the local asset id in which we can pay for fees
296		let (give_fungibles_id, _) =
297			Matcher::matches_fungibles(&give_matcher).map_err(|_| XcmError::AssetNotFound)?;
298
299		// Calculate how much we should charge in the asset_id for such amount of weight
300		// Require at least a payment of minimum_balance
301		// Necessary for fully collateral-backed assets
302		let required_amount: u128 =
303			FeeCharger::charge_weight_in_fungibles(give_fungibles_id.clone(), weight)
304				.map(|amount| amount.max(Fungibles::minimum_balance(give_fungibles_id.clone())))
305				.map_err(|_| XcmError::Overflow)?;
306
307		// Convert to the same kind of asset, with the required fungible balance
308		let required = given_id.into_asset(required_amount.into());
309		Ok(required)
310	}
311}
312
313impl<
314		AccountId: Eq,
315		FeeCharger: ChargeWeightInFungibles<AccountId, Fungibles>,
316		Matcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
317		Fungibles: fungibles::Balanced<AccountId>,
318		OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
319	> Drop for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, Fungibles, OnUnbalanced>
320{
321	fn drop(&mut self) {
322		self.outstanding_credit
323			.take()
324			.filter(|credit| !credit.peek().is_zero())
325			.map(OnUnbalanced::on_unbalanced);
326	}
327}
328
329/// ChargeWeightInFungibles trait, which converts a given amount of weight
330/// and an assetId, and it returns the balance amount that should be charged
331/// in such assetId for that amount of weight
332pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
333	fn charge_weight_in_fungibles(
334		asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
335		weight: Weight,
336	) -> Result<<Assets as fungibles::Inspect<AccountId>>::Balance, XcmError>;
337}
338
339/// Provides an implementation of [`WeightTrader`] to charge for weight using the first asset
340/// specified in the `payment` argument.
341///
342/// The asset used to pay for the weight must differ from the `Target` asset and be exchangeable for
343/// the same `Target` asset through `SwapCredit`.
344///
345/// ### Parameters:
346/// - `Target`: the asset into which the user's payment will be exchanged using `SwapCredit`.
347/// - `SwapCredit`: mechanism used for the exchange of the user's payment asset into the `Target`.
348/// - `WeightToFee`: weight to the `Target` asset fee calculator.
349/// - `Fungibles`: registry of fungible assets.
350/// - `FungiblesAssetMatcher`: utility for mapping [`Asset`] to `Fungibles::AssetId` and
351///   `Fungibles::Balance`.
352/// - `OnUnbalanced`: handler for the fee payment.
353/// - `AccountId`: the account identifier type.
354pub struct SwapFirstAssetTrader<
355	Target: Get<Fungibles::AssetId>,
356	SwapCredit: SwapCreditT<
357			AccountId,
358			Balance = Fungibles::Balance,
359			AssetKind = Fungibles::AssetId,
360			Credit = fungibles::Credit<AccountId, Fungibles>,
361		> + QuotePrice,
362	WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
363	Fungibles: fungibles::Balanced<AccountId>,
364	FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
365	OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
366	AccountId,
367> where
368	Fungibles::Balance: From<u128> + Into<u128>,
369{
370	/// Accumulated fee paid for XCM execution.
371	total_fee: fungibles::Credit<AccountId, Fungibles>,
372	/// Last asset utilized by a client to settle a fee.
373	last_fee_asset: Option<AssetId>,
374	_phantom_data: PhantomData<(
375		Target,
376		SwapCredit,
377		WeightToFee,
378		Fungibles,
379		FungiblesAssetMatcher,
380		OnUnbalanced,
381		AccountId,
382	)>,
383}
384
385impl<
386		Target: Get<Fungibles::AssetId>,
387		SwapCredit: SwapCreditT<
388				AccountId,
389				Balance = Fungibles::Balance,
390				AssetKind = Fungibles::AssetId,
391				Credit = fungibles::Credit<AccountId, Fungibles>,
392			> + QuotePrice<AssetKind = Fungibles::AssetId, Balance = Fungibles::Balance>,
393		WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
394		Fungibles: fungibles::Balanced<
395			AccountId,
396			AssetId: 'static,
397			OnDropCredit: 'static,
398			OnDropDebt: 'static,
399		>,
400		FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
401		OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
402		AccountId,
403	> WeightTrader
404	for SwapFirstAssetTrader<
405		Target,
406		SwapCredit,
407		WeightToFee,
408		Fungibles,
409		FungiblesAssetMatcher,
410		OnUnbalanced,
411		AccountId,
412	>
413where
414	Fungibles::Balance: From<u128> + Into<u128>,
415{
416	fn new() -> Self {
417		Self {
418			total_fee: fungibles::Credit::<AccountId, Fungibles>::zero(Target::get()),
419			last_fee_asset: None,
420			_phantom_data: PhantomData,
421		}
422	}
423
424	fn buy_weight(
425		&mut self,
426		weight: Weight,
427		mut payment: AssetsInHolding,
428		_context: &XcmContext,
429	) -> Result<AssetsInHolding, (AssetsInHolding, XcmError)> {
430		log::trace!(
431			target: "xcm::weight",
432			"SwapFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}",
433			weight,
434			payment,
435		);
436		let Some((id, given_credit)) = payment.fungible.first_key_value() else {
437			return Err((payment, XcmError::AssetNotFound));
438		};
439		let id = id.clone();
440		let given_credit_amount = given_credit.amount();
441		let first_asset: Asset = (id.clone(), given_credit_amount).into();
442		let Ok((fungibles_id, _)) = FungiblesAssetMatcher::matches_fungibles(&first_asset) else {
443			log::trace!(
444				target: "xcm::weight",
445				"SwapFirstAssetTrader::buy_weight asset {:?} didn't match",
446				first_asset,
447			);
448			return Err((payment, XcmError::AssetNotFound));
449		};
450
451		let swap_asset = fungibles_id.clone().into();
452		if Target::get().eq(&swap_asset) {
453			log::trace!(
454				target: "xcm::weight",
455				"SwapFirstAssetTrader::buy_weight Asset was same as Target, swap not needed.",
456			);
457			// current trader is not applicable.
458			return Err((payment, XcmError::FeesNotMet));
459		}
460		// Subtract required from payment
461		let Some(imbalance) = payment.fungible.remove(&first_asset.id) else {
462			return Err((payment, XcmError::TooExpensive));
463		};
464		// "manually" build the concrete credit and move the imbalance there.
465		let mut credit_in = fungibles::Credit::<AccountId, Fungibles>::zero(fungibles_id);
466		credit_in.saturating_subsume(imbalance);
467
468		let fee = WeightToFee::weight_to_fee(&weight);
469		// swap the user's asset for the `Target` asset.
470		let (credit_out, credit_change) = match SwapCredit::swap_tokens_for_exact_tokens(
471			vec![swap_asset, Target::get()],
472			credit_in,
473			fee,
474		) {
475			Ok(a) => a,
476			Err((credit_in, error)) => {
477				log::trace!(
478					target: "xcm::weight",
479					"SwapFirstAssetTrader::buy_weight swap couldn't be done. Error was: {:?}",
480					error,
481				);
482				// put back the taken credit
483				let taken =
484					AssetsInHolding::new_from_fungible_credit(id.clone(), Box::new(credit_in));
485				payment.subsume_assets(taken);
486				return Err((payment, XcmError::FeesNotMet));
487			},
488		};
489
490		match self.total_fee.subsume(credit_out) {
491			Err(credit_out) => {
492				// error may occur if `total_fee.asset` differs from `credit_out.asset`, which does
493				// not apply in this context.
494				defensive!(
495					"`total_fee.asset` must be equal to `credit_out.asset`",
496					(self.total_fee.asset(), credit_out.asset())
497				);
498				return Err((payment, XcmError::FeesNotMet));
499			},
500			_ => (),
501		};
502		self.last_fee_asset = Some(id.clone());
503
504		if credit_change.peek() != Zero::zero() {
505			let unspent = AssetsInHolding::new_from_fungible_credit(id, Box::new(credit_change));
506			payment.subsume_assets(unspent);
507		}
508		Ok(payment)
509	}
510
511	fn refund_weight(&mut self, weight: Weight, _context: &XcmContext) -> Option<AssetsInHolding> {
512		log::trace!(
513			target: "xcm::weight",
514			"SwapFirstAssetTrader::refund_weight weight: {:?}, self.total_fee: {:?}",
515			weight,
516			self.total_fee,
517		);
518		if weight.is_zero() || self.total_fee.peek().is_zero() {
519			// noting to refund.
520			return None;
521		}
522		let refund_asset = if let Some(asset) = &self.last_fee_asset {
523			// create an initial zero refund in the asset used in the last `buy_weight`.
524			(asset.clone(), Fungible(0)).into()
525		} else {
526			return None;
527		};
528		let refund_amount = WeightToFee::weight_to_fee(&weight);
529		if refund_amount >= self.total_fee.peek() {
530			// not enough was paid to refund the `weight`.
531			return None;
532		}
533
534		let refund_swap_asset = FungiblesAssetMatcher::matches_fungibles(&refund_asset)
535			.map(|(a, _)| a.into())
536			.ok()?;
537
538		let refund = self.total_fee.extract(refund_amount);
539		let refund = match SwapCredit::swap_exact_tokens_for_tokens(
540			vec![Target::get(), refund_swap_asset],
541			refund,
542			None,
543		) {
544			Ok(refund_in_target) => refund_in_target,
545			Err((refund, _)) => {
546				// return an attempted refund back to the `total_fee`.
547				let _ = self.total_fee.subsume(refund).map_err(|refund| {
548					// error may occur if `total_fee.asset` differs from `refund.asset`, which does
549					// not apply in this context.
550					defensive!(
551						"`total_fee.asset` must be equal to `refund.asset`",
552						(self.total_fee.asset(), refund.asset())
553					);
554				});
555				return None;
556			},
557		};
558
559		let refund = AssetsInHolding::new_from_fungible_credit(refund_asset.id, Box::new(refund));
560		Some(refund)
561	}
562
563	fn quote_weight(
564		&mut self,
565		weight: Weight,
566		given_id: AssetId,
567		_context: &XcmContext,
568	) -> Result<Asset, XcmError> {
569		log::trace!(
570			target: "xcm::weight",
571			"SwapFirstAssetTrader::quote_weight weight: {:?}, given_id: {:?}",
572			weight,
573			given_id,
574		);
575		if weight.is_zero() {
576			return Err(XcmError::NoDeal);
577		}
578
579		let give_matcher: Asset = (given_id.clone(), 1).into();
580		let (give_fungibles_id, _) = FungiblesAssetMatcher::matches_fungibles(&give_matcher)
581			.map_err(|_| XcmError::AssetNotFound)?;
582		let want_fungibles_id = Target::get();
583		if give_fungibles_id.eq(&want_fungibles_id.clone().into()) {
584			return Err(XcmError::FeesNotMet);
585		}
586
587		let want_amount = WeightToFee::weight_to_fee(&weight);
588		// The `give` amount required to obtain `want`.
589		let necessary_give: u128 = <SwapCredit as QuotePrice>::quote_price_tokens_for_exact_tokens(
590			give_fungibles_id,
591			want_fungibles_id,
592			want_amount,
593			true, // Include fee.
594		)
595		.filter(|amount| *amount > 0u128.into())
596		.ok_or(XcmError::FeesNotMet)?
597		.into();
598		Ok((given_id, necessary_give).into())
599	}
600}
601
602impl<
603		Target: Get<Fungibles::AssetId>,
604		SwapCredit: SwapCreditT<
605				AccountId,
606				Balance = Fungibles::Balance,
607				AssetKind = Fungibles::AssetId,
608				Credit = fungibles::Credit<AccountId, Fungibles>,
609			> + QuotePrice,
610		WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
611		Fungibles: fungibles::Balanced<AccountId>,
612		FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
613		OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
614		AccountId,
615	> Drop
616	for SwapFirstAssetTrader<
617		Target,
618		SwapCredit,
619		WeightToFee,
620		Fungibles,
621		FungiblesAssetMatcher,
622		OnUnbalanced,
623		AccountId,
624	>
625where
626	Fungibles::Balance: From<u128> + Into<u128>,
627{
628	fn drop(&mut self) {
629		if self.total_fee.peek().is_zero() {
630			return;
631		}
632		let total_fee = self.total_fee.extract(self.total_fee.peek());
633		OnUnbalanced::on_unbalanced(total_fee);
634	}
635}
636
637#[cfg(test)]
638mod test_xcm_router {
639	use super::*;
640	use cumulus_primitives_core::UpwardMessage;
641	use frame_support::assert_ok;
642	use xcm::MAX_XCM_DECODE_DEPTH;
643
644	/// Validates [`validate`] for required Some(destination) and Some(message)
645	struct OkFixedXcmHashWithAssertingRequiredInputsSender;
646
647	impl OkFixedXcmHashWithAssertingRequiredInputsSender {
648		const FIXED_XCM_HASH: [u8; 32] = [9; 32];
649
650		fn fixed_delivery_asset() -> Assets {
651			Assets::new()
652		}
653
654		fn expected_delivery_result() -> Result<(XcmHash, Assets), SendError> {
655			Ok((Self::FIXED_XCM_HASH, Self::fixed_delivery_asset()))
656		}
657	}
658
659	impl SendXcm for OkFixedXcmHashWithAssertingRequiredInputsSender {
660		type Ticket = ();
661
662		fn validate(
663			destination: &mut Option<Location>,
664			message: &mut Option<Xcm<()>>,
665		) -> SendResult<Self::Ticket> {
666			assert!(destination.is_some());
667			assert!(message.is_some());
668			Ok(((), OkFixedXcmHashWithAssertingRequiredInputsSender::fixed_delivery_asset()))
669		}
670
671		fn deliver(_: Self::Ticket) -> Result<XcmHash, SendError> {
672			Ok(Self::FIXED_XCM_HASH)
673		}
674	}
675
676	/// Impl [`UpwardMessageSender`] that return `Ok` for `can_send_upward_message`.
677	struct CanSendUpwardMessageSender;
678
679	impl UpwardMessageSender for CanSendUpwardMessageSender {
680		fn send_upward_message(_: UpwardMessage) -> Result<(u32, XcmHash), MessageSendError> {
681			Err(MessageSendError::Other)
682		}
683
684		fn can_send_upward_message(_: &UpwardMessage) -> Result<(), MessageSendError> {
685			Ok(())
686		}
687	}
688
689	#[test]
690	fn parent_as_ump_does_not_consume_dest_or_msg_on_not_applicable() {
691		// dummy message
692		let message = Xcm(vec![Trap(5)]);
693
694		// ParentAsUmp - check dest is really not applicable
695		let dest = (Parent, Parent, Parent);
696		let mut dest_wrapper = Some(dest.into());
697		let mut msg_wrapper = Some(message.clone());
698		assert_eq!(
699			Err(SendError::NotApplicable),
700			<ParentAsUmp<(), (), ()> as SendXcm>::validate(&mut dest_wrapper, &mut msg_wrapper)
701		);
702
703		// check wrapper were not consumed
704		assert_eq!(Some(dest.into()), dest_wrapper.take());
705		assert_eq!(Some(message.clone()), msg_wrapper.take());
706
707		// another try with router chain with asserting sender
708		assert_eq!(
709			OkFixedXcmHashWithAssertingRequiredInputsSender::expected_delivery_result(),
710			send_xcm::<(ParentAsUmp<(), (), ()>, OkFixedXcmHashWithAssertingRequiredInputsSender)>(
711				dest.into(),
712				message,
713			)
714		);
715	}
716
717	#[test]
718	fn parent_as_ump_consumes_dest_and_msg_on_ok_validate() {
719		// dummy message
720		let message = Xcm(vec![Trap(5)]);
721
722		// ParentAsUmp - check dest/msg is valid
723		let dest = (Parent, Here);
724		let mut dest_wrapper = Some(dest.clone().into());
725		let mut msg_wrapper = Some(message.clone());
726		assert!(<ParentAsUmp<CanSendUpwardMessageSender, (), ()> as SendXcm>::validate(
727			&mut dest_wrapper,
728			&mut msg_wrapper,
729		)
730		.is_ok());
731
732		// check wrapper were consumed
733		assert_eq!(None, dest_wrapper.take());
734		assert_eq!(None, msg_wrapper.take());
735
736		// another try with router chain with asserting sender
737		assert_eq!(
738			Err(SendError::Transport("Other")),
739			send_xcm::<(
740				ParentAsUmp<CanSendUpwardMessageSender, (), ()>,
741				OkFixedXcmHashWithAssertingRequiredInputsSender
742			)>(dest.into(), message)
743		);
744	}
745
746	#[test]
747	fn parent_as_ump_validate_nested_xcm_works() {
748		let dest = Parent;
749
750		type Router = ParentAsUmp<CanSendUpwardMessageSender, (), ()>;
751
752		// Message that is not too deeply nested:
753		let mut good = Xcm(vec![ClearOrigin]);
754		for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
755			good = Xcm(vec![SetAppendix(good)]);
756		}
757
758		// Check that the good message is validated:
759		assert_ok!(<Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(good.clone())));
760
761		// Nesting the message one more time should reject it:
762		let bad = Xcm(vec![SetAppendix(good)]);
763		assert_eq!(
764			Err(SendError::ExceedsMaxMessageSize),
765			<Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(bad))
766		);
767	}
768}
769
770#[cfg(test)]
771mod test_trader {
772	use super::{test_helpers::asset_to_holding, *};
773	use frame_support::{
774		assert_ok,
775		traits::tokens::{
776			DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
777		},
778	};
779	use sp_runtime::DispatchError;
780	use xcm_builder::TakeRevenue;
781	use xcm_executor::traits::Error;
782
783	#[test]
784	fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
785		const AMOUNT: u128 = 100;
786
787		// prepare prerequisites to instantiate `TakeFirstAssetTrader`
788		type TestAccountId = u32;
789		type TestAssetId = Location; // Use Location directly as AssetId
790		type TestBalance = u128;
791
792		struct TestAssets;
793		impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
794			fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
795				match a {
796					Asset { fun: Fungible(amount), id: AssetId(_id) } => {
797						Ok((Location::new(0, [GeneralIndex(1)]), *amount))
798					},
799					_ => Err(Error::AssetNotHandled),
800				}
801			}
802		}
803		impl fungibles::Inspect<TestAccountId> for TestAssets {
804			type AssetId = TestAssetId;
805			type Balance = TestBalance;
806
807			fn total_issuance(_: Self::AssetId) -> Self::Balance {
808				0
809			}
810
811			fn minimum_balance(_: Self::AssetId) -> Self::Balance {
812				0
813			}
814
815			fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
816				0
817			}
818
819			fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
820				0
821			}
822
823			fn reducible_balance(
824				_: Self::AssetId,
825				_: &TestAccountId,
826				_: Preservation,
827				_: Fortitude,
828			) -> Self::Balance {
829				0
830			}
831
832			fn can_deposit(
833				_: Self::AssetId,
834				_: &TestAccountId,
835				_: Self::Balance,
836				_: Provenance,
837			) -> DepositConsequence {
838				DepositConsequence::Success
839			}
840
841			fn can_withdraw(
842				_: Self::AssetId,
843				_: &TestAccountId,
844				_: Self::Balance,
845			) -> WithdrawConsequence<Self::Balance> {
846				WithdrawConsequence::Success
847			}
848
849			fn asset_exists(_: Self::AssetId) -> bool {
850				true
851			}
852		}
853		impl fungibles::Mutate<TestAccountId> for TestAssets {}
854		impl fungibles::Balanced<TestAccountId> for TestAssets {
855			type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
856			type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
857		}
858		impl fungibles::Unbalanced<TestAccountId> for TestAssets {
859			fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {}
860			fn write_balance(
861				_: Self::AssetId,
862				_: &TestAccountId,
863				_: Self::Balance,
864			) -> Result<Option<Self::Balance>, DispatchError> {
865				Ok(None)
866			}
867
868			fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {}
869		}
870
871		struct FeeChargerAssetsHandleRefund;
872		impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
873			fn charge_weight_in_fungibles(
874				_: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
875				_: Weight,
876			) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
877				Ok(AMOUNT)
878			}
879		}
880		impl TakeRevenue for FeeChargerAssetsHandleRefund {
881			fn take_revenue(_: AssetsInHolding) {}
882		}
883
884		// Implement OnUnbalanced for the test
885		struct HandleFees;
886		impl OnUnbalancedT<fungibles::Credit<TestAccountId, TestAssets>> for HandleFees {
887			fn on_unbalanced(_: fungibles::Credit<TestAccountId, TestAssets>) {
888				// Just drop it for tests
889			}
890		}
891
892		// create new instance
893		type Trader = TakeFirstAssetTrader<
894			TestAccountId,
895			FeeChargerAssetsHandleRefund,
896			TestAssets,
897			TestAssets,
898			HandleFees,
899		>;
900		let mut trader = <Trader as WeightTrader>::new();
901		let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
902
903		// prepare test data
904		let asset: Asset = (Here, AMOUNT).into();
905		let payment1 = asset_to_holding(asset.clone());
906		let payment2 = asset_to_holding(asset);
907		let weight_to_buy = Weight::from_parts(1_000, 1_000);
908
909		// lets do first call (success)
910		assert_ok!(trader.buy_weight(weight_to_buy, payment1, &ctx));
911
912		// lets do second call (error)
913		let (_, error) = trader.buy_weight(weight_to_buy, payment2, &ctx).unwrap_err();
914		assert_eq!(error, XcmError::NotWithdrawable);
915	}
916
917	#[test]
918	fn take_first_asset_trader_returns_unused_amount() {
919		// Regression test for fix: buy_weight should only take the required amount,
920		// not the entire balance from payment
921		const REQUIRED_AMOUNT: u128 = 100;
922		const TOTAL_AMOUNT: u128 = 500; // More than required
923
924		// prepare prerequisites to instantiate `TakeFirstAssetTrader`
925		type TestAccountId = u32;
926		type TestAssetId = Location;
927		type TestBalance = u128;
928
929		struct TestAssets;
930		impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
931			fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
932				match a {
933					Asset { fun: Fungible(amount), id: AssetId(_id) } => {
934						Ok((Location::new(0, [GeneralIndex(1)]), *amount))
935					},
936					_ => Err(Error::AssetNotHandled),
937				}
938			}
939		}
940		impl fungibles::Inspect<TestAccountId> for TestAssets {
941			type AssetId = TestAssetId;
942			type Balance = TestBalance;
943
944			fn total_issuance(_: Self::AssetId) -> Self::Balance {
945				0
946			}
947
948			fn minimum_balance(_: Self::AssetId) -> Self::Balance {
949				0
950			}
951
952			fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
953				0
954			}
955
956			fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
957				0
958			}
959
960			fn reducible_balance(
961				_: Self::AssetId,
962				_: &TestAccountId,
963				_: Preservation,
964				_: Fortitude,
965			) -> Self::Balance {
966				0
967			}
968
969			fn can_deposit(
970				_: Self::AssetId,
971				_: &TestAccountId,
972				_: Self::Balance,
973				_: Provenance,
974			) -> DepositConsequence {
975				DepositConsequence::Success
976			}
977
978			fn can_withdraw(
979				_: Self::AssetId,
980				_: &TestAccountId,
981				_: Self::Balance,
982			) -> WithdrawConsequence<Self::Balance> {
983				WithdrawConsequence::Success
984			}
985
986			fn asset_exists(_: Self::AssetId) -> bool {
987				true
988			}
989		}
990		impl fungibles::Mutate<TestAccountId> for TestAssets {}
991		impl fungibles::Balanced<TestAccountId> for TestAssets {
992			type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
993			type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
994		}
995		impl fungibles::Unbalanced<TestAccountId> for TestAssets {
996			fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {}
997			fn write_balance(
998				_: Self::AssetId,
999				_: &TestAccountId,
1000				_: Self::Balance,
1001			) -> Result<Option<Self::Balance>, DispatchError> {
1002				Ok(None)
1003			}
1004
1005			fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {}
1006		}
1007
1008		struct FeeChargerAssetsHandleRefund;
1009		impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
1010			fn charge_weight_in_fungibles(
1011				_: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
1012				_: Weight,
1013			) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
1014				Ok(REQUIRED_AMOUNT)
1015			}
1016		}
1017		impl TakeRevenue for FeeChargerAssetsHandleRefund {
1018			fn take_revenue(_: AssetsInHolding) {}
1019		}
1020
1021		struct HandleFees;
1022		impl OnUnbalancedT<fungibles::Credit<TestAccountId, TestAssets>> for HandleFees {
1023			fn on_unbalanced(_: fungibles::Credit<TestAccountId, TestAssets>) {}
1024		}
1025
1026		// create new instance
1027		type Trader = TakeFirstAssetTrader<
1028			TestAccountId,
1029			FeeChargerAssetsHandleRefund,
1030			TestAssets,
1031			TestAssets,
1032			HandleFees,
1033		>;
1034		let mut trader = <Trader as WeightTrader>::new();
1035		let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
1036
1037		// prepare test data - payment with MORE than required
1038		let asset: Asset = (Here, TOTAL_AMOUNT).into();
1039		let payment = asset_to_holding(asset.clone());
1040		let weight_to_buy = Weight::from_parts(1_000, 1_000);
1041
1042		// call buy_weight - should succeed and return the excess
1043		let result = trader.buy_weight(weight_to_buy, payment, &ctx);
1044		assert_ok!(&result);
1045
1046		let unused_payment = result.unwrap();
1047
1048		// verify that the unused payment contains the excess amount
1049		let expected_excess = TOTAL_AMOUNT - REQUIRED_AMOUNT;
1050		let unused_assets: Vec<Asset> = unused_payment.fungible_assets_iter().collect();
1051
1052		// should have exactly one asset remaining
1053		assert_eq!(unused_assets.len(), 1);
1054
1055		// verify it's the correct amount (excess)
1056		match &unused_assets[0] {
1057			Asset { fun: Fungible(amount), .. } => {
1058				assert_eq!(*amount, expected_excess, "Expected excess amount to be returned");
1059			},
1060			_ => panic!("Expected fungible asset"),
1061		}
1062	}
1063}
1064
1065/// Implementation of `xcm_builder::EnsureDelivery` which helps to ensure delivery to the
1066/// parent relay chain. Deposits existential deposit for origin (if needed).
1067/// Deposits estimated fee to the origin account (if needed).
1068/// Allows triggering of additional logic for a specific `ParaId` (e.g. to open an HRMP channel) if
1069/// needed.
1070#[cfg(feature = "runtime-benchmarks")]
1071pub struct ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>(
1072	core::marker::PhantomData<(XcmConfig, ExistentialDeposit, PriceForDelivery)>,
1073);
1074
1075#[cfg(feature = "runtime-benchmarks")]
1076impl<
1077		XcmConfig: xcm_executor::Config,
1078		ExistentialDeposit: Get<Option<Asset>>,
1079		PriceForDelivery: PriceForMessageDelivery<Id = ()>,
1080	> xcm_builder::EnsureDelivery
1081	for ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>
1082{
1083	fn ensure_successful_delivery(
1084		origin_ref: &Location,
1085		dest: &Location,
1086		fee_reason: xcm_executor::traits::FeeReason,
1087	) -> (Option<xcm_executor::FeesMode>, Option<Assets>) {
1088		use xcm::{latest::MAX_ITEMS_IN_ASSETS, MAX_INSTRUCTIONS_TO_DECODE};
1089		use xcm_executor::{
1090			traits::{FeeManager, TransactAsset},
1091			FeesMode,
1092		};
1093
1094		// check if the destination is relay/parent
1095		if dest.ne(&Location::parent()) {
1096			return (None, None);
1097		}
1098
1099		// Ensure routers
1100		XcmConfig::XcmSender::ensure_successful_delivery(Some(Location::parent()));
1101
1102		let mut fees_mode = None;
1103		if !XcmConfig::FeeManager::is_waived(Some(origin_ref), fee_reason) {
1104			// if not waived, we need to set up accounts for paying and receiving fees
1105			let context = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
1106
1107			// mint ED to origin if needed
1108			if let Some(ed) = ExistentialDeposit::get() {
1109				let holdings = XcmConfig::AssetTransactor::mint_asset(&ed, &context).unwrap();
1110				XcmConfig::AssetTransactor::deposit_asset(holdings, &origin_ref, Some(&context))
1111					.unwrap();
1112			}
1113
1114			// overestimate delivery fee
1115			let mut max_assets: Vec<Asset> = Vec::new();
1116			for i in 0..MAX_ITEMS_IN_ASSETS {
1117				max_assets.push((GeneralIndex(i as u128), 100u128).into());
1118			}
1119			let overestimated_xcm =
1120				vec![WithdrawAsset(max_assets.into()); MAX_INSTRUCTIONS_TO_DECODE as usize].into();
1121			let overestimated_fees = PriceForDelivery::price_for_delivery((), &overestimated_xcm);
1122
1123			// mint overestimated fee to origin
1124			for fee in overestimated_fees.inner() {
1125				let holdings = XcmConfig::AssetTransactor::mint_asset(fee, &context).unwrap();
1126				XcmConfig::AssetTransactor::deposit_asset(holdings, &origin_ref, Some(&context))
1127					.unwrap();
1128			}
1129
1130			// expected worst case - direct withdraw
1131			fees_mode = Some(FeesMode { jit_withdraw: true });
1132		}
1133		(fees_mode, None)
1134	}
1135}