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")]
23
mod benchmarking;
24
#[cfg(test)]
25
mod mock;
26
#[cfg(test)]
27
mod tests;
28

            
29
pub mod weights;
30

            
31
pub use pallet::*;
32
pub use weights::WeightInfo;
33

            
34
use frame_support::pallet;
35
use frame_support::pallet_prelude::*;
36
use frame_support::traits::Contains;
37
use frame_support::weights::WeightToFee;
38
use frame_system::pallet_prelude::*;
39
use sp_runtime::traits::{Convert, Zero};
40
use sp_std::vec::Vec;
41
use xcm::v4::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
42
use xcm::{IntoVersion, VersionedAssetId};
43
use xcm_executor::traits::{TransactAsset, WeightTrader};
44
use xcm_runtime_apis::fees::Error as XcmPaymentApiError;
45

            
46
pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
47

            
48
540
#[pallet]
49
pub mod pallet {
50
	use super::*;
51

            
52
	/// Pallet for multi block migrations
53
79
	#[pallet::pallet]
54
	pub struct Pallet<T>(PhantomData<T>);
55

            
56
	/// Configuration trait of this pallet.
57
	#[pallet::config]
58
	pub trait Config: frame_system::Config {
59
		/// Convert `T::AccountId` to `Location`.
60
		type AccountIdToLocation: Convert<Self::AccountId, Location>;
61

            
62
		/// Origin that is allowed to register a supported asset
63
		type AddSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
64

            
65
		/// A filter to forbid some XCM Location to be supported for fees.
66
		/// if you don't use it, put "Everything".
67
		type AssetLocationFilter: Contains<Location>;
68

            
69
		/// How to withdraw and deposit an asset.
70
		type AssetTransactor: TransactAsset;
71

            
72
		/// The native balance type.
73
		type Balance: TryInto<u128>;
74

            
75
		/// Origin that is allowed to edit a supported asset units per seconds
76
		type EditSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
77

            
78
		/// XCM Location for native curreny
79
		type NativeLocation: Get<Location>;
80

            
81
		/// Origin that is allowed to pause a supported asset
82
		type PauseSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
83

            
84
		/// Origin that is allowed to remove a supported asset
85
		type RemoveSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
86

            
87
		/// The overarching event type.
88
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
89

            
90
		/// Origin that is allowed to unpause a supported asset
91
		type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
92

            
93
		/// Weight information for extrinsics in this pallet.
94
		type WeightInfo: WeightInfo;
95

            
96
		/// Convert a weight value into deductible native balance.
97
		type WeightToFee: WeightToFee<Balance = Self::Balance>;
98

            
99
		/// Account that will receive xcm fees
100
		type XcmFeesAccount: Get<Self::AccountId>;
101

            
102
		/// The benchmarks need a location that pass the filter AssetLocationFilter
103
		#[cfg(feature = "runtime-benchmarks")]
104
		type NotFilteredLocation: Get<Location>;
105
	}
106

            
107
	/// Stores all supported assets per XCM Location.
108
	/// The u128 is the asset price relative to native asset with 18 decimals
109
	/// The boolean specify if the support for this asset is active
110
306
	#[pallet::storage]
111
	#[pallet::getter(fn supported_assets)]
112
	pub type SupportedAssets<T: Config> = StorageMap<_, Blake2_128Concat, Location, (bool, u128)>;
113

            
114
22
	#[pallet::error]
115
	pub enum Error<T> {
116
		/// The given asset was already added
117
		AssetAlreadyAdded,
118
		/// The given asset was already paused
119
		AssetAlreadyPaused,
120
		/// The given asset was not found
121
		AssetNotFound,
122
		/// The given asset is not paused
123
		AssetNotPaused,
124
		/// XCM location filtered
125
		XcmLocationFiltered,
126
		/// The relative price cannot be zero
127
		PriceCannotBeZero,
128
	}
129

            
130
	#[pallet::event]
131
18
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
132
	pub enum Event<T: Config> {
133
		/// New supported asset is registered
134
		SupportedAssetAdded {
135
			location: Location,
136
			relative_price: u128,
137
		},
138
		/// Changed the amount of units we are charging per execution second for a given asset
139
		SupportedAssetEdited {
140
			location: Location,
141
			relative_price: u128,
142
		},
143
		/// Pause support for a given asset
144
		PauseAssetSupport { location: Location },
145
		/// Resume support for a given asset
146
		ResumeAssetSupport { location: Location },
147
		/// Supported asset type for fee payment removed
148
		SupportedAssetRemoved { location: Location },
149
	}
150

            
151
	#[pallet::call]
152
	impl<T: Config> Pallet<T> {
153
		#[pallet::call_index(0)]
154
		#[pallet::weight(T::WeightInfo::add_asset())]
155
		pub fn add_asset(
156
			origin: OriginFor<T>,
157
			location: Location,
158
			relative_price: u128,
159
12
		) -> DispatchResult {
160
12
			T::AddSupportedAssetOrigin::ensure_origin(origin)?;
161

            
162
11
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
163
10
			ensure!(
164
10
				!SupportedAssets::<T>::contains_key(&location),
165
1
				Error::<T>::AssetAlreadyAdded
166
			);
167
9
			ensure!(
168
9
				T::AssetLocationFilter::contains(&location),
169
1
				Error::<T>::XcmLocationFiltered
170
			);
171

            
172
8
			SupportedAssets::<T>::insert(&location, (true, relative_price));
173
8

            
174
8
			Self::deposit_event(Event::SupportedAssetAdded {
175
8
				location,
176
8
				relative_price,
177
8
			});
178
8

            
179
8
			Ok(())
180
		}
181

            
182
		#[pallet::call_index(1)]
183
		#[pallet::weight(T::WeightInfo::edit_asset())]
184
		pub fn edit_asset(
185
			origin: OriginFor<T>,
186
			location: Location,
187
			relative_price: u128,
188
6
		) -> DispatchResult {
189
6
			T::EditSupportedAssetOrigin::ensure_origin(origin)?;
190

            
191
5
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
192

            
193
4
			let enabled = SupportedAssets::<T>::get(&location)
194
4
				.ok_or(Error::<T>::AssetNotFound)?
195
				.0;
196

            
197
3
			SupportedAssets::<T>::insert(&location, (enabled, relative_price));
198
3

            
199
3
			Self::deposit_event(Event::SupportedAssetEdited {
200
3
				location,
201
3
				relative_price,
202
3
			});
203
3

            
204
3
			Ok(())
205
		}
206

            
207
		#[pallet::call_index(2)]
208
		#[pallet::weight(T::WeightInfo::pause_asset_support())]
209
7
		pub fn pause_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
210
7
			T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
211

            
212
6
			match SupportedAssets::<T>::get(&location) {
213
4
				Some((true, relative_price)) => {
214
4
					SupportedAssets::<T>::insert(&location, (false, relative_price));
215
4
					Self::deposit_event(Event::PauseAssetSupport { location });
216
4
					Ok(())
217
				}
218
1
				Some((false, _)) => Err(Error::<T>::AssetAlreadyPaused.into()),
219
1
				None => Err(Error::<T>::AssetNotFound.into()),
220
			}
221
		}
222

            
223
		#[pallet::call_index(3)]
224
		#[pallet::weight(T::WeightInfo::resume_asset_support())]
225
5
		pub fn resume_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
226
5
			T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
227

            
228
4
			match SupportedAssets::<T>::get(&location) {
229
2
				Some((false, relative_price)) => {
230
2
					SupportedAssets::<T>::insert(&location, (true, relative_price));
231
2
					Self::deposit_event(Event::ResumeAssetSupport { location });
232
2
					Ok(())
233
				}
234
1
				Some((true, _)) => Err(Error::<T>::AssetNotPaused.into()),
235
1
				None => Err(Error::<T>::AssetNotFound.into()),
236
			}
237
		}
238

            
239
		#[pallet::call_index(4)]
240
		#[pallet::weight(T::WeightInfo::remove_asset())]
241
4
		pub fn remove_asset(origin: OriginFor<T>, location: Location) -> DispatchResult {
242
4
			T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
243

            
244
3
			ensure!(
245
3
				SupportedAssets::<T>::contains_key(&location),
246
2
				Error::<T>::AssetNotFound
247
			);
248

            
249
1
			SupportedAssets::<T>::remove(&location);
250
1

            
251
1
			Self::deposit_event(Event::SupportedAssetRemoved { location });
252
1

            
253
1
			Ok(())
254
		}
255
	}
256

            
257
	impl<T: Config> Pallet<T> {
258
143
		pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
259
143
			if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
260
135
				Some(ratio)
261
			} else {
262
8
				None
263
			}
264
143
		}
265
5
		pub fn query_acceptable_payment_assets(
266
5
			xcm_version: xcm::Version,
267
5
		) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
268
5
			if !matches!(xcm_version, 3 | 4) {
269
1
				return Err(XcmPaymentApiError::UnhandledXcmVersion);
270
4
			}
271
4

            
272
4
			let v4_assets = [VersionedAssetId::V4(XcmAssetId::from(
273
4
				T::NativeLocation::get(),
274
4
			))]
275
4
			.into_iter()
276
4
			.chain(
277
4
				SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
278
2
					enabled.then(|| VersionedAssetId::V4(XcmAssetId(asset_location)))
279
4
				}),
280
4
			)
281
4
			.collect::<Vec<_>>();
282
4

            
283
4
			if xcm_version == 3 {
284
1
				v4_assets
285
1
					.into_iter()
286
1
					.map(|v4_asset| v4_asset.into_version(3))
287
1
					.collect::<Result<_, _>>()
288
1
					.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)
289
			} else {
290
3
				Ok(v4_assets)
291
			}
292
5
		}
293
5
		pub fn query_weight_to_asset_fee(
294
5
			weight: Weight,
295
5
			asset: VersionedAssetId,
296
5
		) -> Result<u128, XcmPaymentApiError> {
297
5
			if let VersionedAssetId::V4(XcmAssetId(asset_location)) = asset
298
5
				.into_version(4)
299
5
				.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
300
			{
301
5
				Trader::<T>::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
302
				{
303
2
					XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
304
					_ => XcmPaymentApiError::WeightNotComputable,
305
5
				})
306
			} else {
307
				Err(XcmPaymentApiError::UnhandledXcmVersion)
308
			}
309
5
		}
310
		#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
311
21
		pub fn set_asset_price(asset_location: Location, relative_price: u128) {
312
21
			SupportedAssets::<T>::insert(&asset_location, (true, relative_price));
313
21
		}
314
	}
315
}
316

            
317
pub struct Trader<T: crate::Config>(Weight, Option<Asset>, core::marker::PhantomData<T>);
318

            
319
impl<T: crate::Config> Trader<T> {
320
169
	fn compute_amount_to_charge(
321
169
		weight: &Weight,
322
169
		asset_location: &Location,
323
169
	) -> Result<u128, XcmError> {
324
169
		if *asset_location == <T as crate::Config>::NativeLocation::get() {
325
41
			<T as crate::Config>::WeightToFee::weight_to_fee(&weight)
326
41
				.try_into()
327
41
				.map_err(|_| XcmError::Overflow)
328
128
		} else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
329
125
			if relative_price == 0u128 {
330
91
				Ok(0u128)
331
			} else {
332
34
				let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
333
34
					.try_into()
334
34
					.map_err(|_| XcmError::Overflow)?;
335
34
				Ok(native_amount
336
34
					.checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
337
34
					.ok_or(XcmError::Overflow)?
338
34
					.checked_div(relative_price)
339
34
					.ok_or(XcmError::Overflow)?)
340
			}
341
		} else {
342
3
			Err(XcmError::AssetNotFound)
343
		}
344
169
	}
345
}
346

            
347
impl<T: crate::Config> WeightTrader for Trader<T> {
348
293
	fn new() -> Self {
349
293
		Self(Weight::zero(), None, PhantomData)
350
293
	}
351
155
	fn buy_weight(
352
155
		&mut self,
353
155
		weight: Weight,
354
155
		payment: xcm_executor::AssetsInHolding,
355
155
		context: &XcmContext,
356
155
	) -> Result<xcm_executor::AssetsInHolding, XcmError> {
357
155
		log::trace!(
358
			target: "xcm::weight",
359
			"UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
360
			weight,
361
			payment,
362
			context
363
		);
364

            
365
		// Can only call one time
366
155
		if self.1.is_some() {
367
1
			return Err(XcmError::NotWithdrawable);
368
154
		}
369
154

            
370
154
		// Consistency check for tests only, we should never panic in release mode
371
154
		debug_assert_eq!(self.0, Weight::zero());
372

            
373
		// We support only one fee asset per buy, so we take the first one.
374
154
		let first_asset = payment
375
154
			.clone()
376
154
			.fungible_assets_iter()
377
154
			.next()
378
154
			.ok_or(XcmError::AssetNotFound)?;
379

            
380
153
		match (first_asset.id, first_asset.fun) {
381
153
			(XcmAssetId(location), Fungibility::Fungible(_)) => {
382
153
				let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
383

            
384
				// We don't need to proceed if the amount is 0
385
				// For cases (specially tests) where the asset is very cheap with respect
386
				// to the weight needed
387
152
				if amount.is_zero() {
388
112
					return Ok(payment);
389
40
				}
390
40

            
391
40
				let required = Asset {
392
40
					fun: Fungibility::Fungible(amount),
393
40
					id: XcmAssetId(location),
394
40
				};
395
40
				let unused = payment
396
40
					.checked_sub(required.clone())
397
40
					.map_err(|_| XcmError::TooExpensive)?;
398

            
399
35
				self.0 = weight;
400
35
				self.1 = Some(required);
401
35

            
402
35
				Ok(unused)
403
			}
404
			_ => Err(XcmError::AssetNotFound),
405
		}
406
155
	}
407

            
408
16
	fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option<Asset> {
409
16
		log::trace!(
410
			target: "xcm-weight-trader",
411
			"refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
412
			actual_weight,
413
			context,
414
			self.0,
415
			self.1
416
		);
417
		if let Some(Asset {
418
12
			fun: Fungibility::Fungible(initial_amount),
419
12
			id: XcmAssetId(location),
420
16
		}) = self.1.take()
421
		{
422
12
			if actual_weight == self.0 {
423
1
				self.1 = Some(Asset {
424
1
					fun: Fungibility::Fungible(initial_amount),
425
1
					id: XcmAssetId(location),
426
1
				});
427
1
				None
428
			} else {
429
11
				let weight = actual_weight.min(self.0);
430
11
				let amount: u128 =
431
11
					Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
432
11
				let final_amount = amount.min(initial_amount);
433
11
				let amount_to_refund = initial_amount.saturating_sub(final_amount);
434
11
				self.0 -= weight;
435
11
				self.1 = Some(Asset {
436
11
					fun: Fungibility::Fungible(final_amount),
437
11
					id: XcmAssetId(location.clone()),
438
11
				});
439
11
				log::trace!(
440
					target: "xcm-weight-trader",
441
					"refund_weight amount to refund: {:?}",
442
					amount_to_refund
443
				);
444
11
				Some(Asset {
445
11
					fun: Fungibility::Fungible(amount_to_refund),
446
11
					id: XcmAssetId(location),
447
11
				})
448
			}
449
		} else {
450
4
			None
451
		}
452
16
	}
453
}
454

            
455
impl<T: crate::Config> Drop for Trader<T> {
456
293
	fn drop(&mut self) {
457
293
		log::trace!(
458
			target: "xcm-weight-trader",
459
			"Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
460
			&self.0,
461
			&self.1
462
		);
463
293
		if let Some(asset) = self.1.take() {
464
35
			let res = T::AssetTransactor::deposit_asset(
465
35
				&asset,
466
35
				&T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
467
35
				None,
468
35
			);
469
35
			debug_assert!(res.is_ok());
470
258
		}
471
293
	}
472
}