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_prelude::*;
35
use frame_support::traits::Contains;
36
use frame_support::weights::WeightToFee;
37
use frame_support::{pallet, Deserialize, Serialize};
38
use frame_system::pallet_prelude::*;
39
use sp_runtime::{
40
	traits::{Convert, Zero},
41
	DispatchError,
42
};
43
use sp_std::{vec, vec::Vec};
44
use xcm::v5::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
45
use xcm::{IntoVersion, VersionedAssetId};
46
use xcm_executor::traits::{TransactAsset, WeightTrader};
47
use xcm_primitives::XcmFeeTrader;
48
use xcm_runtime_apis::fees::Error as XcmPaymentApiError;
49

            
50
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51
pub struct XcmWeightTraderAssetInfo {
52
	pub location: Location,
53
	pub relative_price: u128,
54
}
55

            
56
pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
57

            
58
#[pallet]
59
pub mod pallet {
60
	use super::*;
61

            
62
	/// Pallet for multi block migrations
63
	#[pallet::pallet]
64
	pub struct Pallet<T>(PhantomData<T>);
65

            
66
	/// Configuration trait of this pallet.
67
	#[pallet::config]
68
	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
69
		/// Convert `T::AccountId` to `Location`.
70
		type AccountIdToLocation: Convert<Self::AccountId, Location>;
71

            
72
		/// Origin that is allowed to register a supported asset
73
		type AddSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
74

            
75
		/// A filter to forbid some XCM Location to be supported for fees.
76
		/// if you don't use it, put "Everything".
77
		type AssetLocationFilter: Contains<Location>;
78

            
79
		/// How to withdraw and deposit an asset.
80
		type AssetTransactor: TransactAsset;
81

            
82
		/// The native balance type.
83
		type Balance: TryInto<u128>;
84

            
85
		/// Origin that is allowed to edit a supported asset units per seconds
86
		type EditSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
87

            
88
		/// XCM Location for native curreny
89
		type NativeLocation: Get<Location>;
90

            
91
		/// Origin that is allowed to pause a supported asset
92
		type PauseSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
93

            
94
		/// Origin that is allowed to remove a supported asset
95
		type RemoveSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
96

            
97
		/// Origin that is allowed to unpause a supported asset
98
		type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
99

            
100
		/// Weight information for extrinsics in this pallet.
101
		type WeightInfo: WeightInfo;
102

            
103
		/// Convert a weight value into deductible native balance.
104
		type WeightToFee: WeightToFee<Balance = Self::Balance>;
105

            
106
		/// Account that will receive xcm fees
107
		type XcmFeesAccount: Get<Self::AccountId>;
108

            
109
		/// The benchmarks need a location that pass the filter AssetLocationFilter
110
		#[cfg(feature = "runtime-benchmarks")]
111
		type NotFilteredLocation: Get<Location>;
112
	}
113

            
114
	/// Stores all supported assets per XCM Location.
115
	/// The u128 is the asset price relative to native asset with 18 decimals
116
	/// The boolean specify if the support for this asset is active
117
	#[pallet::storage]
118
	#[pallet::getter(fn supported_assets)]
119
	pub type SupportedAssets<T: Config> = StorageMap<_, Blake2_128Concat, Location, (bool, u128)>;
120

            
121
	#[pallet::error]
122
	pub enum Error<T> {
123
		/// The given asset was already added
124
		AssetAlreadyAdded,
125
		/// The given asset was already paused
126
		AssetAlreadyPaused,
127
		/// The given asset was not found
128
		AssetNotFound,
129
		/// The given asset is not paused
130
		AssetNotPaused,
131
		/// XCM location filtered
132
		XcmLocationFiltered,
133
		/// The relative price cannot be zero
134
		PriceCannotBeZero,
135
		/// The relative price calculation overflowed
136
		PriceOverflow,
137
	}
138

            
139
	#[pallet::event]
140
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
141
	pub enum Event<T: Config> {
142
		/// New supported asset is registered
143
		SupportedAssetAdded {
144
			location: Location,
145
			relative_price: u128,
146
		},
147
		/// Changed the amount of units we are charging per execution second for a given asset
148
		SupportedAssetEdited {
149
			location: Location,
150
			relative_price: u128,
151
		},
152
		/// Pause support for a given asset
153
		PauseAssetSupport { location: Location },
154
		/// Resume support for a given asset
155
		ResumeAssetSupport { location: Location },
156
		/// Supported asset type for fee payment removed
157
		SupportedAssetRemoved { location: Location },
158
	}
159

            
160
	#[pallet::genesis_config]
161
	pub struct GenesisConfig<T: Config> {
162
		pub assets: Vec<XcmWeightTraderAssetInfo>,
163
		pub _phantom: PhantomData<T>,
164
	}
165

            
166
	impl<T: Config> Default for GenesisConfig<T> {
167
5
		fn default() -> Self {
168
5
			Self {
169
5
				assets: vec![],
170
5
				_phantom: Default::default(),
171
5
			}
172
5
		}
173
	}
174

            
175
	#[pallet::genesis_build]
176
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
177
5
		fn build(&self) {
178
5
			for asset in self.assets.clone() {
179
				Pallet::<T>::do_add_asset(asset.location, asset.relative_price)
180
					.expect("couldn't add asset");
181
			}
182
5
		}
183
	}
184

            
185
	#[pallet::call]
186
	impl<T: Config> Pallet<T> {
187
		#[pallet::call_index(0)]
188
		#[pallet::weight(T::WeightInfo::add_asset())]
189
		pub fn add_asset(
190
			origin: OriginFor<T>,
191
			location: Location,
192
			relative_price: u128,
193
28
		) -> DispatchResult {
194
28
			T::AddSupportedAssetOrigin::ensure_origin(origin)?;
195

            
196
27
			Self::do_add_asset(location, relative_price)
197
		}
198

            
199
		#[pallet::call_index(1)]
200
		#[pallet::weight(<T as pallet::Config>::WeightInfo::edit_asset())]
201
		pub fn edit_asset(
202
			origin: OriginFor<T>,
203
			location: Location,
204
			relative_price: u128,
205
6
		) -> DispatchResult {
206
6
			T::EditSupportedAssetOrigin::ensure_origin(origin)?;
207

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

            
210
4
			let enabled = SupportedAssets::<T>::get(&location)
211
4
				.ok_or(Error::<T>::AssetNotFound)?
212
				.0;
213

            
214
3
			SupportedAssets::<T>::insert(&location, (enabled, relative_price));
215

            
216
3
			Self::deposit_event(Event::SupportedAssetEdited {
217
3
				location,
218
3
				relative_price,
219
3
			});
220

            
221
3
			Ok(())
222
		}
223

            
224
		#[pallet::call_index(2)]
225
		#[pallet::weight(<T as pallet::Config>::WeightInfo::pause_asset_support())]
226
7
		pub fn pause_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
227
7
			T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
228

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

            
240
		#[pallet::call_index(3)]
241
		#[pallet::weight(<T as pallet::Config>::WeightInfo::resume_asset_support())]
242
5
		pub fn resume_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
243
5
			T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
244

            
245
4
			match SupportedAssets::<T>::get(&location) {
246
2
				Some((false, relative_price)) => {
247
2
					SupportedAssets::<T>::insert(&location, (true, relative_price));
248
2
					Self::deposit_event(Event::ResumeAssetSupport { location });
249
2
					Ok(())
250
				}
251
1
				Some((true, _)) => Err(Error::<T>::AssetNotPaused.into()),
252
1
				None => Err(Error::<T>::AssetNotFound.into()),
253
			}
254
		}
255

            
256
		#[pallet::call_index(4)]
257
		#[pallet::weight(<T as pallet::Config>::WeightInfo::remove_asset())]
258
4
		pub fn remove_asset(origin: OriginFor<T>, location: Location) -> DispatchResult {
259
4
			T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
260

            
261
3
			Self::do_remove_asset(location)
262
		}
263
	}
264

            
265
	impl<T: Config> Pallet<T> {
266
27
		pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
267
27
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
268
26
			ensure!(
269
26
				!SupportedAssets::<T>::contains_key(&location),
270
1
				Error::<T>::AssetAlreadyAdded
271
			);
272
25
			ensure!(
273
25
				T::AssetLocationFilter::contains(&location),
274
1
				Error::<T>::XcmLocationFiltered
275
			);
276

            
277
24
			SupportedAssets::<T>::insert(&location, (true, relative_price));
278

            
279
24
			Self::deposit_event(Event::SupportedAssetAdded {
280
24
				location,
281
24
				relative_price,
282
24
			});
283

            
284
24
			Ok(())
285
27
		}
286

            
287
3
		pub fn do_remove_asset(location: Location) -> DispatchResult {
288
3
			ensure!(
289
3
				SupportedAssets::<T>::contains_key(&location),
290
2
				Error::<T>::AssetNotFound
291
			);
292

            
293
1
			SupportedAssets::<T>::remove(&location);
294

            
295
1
			Self::deposit_event(Event::SupportedAssetRemoved { location });
296

            
297
1
			Ok(())
298
3
		}
299

            
300
173
		pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
301
173
			if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
302
165
				Some(ratio)
303
			} else {
304
8
				None
305
			}
306
173
		}
307
5
		pub fn query_acceptable_payment_assets(
308
5
			xcm_version: xcm::Version,
309
5
		) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
310
5
			let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
311
5
				T::NativeLocation::get(),
312
5
			))]
313
5
			.into_iter()
314
5
			.chain(
315
5
				SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
316
2
					enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
317
2
				}),
318
			)
319
5
			.collect::<Vec<_>>();
320

            
321
5
			match xcm_version {
322
1
				xcm::v3::VERSION => v5_assets
323
1
					.into_iter()
324
1
					.map(|v5_asset| v5_asset.into_version(xcm::v3::VERSION))
325
1
					.collect::<Result<_, _>>()
326
1
					.map_err(|_| XcmPaymentApiError::VersionedConversionFailed),
327
				xcm::v4::VERSION => v5_assets
328
					.into_iter()
329
					.map(|v5_asset| v5_asset.into_version(xcm::v4::VERSION))
330
					.collect::<Result<_, _>>()
331
					.map_err(|_| XcmPaymentApiError::VersionedConversionFailed),
332
3
				xcm::v5::VERSION => Ok(v5_assets),
333
1
				_ => Err(XcmPaymentApiError::UnhandledXcmVersion),
334
			}
335
5
		}
336
5
		pub fn query_weight_to_asset_fee(
337
5
			weight: Weight,
338
5
			asset: VersionedAssetId,
339
5
		) -> Result<u128, XcmPaymentApiError> {
340
5
			if let VersionedAssetId::V5(XcmAssetId(asset_location)) = asset
341
5
				.into_version(xcm::latest::VERSION)
342
5
				.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
343
			{
344
5
				Trader::<T>::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
345
				{
346
2
					XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
347
					_ => XcmPaymentApiError::WeightNotComputable,
348
2
				})
349
			} else {
350
				Err(XcmPaymentApiError::UnhandledXcmVersion)
351
			}
352
5
		}
353
21
		pub fn set_asset_price(asset_location: Location, relative_price: u128) {
354
21
			SupportedAssets::<T>::insert(&asset_location, (true, relative_price));
355
21
		}
356
	}
357
}
358

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

            
361
impl<T: crate::Config> Trader<T> {
362
174
	pub(crate) fn compute_amount_to_charge(
363
174
		weight: &Weight,
364
174
		asset_location: &Location,
365
174
	) -> Result<u128, XcmError> {
366
174
		if *asset_location == <T as crate::Config>::NativeLocation::get() {
367
41
			<T as crate::Config>::WeightToFee::weight_to_fee(&weight)
368
41
				.try_into()
369
41
				.map_err(|_| XcmError::Overflow)
370
133
		} else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
371
130
			if relative_price == 0u128 {
372
88
				Ok(0u128)
373
			} else {
374
42
				let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
375
42
					.try_into()
376
42
					.map_err(|_| XcmError::Overflow)?;
377
42
				Ok(native_amount
378
42
					.checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
379
42
					.ok_or(XcmError::Overflow)?
380
42
					.checked_div(relative_price)
381
42
					.ok_or(XcmError::Overflow)?)
382
			}
383
		} else {
384
3
			Err(XcmError::AssetNotFound)
385
		}
386
174
	}
387
}
388

            
389
impl<T: crate::Config> WeightTrader for Trader<T> {
390
266
	fn new() -> Self {
391
266
		Self(Weight::zero(), None, PhantomData)
392
266
	}
393
154
	fn buy_weight(
394
154
		&mut self,
395
154
		weight: Weight,
396
154
		payment: xcm_executor::AssetsInHolding,
397
154
		context: &XcmContext,
398
154
	) -> Result<xcm_executor::AssetsInHolding, XcmError> {
399
154
		log::trace!(
400
3
			target: "xcm::weight",
401
3
			"UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
402
			weight,
403
			payment,
404
			context
405
		);
406

            
407
		// Can only call one time
408
154
		if self.1.is_some() {
409
1
			return Err(XcmError::NotWithdrawable);
410
153
		}
411

            
412
		// Consistency check for tests only, we should never panic in release mode
413
153
		debug_assert_eq!(self.0, Weight::zero());
414

            
415
		// We support only one fee asset per buy, so we take the first one.
416
153
		let first_asset = payment
417
153
			.clone()
418
153
			.fungible_assets_iter()
419
153
			.next()
420
153
			.ok_or(XcmError::AssetNotFound)?;
421

            
422
152
		match (first_asset.id, first_asset.fun) {
423
152
			(XcmAssetId(location), Fungibility::Fungible(_)) => {
424
152
				let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
425

            
426
				// We don't need to proceed if the amount is 0
427
				// For cases (specially tests) where the asset is very cheap with respect
428
				// to the weight needed
429
151
				if amount.is_zero() {
430
109
					return Ok(payment);
431
42
				}
432

            
433
42
				let required = Asset {
434
42
					fun: Fungibility::Fungible(amount),
435
42
					id: XcmAssetId(location),
436
42
				};
437
42
				let unused = payment
438
42
					.checked_sub(required.clone())
439
42
					.map_err(|_| XcmError::TooExpensive)?;
440

            
441
37
				self.0 = weight;
442
37
				self.1 = Some(required);
443

            
444
37
				Ok(unused)
445
			}
446
			_ => Err(XcmError::AssetNotFound),
447
		}
448
154
	}
449

            
450
16
	fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option<Asset> {
451
16
		log::trace!(
452
			target: "xcm-weight-trader",
453
			"refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
454
			actual_weight,
455
			context,
456
			self.0,
457
			self.1
458
		);
459
		if let Some(Asset {
460
12
			fun: Fungibility::Fungible(initial_amount),
461
12
			id: XcmAssetId(location),
462
16
		}) = self.1.take()
463
		{
464
12
			if actual_weight == self.0 {
465
1
				self.1 = Some(Asset {
466
1
					fun: Fungibility::Fungible(initial_amount),
467
1
					id: XcmAssetId(location),
468
1
				});
469
1
				None
470
			} else {
471
11
				let weight = actual_weight.min(self.0);
472
11
				let amount: u128 =
473
11
					Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
474
11
				let final_amount = amount.min(initial_amount);
475
11
				let amount_to_refund = initial_amount.saturating_sub(final_amount);
476
11
				self.0 -= weight;
477
11
				self.1 = Some(Asset {
478
11
					fun: Fungibility::Fungible(final_amount),
479
11
					id: XcmAssetId(location.clone()),
480
11
				});
481
11
				log::trace!(
482
					target: "xcm-weight-trader",
483
					"refund_weight amount to refund: {:?}",
484
					amount_to_refund
485
				);
486
11
				Some(Asset {
487
11
					fun: Fungibility::Fungible(amount_to_refund),
488
11
					id: XcmAssetId(location),
489
11
				})
490
			}
491
		} else {
492
4
			None
493
		}
494
16
	}
495
}
496

            
497
impl<T: crate::Config> Drop for Trader<T> {
498
266
	fn drop(&mut self) {
499
266
		log::trace!(
500
5
			target: "xcm-weight-trader",
501
5
			"Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
502
5
			&self.0,
503
5
			&self.1
504
		);
505
266
		if let Some(asset) = self.1.take() {
506
37
			let res = T::AssetTransactor::deposit_asset(
507
37
				&asset,
508
37
				&T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
509
37
				None,
510
			);
511
37
			debug_assert!(res.is_ok());
512
229
		}
513
266
	}
514
}
515

            
516
/// Helper function to compute fee amount from weight and asset location.
517
/// This is used by the XcmTransactorFeeTrader adapter implementation.
518
pub fn compute_fee_amount<T: Config>(
519
	weight: Weight,
520
	asset_location: &Location,
521
) -> Result<u128, xcm::v5::Error> {
522
	Trader::<T>::compute_amount_to_charge(&weight, asset_location)
523
}
524

            
525
/// Implementation of XcmFeeTrader for pallet-xcm-weight-trader.
526
/// This allows the pallet to be used directly as a fee trader.
527
impl<T: Config> XcmFeeTrader for Pallet<T> {
528
6
	fn compute_fee(
529
6
		weight: frame_support::weights::Weight,
530
6
		asset_location: &xcm::latest::Location,
531
6
		explicit_amount: Option<u128>,
532
6
	) -> Result<u128, DispatchError> {
533
		use xcm::v5::Error as XcmError;
534

            
535
		// If explicit amount is provided, use it directly
536
6
		if let Some(amount) = explicit_amount {
537
			return Ok(amount);
538
6
		}
539

            
540
		// Convert xcm::latest::Location to xcm::v5::Location for internal computation
541
6
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone())
542
6
			.map_err(|_| DispatchError::Other("Failed to convert location"))?;
543

            
544
		// Use the weight-trader's compute logic
545
6
		let amount = Trader::<T>::compute_amount_to_charge(&weight, &asset_location_v5).map_err(
546
			|e| match e {
547
				XcmError::AssetNotFound => DispatchError::Other("Asset not found"),
548
				XcmError::Overflow => DispatchError::Other("Overflow"),
549
				_ => DispatchError::Other("Unable to compute fee"),
550
			},
551
		)?;
552

            
553
		// Note: Reserve validation is done at the pallet-xcm-transactor level,
554
		// as it requires access to the ReserveProvider which is configured there.
555
		// This implementation just computes the fee amount.
556

            
557
6
		Ok(amount)
558
6
	}
559

            
560
	fn get_asset_price(asset_location: &xcm::latest::Location) -> Option<u128> {
561
		// Convert xcm::latest::Location to xcm::v5::Location for storage lookup
562
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone()).ok()?;
563

            
564
		// Return the relative_price if the asset is enabled
565
		if let Some((true, relative_price)) = SupportedAssets::<T>::get(&asset_location_v5) {
566
			Some(relative_price)
567
		} else {
568
			None
569
		}
570
	}
571

            
572
	fn set_asset_price(
573
		asset_location: xcm::latest::Location,
574
		value: u128,
575
	) -> Result<(), DispatchError> {
576
		// Convert latest location into v5 for internal storage
577
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
578
			.map_err(|_| DispatchError::Other("Invalid location"))?;
579

            
580
		Pallet::<T>::set_asset_price(asset_location_v5, value);
581
		Ok(())
582
	}
583

            
584
	fn remove_asset(asset_location: xcm::latest::Location) -> Result<(), DispatchError> {
585
		// Convert latest location into v5 for internal storage
586
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
587
			.map_err(|_| DispatchError::Other("Invalid location"))?;
588

            
589
		Pallet::<T>::do_remove_asset(asset_location_v5)
590
	}
591
}