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::traits::{Convert, Zero};
40
use sp_std::{vec, vec::Vec};
41
use xcm::v5::{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
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47
pub struct XcmWeightTraderAssetInfo {
48
	pub location: Location,
49
	pub relative_price: u128,
50
}
51

            
52
pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
53

            
54
520
#[pallet]
55
pub mod pallet {
56
	use super::*;
57

            
58
	/// Pallet for multi block migrations
59
130
	#[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 {
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
		/// The overarching event type.
94
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
95

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

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

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

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

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

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

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

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

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

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

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

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

            
195
31
			Self::do_add_asset(location, relative_price)
196
		}
197

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

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

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

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

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

            
220
3
			Ok(())
221
		}
222

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

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

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

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

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

            
260
3
			ensure!(
261
3
				SupportedAssets::<T>::contains_key(&location),
262
2
				Error::<T>::AssetNotFound
263
			);
264

            
265
1
			SupportedAssets::<T>::remove(&location);
266
1

            
267
1
			Self::deposit_event(Event::SupportedAssetRemoved { location });
268
1

            
269
1
			Ok(())
270
		}
271
	}
272

            
273
	impl<T: Config> Pallet<T> {
274
31
		pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
275
31
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
276
30
			ensure!(
277
30
				!SupportedAssets::<T>::contains_key(&location),
278
1
				Error::<T>::AssetAlreadyAdded
279
			);
280
29
			ensure!(
281
29
				T::AssetLocationFilter::contains(&location),
282
1
				Error::<T>::XcmLocationFiltered
283
			);
284

            
285
28
			SupportedAssets::<T>::insert(&location, (true, relative_price));
286
28

            
287
28
			Self::deposit_event(Event::SupportedAssetAdded {
288
28
				location,
289
28
				relative_price,
290
28
			});
291
28

            
292
28
			Ok(())
293
31
		}
294

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

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

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

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

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

            
403
		// Can only call one time
404
154
		if self.1.is_some() {
405
1
			return Err(XcmError::NotWithdrawable);
406
153
		}
407
153

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

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

            
418
152
		match (first_asset.id, first_asset.fun) {
419
152
			(XcmAssetId(location), Fungibility::Fungible(_)) => {
420
152
				let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
421

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

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

            
437
37
				self.0 = weight;
438
37
				self.1 = Some(required);
439
37

            
440
37
				Ok(unused)
441
			}
442
			_ => Err(XcmError::AssetNotFound),
443
		}
444
154
	}
445

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

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