moonbeam_runtime_common/tests/
xcm.rs

1// Copyright 2025 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//! # Common XCM tests
18//!
19//! A collection of XCM tests common to all runtimes
20
21#[macro_export]
22macro_rules! generate_common_xcm_tests {
23	($runtime: ident) => {
24		#[cfg(test)]
25		pub mod common_xcm_tests {
26			use crate::common::{ExtBuilder, ALICE};
27			use cumulus_primitives_core::ExecuteXcm;
28			use frame_support::assert_ok;
29			use frame_support::traits::fungible::Inspect;
30			use frame_support::traits::EnsureOrigin;
31			use frame_support::weights::{constants::WEIGHT_REF_TIME_PER_SECOND, WeightToFee as _};
32			use moonbeam_core_primitives::{AccountId, Balance};
33			use pallet_xcm_weight_trader::{SupportedAssets, RELATIVE_PRICE_DECIMALS};
34			use parity_scale_codec::Encode;
35			use sp_weights::Weight;
36			use xcm::latest::Location;
37			use xcm::{
38				latest::{prelude::AccountKey20, Assets as XcmAssets, Xcm},
39				VersionedAssets, VersionedLocation, VersionedXcm,
40			};
41			use $runtime::{
42				xcm_config::SelfReserve, Balances, PolkadotXcm, Runtime, RuntimeEvent,
43				RuntimeOrigin, System, XcmTransactor, XcmWeightTrader,
44			};
45
46			pub(crate) fn last_events(n: usize) -> Vec<RuntimeEvent> {
47				System::events()
48					.into_iter()
49					.map(|e| e.event)
50					.rev()
51					.take(n)
52					.rev()
53					.collect()
54			}
55
56			#[test]
57			fn dest_asset_fee_per_second_matches_configured_fee_not_relative_price() {
58				fn set_fee_per_second_for_location(
59					location: Location,
60					fee_per_second: u128,
61				) -> Result<(), ()> {
62					let native_amount_per_second: u128 =
63						<Runtime as pallet_xcm_weight_trader::Config>::WeightToFee::weight_to_fee(
64							&Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0),
65						)
66						.try_into()
67						.map_err(|_| ())?;
68					let precision_factor = 10u128.pow(RELATIVE_PRICE_DECIMALS);
69					let relative_price: u128 = if fee_per_second > 0u128 {
70						native_amount_per_second
71							.saturating_mul(precision_factor)
72							.saturating_div(fee_per_second)
73					} else {
74						0u128
75					};
76					if SupportedAssets::<Runtime>::contains_key(&location) {
77						let enabled = SupportedAssets::<Runtime>::get(&location).ok_or(())?.0;
78						SupportedAssets::<Runtime>::insert(&location, (enabled, relative_price));
79					} else {
80						SupportedAssets::<Runtime>::insert(&location, (true, relative_price));
81					}
82					Ok(())
83				}
84
85				ExtBuilder::default().build().execute_with(|| {
86					// Scenario: the reserve asset is 5x more valuable than the native asset.
87					// The actual fee-per-second on the reserve chain is native_fee_per_second / 5.
88					let native_fee_per_second = WEIGHT_REF_TIME_PER_SECOND as u128;
89					let actual_fee_per_second = native_fee_per_second
90						.checked_div(5)
91						.expect("division by 5 should not overflow");
92
93					let location = Location::parent();
94
95					// Configure weight-trader storage using a helper that writes the relative price.
96					set_fee_per_second_for_location(location.clone(), actual_fee_per_second)
97						.expect("must be able to configure fee per second");
98
99					// dest_asset_fee_per_second must return the true fee-per-second that callers
100					// expect.
101					let reported = XcmTransactor::dest_asset_fee_per_second(&location)
102						.expect("fee should be set");
103
104					assert_eq!(reported, actual_fee_per_second);
105				});
106			}
107
108			#[test]
109			fn claim_assets_works() {
110				const INITIAL_BALANCE: Balance = 10_000_000_000_000_000_000;
111				const SEND_AMOUNT: Balance = 1_000_000_000_000_000_000;
112
113				let alice = AccountId::from(ALICE);
114				let balances = vec![(alice, INITIAL_BALANCE)];
115
116				ExtBuilder::default()
117					.with_balances(balances)
118					.build()
119					.execute_with(|| {
120						let assets = XcmAssets::from((SelfReserve::get(), SEND_AMOUNT));
121						// First trap some assets.
122						let trapping_program =
123							Xcm::builder_unsafe().withdraw_asset(assets.clone()).build();
124						// Even though assets are trapped, the extrinsic returns success.
125						let origin_location =
126							<Runtime as pallet_xcm::Config>::ExecuteXcmOrigin::ensure_origin(
127								RuntimeOrigin::signed(alice),
128							)
129							.expect("qed");
130						let message = Box::new(VersionedXcm::V5(trapping_program));
131						let mut hash = message.using_encoded(sp_io::hashing::blake2_256);
132						let message = (*message).try_into().expect("qed");
133						let _ = <Runtime as pallet_xcm::Config>::XcmExecutor::prepare_and_execute(
134							origin_location,
135							message,
136							&mut hash,
137							Weight::MAX,
138							Weight::MAX,
139						);
140						assert_eq!(
141							Balances::total_balance(&alice),
142							INITIAL_BALANCE - SEND_AMOUNT
143						);
144
145						// Assets were indeed trapped.
146						assert!(last_events(2).iter().any(|evt| matches!(
147							evt,
148							RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. })
149						)));
150
151						// Now claim them with the extrinsic.
152						assert_ok!(PolkadotXcm::claim_assets(
153							RuntimeOrigin::signed(alice),
154							Box::new(VersionedAssets::V5(assets)),
155							Box::new(VersionedLocation::V5(
156								AccountKey20 {
157									network: None,
158									key: alice.clone().into()
159								}
160								.into()
161							)),
162						));
163						// Confirm that trapped assets were claimed back
164						assert_eq!(Balances::total_balance(&alice), INITIAL_BALANCE);
165					});
166			}
167		}
168	};
169}