diff --git a/Cargo.toml b/Cargo.toml
index 52cf6e823b..e1bfe21981 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ members = [
"pallets/precompile-benchmarks",
"pallets/proxy-genesis-companion",
"pallets/xcm-transactor",
+ "pallets/xcm-weight-trader",
"precompiles/balances-erc20",
"precompiles/batch",
"precompiles/call-permit",
@@ -105,6 +106,7 @@ pallet-precompile-benchmarks = { path = "pallets/precompile-benchmarks", default
pallet-proxy-genesis-companion = { path = "pallets/proxy-genesis-companion", default-features = false }
pallet-xcm-transactor = { path = "pallets/xcm-transactor", default-features = false }
pallet-moonbeam-lazy-migrations = { path = "pallets/moonbeam-lazy-migrations", default-features = false }
+pallet-xcm-weight-trader = { path = "pallets/xcm-weight-trader", default-features = false }
precompile-utils = { path = "precompiles/utils", default-features = false }
xcm-primitives = { path = "primitives/xcm", default-features = false }
diff --git a/pallets/xcm-weight-trader/Cargo.toml b/pallets/xcm-weight-trader/Cargo.toml
new file mode 100644
index 0000000000..680c3c234f
--- /dev/null
+++ b/pallets/xcm-weight-trader/Cargo.toml
@@ -0,0 +1,54 @@
+[package]
+authors = {workspace = true}
+description = "A pallet to trade weight for XCM execution"
+edition = "2021"
+name = "pallet-xcm-weight-trader"
+version = "0.1.0"
+
+[dependencies]
+log = {workspace = true}
+
+# Substrate
+frame-support = {workspace = true}
+frame-system = {workspace = true}
+pallet-balances = {workspace = true}
+parity-scale-codec = {workspace = true}
+scale-info = {workspace = true, features = ["derive"]}
+sp-core = {workspace = true}
+sp-io = {workspace = true}
+sp-runtime = {workspace = true}
+sp-std = {workspace = true}
+
+# Polkadot
+xcm = { workspace = true }
+xcm-executor = { workspace = true }
+xcm-fee-payment-runtime-api = { workspace = true }
+
+# Benchmarks
+frame-benchmarking = {workspace = true, optional = true}
+
+[dev-dependencies]
+frame-benchmarking = {workspace = true, features = ["std"]}
+pallet-balances = {workspace = true, features = ["std", "insecure_zero_ed"]}
+sp-tracing = {workspace = true, features = ["std"] }
+
+[features]
+default = ["std"]
+runtime-benchmarks = [
+ "frame-benchmarking",
+ "frame-system/runtime-benchmarks"
+]
+std = [
+ "frame-support/std",
+ "frame-system/std",
+ "pallet-balances/std",
+ "scale-info/std",
+ "sp-core/std",
+ "sp-io/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "xcm/std",
+ "xcm-executor/std",
+ "xcm-fee-payment-runtime-api/std",
+]
+try-runtime = ["frame-support/try-runtime"]
\ No newline at end of file
diff --git a/pallets/xcm-weight-trader/src/benchmarking.rs b/pallets/xcm-weight-trader/src/benchmarking.rs
new file mode 100644
index 0000000000..1d711289eb
--- /dev/null
+++ b/pallets/xcm-weight-trader/src/benchmarking.rs
@@ -0,0 +1,139 @@
+// Copyright 2024 Moonbeam foundation
+// This file is part of Moonbeam.
+
+// Moonbeam is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Moonbeam is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Moonbeam. If not, see .
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use super::*;
+
+use frame_benchmarking::{v2::*, BenchmarkError};
+use frame_support::traits::EnsureOrigin;
+use frame_system::EventRecord;
+
+fn assert_last_event(generic_event: ::RuntimeEvent) {
+ let events = frame_system::Pallet::::events();
+ let system_event: ::RuntimeEvent = generic_event.into();
+ // compare to the last event record
+ let EventRecord { event, .. } = &events[events.len() - 1];
+ assert_eq!(event, &system_event);
+}
+
+fn setup_one_asset() -> Result {
+ let origin = T::AddSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ let location = T::NotFilteredLocation::get();
+
+ Pallet::::add_asset(origin, location.clone(), 1_000).expect("fail to setup asset");
+
+ Ok(location)
+}
+
+#[benchmarks]
+mod benchmarks {
+ use super::*;
+
+ #[benchmark]
+ fn add_asset() -> Result<(), BenchmarkError> {
+ let origin = T::AddSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ let location = T::NotFilteredLocation::get();
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, location.clone(), 1_000);
+
+ assert_last_event::(
+ Event::SupportedAssetAdded {
+ location,
+ relative_price: 1_000,
+ }
+ .into(),
+ );
+ Ok(())
+ }
+
+ #[benchmark]
+ fn edit_asset() -> Result<(), BenchmarkError> {
+ // Setup one asset
+ let location = setup_one_asset::()?;
+
+ let origin = T::EditSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, location.clone(), 2_000);
+
+ assert_last_event::(
+ Event::SupportedAssetEdited {
+ location,
+ relative_price: 2_000,
+ }
+ .into(),
+ );
+ Ok(())
+ }
+
+ #[benchmark]
+ fn resume_asset_support() -> Result<(), BenchmarkError> {
+ // Setup one asset
+ let location = setup_one_asset::()?;
+ let pause_origin = T::PauseSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+ Pallet::::pause_asset_support(pause_origin, location.clone())
+ .expect("fail to pause asset");
+
+ let origin = T::ResumeSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, location.clone());
+
+ assert_last_event::(Event::ResumeAssetSupport { location }.into());
+ Ok(())
+ }
+
+ #[benchmark]
+ fn pause_asset_support() -> Result<(), BenchmarkError> {
+ // Setup one asset
+ let location = setup_one_asset::()?;
+
+ let origin = T::PauseSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, location.clone());
+
+ assert_last_event::(Event::PauseAssetSupport { location }.into());
+ Ok(())
+ }
+
+ #[benchmark]
+ fn remove_asset() -> Result<(), BenchmarkError> {
+ // Setup one asset
+ let location = setup_one_asset::()?;
+
+ let origin = T::RemoveSupportedAssetOrigin::try_successful_origin()
+ .map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, location.clone());
+
+ assert_last_event::(Event::SupportedAssetRemoved { location }.into());
+ Ok(())
+ }
+
+ impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test,);
+}
diff --git a/pallets/xcm-weight-trader/src/lib.rs b/pallets/xcm-weight-trader/src/lib.rs
new file mode 100644
index 0000000000..6fef42f452
--- /dev/null
+++ b/pallets/xcm-weight-trader/src/lib.rs
@@ -0,0 +1,472 @@
+// Copyright 2024 Moonbeam foundation
+// This file is part of Moonbeam.
+
+// Moonbeam is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Moonbeam is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Moonbeam. If not, see .
+
+//! # A pallet to trade weight for XCM execution
+
+#![allow(non_camel_case_types)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+#[cfg(test)]
+mod mock;
+#[cfg(test)]
+mod tests;
+
+pub mod weights;
+
+pub use pallet::*;
+pub use weights::WeightInfo;
+
+use frame_support::pallet;
+use frame_support::pallet_prelude::*;
+use frame_support::traits::Contains;
+use frame_support::weights::WeightToFee;
+use frame_system::pallet_prelude::*;
+use sp_runtime::traits::{Convert, Zero};
+use sp_std::vec::Vec;
+use xcm::v4::{Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext};
+use xcm::{IntoVersion, VersionedAssetId};
+use xcm_executor::traits::{TransactAsset, WeightTrader};
+use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError;
+
+pub const RELATIVE_PRICE_DECIMALS: u32 = 18;
+
+#[pallet]
+pub mod pallet {
+ use super::*;
+
+ /// Pallet for multi block migrations
+ #[pallet::pallet]
+ pub struct Pallet(PhantomData);
+
+ /// Configuration trait of this pallet.
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// Convert `T::AccountId` to `Location`.
+ type AccountIdToLocation: Convert;
+
+ /// Origin that is allowed to register a supported asset
+ type AddSupportedAssetOrigin: EnsureOrigin;
+
+ /// A filter to forbid some XCM Location to be supported for fees.
+ /// if you don't use it, put "Everything".
+ type AssetLocationFilter: Contains;
+
+ /// How to withdraw and deposit an asset.
+ type AssetTransactor: TransactAsset;
+
+ /// The native balance type.
+ type Balance: TryInto;
+
+ /// Origin that is allowed to edit a supported asset units per seconds
+ type EditSupportedAssetOrigin: EnsureOrigin;
+
+ /// XCM Location for native curreny
+ type NativeLocation: Get;
+
+ /// Origin that is allowed to pause a supported asset
+ type PauseSupportedAssetOrigin: EnsureOrigin;
+
+ /// Origin that is allowed to remove a supported asset
+ type RemoveSupportedAssetOrigin: EnsureOrigin;
+
+ /// The overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ /// Origin that is allowed to unpause a supported asset
+ type ResumeSupportedAssetOrigin: EnsureOrigin;
+
+ /// Weight information for extrinsics in this pallet.
+ type WeightInfo: WeightInfo;
+
+ /// Convert a weight value into deductible native balance.
+ type WeightToFee: WeightToFee;
+
+ /// Account that will receive xcm fees
+ type XcmFeesAccount: Get;
+
+ /// The benchmarks need a location that pass the filter AssetLocationFilter
+ #[cfg(feature = "runtime-benchmarks")]
+ type NotFilteredLocation: Get;
+ }
+
+ /// Stores all supported assets per XCM Location.
+ /// The u128 is the asset price relative to native asset with 18 decimals
+ /// The boolean specify if the support for this asset is active
+ #[pallet::storage]
+ #[pallet::getter(fn supported_assets)]
+ pub type SupportedAssets = StorageMap<_, Blake2_128Concat, Location, (bool, u128)>;
+
+ #[pallet::error]
+ pub enum Error {
+ /// The given asset was already added
+ AssetAlreadyAdded,
+ /// The given asset was already paused
+ AssetAlreadyPaused,
+ /// The given asset was not found
+ AssetNotFound,
+ /// The given asset is not paused
+ AssetNotPaused,
+ /// XCM location filtered
+ XcmLocationFiltered,
+ /// The relative price cannot be zero
+ PriceCannotBeZero,
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(crate) fn deposit_event)]
+ pub enum Event {
+ /// New supported asset is registered
+ SupportedAssetAdded {
+ location: Location,
+ relative_price: u128,
+ },
+ /// Changed the amount of units we are charging per execution second for a given asset
+ SupportedAssetEdited {
+ location: Location,
+ relative_price: u128,
+ },
+ /// Pause support for a given asset
+ PauseAssetSupport { location: Location },
+ /// Resume support for a given asset
+ ResumeAssetSupport { location: Location },
+ /// Supported asset type for fee payment removed
+ SupportedAssetRemoved { location: Location },
+ }
+
+ #[pallet::call]
+ impl Pallet {
+ #[pallet::call_index(0)]
+ #[pallet::weight(T::WeightInfo::add_asset())]
+ pub fn add_asset(
+ origin: OriginFor,
+ location: Location,
+ relative_price: u128,
+ ) -> DispatchResult {
+ T::AddSupportedAssetOrigin::ensure_origin(origin)?;
+
+ ensure!(relative_price != 0, Error::::PriceCannotBeZero);
+ ensure!(
+ !SupportedAssets::::contains_key(&location),
+ Error::::AssetAlreadyAdded
+ );
+ ensure!(
+ T::AssetLocationFilter::contains(&location),
+ Error::::XcmLocationFiltered
+ );
+
+ SupportedAssets::::insert(&location, (true, relative_price));
+
+ Self::deposit_event(Event::SupportedAssetAdded {
+ location,
+ relative_price,
+ });
+
+ Ok(())
+ }
+
+ #[pallet::call_index(1)]
+ #[pallet::weight(T::WeightInfo::edit_asset())]
+ pub fn edit_asset(
+ origin: OriginFor,
+ location: Location,
+ relative_price: u128,
+ ) -> DispatchResult {
+ T::EditSupportedAssetOrigin::ensure_origin(origin)?;
+
+ ensure!(relative_price != 0, Error::::PriceCannotBeZero);
+
+ let enabled = SupportedAssets::::get(&location)
+ .ok_or(Error::::AssetNotFound)?
+ .0;
+
+ SupportedAssets::::insert(&location, (enabled, relative_price));
+
+ Self::deposit_event(Event::SupportedAssetEdited {
+ location,
+ relative_price,
+ });
+
+ Ok(())
+ }
+
+ #[pallet::call_index(2)]
+ #[pallet::weight(T::WeightInfo::pause_asset_support())]
+ pub fn pause_asset_support(origin: OriginFor, location: Location) -> DispatchResult {
+ T::PauseSupportedAssetOrigin::ensure_origin(origin)?;
+
+ match SupportedAssets::::get(&location) {
+ Some((true, relative_price)) => {
+ SupportedAssets::::insert(&location, (false, relative_price));
+ Self::deposit_event(Event::PauseAssetSupport { location });
+ Ok(())
+ }
+ Some((false, _)) => Err(Error::::AssetAlreadyPaused.into()),
+ None => Err(Error::::AssetNotFound.into()),
+ }
+ }
+
+ #[pallet::call_index(3)]
+ #[pallet::weight(T::WeightInfo::resume_asset_support())]
+ pub fn resume_asset_support(origin: OriginFor, location: Location) -> DispatchResult {
+ T::ResumeSupportedAssetOrigin::ensure_origin(origin)?;
+
+ match SupportedAssets::::get(&location) {
+ Some((false, relative_price)) => {
+ SupportedAssets::::insert(&location, (true, relative_price));
+ Self::deposit_event(Event::ResumeAssetSupport { location });
+ Ok(())
+ }
+ Some((true, _)) => Err(Error::::AssetNotPaused.into()),
+ None => Err(Error::::AssetNotFound.into()),
+ }
+ }
+
+ #[pallet::call_index(4)]
+ #[pallet::weight(T::WeightInfo::remove_asset())]
+ pub fn remove_asset(origin: OriginFor, location: Location) -> DispatchResult {
+ T::RemoveSupportedAssetOrigin::ensure_origin(origin)?;
+
+ ensure!(
+ SupportedAssets::::contains_key(&location),
+ Error::::AssetNotFound
+ );
+
+ SupportedAssets::::remove(&location);
+
+ Self::deposit_event(Event::SupportedAssetRemoved { location });
+
+ Ok(())
+ }
+ }
+
+ impl Pallet {
+ pub fn get_asset_relative_price(location: &Location) -> Option {
+ if let Some((true, ratio)) = SupportedAssets::::get(location) {
+ Some(ratio)
+ } else {
+ None
+ }
+ }
+ pub fn query_acceptable_payment_assets(
+ xcm_version: xcm::Version,
+ ) -> Result, XcmPaymentApiError> {
+ if !matches!(xcm_version, 3 | 4) {
+ return Err(XcmPaymentApiError::UnhandledXcmVersion);
+ }
+
+ let v4_assets = [VersionedAssetId::V4(XcmAssetId::from(
+ T::NativeLocation::get(),
+ ))]
+ .into_iter()
+ .chain(
+ SupportedAssets::::iter().filter_map(|(asset_location, (enabled, _))| {
+ enabled.then(|| VersionedAssetId::V4(XcmAssetId(asset_location)))
+ }),
+ )
+ .collect::>();
+
+ if xcm_version == 3 {
+ v4_assets
+ .into_iter()
+ .map(|v4_asset| v4_asset.into_version(3))
+ .collect::>()
+ .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)
+ } else {
+ Ok(v4_assets)
+ }
+ }
+ pub fn query_weight_to_asset_fee(
+ weight: Weight,
+ asset: VersionedAssetId,
+ ) -> Result {
+ if let VersionedAssetId::V4(XcmAssetId(asset_location)) = asset
+ .into_version(4)
+ .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?
+ {
+ Trader::::compute_amount_to_charge(&weight, &asset_location).map_err(|e| match e
+ {
+ XcmError::AssetNotFound => XcmPaymentApiError::AssetNotFound,
+ _ => XcmPaymentApiError::WeightNotComputable,
+ })
+ } else {
+ Err(XcmPaymentApiError::UnhandledXcmVersion)
+ }
+ }
+ #[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
+ pub fn set_asset_price(asset_location: Location, relative_price: u128) {
+ SupportedAssets::::insert(&asset_location, (true, relative_price));
+ }
+ }
+}
+
+pub struct Trader(Weight, Option, core::marker::PhantomData);
+
+impl Trader {
+ fn compute_amount_to_charge(
+ weight: &Weight,
+ asset_location: &Location,
+ ) -> Result {
+ if *asset_location == ::NativeLocation::get() {
+ ::WeightToFee::weight_to_fee(&weight)
+ .try_into()
+ .map_err(|_| XcmError::Overflow)
+ } else if let Some(relative_price) = Pallet::::get_asset_relative_price(asset_location) {
+ if relative_price == 0u128 {
+ Ok(0u128)
+ } else {
+ let native_amount: u128 = ::WeightToFee::weight_to_fee(&weight)
+ .try_into()
+ .map_err(|_| XcmError::Overflow)?;
+ Ok(native_amount
+ .checked_mul(10u128.pow(RELATIVE_PRICE_DECIMALS))
+ .ok_or(XcmError::Overflow)?
+ .checked_div(relative_price)
+ .ok_or(XcmError::Overflow)?)
+ }
+ } else {
+ Err(XcmError::AssetNotFound)
+ }
+ }
+}
+
+impl WeightTrader for Trader {
+ fn new() -> Self {
+ Self(Weight::zero(), None, PhantomData)
+ }
+ fn buy_weight(
+ &mut self,
+ weight: Weight,
+ payment: xcm_executor::AssetsInHolding,
+ context: &XcmContext,
+ ) -> Result {
+ log::trace!(
+ target: "xcm::weight",
+ "UsingComponents::buy_weight weight: {:?}, payment: {:?}, context: {:?}",
+ weight,
+ payment,
+ context
+ );
+
+ // Can only call one time
+ if self.1.is_some() {
+ return Err(XcmError::NotWithdrawable);
+ }
+
+ // Consistency check for tests only, we should never panic in release mode
+ debug_assert_eq!(self.0, Weight::zero());
+
+ // We support only one fee asset per buy, so we take the first one.
+ let first_asset = payment
+ .clone()
+ .fungible_assets_iter()
+ .next()
+ .ok_or(XcmError::AssetNotFound)?;
+
+ match (first_asset.id, first_asset.fun) {
+ (XcmAssetId(location), Fungibility::Fungible(_)) => {
+ let amount: u128 = Self::compute_amount_to_charge(&weight, &location)?;
+
+ // We don't need to proceed if the amount is 0
+ // For cases (specially tests) where the asset is very cheap with respect
+ // to the weight needed
+ if amount.is_zero() {
+ return Ok(payment);
+ }
+
+ let required = Asset {
+ fun: Fungibility::Fungible(amount),
+ id: XcmAssetId(location),
+ };
+ let unused = payment
+ .checked_sub(required.clone())
+ .map_err(|_| XcmError::TooExpensive)?;
+
+ self.0 = weight;
+ self.1 = Some(required);
+
+ Ok(unused)
+ }
+ _ => Err(XcmError::AssetNotFound),
+ }
+ }
+
+ fn refund_weight(&mut self, actual_weight: Weight, context: &XcmContext) -> Option {
+ log::trace!(
+ target: "xcm-weight-trader",
+ "refund_weight weight: {:?}, context: {:?}, available weight: {:?}, asset: {:?}",
+ actual_weight,
+ context,
+ self.0,
+ self.1
+ );
+ if let Some(Asset {
+ fun: Fungibility::Fungible(initial_amount),
+ id: XcmAssetId(location),
+ }) = self.1.take()
+ {
+ if actual_weight == self.0 {
+ self.1 = Some(Asset {
+ fun: Fungibility::Fungible(initial_amount),
+ id: XcmAssetId(location),
+ });
+ None
+ } else {
+ let weight = actual_weight.min(self.0);
+ let amount: u128 =
+ Self::compute_amount_to_charge(&weight, &location).unwrap_or(u128::MAX);
+ let final_amount = amount.min(initial_amount);
+ let amount_to_refund = initial_amount.saturating_sub(final_amount);
+ self.0 -= weight;
+ self.1 = Some(Asset {
+ fun: Fungibility::Fungible(final_amount),
+ id: XcmAssetId(location.clone()),
+ });
+ log::trace!(
+ target: "xcm-weight-trader",
+ "refund_weight amount to refund: {:?}",
+ amount_to_refund
+ );
+ Some(Asset {
+ fun: Fungibility::Fungible(amount_to_refund),
+ id: XcmAssetId(location),
+ })
+ }
+ } else {
+ None
+ }
+ }
+}
+
+impl Drop for Trader {
+ fn drop(&mut self) {
+ log::trace!(
+ target: "xcm-weight-trader",
+ "Dropping `Trader` instance: (weight: {:?}, asset: {:?})",
+ &self.0,
+ &self.1
+ );
+ if let Some(asset) = self.1.take() {
+ let res = T::AssetTransactor::deposit_asset(
+ &asset,
+ &T::AccountIdToLocation::convert(T::XcmFeesAccount::get()),
+ None,
+ );
+ debug_assert!(res.is_ok());
+ }
+ }
+}
diff --git a/pallets/xcm-weight-trader/src/mock.rs b/pallets/xcm-weight-trader/src/mock.rs
new file mode 100644
index 0000000000..b1ce8166eb
--- /dev/null
+++ b/pallets/xcm-weight-trader/src/mock.rs
@@ -0,0 +1,200 @@
+// Copyright 2024 Moonbeam foundation
+// This file is part of Moonbeam.
+
+// Moonbeam is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Moonbeam is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Moonbeam. If not, see .
+
+//! A minimal runtime including the multi block migrations pallet
+
+use super::*;
+use crate as pallet_xcm_weight_trader;
+use frame_support::{
+ construct_runtime, ord_parameter_types, parameter_types,
+ traits::{Currency, Everything},
+ weights::{constants::RocksDbWeight, IdentityFee},
+};
+use frame_system::EnsureSignedBy;
+use sp_core::H256;
+use sp_runtime::{
+ traits::{BlakeTwo256, IdentityLookup},
+ BuildStorage,
+};
+use xcm::v4::{Asset, Error as XcmError, Junction, Location, Result as XcmResult, XcmContext};
+
+type AccountId = u64;
+type Balance = u128;
+type Block = frame_system::mocking::MockBlock;
+
+// Configure a mock runtime to test the pallet.
+construct_runtime!(
+ pub enum Test
+ {
+ System: frame_system::{Pallet, Call, Config, Storage, Event},
+ Balances: pallet_balances::{Pallet, Call, Storage, Config, Event},
+ XcmWeightTrader: pallet_xcm_weight_trader::{Pallet, Call, Storage, Event},
+ }
+);
+
+parameter_types! {
+ pub const BlockHashCount: u32 = 250;
+ pub const SS58Prefix: u8 = 42;
+}
+impl frame_system::Config for Test {
+ type BaseCallFilter = Everything;
+ type DbWeight = RocksDbWeight;
+ type RuntimeOrigin = RuntimeOrigin;
+ type RuntimeTask = RuntimeTask;
+ type Nonce = u64;
+ type Block = Block;
+ type RuntimeCall = RuntimeCall;
+ type Hash = H256;
+ type Hashing = BlakeTwo256;
+ type AccountId = AccountId;
+ type Lookup = IdentityLookup;
+ type RuntimeEvent = RuntimeEvent;
+ type BlockHashCount = BlockHashCount;
+ type Version = ();
+ type PalletInfo = PalletInfo;
+ type AccountData = pallet_balances::AccountData;
+ type OnNewAccount = ();
+ type OnKilledAccount = ();
+ type SystemWeightInfo = ();
+ type BlockWeights = ();
+ type BlockLength = ();
+ type SS58Prefix = SS58Prefix;
+ type OnSetCode = ();
+ type MaxConsumers = frame_support::traits::ConstU32<16>;
+ type SingleBlockMigrations = ();
+ type MultiBlockMigrator = ();
+ type PreInherents = ();
+ type PostInherents = ();
+ type PostTransactions = ();
+}
+
+parameter_types! {
+ pub const ExistentialDeposit: u128 = 0;
+}
+impl pallet_balances::Config for Test {
+ type MaxReserves = ();
+ type ReserveIdentifier = ();
+ type MaxLocks = ();
+ type Balance = Balance;
+ type RuntimeEvent = RuntimeEvent;
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ type RuntimeHoldReason = ();
+ type FreezeIdentifier = ();
+ type MaxFreezes = ();
+ type RuntimeFreezeReason = ();
+}
+
+pub struct AccountIdToLocation;
+impl Convert for AccountIdToLocation {
+ fn convert(account: AccountId) -> Location {
+ Location::new(
+ 0,
+ [Junction::AccountIndex64 {
+ network: None,
+ index: account,
+ }],
+ )
+ }
+}
+
+pub struct AssetLocationFilter;
+impl Contains for AssetLocationFilter {
+ fn contains(location: &Location) -> bool {
+ *location == ::NativeLocation::get() || *location == Location::parent()
+ }
+}
+
+pub fn get_parent_asset_deposited() -> Option<(AccountId, Balance)> {
+ storage::unhashed::get_raw(b"____parent_asset_deposited")
+ .map(|output| Decode::decode(&mut output.as_slice()).expect("Decoding should work"))
+}
+
+pub struct MockAssetTransactor;
+impl TransactAsset for MockAssetTransactor {
+ fn deposit_asset(asset: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
+ match (asset.id.clone(), asset.fun.clone()) {
+ (XcmAssetId(location), Fungibility::Fungible(amount)) => {
+ let who = match who.interior.iter().next() {
+ Some(Junction::AccountIndex64 { index, .. }) => index,
+ _ => panic!("invalid location"),
+ };
+ if location == ::NativeLocation::get() {
+ let _ = Balances::deposit_creating(who, amount);
+ Ok(())
+ } else if location == Location::parent() {
+ storage::unhashed::put_raw(
+ b"____parent_asset_deposited",
+ (who, amount).encode().as_slice(),
+ );
+ Ok(())
+ } else {
+ Err(XcmError::AssetNotFound)
+ }
+ }
+ _ => Err(XcmError::AssetNotFound),
+ }
+ }
+}
+
+ord_parameter_types! {
+ pub const AddAccount: u64 = 1;
+ pub const EditAccount: u64 = 2;
+ pub const PauseAccount: u64 = 3;
+ pub const ResumeAccount: u64 = 4;
+ pub const RemoveAccount: u64 = 5;
+}
+
+parameter_types! {
+ pub NativeLocation: Location = Location::here();
+ pub XcmFeesAccount: AccountId = 101;
+ pub NotFilteredLocation: Location = Location::parent();
+}
+
+impl Config for Test {
+ type AccountIdToLocation = AccountIdToLocation;
+ type AddSupportedAssetOrigin = EnsureSignedBy;
+ type AssetLocationFilter = AssetLocationFilter;
+ type AssetTransactor = MockAssetTransactor;
+ type Balance = Balance;
+ type EditSupportedAssetOrigin = EnsureSignedBy;
+ type NativeLocation = NativeLocation;
+ type PauseSupportedAssetOrigin = EnsureSignedBy;
+ type RemoveSupportedAssetOrigin = EnsureSignedBy;
+ type RuntimeEvent = RuntimeEvent;
+ type ResumeSupportedAssetOrigin = EnsureSignedBy;
+ type WeightInfo = ();
+ type WeightToFee = IdentityFee;
+ type XcmFeesAccount = XcmFeesAccount;
+ #[cfg(feature = "runtime-benchmarks")]
+ type NotFilteredLocation = NotFilteredLocation;
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ sp_tracing::try_init_simple();
+ let mut t = frame_system::GenesisConfig::::default()
+ .build_storage()
+ .unwrap();
+
+ let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
+ pallet_balances::GenesisConfig:: { balances }
+ .assimilate_storage(&mut t)
+ .unwrap();
+
+ t.into()
+}
diff --git a/pallets/xcm-weight-trader/src/tests.rs b/pallets/xcm-weight-trader/src/tests.rs
new file mode 100644
index 0000000000..daceed2445
--- /dev/null
+++ b/pallets/xcm-weight-trader/src/tests.rs
@@ -0,0 +1,704 @@
+// Copyright 2024 Moonbeam foundation
+// This file is part of Moonbeam.
+
+// Moonbeam is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Moonbeam is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Moonbeam. If not, see .
+
+//! Unit testing
+use {
+ crate::mock::*,
+ crate::{Error, Trader, XcmPaymentApiError},
+ frame_support::pallet_prelude::Weight,
+ frame_support::{assert_noop, assert_ok},
+ sp_runtime::DispatchError,
+ xcm::v4::{
+ Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, XcmContext, XcmHash,
+ },
+ xcm::{IntoVersion, VersionedAssetId},
+ xcm_executor::traits::WeightTrader,
+};
+
+fn xcm_fees_account() -> ::AccountId {
+ ::XcmFeesAccount::get()
+}
+
+#[test]
+fn test_add_supported_asset() {
+ new_test_ext().execute_with(|| {
+ // Call with bad origin
+ assert_noop!(
+ XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 1_000,
+ ),
+ DispatchError::BadOrigin
+ );
+
+ // Call with invalid location
+ assert_noop!(
+ XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::new(2, []),
+ 1_000,
+ ),
+ Error::::XcmLocationFiltered
+ );
+
+ // Call with invalid price
+ assert_noop!(
+ XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 0,
+ ),
+ Error::::PriceCannotBeZero
+ );
+
+ // Call with the right origin
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ));
+
+ // The account should be supported
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(1_000),
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((true, 1_000))
+ );
+
+ // Try to add the same asset twice (should fail)
+ assert_noop!(
+ XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ),
+ Error::::AssetAlreadyAdded
+ );
+ })
+}
+
+#[test]
+fn test_edit_supported_asset() {
+ new_test_ext().execute_with(|| {
+ // Should not be able to edit an asset not added yet
+ assert_noop!(
+ XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 2_000,
+ ),
+ Error::::AssetNotFound
+ );
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(1_000),
+ );
+
+ // Call with bad origin
+ assert_noop!(
+ XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 2_000,
+ ),
+ DispatchError::BadOrigin
+ );
+
+ // Call with invalid price
+ assert_noop!(
+ XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 0,
+ ),
+ Error::::PriceCannotBeZero
+ );
+
+ // Call with right origin and valid params
+ assert_ok!(XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 2_000,
+ ),);
+
+ // The account should be supported
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(2_000),
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((true, 2_000))
+ );
+ })
+}
+
+#[test]
+fn test_pause_asset_support() {
+ new_test_ext().execute_with(|| {
+ // Should not be able to pause an asset not added yet
+ assert_noop!(
+ XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ),
+ Error::::AssetNotFound
+ );
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(1_000),
+ );
+
+ // Call with bad origin
+ assert_noop!(
+ XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ ),
+ DispatchError::BadOrigin
+ );
+
+ // Call with right origin and valid params
+ assert_ok!(XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ));
+
+ // The asset should be paused
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ None,
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((false, 1_000))
+ );
+
+ // Should not be able to pause an asset already paused
+ assert_noop!(
+ XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ),
+ Error::::AssetAlreadyPaused
+ );
+
+ // Should be able to udpate relative price of paused asset
+ assert_ok!(XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 500
+ ));
+
+ // The asset should still be paused
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ None,
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((false, 500))
+ );
+ })
+}
+
+#[test]
+fn test_resume_asset_support() {
+ new_test_ext().execute_with(|| {
+ // Setup (add a supported asset and pause it)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ));
+ assert_ok!(XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ));
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((false, 1_000))
+ );
+
+ // Call with bad origin
+ assert_noop!(
+ XcmWeightTrader::resume_asset_support(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ ),
+ DispatchError::BadOrigin
+ );
+
+ // Call with invalid location
+ assert_noop!(
+ XcmWeightTrader::resume_asset_support(
+ RuntimeOrigin::signed(ResumeAccount::get()),
+ Location::new(2, []),
+ ),
+ Error::::AssetNotFound
+ );
+
+ // Call with right origin and valid params
+ assert_ok!(XcmWeightTrader::resume_asset_support(
+ RuntimeOrigin::signed(ResumeAccount::get()),
+ Location::parent(),
+ ));
+
+ // The asset should be supported again
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(1_000),
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ Some((true, 1_000))
+ );
+
+ // Should not be able to resume an asset already active
+ assert_noop!(
+ XcmWeightTrader::resume_asset_support(
+ RuntimeOrigin::signed(ResumeAccount::get()),
+ Location::parent(),
+ ),
+ Error::::AssetNotPaused
+ );
+ })
+}
+
+#[test]
+fn test_remove_asset_support() {
+ new_test_ext().execute_with(|| {
+ // Should not be able to remove an asset not added yet
+ assert_noop!(
+ XcmWeightTrader::remove_asset(
+ RuntimeOrigin::signed(RemoveAccount::get()),
+ Location::parent(),
+ ),
+ Error::::AssetNotFound
+ );
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 1_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(1_000),
+ );
+
+ // Call with bad origin
+ assert_noop!(
+ XcmWeightTrader::remove_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ ),
+ DispatchError::BadOrigin
+ );
+
+ // Call with right origin and valid params
+ assert_ok!(XcmWeightTrader::remove_asset(
+ RuntimeOrigin::signed(RemoveAccount::get()),
+ Location::parent(),
+ ));
+
+ // The account should be removed
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ None,
+ );
+
+ // Check storage
+ assert_eq!(
+ crate::pallet::SupportedAssets::::get(&Location::parent()),
+ None
+ );
+
+ // Should not be able to pause an asset already removed
+ assert_noop!(
+ XcmWeightTrader::remove_asset(
+ RuntimeOrigin::signed(RemoveAccount::get()),
+ Location::parent(),
+ ),
+ Error::::AssetNotFound
+ );
+ })
+}
+
+#[test]
+fn test_trader_native_asset() {
+ new_test_ext().execute_with(|| {
+ let weight_to_buy = Weight::from_parts(10_000, 0);
+ let dummy_xcm_context = XcmContext::with_message_id(XcmHash::default());
+
+ // Should not be able to buy weight with too low asset balance
+ assert_eq!(
+ Trader::::new().buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(9_999),
+ id: XcmAssetId(Location::here()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Err(XcmError::TooExpensive)
+ );
+
+ // Should not be able to buy weight with unsupported asset
+ assert_eq!(
+ Trader::::new().buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(10_000),
+ id: XcmAssetId(Location::parent()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Err(XcmError::AssetNotFound)
+ );
+
+ // Should not be able to buy weight without asset
+ assert_eq!(
+ Trader::::new().buy_weight(weight_to_buy, Default::default(), &dummy_xcm_context),
+ Err(XcmError::AssetNotFound)
+ );
+
+ // Should be able to buy weight with just enough native asset
+ let mut trader = Trader::::new();
+ assert_eq!(
+ trader.buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(10_000),
+ id: XcmAssetId(Location::here()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Ok(Default::default())
+ );
+
+ // Should not refund any funds
+ let actual_weight = weight_to_buy;
+ assert_eq!(
+ trader.refund_weight(actual_weight, &dummy_xcm_context),
+ None
+ );
+
+ // Should not be able to buy weight again with the same trader
+ assert_eq!(
+ trader.buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(10_000),
+ id: XcmAssetId(Location::here()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Err(XcmError::NotWithdrawable)
+ );
+
+ // Fees asset should be deposited into XcmFeesAccount
+ drop(trader);
+ assert_eq!(Balances::free_balance(&xcm_fees_account()), 10_000);
+
+ // Should be able to buy weight with more native asset (and get back unused amount)
+ let mut trader = Trader::::new();
+ assert_eq!(
+ trader.buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(11_000),
+ id: XcmAssetId(Location::here()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Ok(Asset {
+ fun: Fungibility::Fungible(1_000),
+ id: XcmAssetId(Location::here()),
+ }
+ .into())
+ );
+
+ // Should be able to refund unused weights
+ let actual_weight = weight_to_buy.saturating_sub(Weight::from_parts(2_000, 0));
+ assert_eq!(
+ trader.refund_weight(actual_weight, &dummy_xcm_context),
+ Some(Asset {
+ fun: Fungibility::Fungible(2_000),
+ id: XcmAssetId(Location::here()),
+ })
+ );
+
+ // Fees asset should be deposited again into XcmFeesAccount (2 times cost minus one refund)
+ drop(trader);
+ assert_eq!(
+ Balances::free_balance(&xcm_fees_account()),
+ (2 * 10_000) - 2_000
+ );
+ })
+}
+
+#[test]
+fn test_trader_parent_asset() {
+ new_test_ext().execute_with(|| {
+ let weight_to_buy = Weight::from_parts(10_000, 0);
+ let dummy_xcm_context = XcmContext::with_message_id(XcmHash::default());
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 500_000_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(500_000_000),
+ );
+
+ // Should be able to pay fees with registered asset
+ let mut trader = Trader::::new();
+ assert_eq!(
+ trader.buy_weight(
+ weight_to_buy,
+ Asset {
+ fun: Fungibility::Fungible(22_000_000_000_000),
+ id: XcmAssetId(Location::parent()),
+ }
+ .into(),
+ &dummy_xcm_context
+ ),
+ Ok(Asset {
+ fun: Fungibility::Fungible(2_000_000_000_000),
+ id: XcmAssetId(Location::parent()),
+ }
+ .into())
+ );
+
+ // Should be able to refund unused weights
+ let actual_weight = weight_to_buy.saturating_sub(Weight::from_parts(2_000, 0));
+ assert_eq!(
+ trader.refund_weight(actual_weight, &dummy_xcm_context),
+ Some(Asset {
+ fun: Fungibility::Fungible(4_000_000_000_000),
+ id: XcmAssetId(Location::parent()),
+ })
+ );
+
+ // Fees asset should be deposited into XcmFeesAccount
+ drop(trader);
+ assert_eq!(
+ get_parent_asset_deposited(),
+ Some((xcm_fees_account(), 20_000_000_000_000 - 4_000_000_000_000))
+ );
+
+ // Should not be able to buy weight if the asset is not a first position
+ assert_eq!(
+ Trader::::new().buy_weight(
+ weight_to_buy,
+ vec![
+ Asset {
+ fun: Fungibility::Fungible(10),
+ id: XcmAssetId(Location::here()),
+ },
+ Asset {
+ fun: Fungibility::Fungible(30_000),
+ id: XcmAssetId(Location::parent()),
+ }
+ ]
+ .into(),
+ &dummy_xcm_context
+ ),
+ Err(XcmError::TooExpensive)
+ );
+ })
+}
+
+#[test]
+fn test_query_acceptable_payment_assets() {
+ new_test_ext().execute_with(|| {
+ // By default, only native asset should be supported
+ assert_eq!(
+ XcmWeightTrader::query_acceptable_payment_assets(4),
+ Ok(vec![VersionedAssetId::V4(XcmAssetId(
+ ::NativeLocation::get()
+ ))])
+ );
+
+ // We should support XCMv3
+ assert_eq!(
+ XcmWeightTrader::query_acceptable_payment_assets(3),
+ Ok(vec![VersionedAssetId::V4(XcmAssetId(
+ ::NativeLocation::get()
+ ))
+ .into_version(3)
+ .expect("native location should be convertible to v3")])
+ );
+
+ // We should not support XCMv2
+ assert_eq!(
+ XcmWeightTrader::query_acceptable_payment_assets(2),
+ Err(XcmPaymentApiError::UnhandledXcmVersion)
+ );
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 500_000_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(500_000_000),
+ );
+
+ // We should support parent asset now
+ assert_eq!(
+ XcmWeightTrader::query_acceptable_payment_assets(4),
+ Ok(vec![
+ VersionedAssetId::V4(XcmAssetId(::NativeLocation::get())),
+ VersionedAssetId::V4(XcmAssetId(Location::parent()))
+ ])
+ );
+
+ // Setup: pause parent asset
+ assert_ok!(XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ None
+ );
+
+ // We should not support paused assets
+ assert_eq!(
+ XcmWeightTrader::query_acceptable_payment_assets(4),
+ Ok(vec![VersionedAssetId::V4(XcmAssetId(
+ ::NativeLocation::get()
+ )),])
+ );
+ })
+}
+
+#[test]
+fn test_query_weight_to_asset_fee() {
+ new_test_ext().execute_with(|| {
+ let native_asset =
+ VersionedAssetId::V4(XcmAssetId(::NativeLocation::get()));
+ let parent_asset = VersionedAssetId::V4(XcmAssetId(Location::parent()));
+ let weight_to_buy = Weight::from_parts(10_000, 0);
+
+ // Native asset price should be 1:1
+ assert_eq!(
+ XcmWeightTrader::query_weight_to_asset_fee(weight_to_buy, native_asset.clone()),
+ Ok(10_000)
+ );
+
+ // Should not be able to query fees for an unsupported asset
+ assert_eq!(
+ XcmWeightTrader::query_weight_to_asset_fee(weight_to_buy, parent_asset.clone()),
+ Err(XcmPaymentApiError::AssetNotFound)
+ );
+
+ // Setup (add a supported asset)
+ assert_ok!(XcmWeightTrader::add_asset(
+ RuntimeOrigin::signed(AddAccount::get()),
+ Location::parent(),
+ 500_000_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(500_000_000),
+ );
+
+ // Parent asset price should be 0.5
+ assert_eq!(
+ XcmWeightTrader::query_weight_to_asset_fee(weight_to_buy, parent_asset.clone()),
+ Ok(2 * 10_000_000_000_000)
+ );
+
+ // Setup: pause parent asset
+ assert_ok!(XcmWeightTrader::pause_asset_support(
+ RuntimeOrigin::signed(PauseAccount::get()),
+ Location::parent(),
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ None
+ );
+
+ // We should not support paused assets
+ assert_eq!(
+ XcmWeightTrader::query_weight_to_asset_fee(weight_to_buy, parent_asset.clone()),
+ Err(XcmPaymentApiError::AssetNotFound)
+ );
+
+ // Setup: unpause parent asset and edit price
+ assert_ok!(XcmWeightTrader::resume_asset_support(
+ RuntimeOrigin::signed(ResumeAccount::get()),
+ Location::parent(),
+ ));
+ assert_ok!(XcmWeightTrader::edit_asset(
+ RuntimeOrigin::signed(EditAccount::get()),
+ Location::parent(),
+ 2_000_000_000,
+ ));
+ assert_eq!(
+ XcmWeightTrader::get_asset_relative_price(&Location::parent()),
+ Some(2_000_000_000),
+ );
+
+ // We should support unpaused asset with new price
+ assert_eq!(
+ XcmWeightTrader::query_weight_to_asset_fee(weight_to_buy, parent_asset),
+ Ok(10_000_000_000_000 / 2)
+ );
+ })
+}
diff --git a/pallets/xcm-weight-trader/src/weights.rs b/pallets/xcm-weight-trader/src/weights.rs
new file mode 100644
index 0000000000..972a946eba
--- /dev/null
+++ b/pallets/xcm-weight-trader/src/weights.rs
@@ -0,0 +1,50 @@
+// Copyright 2024 Moonbeam foundation
+// This file is part of Moonbeam.
+
+// Moonbeam is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Moonbeam is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Moonbeam. If not, see .
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_xcm_weight_trader
+pub trait WeightInfo {
+ fn add_asset() -> Weight;
+ fn edit_asset() -> Weight;
+ fn pause_asset_support() -> Weight;
+ fn resume_asset_support() -> Weight;
+ fn remove_asset() -> Weight;
+}
+
+// For tests only
+impl WeightInfo for () {
+ fn add_asset() -> Weight {
+ Weight::default()
+ }
+ fn edit_asset() -> Weight {
+ Weight::default()
+ }
+ fn pause_asset_support() -> Weight {
+ Weight::default()
+ }
+ fn resume_asset_support() -> Weight {
+ Weight::default()
+ }
+ fn remove_asset() -> Weight {
+ Weight::default()
+ }
+}
\ No newline at end of file