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