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::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
pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
47

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

            
52
	/// Pallet for multi block migrations
53
73
	#[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
328
	#[pallet::storage]
111
	#[pallet::getter(fn supported_assets)]
112
	pub type SupportedAssets<T: Config> = StorageMap<_, Blake2_128Concat, Location, (bool, u128)>;
113

            
114
44
	#[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
20
	#[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
			let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
269
5
				T::NativeLocation::get(),
270
5
			))]
271
5
			.into_iter()
272
5
			.chain(
273
5
				SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
274
2
					enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
275
5
				}),
276
5
			)
277
5
			.collect::<Vec<_>>();
278
5

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

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

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

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

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

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

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

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

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

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

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

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

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

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