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
6
		fn default() -> Self {
168
6
			Self {
169
6
				assets: vec![],
170
6
				_phantom: Default::default(),
171
6
			}
172
6
		}
173
	}
174

            
175
	#[pallet::genesis_build]
176
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
177
16
		fn build(&self) {
178
16
			for asset in self.assets.clone() {
179
10
				Pallet::<T>::do_add_asset(asset.location, asset.relative_price)
180
10
					.expect("couldn't add asset");
181
10
			}
182
16
		}
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
37
		pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
267
37
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
268
36
			ensure!(
269
36
				!SupportedAssets::<T>::contains_key(&location),
270
1
				Error::<T>::AssetAlreadyAdded
271
			);
272
35
			ensure!(
273
35
				T::AssetLocationFilter::contains(&location),
274
1
				Error::<T>::XcmLocationFiltered
275
			);
276

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

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

            
284
34
			Ok(())
285
37
		}
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
182
		pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
301
182
			if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
302
174
				Some(ratio)
303
			} else {
304
8
				None
305
			}
306
182
		}
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
180
	pub(crate) fn compute_amount_to_charge(
363
180
		weight: &Weight,
364
180
		asset_location: &Location,
365
180
	) -> Result<u128, XcmError> {
366
180
		if *asset_location == <T as crate::Config>::NativeLocation::get() {
367
42
			<T as crate::Config>::WeightToFee::weight_to_fee(&weight)
368
42
				.try_into()
369
42
				.map_err(|_| XcmError::Overflow)
370
138
		} else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
371
135
			if relative_price == 0u128 {
372
88
				Ok(0u128)
373
			} else {
374
47
				let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
375
47
					.try_into()
376
47
					.map_err(|_| XcmError::Overflow)?;
377
47
				Ok(native_amount
378
47
					.checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
379
47
					.ok_or(XcmError::Overflow)?
380
47
					.checked_div(relative_price)
381
47
					.ok_or(XcmError::Overflow)?)
382
			}
383
		} else {
384
3
			Err(XcmError::AssetNotFound)
385
		}
386
180
	}
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
151
	fn buy_weight(
394
151
		&mut self,
395
151
		weight: Weight,
396
151
		payment: xcm_executor::AssetsInHolding,
397
151
		context: &XcmContext,
398
151
	) -> Result<xcm_executor::AssetsInHolding, XcmError> {
399
151
		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
151
		if self.1.is_some() {
409
1
			return Err(XcmError::NotWithdrawable);
410
150
		}
411

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

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

            
422
149
		match (first_asset.id, first_asset.fun) {
423
149
			(XcmAssetId(location), Fungibility::Fungible(_)) => {
424
149
				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
148
				if amount.is_zero() {
430
109
					return Ok(payment);
431
39
				}
432

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

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

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

            
450
16
	fn refund_weight(&mut self, weight_to_refund: Weight, context: &XcmContext) -> Option<Asset> {
451
16
		log::trace!(
452
			target: "xcm-weight-trader",
453
			"refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
454
			weight_to_refund,
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
			let weight_to_refund = weight_to_refund.min(self.0);
465
			// `xcm-executor` passes the *surplus* weight to refund here (not the weight used).
466
			// We therefore refund the proportional amount that was originally charged for
467
			// `weight_to_refund`, and keep the remainder to be deposited to the fees account.
468
12
			let computed_refund_amount: u128 =
469
12
				Self::compute_amount_to_charge(&weight_to_refund, &location).unwrap_or(u128::MAX);
470
12
			let refund_amount = computed_refund_amount.min(initial_amount);
471
12
			let final_amount = initial_amount.saturating_sub(refund_amount);
472
12
			self.0 -= weight_to_refund;
473
12
			self.1 = Some(Asset {
474
12
				fun: Fungibility::Fungible(final_amount),
475
12
				id: XcmAssetId(location.clone()),
476
12
			});
477
12
			log::trace!(
478
				target: "xcm-weight-trader",
479
				"refund_weight amount to refund: {:?}",
480
				refund_amount
481
			);
482
12
			if refund_amount > 0 {
483
11
				Some(Asset {
484
11
					fun: Fungibility::Fungible(refund_amount),
485
11
					id: XcmAssetId(location),
486
11
				})
487
			} else {
488
1
				None
489
			}
490
		} else {
491
4
			None
492
		}
493
16
	}
494
}
495

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

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

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

            
533
		// Convert xcm::latest::Location to xcm::v5::Location for internal computation
534
10
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone())
535
10
			.map_err(|_| DispatchError::Other("Failed to convert location"))?;
536

            
537
		// Use the weight-trader's compute logic
538
10
		let amount = Trader::<T>::compute_amount_to_charge(&weight, &asset_location_v5).map_err(
539
			|e| match e {
540
				XcmError::AssetNotFound => DispatchError::Other("Asset not found"),
541
				XcmError::Overflow => DispatchError::Other("Overflow"),
542
				_ => DispatchError::Other("Unable to compute fee"),
543
			},
544
		)?;
545

            
546
		// Note: Reserve validation is done at the pallet-xcm-transactor level,
547
		// as it requires access to the ReserveProvider which is configured there.
548
		// This implementation just computes the fee amount.
549

            
550
10
		Ok(amount)
551
10
	}
552

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

            
557
		// Return the relative_price if the asset is enabled
558
		if let Some((true, relative_price)) = SupportedAssets::<T>::get(&asset_location_v5) {
559
			Some(relative_price)
560
		} else {
561
			None
562
		}
563
	}
564

            
565
	fn set_asset_price(
566
		asset_location: xcm::latest::Location,
567
		value: u128,
568
	) -> Result<(), DispatchError> {
569
		// Convert latest location into v5 for internal storage
570
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
571
			.map_err(|_| DispatchError::Other("Invalid location"))?;
572

            
573
		Pallet::<T>::set_asset_price(asset_location_v5, value);
574
		Ok(())
575
	}
576

            
577
	fn remove_asset(asset_location: xcm::latest::Location) -> Result<(), DispatchError> {
578
		// Convert latest location into v5 for internal storage
579
		let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
580
			.map_err(|_| DispatchError::Other("Invalid location"))?;
581

            
582
		Pallet::<T>::do_remove_asset(asset_location_v5)
583
	}
584
}