referrerpolicy=no-referrer-when-downgrade

asset_test_utils/
test_cases_over_bridge.rs

1// Copyright (C) 2023 Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Module contains predefined test-case scenarios for `Runtime` with various assets transferred
17//! over a bridge.
18
19use crate::{assert_matches_reserve_asset_deposited_instructions, get_fungible_delivery_fees};
20use codec::Encode;
21use cumulus_primitives_core::XcmpMessageSource;
22use frame_support::{
23	assert_ok,
24	traits::{Currency, Get, OnFinalize, OnInitialize, OriginTrait, ProcessMessageError},
25};
26use frame_system::pallet_prelude::BlockNumberFor;
27use parachains_common::{AccountId, Balance};
28use parachains_runtimes_test_utils::{
29	mock_open_hrmp_channel, AccountIdOf, BalanceOf, CollatorSessionKeys, ExtBuilder, RuntimeHelper,
30	SlotDurations, ValidatorIdOf, XcmReceivedFrom,
31};
32use sp_runtime::{traits::StaticLookup, Saturating};
33use xcm::{latest::prelude::*, VersionedAssetId, VersionedAssets, VersionedXcm};
34use xcm_builder::{CreateMatcher, MatchXcm};
35use xcm_executor::{
36	traits::{ConvertLocation, TransferType},
37	XcmExecutor,
38};
39
40pub struct TestBridgingConfig {
41	pub bridged_network: NetworkId,
42	pub local_bridge_hub_para_id: u32,
43	pub local_bridge_hub_location: Location,
44	pub bridged_target_location: Location,
45}
46
47/// Test-case makes sure that `Runtime` can initiate **reserve transfer assets** over bridge.
48pub fn limited_reserve_transfer_assets_for_native_asset_works<
49	Runtime,
50	AllPalletsWithoutSystem,
51	XcmConfig,
52	HrmpChannelOpener,
53	HrmpChannelSource,
54	LocationToAccountId,
55>(
56	collator_session_keys: CollatorSessionKeys<Runtime>,
57	slot_durations: SlotDurations,
58	existential_deposit: BalanceOf<Runtime>,
59	alice_account: AccountIdOf<Runtime>,
60	unwrap_pallet_xcm_event: Box<dyn Fn(Vec<u8>) -> Option<pallet_xcm::Event<Runtime>>>,
61	unwrap_xcmp_queue_event: Box<
62		dyn Fn(Vec<u8>) -> Option<cumulus_pallet_xcmp_queue::Event<Runtime>>,
63	>,
64	prepare_configuration: fn() -> TestBridgingConfig,
65	weight_limit: WeightLimit,
66	maybe_paid_export_message: Option<AssetId>,
67	delivery_fees_account: Option<AccountIdOf<Runtime>>,
68) where
69	Runtime: frame_system::Config
70		+ pallet_balances::Config
71		+ pallet_session::Config
72		+ pallet_xcm::Config
73		+ parachain_info::Config
74		+ pallet_collator_selection::Config
75		+ cumulus_pallet_parachain_system::Config
76		+ cumulus_pallet_xcmp_queue::Config
77		+ pallet_timestamp::Config,
78	AllPalletsWithoutSystem:
79		OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>,
80	AccountIdOf<Runtime>: Into<[u8; 32]>,
81	ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>,
82	BalanceOf<Runtime>: From<Balance>,
83	<Runtime as pallet_balances::Config>::Balance: From<Balance> + Into<u128>,
84	XcmConfig: xcm_executor::Config,
85	LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>,
86	<Runtime as frame_system::Config>::AccountId:
87		Into<<<Runtime as frame_system::Config>::RuntimeOrigin as OriginTrait>::AccountId>,
88	<<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source:
89		From<<Runtime as frame_system::Config>::AccountId>,
90	<Runtime as frame_system::Config>::AccountId: From<AccountId>,
91	HrmpChannelOpener: frame_support::inherent::ProvideInherent<
92		Call = cumulus_pallet_parachain_system::Call<Runtime>,
93	>,
94	HrmpChannelSource: XcmpMessageSource,
95{
96	let runtime_para_id = 1000;
97	ExtBuilder::<Runtime>::default()
98		.with_collators(collator_session_keys.collators())
99		.with_session_keys(collator_session_keys.session_keys())
100		.with_tracing()
101		.with_safe_xcm_version(3)
102		.with_para_id(runtime_para_id.into())
103		.build()
104		.execute_with(|| {
105			let mut alice = [0u8; 32];
106			alice[0] = 1;
107			let included_head = RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::run_to_block(
108				2,
109				AccountId::from(alice).into(),
110			);
111
112			// prepare bridge config
113			let TestBridgingConfig {
114				bridged_network,
115				local_bridge_hub_para_id,
116				bridged_target_location: target_location_from_different_consensus,
117				..
118			} = prepare_configuration();
119
120			let reserve_account =
121				LocationToAccountId::convert_location(&target_location_from_different_consensus)
122					.expect("Sovereign account for reserves");
123			let balance_to_transfer = 1_000_000_000_000_u128;
124			let native_asset = Location::parent();
125
126			// open HRMP to bridge hub
127			mock_open_hrmp_channel::<Runtime, HrmpChannelOpener>(
128				runtime_para_id.into(),
129				local_bridge_hub_para_id.into(),
130				included_head,
131				&alice,
132				&slot_durations,
133			);
134
135			// we calculate exact delivery fees _after_ sending the message by weighing the sent
136			// xcm, and this delivery fee varies for different runtimes, so just add enough buffer,
137			// then verify the arithmetics check out on final balance.
138			let delivery_fees_buffer = 8_000_000_000_000u128;
139			// drip ED + transfer_amount + delivery_fees_buffer to Alice account
140			let alice_account_init_balance =
141				existential_deposit + balance_to_transfer.into() + delivery_fees_buffer.into();
142			let _ = <pallet_balances::Pallet<Runtime>>::deposit_creating(
143				&alice_account,
144				alice_account_init_balance,
145			);
146			// SA of target location needs to have at least ED, otherwise making reserve fails
147			let _ = <pallet_balances::Pallet<Runtime>>::deposit_creating(
148				&reserve_account,
149				existential_deposit,
150			);
151
152			// we just check here, that user retains enough balance after withdrawal
153			// and also we check if `balance_to_transfer` is more than `existential_deposit`,
154			assert!(
155				(<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account) -
156					balance_to_transfer.into()) >=
157					existential_deposit
158			);
159			// SA has just ED
160			assert_eq!(
161				<pallet_balances::Pallet<Runtime>>::free_balance(&reserve_account),
162				existential_deposit
163			);
164
165			let delivery_fees_account_balance_before = delivery_fees_account
166				.as_ref()
167				.map(|dfa| <pallet_balances::Pallet<Runtime>>::free_balance(dfa))
168				.unwrap_or(0.into());
169
170			// local native asset (pallet_balances)
171			let asset_to_transfer =
172				Asset { fun: Fungible(balance_to_transfer.into()), id: native_asset.into() };
173
174			// destination is (some) account relative to the destination different consensus
175			let target_destination_account = Location::new(
176				0,
177				[AccountId32 {
178					network: Some(bridged_network),
179					id: sp_runtime::AccountId32::new([3; 32]).into(),
180				}],
181			);
182
183			let assets_to_transfer = Assets::from(asset_to_transfer);
184			let mut expected_assets = assets_to_transfer.clone();
185			let context = XcmConfig::UniversalLocation::get();
186			expected_assets
187				.reanchor(&target_location_from_different_consensus, &context)
188				.unwrap();
189
190			let expected_beneficiary = target_destination_account.clone();
191
192			// do cross-chain transfer
193			assert_ok!(<pallet_xcm::Pallet<Runtime>>::transfer_assets_using_type_and_then(
194				RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::origin_of(alice_account.clone()),
195				Box::new(target_location_from_different_consensus.clone().into_versioned()),
196				Box::new(VersionedAssets::from(assets_to_transfer)),
197				Box::new(TransferType::LocalReserve),
198				Box::new(VersionedAssetId::from(AssetId(Location::parent()))),
199				Box::new(TransferType::LocalReserve),
200				Box::new(VersionedXcm::from(
201					Xcm::<()>::builder_unsafe()
202						.deposit_asset(AllCounted(1), target_destination_account)
203						.build()
204				)),
205				weight_limit,
206			));
207
208			// check events
209			// check pallet_xcm attempted
210			RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::assert_pallet_xcm_event_outcome(
211				&unwrap_pallet_xcm_event,
212				|outcome| {
213					assert_ok!(outcome.ensure_complete());
214				},
215			);
216
217			// check that xcm was sent
218			let xcm_sent_message_hash = <frame_system::Pallet<Runtime>>::events()
219				.into_iter()
220				.filter_map(|e| unwrap_xcmp_queue_event(e.event.encode()))
221				.find_map(|e| match e {
222					cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { message_hash } =>
223						Some(message_hash),
224					_ => None,
225				});
226
227			// read xcm
228			let xcm_sent = RuntimeHelper::<HrmpChannelSource, AllPalletsWithoutSystem>::take_xcm(
229				local_bridge_hub_para_id.into(),
230			)
231			.unwrap();
232			assert_eq!(
233				xcm_sent_message_hash,
234				Some(xcm_sent.using_encoded(sp_io::hashing::blake2_256))
235			);
236			let mut xcm_sent: Xcm<()> = xcm_sent.try_into().expect("versioned xcm");
237
238			// check sent XCM ExportMessage to BridgeHub
239
240			let mut delivery_fees = 0;
241			// 1. check paid or unpaid
242			if let Some(expected_fee_asset_id) = maybe_paid_export_message {
243				xcm_sent
244					.0
245					.matcher()
246					.match_next_inst(|instr| match instr {
247						WithdrawAsset(_) => Ok(()),
248						_ => Err(ProcessMessageError::BadFormat),
249					})
250					.expect("contains WithdrawAsset")
251					.match_next_inst(|instr| match instr {
252						BuyExecution { fees, .. } if fees.id.eq(&expected_fee_asset_id) => Ok(()),
253						_ => Err(ProcessMessageError::BadFormat),
254					})
255					.expect("contains BuyExecution")
256					.match_next_inst(|instr| match instr {
257						SetAppendix(_) => Ok(()),
258						_ => Err(ProcessMessageError::BadFormat),
259					})
260					.expect("contains SetAppendix")
261			} else {
262				xcm_sent
263					.0
264					.matcher()
265					.match_next_inst(|instr| match instr {
266						// first instruction could be UnpaidExecution (because we could have
267						// explicit unpaid execution on BridgeHub)
268						UnpaidExecution { weight_limit, check_origin }
269							if weight_limit == &Unlimited && check_origin.is_none() =>
270							Ok(()),
271						_ => Err(ProcessMessageError::BadFormat),
272					})
273					.expect("contains UnpaidExecution")
274			}
275			// 2. check ExportMessage
276			.match_next_inst(|instr| match instr {
277				// next instruction is ExportMessage
278				ExportMessage { network, destination, xcm: inner_xcm } => {
279					assert_eq!(network, &bridged_network);
280					let (_, target_location_junctions_without_global_consensus) =
281						target_location_from_different_consensus
282							.interior
283							.clone()
284							.split_global()
285							.expect("split works");
286					assert_eq!(destination, &target_location_junctions_without_global_consensus);
287					// Call `SendXcm::validate` to get delivery fees.
288					delivery_fees = get_fungible_delivery_fees::<
289						<XcmConfig as xcm_executor::Config>::XcmSender,
290					>(
291						target_location_from_different_consensus.clone(),
292						inner_xcm.clone(),
293					);
294					assert_matches_reserve_asset_deposited_instructions(
295						inner_xcm,
296						&expected_assets,
297						&expected_beneficiary,
298					);
299					Ok(())
300				},
301				_ => Err(ProcessMessageError::BadFormat),
302			})
303			.expect("contains ExportMessage");
304
305			// check alice account decreased by balance_to_transfer
306			assert_eq!(
307				<pallet_balances::Pallet<Runtime>>::free_balance(&alice_account),
308				alice_account_init_balance
309					.saturating_sub(balance_to_transfer.into())
310					.saturating_sub(delivery_fees.into())
311			);
312
313			// check reserve account increased by balance_to_transfer
314			assert_eq!(
315				<pallet_balances::Pallet<Runtime>>::free_balance(&reserve_account),
316				existential_deposit + balance_to_transfer.into()
317			);
318
319			// check dedicated account increased by delivery fees (if configured)
320			if let Some(delivery_fees_account) = delivery_fees_account {
321				let delivery_fees_account_balance_after =
322					<pallet_balances::Pallet<Runtime>>::free_balance(&delivery_fees_account);
323				assert!(
324					delivery_fees_account_balance_after - delivery_fees.into() >=
325						delivery_fees_account_balance_before
326				);
327			}
328		})
329}
330
331pub fn receive_reserve_asset_deposited_from_different_consensus_works<
332	Runtime,
333	AllPalletsWithoutSystem,
334	XcmConfig,
335	ForeignAssetsPalletInstance,
336>(
337	collator_session_keys: CollatorSessionKeys<Runtime>,
338	existential_deposit: BalanceOf<Runtime>,
339	target_account: AccountIdOf<Runtime>,
340	block_author_account: AccountIdOf<Runtime>,
341	(foreign_asset_owner, foreign_asset_id_location, foreign_asset_id_minimum_balance): (
342		AccountIdOf<Runtime>,
343		xcm::v5::Location,
344		u128,
345	),
346	foreign_asset_id_amount_to_transfer: u128,
347	prepare_configuration: impl FnOnce() -> TestBridgingConfig,
348	(bridge_instance, universal_origin, descend_origin): (Junctions, Junction, Junctions), /* bridge adds origin manipulation on the way */
349	additional_checks_before: impl FnOnce(),
350	additional_checks_after: impl FnOnce(),
351) where
352	Runtime: frame_system::Config
353		+ pallet_balances::Config
354		+ pallet_session::Config
355		+ pallet_xcm::Config
356		+ parachain_info::Config
357		+ pallet_collator_selection::Config
358		+ cumulus_pallet_parachain_system::Config
359		+ cumulus_pallet_xcmp_queue::Config
360		+ pallet_assets::Config<ForeignAssetsPalletInstance>
361		+ pallet_timestamp::Config,
362	AllPalletsWithoutSystem:
363		OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>,
364	AccountIdOf<Runtime>: Into<[u8; 32]> + From<[u8; 32]>,
365	ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>,
366	BalanceOf<Runtime>: From<Balance> + Into<Balance>,
367	XcmConfig: xcm_executor::Config,
368	<Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::AssetId:
369		From<xcm::v5::Location> + Into<xcm::v5::Location>,
370	<Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::AssetIdParameter:
371		From<xcm::v5::Location> + Into<xcm::v5::Location>,
372	<Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::Balance:
373		From<Balance> + Into<u128> + From<u128>,
374	<Runtime as frame_system::Config>::AccountId: Into<<<Runtime as frame_system::Config>::RuntimeOrigin as OriginTrait>::AccountId>
375		+ Into<AccountId>,
376	<<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source:
377		From<<Runtime as frame_system::Config>::AccountId>,
378	ForeignAssetsPalletInstance: 'static,
379{
380	ExtBuilder::<Runtime>::default()
381		.with_collators(collator_session_keys.collators())
382		.with_session_keys(collator_session_keys.session_keys())
383		.with_tracing()
384		.build()
385		.execute_with(|| {
386			// Set account as block author, who will receive fees
387			RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::run_to_block(
388				2,
389				block_author_account.clone().into(),
390			);
391
392			// drip 'ED' user target account
393			let _ = <pallet_balances::Pallet<Runtime>>::deposit_creating(
394				&target_account,
395				existential_deposit,
396			);
397
398			// create foreign asset for wrapped/derived representation
399			assert_ok!(
400				<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::force_create(
401					RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::root_origin(),
402					foreign_asset_id_location.clone().into(),
403					foreign_asset_owner.into(),
404					true, // is_sufficient=true
405					foreign_asset_id_minimum_balance.into()
406				)
407			);
408
409			// prepare bridge config
410			let TestBridgingConfig { local_bridge_hub_location, .. } = prepare_configuration();
411
412			// Balances before
413			assert_eq!(
414				<pallet_balances::Pallet<Runtime>>::free_balance(&target_account),
415				existential_deposit.clone()
416			);
417
418			// ForeignAssets balances before
419			assert_eq!(
420				<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
421					foreign_asset_id_location.clone().into(),
422					&target_account
423				),
424				0.into()
425			);
426
427			// additional check before
428			additional_checks_before();
429
430			let expected_assets = Assets::from(vec![Asset {
431				id: AssetId(foreign_asset_id_location.clone()),
432				fun: Fungible(foreign_asset_id_amount_to_transfer),
433			}]);
434			let expected_beneficiary = Location::new(
435				0,
436				[AccountId32 { network: None, id: target_account.clone().into() }],
437			);
438
439			// Call received XCM execution
440			let xcm = Xcm(vec![
441				DescendOrigin(bridge_instance),
442				UniversalOrigin(universal_origin),
443				DescendOrigin(descend_origin),
444				ReserveAssetDeposited(expected_assets.clone()),
445				ClearOrigin,
446				BuyExecution {
447					fees: Asset {
448						id: AssetId(foreign_asset_id_location.clone()),
449						fun: Fungible(foreign_asset_id_amount_to_transfer),
450					},
451					weight_limit: Unlimited,
452				},
453				DepositAsset {
454					assets: Wild(AllCounted(1)),
455					beneficiary: expected_beneficiary.clone(),
456				},
457				SetTopic([
458					220, 188, 144, 32, 213, 83, 111, 175, 44, 210, 111, 19, 90, 165, 191, 112, 140,
459					247, 192, 124, 42, 17, 153, 141, 114, 34, 189, 20, 83, 69, 237, 173,
460				]),
461			]);
462			assert_matches_reserve_asset_deposited_instructions(
463				&mut xcm.clone(),
464				&expected_assets,
465				&expected_beneficiary,
466			);
467
468			let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
469
470			// execute xcm as XcmpQueue would do
471			let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
472				local_bridge_hub_location,
473				xcm,
474				&mut hash,
475				RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::xcm_max_weight(
476					XcmReceivedFrom::Sibling,
477				),
478				Weight::zero(),
479			);
480			assert_ok!(outcome.ensure_complete());
481
482			// Balances after
483			assert_eq!(
484				<pallet_balances::Pallet<Runtime>>::free_balance(&target_account),
485				existential_deposit.clone()
486			);
487
488			// ForeignAssets balances after
489			assert!(
490				<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
491					foreign_asset_id_location.into(),
492					&target_account
493				) > 0.into()
494			);
495
496			// additional check after
497			additional_checks_after();
498		})
499}
500
501pub fn report_bridge_status_from_xcm_bridge_router_works<
502	Runtime,
503	AllPalletsWithoutSystem,
504	XcmConfig,
505	LocationToAccountId,
506	XcmBridgeHubRouterInstance,
507>(
508	collator_session_keys: CollatorSessionKeys<Runtime>,
509	prepare_configuration: fn() -> TestBridgingConfig,
510	congested_message: fn() -> Xcm<XcmConfig::RuntimeCall>,
511	uncongested_message: fn() -> Xcm<XcmConfig::RuntimeCall>,
512) where
513	Runtime: frame_system::Config
514		+ pallet_balances::Config
515		+ pallet_session::Config
516		+ pallet_xcm::Config
517		+ parachain_info::Config
518		+ pallet_collator_selection::Config
519		+ cumulus_pallet_parachain_system::Config
520		+ cumulus_pallet_xcmp_queue::Config
521		+ pallet_xcm_bridge_hub_router::Config<XcmBridgeHubRouterInstance>
522		+ pallet_timestamp::Config,
523	AllPalletsWithoutSystem:
524		OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>,
525	AccountIdOf<Runtime>: Into<[u8; 32]>,
526	ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>,
527	BalanceOf<Runtime>: From<Balance>,
528	<Runtime as pallet_balances::Config>::Balance: From<Balance> + Into<u128>,
529	XcmConfig: xcm_executor::Config,
530	LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>,
531	<Runtime as frame_system::Config>::AccountId:
532		Into<<<Runtime as frame_system::Config>::RuntimeOrigin as OriginTrait>::AccountId>,
533	<<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source:
534		From<<Runtime as frame_system::Config>::AccountId>,
535	<Runtime as frame_system::Config>::AccountId: From<AccountId>,
536	XcmBridgeHubRouterInstance: 'static,
537{
538	ExtBuilder::<Runtime>::default()
539		.with_collators(collator_session_keys.collators())
540		.with_session_keys(collator_session_keys.session_keys())
541		.with_tracing()
542		.build()
543		.execute_with(|| {
544			let report_bridge_status = |is_congested: bool| {
545				// prepare bridge config
546				let TestBridgingConfig { local_bridge_hub_location, .. } = prepare_configuration();
547
548				// Call received XCM execution
549				let xcm = if is_congested { congested_message() } else { uncongested_message() };
550				let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
551
552				// execute xcm as XcmpQueue would do
553				let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
554					local_bridge_hub_location.clone(),
555					xcm,
556					&mut hash,
557					RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::xcm_max_weight(
558						XcmReceivedFrom::Sibling,
559					),
560					Weight::zero(),
561				);
562				assert_ok!(outcome.ensure_complete());
563				assert_eq!(is_congested, pallet_xcm_bridge_hub_router::Pallet::<Runtime, XcmBridgeHubRouterInstance>::bridge().is_congested);
564			};
565
566			report_bridge_status(true);
567			report_bridge_status(false);
568		})
569}