pallet_xcm_weight_trader/
lib.rs1#![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::traits::{Convert, Zero};
40use sp_std::{vec, vec::Vec};
41use xcm::v5::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
42use xcm::{IntoVersion, VersionedAssetId};
43use xcm_executor::traits::{TransactAsset, WeightTrader};
44use xcm_runtime_apis::fees::Error as XcmPaymentApiError;
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct XcmWeightTraderAssetInfo {
48 pub location: Location,
49 pub relative_price: u128,
50}
51
52pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
53
54#[pallet]
55pub mod pallet {
56 use super::*;
57
58 #[pallet::pallet]
60 pub struct Pallet<T>(PhantomData<T>);
61
62 #[pallet::config]
64 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
65 type AccountIdToLocation: Convert<Self::AccountId, Location>;
67
68 type AddSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
70
71 type AssetLocationFilter: Contains<Location>;
74
75 type AssetTransactor: TransactAsset;
77
78 type Balance: TryInto<u128>;
80
81 type EditSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
83
84 type NativeLocation: Get<Location>;
86
87 type PauseSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
89
90 type RemoveSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
92
93 type ResumeSupportedAssetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
95
96 type WeightInfo: WeightInfo;
98
99 type WeightToFee: WeightToFee<Balance = Self::Balance>;
101
102 type XcmFeesAccount: Get<Self::AccountId>;
104
105 #[cfg(feature = "runtime-benchmarks")]
107 type NotFilteredLocation: Get<Location>;
108 }
109
110 #[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 AssetAlreadyAdded,
121 AssetAlreadyPaused,
123 AssetNotFound,
125 AssetNotPaused,
127 XcmLocationFiltered,
129 PriceCannotBeZero,
131 PriceOverflow,
133 }
134
135 #[pallet::event]
136 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
137 pub enum Event<T: Config> {
138 SupportedAssetAdded {
140 location: Location,
141 relative_price: u128,
142 },
143 SupportedAssetEdited {
145 location: Location,
146 relative_price: u128,
147 },
148 PauseAssetSupport { location: Location },
150 ResumeAssetSupport { location: Location },
152 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 fn default() -> Self {
164 Self {
165 assets: vec![],
166 _phantom: Default::default(),
167 }
168 }
169 }
170
171 #[pallet::genesis_build]
172 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
173 fn build(&self) {
174 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 }
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 ) -> DispatchResult {
190 T::AddSupportedAssetOrigin::ensure_origin(origin)?;
191
192 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 ) -> DispatchResult {
202 T::EditSupportedAssetOrigin::ensure_origin(origin)?;
203
204 ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
205
206 let enabled = SupportedAssets::<T>::get(&location)
207 .ok_or(Error::<T>::AssetNotFound)?
208 .0;
209
210 SupportedAssets::<T>::insert(&location, (enabled, relative_price));
211
212 Self::deposit_event(Event::SupportedAssetEdited {
213 location,
214 relative_price,
215 });
216
217 Ok(())
218 }
219
220 #[pallet::call_index(2)]
221 #[pallet::weight(<T as pallet::Config>::WeightInfo::pause_asset_support())]
222 pub fn pause_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
223 T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
224
225 match SupportedAssets::<T>::get(&location) {
226 Some((true, relative_price)) => {
227 SupportedAssets::<T>::insert(&location, (false, relative_price));
228 Self::deposit_event(Event::PauseAssetSupport { location });
229 Ok(())
230 }
231 Some((false, _)) => Err(Error::<T>::AssetAlreadyPaused.into()),
232 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 pub fn resume_asset_support(origin: OriginFor<T>, location: Location) -> DispatchResult {
239 T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
240
241 match SupportedAssets::<T>::get(&location) {
242 Some((false, relative_price)) => {
243 SupportedAssets::<T>::insert(&location, (true, relative_price));
244 Self::deposit_event(Event::ResumeAssetSupport { location });
245 Ok(())
246 }
247 Some((true, _)) => Err(Error::<T>::AssetNotPaused.into()),
248 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 pub fn remove_asset(origin: OriginFor<T>, location: Location) -> DispatchResult {
255 T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
256
257 ensure!(
258 SupportedAssets::<T>::contains_key(&location),
259 Error::<T>::AssetNotFound
260 );
261
262 SupportedAssets::<T>::remove(&location);
263
264 Self::deposit_event(Event::SupportedAssetRemoved { location });
265
266 Ok(())
267 }
268 }
269
270 impl<T: Config> Pallet<T> {
271 pub fn do_add_asset(location: Location, relative_price: u128) -> DispatchResult {
272 ensure!(relative_price != 0, Error::<T>::PriceCannotBeZero);
273 ensure!(
274 !SupportedAssets::<T>::contains_key(&location),
275 Error::<T>::AssetAlreadyAdded
276 );
277 ensure!(
278 T::AssetLocationFilter::contains(&location),
279 Error::<T>::XcmLocationFiltered
280 );
281
282 SupportedAssets::<T>::insert(&location, (true, relative_price));
283
284 Self::deposit_event(Event::SupportedAssetAdded {
285 location,
286 relative_price,
287 });
288
289 Ok(())
290 }
291
292 pub fn get_asset_relative_price(location: &Location) -> Option<u128> {
293 if let Some((true, ratio)) = SupportedAssets::<T>::get(location) {
294 Some(ratio)
295 } else {
296 None
297 }
298 }
299 pub fn query_acceptable_payment_assets(
300 xcm_version: xcm::Version,
301 ) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
302 let v5_assets = [VersionedAssetId::from(XcmAssetId::from(
303 T::NativeLocation::get(),
304 ))]
305 .into_iter()
306 .chain(
307 SupportedAssets::<T>::iter().filter_map(|(asset_location, (enabled, _))| {
308 enabled.then(|| VersionedAssetId::from(XcmAssetId(asset_location)))
309 }),
310 )
311 .collect::<Vec<_>>();
312
313 match xcm_version {
314 xcm::v3::VERSION => v5_assets
315 .into_iter()
316 .map(|v5_asset| v5_asset.into_version(xcm::v3::VERSION))
317 .collect::<Result<_, _>>()
318 .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 xcm::v5::VERSION => Ok(v5_assets),
325 _ => Err(XcmPaymentApiError::UnhandledXcmVersion),
326 }
327 }
328 pub fn query_weight_to_asset_fee(
329 weight: Weight,
330 asset: VersionedAssetId,
331 ) -> Result<u128, XcmPaymentApiError> {
332 if let VersionedAssetId::V5(XcmAssetId(asset_location)) = asset
333 .into_version(xcm::latest::VERSION)
334 .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
335 {
336 Trader::<T>::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
337 {
338 XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
339 _ => XcmPaymentApiError::WeightNotComputable,
340 })
341 } else {
342 Err(XcmPaymentApiError::UnhandledXcmVersion)
343 }
344 }
345 #[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
346 pub fn set_asset_price(asset_location: Location, relative_price: u128) {
347 SupportedAssets::<T>::insert(&asset_location, (true, relative_price));
348 }
349 }
350}
351
352pub struct Trader<T: crate::Config>(Weight, Option<Asset>, core::marker::PhantomData<T>);
353
354impl<T: crate::Config> Trader<T> {
355 fn compute_amount_to_charge(
356 weight: &Weight,
357 asset_location: &Location,
358 ) -> Result<u128, XcmError> {
359 if *asset_location == <T as crate::Config>::NativeLocation::get() {
360 <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
361 .try_into()
362 .map_err(|_| XcmError::Overflow)
363 } else if let Some(relative_price) = Pallet::<T>::get_asset_relative_price(asset_location) {
364 if relative_price == 0u128 {
365 Ok(0u128)
366 } else {
367 let native_amount: u128 = <T as crate::Config>::WeightToFee::weight_to_fee(&weight)
368 .try_into()
369 .map_err(|_| XcmError::Overflow)?;
370 Ok(native_amount
371 .checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
372 .ok_or(XcmError::Overflow)?
373 .checked_div(relative_price)
374 .ok_or(XcmError::Overflow)?)
375 }
376 } else {
377 Err(XcmError::AssetNotFound)
378 }
379 }
380}
381
382impl<T: crate::Config> WeightTrader for Trader<T> {
383 fn new() -> Self {
384 Self(Weight::zero(), None, PhantomData)
385 }
386 fn buy_weight(
387 &mut self,
388 weight: Weight,
389 payment: xcm_executor::AssetsInHolding,
390 context: &XcmContext,
391 ) -> Result<xcm_executor::AssetsInHolding, XcmError> {
392 log::trace!(
393 target: "xcm::weight",
394 "UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
395 weight,
396 payment,
397 context
398 );
399
400 if self.1.is_some() {
402 return Err(XcmError::NotWithdrawable);
403 }
404
405 debug_assert_eq!(self.0, Weight::zero());
407
408 let first_asset = payment
410 .clone()
411 .fungible_assets_iter()
412 .next()
413 .ok_or(XcmError::AssetNotFound)?;
414
415 match (first_asset.id, first_asset.fun) {
416 (XcmAssetId(location), Fungibility::Fungible(_)) => {
417 let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
418
419 if amount.is_zero() {
423 return Ok(payment);
424 }
425
426 let required = Asset {
427 fun: Fungibility::Fungible(amount),
428 id: XcmAssetId(location),
429 };
430 let unused = payment
431 .checked_sub(required.clone())
432 .map_err(|_| XcmError::TooExpensive)?;
433
434 self.0 = weight;
435 self.1 = Some(required);
436
437 Ok(unused)
438 }
439 _ => Err(XcmError::AssetNotFound),
440 }
441 }
442
443 fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option<Asset> {
444 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 fun: Fungibility::Fungible(initial_amount),
454 id: XcmAssetId(location),
455 }) = self.1.take()
456 {
457 if actual_weight == self.0 {
458 self.1 = Some(Asset {
459 fun: Fungibility::Fungible(initial_amount),
460 id: XcmAssetId(location),
461 });
462 None
463 } else {
464 let weight = actual_weight.min(self.0);
465 let amount: u128 =
466 Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
467 let final_amount = amount.min(initial_amount);
468 let amount_to_refund = initial_amount.saturating_sub(final_amount);
469 self.0 -= weight;
470 self.1 = Some(Asset {
471 fun: Fungibility::Fungible(final_amount),
472 id: XcmAssetId(location.clone()),
473 });
474 log::trace!(
475 target: "xcm-weight-trader",
476 "refund_weight amount to refund: {:?}",
477 amount_to_refund
478 );
479 Some(Asset {
480 fun: Fungibility::Fungible(amount_to_refund),
481 id: XcmAssetId(location),
482 })
483 }
484 } else {
485 None
486 }
487 }
488}
489
490impl<T: crate::Config> Drop for Trader<T> {
491 fn drop(&mut self) {
492 log::trace!(
493 target: "xcm-weight-trader",
494 "Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
495 &self.0,
496 &self.1
497 );
498 if let Some(asset) = self.1.take() {
499 let res = T::AssetTransactor::deposit_asset(
500 &asset,
501 &T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
502 None,
503 );
504 debug_assert!(res.is_ok());
505 }
506 }
507}