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]
22
macro_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
3
			pub(crate) fn last_events(n: usize) -> Vec<RuntimeEvent> {
47
3
				System::events()
48
3
					.into_iter()
49
3
					.map(|e| e.event)
50
3
					.rev()
51
3
					.take(n)
52
3
					.rev()
53
3
					.collect()
54
3
			}
55

            
56
			#[test]
57
3
			fn dest_asset_fee_per_second_matches_configured_fee_not_relative_price() {
58
3
				fn set_fee_per_second_for_location(
59
3
					location: Location,
60
3
					fee_per_second: u128,
61
3
				) -> Result<(), ()> {
62
3
					let native_amount_per_second: u128 =
63
3
						<Runtime as pallet_xcm_weight_trader::Config>::WeightToFee::weight_to_fee(
64
3
							&Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0),
65
						)
66
3
						.try_into()
67
3
						.map_err(|_| ())?;
68
3
					let precision_factor = 10u128.pow(RELATIVE_PRICE_DECIMALS);
69
3
					let relative_price: u128 = if fee_per_second > 0u128 {
70
3
						native_amount_per_second
71
3
							.saturating_mul(precision_factor)
72
3
							.saturating_div(fee_per_second)
73
					} else {
74
						0u128
75
					};
76
3
					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
3
					} else {
80
3
						SupportedAssets::<Runtime>::insert(&location, (true, relative_price));
81
3
					}
82
3
					Ok(())
83
3
				}
84

            
85
3
				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
3
					let native_fee_per_second = WEIGHT_REF_TIME_PER_SECOND as u128;
89
3
					let actual_fee_per_second = native_fee_per_second
90
3
						.checked_div(5)
91
3
						.expect("division by 5 should not overflow");
92

            
93
3
					let location = Location::parent();
94

            
95
					// Configure weight-trader storage using a helper that writes the relative price.
96
3
					set_fee_per_second_for_location(location.clone(), actual_fee_per_second)
97
3
						.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
3
					let reported = XcmTransactor::dest_asset_fee_per_second(&location)
102
3
						.expect("fee should be set");
103

            
104
3
					assert_eq!(reported, actual_fee_per_second);
105
3
				});
106
3
			}
107

            
108
			#[test]
109
3
			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
3
				let alice = AccountId::from(ALICE);
114
3
				let balances = vec![(alice, INITIAL_BALANCE)];
115

            
116
3
				ExtBuilder::default()
117
3
					.with_balances(balances)
118
3
					.build()
119
3
					.execute_with(|| {
120
3
						let assets = XcmAssets::from((SelfReserve::get(), SEND_AMOUNT));
121
						// First trap some assets.
122
3
						let trapping_program =
123
3
							Xcm::builder_unsafe().withdraw_asset(assets.clone()).build();
124
						// Even though assets are trapped, the extrinsic returns success.
125
3
						let origin_location =
126
3
							<Runtime as pallet_xcm::Config>::ExecuteXcmOrigin::ensure_origin(
127
3
								RuntimeOrigin::signed(alice),
128
							)
129
3
							.expect("qed");
130
3
						let message = Box::new(VersionedXcm::V5(trapping_program));
131
3
						let mut hash = message.using_encoded(sp_io::hashing::blake2_256);
132
3
						let message = (*message).try_into().expect("qed");
133
3
						let _ = <Runtime as pallet_xcm::Config>::XcmExecutor::prepare_and_execute(
134
3
							origin_location,
135
3
							message,
136
3
							&mut hash,
137
3
							Weight::MAX,
138
3
							Weight::MAX,
139
3
						);
140
3
						assert_eq!(
141
3
							Balances::total_balance(&alice),
142
							INITIAL_BALANCE - SEND_AMOUNT
143
						);
144

            
145
						// Assets were indeed trapped.
146
3
						assert!(last_events(2).iter().any(|evt| matches!(
147
3
							evt,
148
							RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. })
149
						)));
150

            
151
						// Now claim them with the extrinsic.
152
3
						assert_ok!(PolkadotXcm::claim_assets(
153
3
							RuntimeOrigin::signed(alice),
154
3
							Box::new(VersionedAssets::V5(assets)),
155
3
							Box::new(VersionedLocation::V5(
156
3
								AccountKey20 {
157
3
									network: None,
158
3
									key: alice.clone().into()
159
3
								}
160
3
								.into()
161
3
							)),
162
						));
163
						// Confirm that trapped assets were claimed back
164
3
						assert_eq!(Balances::total_balance(&alice), INITIAL_BALANCE);
165
3
					});
166
3
			}
167
		}
168
	};
169
}