1#![allow(non_camel_case_types)]
20#![cfg_attr(not(feature = "std"), no_std)]
21
22#[cfg(feature = "runtime-benchmarks")]
23mod benchmarking;
24#[cfg(test)]
25mod mock;
26#[cfg(test)]
27mod tests;
28
29pub mod weights;
30
31pub use pallet::*;
32pub use weights::WeightInfo;
33
34use frame_support::pallet_prelude::*;
35use frame_support::traits::Contains;
36use frame_support::weights::WeightToFee;
37use frame_support::{pallet, Deserialize, Serialize};
38use frame_system::pallet_prelude::*;
39use sp_runtime::{
40 traits::{Convert, Zero},
41 DispatchError,
42};
43use sp_std::{vec, vec::Vec};
44use xcm::v5::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
45use xcm::{IntoVersion, VersionedAssetId};
46use xcm_executor::traits::{TransactAsset, WeightTrader};
47use xcm_primitives::XcmFeeTrader;
48use xcm_runtime_apis::fees::Error as XcmPaymentApiError;
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct XcmWeightTraderAssetInfo {
52 pub location: Location,
53 pub relative_price: u128,
54}
55
56pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
57
58#[pallet]
59pub mod pallet {
60 use super::*;
61
62 #[pallet::pallet]
64 pub struct Pallet<T>(PhantomData<T>);
65
66 #[pallet::config]
68 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
69 type AccountIdToLocation: Convert<Self::AccountId, Location>;
71
72 type AddSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
74
75 type AssetLocationFilter: Contains<Location>;
78
79 type AssetTransactor: TransactAsset;
81
82 type Balance: TryInto<u128>;
84
85 type EditSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
87
88 type NativeLocation: Get<Location>;
90
91 type PauseSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
93
94 type RemoveSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
96
97 type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
99
100 type WeightInfo: WeightInfo;
102
103 type WeightToFee: WeightToFee<Balance = Self::Balance>;
105
106 type XcmFeesAccount: Get<Self::AccountId>;
108
109 #[cfg(feature = "runtime-benchmarks")]
111 type NotFilteredLocation: Get<Location>;
112 }
113
114 #[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 AssetAlreadyAdded,
125 AssetAlreadyPaused,
127 AssetNotFound,
129 AssetNotPaused,
131 XcmLocationFiltered,
133 PriceCannotBeZero,
135 PriceOverflow,
137 }
138
139 #[pallet::event]
140 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
141 pub enum Event<T: Config> {
142 SupportedAssetAdded {
144 location: Location,
145 relative_price: u128,
146 },
147 SupportedAssetEdited {
149 location: Location,
150 relative_price: u128,
151 },
152 PauseAssetSupport { location: Location },
154 ResumeAssetSupport { location: Location },
156 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 fn default() -> Self {
168 Self {
169 assets: vec![],
170 _phantom: Default::default(),
171 }
172 }
173 }
174
175 #[pallet::genesis_build]
176 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
177 fn build(&self) {
178 for asset in self.assets.clone() {
179 Pallet::<T>::do_add_asset(asset.location, asset.relative_price)
180 .expect("couldn't add asset");
181 }
182 }
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 ) -> DispatchResult {
194 T::AddSupportedAssetOrigin::ensure_origin(origin)?;
195
196 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 ) -> DispatchResult {
206 T::EditSupportedAssetOrigin::ensure_origin(origin)?;
207
208 ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
209
210 let enabled = SupportedAssets::<T>::get(&location)
211 .ok_or(Error::<T>::AssetNotFound)?
212 .0;
213
214 SupportedAssets::<T>::insert(&location, (enabled, relative_price));
215
216 Self::deposit_event(Event::SupportedAssetEdited {
217 location,
218 relative_price,
219 });
220
221 Ok(())
222 }
223
224 #[pallet::call_index(2)]
225 #[pallet::weight(<T as pallet::Config>::WeightInfo::pause_asset_support())]
226 pub fn pause_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
227 T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
228
229 match SupportedAssets::<T>::get(&location) {
230 Some((true, relative_price)) => {
231 SupportedAssets::<T>::insert(&location, (false, relative_price));
232 Self::deposit_event(Event::PauseAssetSupport { location });
233 Ok(())
234 }
235 Some((false, _)) => Err(Error::<T>::AssetAlreadyPaused.into()),
236 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 pub fn resume_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
243 T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
244
245 match SupportedAssets::<T>::get(&location) {
246 Some((false, relative_price)) => {
247 SupportedAssets::<T>::insert(&location, (true, relative_price));
248 Self::deposit_event(Event::ResumeAssetSupport { location });
249 Ok(())
250 }
251 Some((true, _)) => Err(Error::<T>::AssetNotPaused.into()),
252 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 pub fn remove_asset(origin: OriginFor<T>, location: Location) -> DispatchResult {
259 T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
260
261 Self::do_remove_asset(location)
262 }
263 }
264
265 impl<T: Config> Pallet<T> {
266 pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
267 ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
268 ensure!(
269 !SupportedAssets::<T>::contains_key(&location),
270 Error::<T>::AssetAlreadyAdded
271 );
272 ensure!(
273 T::AssetLocationFilter::contains(&location),
274 Error::<T>::XcmLocationFiltered
275 );
276
277 SupportedAssets::<T>::insert(&location, (true, relative_price));
278
279 Self::deposit_event(Event::SupportedAssetAdded {
280 location,
281 relative_price,
282 });
283
284 Ok(())
285 }
286
287 pub fn do_remove_asset(location: Location) -> DispatchResult {
288 ensure!(
289 SupportedAssets::<T>::contains_key(&location),
290 Error::<T>::AssetNotFound
291 );
292
293 SupportedAssets::<T>::remove(&location);
294
295 Self::deposit_event(Event::SupportedAssetRemoved { location });
296
297 Ok(())
298 }
299
300 pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
301 if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
302 Some(ratio)
303 } else {
304 None
305 }
306 }
307 pub fn query_acceptable_payment_assets(
308 xcm_version: xcm::Version,
309 ) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
310 let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
311 T::NativeLocation::get(),
312 ))]
313 .into_iter()
314 .chain(
315 SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
316 enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
317 }),
318 )
319 .collect::<Vec<_>>();
320
321 match xcm_version {
322 xcm::v3::VERSION => v5_assets
323 .into_iter()
324 .map(|v5_asset| v5_asset.into_version(xcm::v3::VERSION))
325 .collect::<Result<_, _>>()
326 .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 xcm::v5::VERSION => Ok(v5_assets),
333 _ => Err(XcmPaymentApiError::UnhandledXcmVersion),
334 }
335 }
336 pub fn query_weight_to_asset_fee(
337 weight: Weight,
338 asset: VersionedAssetId,
339 ) -> Result<u128, XcmPaymentApiError> {
340 if let VersionedAssetId::V5(XcmAssetId(asset_location)) = asset
341 .into_version(xcm::latest::VERSION)
342 .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
343 {
344 Trader::<T>::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
345 {
346 XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
347 _ => XcmPaymentApiError::WeightNotComputable,
348 })
349 } else {
350 Err(XcmPaymentApiError::UnhandledXcmVersion)
351 }
352 }
353 pub fn set_asset_price(asset_location: Location, relative_price: u128) {
354 SupportedAssets::<T>::insert(&asset_location, (true, relative_price));
355 }
356 }
357}
358
359pub struct Trader<T: crate::Config>(Weight, Option<Asset>, core::marker::PhantomData<T>);
360
361impl<T: crate::Config> Trader<T> {
362 pub(crate) fn compute_amount_to_charge(
363 weight: &Weight,
364 asset_location: &Location,
365 ) -> Result<u128, XcmError> {
366 if *asset_location == <T as crate::Config>::NativeLocation::get() {
367 <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
368 .try_into()
369 .map_err(|_| XcmError::Overflow)
370 } else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
371 if relative_price == 0u128 {
372 Ok(0u128)
373 } else {
374 let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
375 .try_into()
376 .map_err(|_| XcmError::Overflow)?;
377 Ok(native_amount
378 .checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
379 .ok_or(XcmError::Overflow)?
380 .checked_div(relative_price)
381 .ok_or(XcmError::Overflow)?)
382 }
383 } else {
384 Err(XcmError::AssetNotFound)
385 }
386 }
387}
388
389impl<T: crate::Config> WeightTrader for Trader<T> {
390 fn new() -> Self {
391 Self(Weight::zero(), None, PhantomData)
392 }
393 fn buy_weight(
394 &mut self,
395 weight: Weight,
396 payment: xcm_executor::AssetsInHolding,
397 context: &XcmContext,
398 ) -> Result<xcm_executor::AssetsInHolding, XcmError> {
399 log::trace!(
400 target: "xcm::weight",
401 "UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
402 weight,
403 payment,
404 context
405 );
406
407 if self.1.is_some() {
409 return Err(XcmError::NotWithdrawable);
410 }
411
412 debug_assert_eq!(self.0, Weight::zero());
414
415 let first_asset = payment
417 .clone()
418 .fungible_assets_iter()
419 .next()
420 .ok_or(XcmError::AssetNotFound)?;
421
422 match (first_asset.id, first_asset.fun) {
423 (XcmAssetId(location), Fungibility::Fungible(_)) => {
424 let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
425
426 if amount.is_zero() {
430 return Ok(payment);
431 }
432
433 let required = Asset {
434 fun: Fungibility::Fungible(amount),
435 id: XcmAssetId(location),
436 };
437 let unused = payment
438 .checked_sub(required.clone())
439 .map_err(|_| XcmError::TooExpensive)?;
440
441 self.0 = weight;
442 self.1 = Some(required);
443
444 Ok(unused)
445 }
446 _ => Err(XcmError::AssetNotFound),
447 }
448 }
449
450 fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option<Asset> {
451 log::trace!(
452 target: "xcm-weight-trader",
453 "refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
454 actual_weight,
455 context,
456 self.0,
457 self.1
458 );
459 if let Some(Asset {
460 fun: Fungibility::Fungible(initial_amount),
461 id: XcmAssetId(location),
462 }) = self.1.take()
463 {
464 if actual_weight == self.0 {
465 self.1 = Some(Asset {
466 fun: Fungibility::Fungible(initial_amount),
467 id: XcmAssetId(location),
468 });
469 None
470 } else {
471 let weight = actual_weight.min(self.0);
472 let amount: u128 =
473 Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
474 let final_amount = amount.min(initial_amount);
475 let amount_to_refund = initial_amount.saturating_sub(final_amount);
476 self.0 -= weight;
477 self.1 = Some(Asset {
478 fun: Fungibility::Fungible(final_amount),
479 id: XcmAssetId(location.clone()),
480 });
481 log::trace!(
482 target: "xcm-weight-trader",
483 "refund_weight amount to refund: {:?}",
484 amount_to_refund
485 );
486 Some(Asset {
487 fun: Fungibility::Fungible(amount_to_refund),
488 id: XcmAssetId(location),
489 })
490 }
491 } else {
492 None
493 }
494 }
495}
496
497impl<T: crate::Config> Drop for Trader<T> {
498 fn drop(&mut self) {
499 log::trace!(
500 target: "xcm-weight-trader",
501 "Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
502 &self.0,
503 &self.1
504 );
505 if let Some(asset) = self.1.take() {
506 let res = T::AssetTransactor::deposit_asset(
507 &asset,
508 &T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
509 None,
510 );
511 debug_assert!(res.is_ok());
512 }
513 }
514}
515
516pub fn compute_fee_amount<T: Config>(
519 weight: Weight,
520 asset_location: &Location,
521) -> Result<u128, xcm::v5::Error> {
522 Trader::<T>::compute_amount_to_charge(&weight, asset_location)
523}
524
525impl<T: Config> XcmFeeTrader for Pallet<T> {
528 fn compute_fee(
529 weight: frame_support::weights::Weight,
530 asset_location: &xcm::latest::Location,
531 ) -> Result<u128, DispatchError> {
532 use xcm::v5::Error as XcmError;
533
534 let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone())
536 .map_err(|_| DispatchError::Other("Failed to convert location"))?;
537
538 let amount = Trader::<T>::compute_amount_to_charge(&weight, &asset_location_v5).map_err(
540 |e| match e {
541 XcmError::AssetNotFound => DispatchError::Other("Asset not found"),
542 XcmError::Overflow => DispatchError::Other("Overflow"),
543 _ => DispatchError::Other("Unable to compute fee"),
544 },
545 )?;
546
547 Ok(amount)
552 }
553
554 fn get_asset_price(asset_location: &xcm::latest::Location) -> Option<u128> {
555 let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone()).ok()?;
557
558 if let Some((true, relative_price)) = SupportedAssets::<T>::get(&asset_location_v5) {
560 Some(relative_price)
561 } else {
562 None
563 }
564 }
565
566 fn set_asset_price(
567 asset_location: xcm::latest::Location,
568 value: u128,
569 ) -> Result<(), DispatchError> {
570 let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
572 .map_err(|_| DispatchError::Other("Invalid location"))?;
573
574 Pallet::<T>::set_asset_price(asset_location_v5, value);
575 Ok(())
576 }
577
578 fn remove_asset(asset_location: xcm::latest::Location) -> Result<(), DispatchError> {
579 let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
581 .map_err(|_| DispatchError::Other("Invalid location"))?;
582
583 Pallet::<T>::do_remove_asset(asset_location_v5)
584 }
585}