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
298
#[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
363
	#[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
		/// The relative price calculation overflowed
129
		PriceOverflow,
130
	}
131

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

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

            
164
15
			ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
165
14
			ensure!(
166
14
				!SupportedAssets::<T>::contains_key(&location),
167
1
				Error::<T>::AssetAlreadyAdded
168
			);
169
13
			ensure!(
170
13
				T::AssetLocationFilter::contains(&location),
171
1
				Error::<T>::XcmLocationFiltered
172
			);
173

            
174
12
			SupportedAssets::<T>::insert(&location, (true, relative_price));
175
12

            
176
12
			Self::deposit_event(Event::SupportedAssetAdded {
177
12
				location,
178
12
				relative_price,
179
12
			});
180
12

            
181
12
			Ok(())
182
		}
183

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

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

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

            
199
3
			SupportedAssets::<T>::insert(&location, (enabled, relative_price));
200
3

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

            
206
3
			Ok(())
207
		}
208

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

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

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

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

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

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

            
251
1
			SupportedAssets::<T>::remove(&location);
252
1

            
253
1
			Self::deposit_event(Event::SupportedAssetRemoved { location });
254
1

            
255
1
			Ok(())
256
		}
257
	}
258

            
259
	impl<T: Config> Pallet<T> {
260
170
		pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
261
170
			if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
262
162
				Some(ratio)
263
			} else {
264
8
				None
265
			}
266
170
		}
267
5
		pub fn query_acceptable_payment_assets(
268
5
			xcm_version: xcm::Version,
269
5
		) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
270
5
			let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
271
5
				T::NativeLocation::get(),
272
5
			))]
273
5
			.into_iter()
274
5
			.chain(
275
5
				SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
276
2
					enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
277
5
				}),
278
5
			)
279
5
			.collect::<Vec<_>>();
280
5

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

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

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

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

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

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

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

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

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

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

            
402
35
				self.0 = weight;
403
35
				self.1 = Some(required);
404
35

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

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

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