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
#[pallet]
55
pub mod pallet {
56
	use super::*;
57

            
58
	/// Pallet for multi block migrations
59
	#[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<RuntimeEvent: From<Event<Self>>> {
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
		/// Origin that is allowed to unpause a supported asset
94
		type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
95

            
96
		/// Weight information for extrinsics in this pallet.
97
		type WeightInfo: WeightInfo;
98

            
99
		/// Convert a weight value into deductible native balance.
100
		type WeightToFee: WeightToFee<Balance = Self::Balance>;
101

            
102
		/// Account that will receive xcm fees
103
		type XcmFeesAccount: Get<Self::AccountId>;
104

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

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

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

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

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

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

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

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

            
192
27
			Self::do_add_asset(location, relative_price)
193
		}
194

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

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

            
206
4
			let enabled = SupportedAssets::<T>::get(&location)
207
4
				.ok_or(Error::<T>::AssetNotFound)?
208
				.0;
209

            
210
3
			SupportedAssets::<T>::insert(&location, (enabled, relative_price));
211

            
212
3
			Self::deposit_event(Event::SupportedAssetEdited {
213
3
				location,
214
3
				relative_price,
215
3
			});
216

            
217
3
			Ok(())
218
		}
219

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

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

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

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

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

            
257
3
			ensure!(
258
3
				SupportedAssets::<T>::contains_key(&location),
259
2
				Error::<T>::AssetNotFound
260
			);
261

            
262
1
			SupportedAssets::<T>::remove(&location);
263

            
264
1
			Self::deposit_event(Event::SupportedAssetRemoved { location });
265

            
266
1
			Ok(())
267
		}
268
	}
269

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

            
282
24
			SupportedAssets::<T>::insert(&location, (true, relative_price));
283

            
284
24
			Self::deposit_event(Event::SupportedAssetAdded {
285
24
				location,
286
24
				relative_price,
287
24
			});
288

            
289
24
			Ok(())
290
27
		}
291

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

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

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

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

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

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

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

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

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

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

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

            
434
37
				self.0 = weight;
435
37
				self.1 = Some(required);
436

            
437
37
				Ok(unused)
438
			}
439
			_ => Err(XcmError::AssetNotFound),
440
		}
441
154
	}
442

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

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