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