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, weight_to_refund: Weight, context: &XcmContext) -> Option<Asset> {
451 log::trace!(
452 target: "xcm-weight-trader",
453 "refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
454 weight_to_refund,
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 let weight_to_refund = weight_to_refund.min(self.0);
465 let computed_refund_amount: u128 =
469 Self::compute_amount_to_charge(&weight_to_refund, &location).unwrap_or(u128::MAX);
470 let refund_amount = computed_refund_amount.min(initial_amount);
471 let final_amount = initial_amount.saturating_sub(refund_amount);
472 self.0 -= weight_to_refund;
473 self.1 = Some(Asset {
474 fun: Fungibility::Fungible(final_amount),
475 id: XcmAssetId(location.clone()),
476 });
477 log::trace!(
478 target: "xcm-weight-trader",
479 "refund_weight amount to refund: {:?}",
480 refund_amount
481 );
482 if refund_amount > 0 {
483 Some(Asset {
484 fun: Fungibility::Fungible(refund_amount),
485 id: XcmAssetId(location),
486 })
487 } else {
488 None
489 }
490 } else {
491 None
492 }
493 }
494}
495
496impl<T: crate::Config> Drop for Trader<T> {
497 fn drop(&mut self) {
498 log::trace!(
499 target: "xcm-weight-trader",
500 "Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
501 &self.0,
502 &self.1
503 );
504 if let Some(asset) = self.1.take() {
505 let res = T::AssetTransactor::deposit_asset(
506 &asset,
507 &T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
508 None,
509 );
510 debug_assert!(res.is_ok());
511 }
512 }
513}
514
515pub fn compute_fee_amount<T: Config>(
518 weight: Weight,
519 asset_location: &Location,
520) -> Result<u128, xcm::v5::Error> {
521 Trader::<T>::compute_amount_to_charge(&weight, asset_location)
522}
523
524impl<T: Config> XcmFeeTrader for Pallet<T> {
527 fn compute_fee(
528 weight: frame_support::weights::Weight,
529 asset_location: &xcm::latest::Location,
530 ) -> Result<u128, DispatchError> {
531 use xcm::v5::Error as XcmError;
532
533 let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone())
535 .map_err(|_| DispatchError::Other("Failed to convert location"))?;
536
537 let amount = Trader::<T>::compute_amount_to_charge(&weight, &asset_location_v5).map_err(
539 |e| match e {
540 XcmError::AssetNotFound => DispatchError::Other("Asset not found"),
541 XcmError::Overflow => DispatchError::Other("Overflow"),
542 _ => DispatchError::Other("Unable to compute fee"),
543 },
544 )?;
545
546 Ok(amount)
551 }
552
553 fn get_asset_price(asset_location: &xcm::latest::Location) -> Option<u128> {
554 let asset_location_v5 = xcm::v5::Location::try_from(asset_location.clone()).ok()?;
556
557 if let Some((true, relative_price)) = SupportedAssets::<T>::get(&asset_location_v5) {
559 Some(relative_price)
560 } else {
561 None
562 }
563 }
564
565 fn set_asset_price(
566 asset_location: xcm::latest::Location,
567 value: u128,
568 ) -> Result<(), DispatchError> {
569 let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
571 .map_err(|_| DispatchError::Other("Invalid location"))?;
572
573 Pallet::<T>::set_asset_price(asset_location_v5, value);
574 Ok(())
575 }
576
577 fn remove_asset(asset_location: xcm::latest::Location) -> Result<(), DispatchError> {
578 let asset_location_v5 = xcm::v5::Location::try_from(asset_location)
580 .map_err(|_| DispatchError::Other("Invalid location"))?;
581
582 Pallet::<T>::do_remove_asset(asset_location_v5)
583 }
584}