pallet_xcm_weight_trader/
lib.rs

1// Copyright 2024 Moonbeam foundation
2// This file is part of Moonbeam.
3
4// Moonbeam 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// Moonbeam 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 Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # A pallet to trade weight for XCM execution
18
19#![allow(non_camel_case_types)]
20#![cfg_attr(not(feature = "std"), no_std)]
21
22#[cfg(feature = "runtime-benchmarks")]
23mod benchmarking;
24#[cfg(test)]
25mod mock;
26#[cfg(test)]
27mod tests;
28
29pub mod weights;
30
31pub use pallet::*;
32pub use weights::WeightInfo;
33
34use frame_support::pallet_prelude::*;
35use frame_support::traits::Contains;
36use frame_support::weights::WeightToFee;
37use frame_support::{pallet, Deserialize, Serialize};
38use frame_system::pallet_prelude::*;
39use sp_runtime::traits::{Convert, Zero};
40use sp_std::{vec, vec::Vec};
41use xcm::v5::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
42use xcm::{IntoVersion, VersionedAssetId};
43use xcm_executor::traits::{TransactAsset, WeightTrader};
44use xcm_runtime_apis::fees::Error as XcmPaymentApiError;
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct XcmWeightTraderAssetInfo {
48	pub location: Location,
49	pub relative_price: u128,
50}
51
52pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
53
54#[pallet]
55pub mod pallet {
56	use super::*;
57
58	/// Pallet for multi block migrations
59	#[pallet::pallet]
60	pub struct Pallet<T>(PhantomData<T>);
61
62	/// Configuration trait of this pallet.
63	#[pallet::config]
64	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
65		/// Convert `T::AccountId` to `Location`.
66		type AccountIdToLocation: Convert<Self::AccountId, Location>;
67
68		/// Origin that is allowed to register a supported asset
69		type AddSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
70
71		/// A filter to forbid some XCM Location to be supported for fees.
72		/// if you don't use it, put "Everything".
73		type AssetLocationFilter: Contains<Location>;
74
75		/// How to withdraw and deposit an asset.
76		type AssetTransactor: TransactAsset;
77
78		/// The native balance type.
79		type Balance: TryInto<u128>;
80
81		/// Origin that is allowed to edit a supported asset units per seconds
82		type EditSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
83
84		/// XCM Location for native curreny
85		type NativeLocation: Get<Location>;
86
87		/// Origin that is allowed to pause a supported asset
88		type PauseSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
89
90		/// Origin that is allowed to remove a supported asset
91		type RemoveSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
92
93		/// Origin that is allowed to unpause a supported asset
94		type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
95
96		/// Weight information for extrinsics in this pallet.
97		type WeightInfo: WeightInfo;
98
99		/// Convert a weight value into deductible native balance.
100		type WeightToFee: WeightToFee<Balance = Self::Balance>;
101
102		/// Account that will receive xcm fees
103		type XcmFeesAccount: Get<Self::AccountId>;
104
105		/// The benchmarks need a location that pass the filter AssetLocationFilter
106		#[cfg(feature = "runtime-benchmarks")]
107		type NotFilteredLocation: Get<Location>;
108	}
109
110	/// Stores all supported assets per XCM Location.
111	/// The u128 is the asset price relative to native asset with 18 decimals
112	/// The boolean specify if the support for this asset is active
113	#[pallet::storage]
114	#[pallet::getter(fn supported_assets)]
115	pub type SupportedAssets<T: Config> = StorageMap<_, Blake2_128Concat, Location, (bool, u128)>;
116
117	#[pallet::error]
118	pub enum Error<T> {
119		/// The given asset was already added
120		AssetAlreadyAdded,
121		/// The given asset was already paused
122		AssetAlreadyPaused,
123		/// The given asset was not found
124		AssetNotFound,
125		/// The given asset is not paused
126		AssetNotPaused,
127		/// XCM location filtered
128		XcmLocationFiltered,
129		/// The relative price cannot be zero
130		PriceCannotBeZero,
131		/// The relative price calculation overflowed
132		PriceOverflow,
133	}
134
135	#[pallet::event]
136	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
137	pub enum Event<T: Config> {
138		/// New supported asset is registered
139		SupportedAssetAdded {
140			location: Location,
141			relative_price: u128,
142		},
143		/// Changed the amount of units we are charging per execution second for a given asset
144		SupportedAssetEdited {
145			location: Location,
146			relative_price: u128,
147		},
148		/// Pause support for a given asset
149		PauseAssetSupport { location: Location },
150		/// Resume support for a given asset
151		ResumeAssetSupport { location: Location },
152		/// Supported asset type for fee payment removed
153		SupportedAssetRemoved { location: Location },
154	}
155
156	#[pallet::genesis_config]
157	pub struct GenesisConfig<T: Config> {
158		pub assets: Vec<XcmWeightTraderAssetInfo>,
159		pub _phantom: PhantomData<T>,
160	}
161
162	impl<T: Config> Default for GenesisConfig<T> {
163		fn default() -> Self {
164			Self {
165				assets: vec![],
166				_phantom: Default::default(),
167			}
168		}
169	}
170
171	#[pallet::genesis_build]
172	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
173		fn build(&self) {
174			for asset in self.assets.clone() {
175				Pallet::<T>::do_add_asset(asset.location, asset.relative_price)
176					.expect("couldn't add asset");
177			}
178		}
179	}
180
181	#[pallet::call]
182	impl<T: Config> Pallet<T> {
183		#[pallet::call_index(0)]
184		#[pallet::weight(T::WeightInfo::add_asset())]
185		pub fn add_asset(
186			origin: OriginFor<T>,
187			location: Location,
188			relative_price: u128,
189		) -> DispatchResult {
190			T::AddSupportedAssetOrigin::ensure_origin(origin)?;
191
192			Self::do_add_asset(location, relative_price)
193		}
194
195		#[pallet::call_index(1)]
196		#[pallet::weight(<T as pallet::Config>::WeightInfo::edit_asset())]
197		pub fn edit_asset(
198			origin: OriginFor<T>,
199			location: Location,
200			relative_price: u128,
201		) -> DispatchResult {
202			T::EditSupportedAssetOrigin::ensure_origin(origin)?;
203
204			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
205
206			let enabled = SupportedAssets::<T>::get(&location)
207				.ok_or(Error::<T>::AssetNotFound)?
208				.0;
209
210			SupportedAssets::<T>::insert(&location, (enabled, relative_price));
211
212			Self::deposit_event(Event::SupportedAssetEdited {
213				location,
214				relative_price,
215			});
216
217			Ok(())
218		}
219
220		#[pallet::call_index(2)]
221		#[pallet::weight(<T as pallet::Config>::WeightInfo::pause_asset_support())]
222		pub fn pause_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
223			T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
224
225			match SupportedAssets::<T>::get(&location) {
226				Some((true, relative_price)) => {
227					SupportedAssets::<T>::insert(&location, (false, relative_price));
228					Self::deposit_event(Event::PauseAssetSupport { location });
229					Ok(())
230				}
231				Some((false, _)) => Err(Error::<T>::AssetAlreadyPaused.into()),
232				None => Err(Error::<T>::AssetNotFound.into()),
233			}
234		}
235
236		#[pallet::call_index(3)]
237		#[pallet::weight(<T as pallet::Config>::WeightInfo::resume_asset_support())]
238		pub fn resume_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
239			T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
240
241			match SupportedAssets::<T>::get(&location) {
242				Some((false, relative_price)) => {
243					SupportedAssets::<T>::insert(&location, (true, relative_price));
244					Self::deposit_event(Event::ResumeAssetSupport { location });
245					Ok(())
246				}
247				Some((true, _)) => Err(Error::<T>::AssetNotPaused.into()),
248				None => Err(Error::<T>::AssetNotFound.into()),
249			}
250		}
251
252		#[pallet::call_index(4)]
253		#[pallet::weight(<T as pallet::Config>::WeightInfo::remove_asset())]
254		pub fn remove_asset(origin: OriginFor<T>, location: Location) -> DispatchResult {
255			T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
256
257			ensure!(
258				SupportedAssets::<T>::contains_key(&location),
259				Error::<T>::AssetNotFound
260			);
261
262			SupportedAssets::<T>::remove(&location);
263
264			Self::deposit_event(Event::SupportedAssetRemoved { location });
265
266			Ok(())
267		}
268	}
269
270	impl<T: Config> Pallet<T> {
271		pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
272			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
273			ensure!(
274				!SupportedAssets::<T>::contains_key(&location),
275				Error::<T>::AssetAlreadyAdded
276			);
277			ensure!(
278				T::AssetLocationFilter::contains(&location),
279				Error::<T>::XcmLocationFiltered
280			);
281
282			SupportedAssets::<T>::insert(&location, (true, relative_price));
283
284			Self::deposit_event(Event::SupportedAssetAdded {
285				location,
286				relative_price,
287			});
288
289			Ok(())
290		}
291
292		pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
293			if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
294				Some(ratio)
295			} else {
296				None
297			}
298		}
299		pub fn query_acceptable_payment_assets(
300			xcm_version: xcm::Version,
301		) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
302			let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
303				T::NativeLocation::get(),
304			))]
305			.into_iter()
306			.chain(
307				SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
308					enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
309				}),
310			)
311			.collect::<Vec<_>>();
312
313			match xcm_version {
314				xcm::v3::VERSION => v5_assets
315					.into_iter()
316					.map(|v5_asset| v5_asset.into_version(xcm::v3::VERSION))
317					.collect::<Result<_, _>>()
318					.map_err(|_| XcmPaymentApiError::VersionedConversionFailed),
319				xcm::v4::VERSION => v5_assets
320					.into_iter()
321					.map(|v5_asset| v5_asset.into_version(xcm::v4::VERSION))
322					.collect::<Result<_, _>>()
323					.map_err(|_| XcmPaymentApiError::VersionedConversionFailed),
324				xcm::v5::VERSION => Ok(v5_assets),
325				_ => Err(XcmPaymentApiError::UnhandledXcmVersion),
326			}
327		}
328		pub fn query_weight_to_asset_fee(
329			weight: Weight,
330			asset: VersionedAssetId,
331		) -> Result<u128, XcmPaymentApiError> {
332			if let VersionedAssetId::V5(XcmAssetId(asset_location)) = asset
333				.into_version(xcm::latest::VERSION)
334				.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
335			{
336				Trader::<T>::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
337				{
338					XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
339					_ => XcmPaymentApiError::WeightNotComputable,
340				})
341			} else {
342				Err(XcmPaymentApiError::UnhandledXcmVersion)
343			}
344		}
345		#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
346		pub fn set_asset_price(asset_location: Location, relative_price: u128) {
347			SupportedAssets::<T>::insert(&asset_location, (true, relative_price));
348		}
349	}
350}
351
352pub struct Trader<T: crate::Config>(Weight, Option<Asset>, core::marker::PhantomData<T>);
353
354impl<T: crate::Config> Trader<T> {
355	fn compute_amount_to_charge(
356		weight: &Weight,
357		asset_location: &Location,
358	) -> Result<u128, XcmError> {
359		if *asset_location == <T as crate::Config>::NativeLocation::get() {
360			<T as crate::Config>::WeightToFee::weight_to_fee(&weight)
361				.try_into()
362				.map_err(|_| XcmError::Overflow)
363		} else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
364			if relative_price == 0u128 {
365				Ok(0u128)
366			} else {
367				let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
368					.try_into()
369					.map_err(|_| XcmError::Overflow)?;
370				Ok(native_amount
371					.checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
372					.ok_or(XcmError::Overflow)?
373					.checked_div(relative_price)
374					.ok_or(XcmError::Overflow)?)
375			}
376		} else {
377			Err(XcmError::AssetNotFound)
378		}
379	}
380}
381
382impl<T: crate::Config> WeightTrader for Trader<T> {
383	fn new() -> Self {
384		Self(Weight::zero(), None, PhantomData)
385	}
386	fn buy_weight(
387		&mut self,
388		weight: Weight,
389		payment: xcm_executor::AssetsInHolding,
390		context: &XcmContext,
391	) -> Result<xcm_executor::AssetsInHolding, XcmError> {
392		log::trace!(
393			target: "xcm::weight",
394			"UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
395			weight,
396			payment,
397			context
398		);
399
400		// Can only call one time
401		if self.1.is_some() {
402			return Err(XcmError::NotWithdrawable);
403		}
404
405		// Consistency check for tests only, we should never panic in release mode
406		debug_assert_eq!(self.0, Weight::zero());
407
408		// We support only one fee asset per buy, so we take the first one.
409		let first_asset = payment
410			.clone()
411			.fungible_assets_iter()
412			.next()
413			.ok_or(XcmError::AssetNotFound)?;
414
415		match (first_asset.id, first_asset.fun) {
416			(XcmAssetId(location), Fungibility::Fungible(_)) => {
417				let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
418
419				// We don't need to proceed if the amount is 0
420				// For cases (specially tests) where the asset is very cheap with respect
421				// to the weight needed
422				if amount.is_zero() {
423					return Ok(payment);
424				}
425
426				let required = Asset {
427					fun: Fungibility::Fungible(amount),
428					id: XcmAssetId(location),
429				};
430				let unused = payment
431					.checked_sub(required.clone())
432					.map_err(|_| XcmError::TooExpensive)?;
433
434				self.0 = weight;
435				self.1 = Some(required);
436
437				Ok(unused)
438			}
439			_ => Err(XcmError::AssetNotFound),
440		}
441	}
442
443	fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option<Asset> {
444		log::trace!(
445			target: "xcm-weight-trader",
446			"refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
447			actual_weight,
448			context,
449			self.0,
450			self.1
451		);
452		if let Some(Asset {
453			fun: Fungibility::Fungible(initial_amount),
454			id: XcmAssetId(location),
455		}) = self.1.take()
456		{
457			if actual_weight == self.0 {
458				self.1 = Some(Asset {
459					fun: Fungibility::Fungible(initial_amount),
460					id: XcmAssetId(location),
461				});
462				None
463			} else {
464				let weight = actual_weight.min(self.0);
465				let amount: u128 =
466					Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
467				let final_amount = amount.min(initial_amount);
468				let amount_to_refund = initial_amount.saturating_sub(final_amount);
469				self.0 -= weight;
470				self.1 = Some(Asset {
471					fun: Fungibility::Fungible(final_amount),
472					id: XcmAssetId(location.clone()),
473				});
474				log::trace!(
475					target: "xcm-weight-trader",
476					"refund_weight amount to refund: {:?}",
477					amount_to_refund
478				);
479				Some(Asset {
480					fun: Fungibility::Fungible(amount_to_refund),
481					id: XcmAssetId(location),
482				})
483			}
484		} else {
485			None
486		}
487	}
488}
489
490impl<T: crate::Config> Drop for Trader<T> {
491	fn drop(&mut self) {
492		log::trace!(
493			target: "xcm-weight-trader",
494			"Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
495			&self.0,
496			&self.1
497		);
498		if let Some(asset) = self.1.take() {
499			let res = T::AssetTransactor::deposit_asset(
500				&asset,
501				&T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
502				None,
503			);
504			debug_assert!(res.is_ok());
505		}
506	}
507}