referrerpolicy=no-referrer-when-downgrade

snowbridge_inbound_queue_primitives/
v1.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Converts messages from Ethereum to XCM messages
4
5use crate::{CallIndex, EthereumLocationsConverterFor};
6use codec::{Decode, DecodeWithMemTracking, Encode};
7use core::marker::PhantomData;
8use frame_support::{traits::tokens::Balance as BalanceT, PalletError};
9use scale_info::TypeInfo;
10use snowbridge_core::TokenId;
11use sp_core::{Get, RuntimeDebug, H160, H256};
12use sp_runtime::{traits::MaybeConvert, MultiAddress};
13use sp_std::prelude::*;
14use xcm::prelude::{Junction::AccountKey20, *};
15
16const MINIMUM_DEPOSIT: u128 = 1;
17
18/// Messages from Ethereum are versioned. This is because in future,
19/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly.
20/// Instead having BridgeHub transcode the messages into XCM.
21#[derive(Clone, Encode, Decode, RuntimeDebug)]
22pub enum VersionedMessage {
23	V1(MessageV1),
24}
25
26/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are
27/// self-contained, in that they can be transcoded using only information in the message.
28#[derive(Clone, Encode, Decode, RuntimeDebug)]
29pub struct MessageV1 {
30	/// EIP-155 chain id of the origin Ethereum network
31	pub chain_id: u64,
32	/// The command originating from the Gateway contract
33	pub command: Command,
34}
35
36#[derive(Clone, Encode, Decode, RuntimeDebug)]
37pub enum Command {
38	/// Register a wrapped token on the AssetHub `ForeignAssets` pallet
39	RegisterToken {
40		/// The address of the ERC20 token to be bridged over to AssetHub
41		token: H160,
42		/// XCM execution fee on AssetHub
43		fee: u128,
44	},
45	/// Send Ethereum token to AssetHub or another parachain
46	SendToken {
47		/// The address of the ERC20 token to be bridged over to AssetHub
48		token: H160,
49		/// The destination for the transfer
50		destination: Destination,
51		/// Amount to transfer
52		amount: u128,
53		/// XCM execution fee on AssetHub
54		fee: u128,
55	},
56	/// Send Polkadot token back to the original parachain
57	SendNativeToken {
58		/// The Id of the token
59		token_id: TokenId,
60		/// The destination for the transfer
61		destination: Destination,
62		/// Amount to transfer
63		amount: u128,
64		/// XCM execution fee on AssetHub
65		fee: u128,
66	},
67}
68
69/// Destination for bridged tokens
70#[derive(Clone, Encode, Decode, RuntimeDebug)]
71pub enum Destination {
72	/// The funds will be deposited into account `id` on AssetHub
73	AccountId32 { id: [u8; 32] },
74	/// The funds will deposited into the sovereign account of destination parachain `para_id` on
75	/// AssetHub, Account `id` on the destination parachain will receive the funds via a
76	/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
77	ForeignAccountId32 {
78		para_id: u32,
79		id: [u8; 32],
80		/// XCM execution fee on final destination
81		fee: u128,
82	},
83	/// The funds will deposited into the sovereign account of destination parachain `para_id` on
84	/// AssetHub, Account `id` on the destination parachain will receive the funds via a
85	/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
86	ForeignAccountId20 {
87		para_id: u32,
88		id: [u8; 20],
89		/// XCM execution fee on final destination
90		fee: u128,
91	},
92}
93
94pub struct MessageToXcm<
95	CreateAssetCall,
96	CreateAssetDeposit,
97	InboundQueuePalletInstance,
98	AccountId,
99	Balance,
100	ConvertAssetId,
101	EthereumUniversalLocation,
102	GlobalAssetHubLocation,
103> where
104	CreateAssetCall: Get<CallIndex>,
105	CreateAssetDeposit: Get<u128>,
106	Balance: BalanceT,
107	ConvertAssetId: MaybeConvert<TokenId, Location>,
108	EthereumUniversalLocation: Get<InteriorLocation>,
109	GlobalAssetHubLocation: Get<Location>,
110{
111	_phantom: PhantomData<(
112		CreateAssetCall,
113		CreateAssetDeposit,
114		InboundQueuePalletInstance,
115		AccountId,
116		Balance,
117		ConvertAssetId,
118		EthereumUniversalLocation,
119		GlobalAssetHubLocation,
120	)>,
121}
122
123/// Reason why a message conversion failed.
124#[derive(
125	Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, RuntimeDebug,
126)]
127pub enum ConvertMessageError {
128	/// The message version is not supported for conversion.
129	UnsupportedVersion,
130	InvalidDestination,
131	InvalidToken,
132	/// The fee asset is not supported for conversion.
133	UnsupportedFeeAsset,
134	CannotReanchor,
135}
136
137/// convert the inbound message to xcm which will be forwarded to the destination chain
138pub trait ConvertMessage {
139	type Balance: BalanceT + From<u128>;
140	type AccountId;
141	/// Converts a versioned message into an XCM message and an optional topicID
142	fn convert(
143		message_id: H256,
144		message: VersionedMessage,
145	) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
146}
147
148impl<
149		CreateAssetCall,
150		CreateAssetDeposit,
151		InboundQueuePalletInstance,
152		AccountId,
153		Balance,
154		ConvertAssetId,
155		EthereumUniversalLocation,
156		GlobalAssetHubLocation,
157	> ConvertMessage
158	for MessageToXcm<
159		CreateAssetCall,
160		CreateAssetDeposit,
161		InboundQueuePalletInstance,
162		AccountId,
163		Balance,
164		ConvertAssetId,
165		EthereumUniversalLocation,
166		GlobalAssetHubLocation,
167	>
168where
169	CreateAssetCall: Get<CallIndex>,
170	CreateAssetDeposit: Get<u128>,
171	InboundQueuePalletInstance: Get<u8>,
172	Balance: BalanceT + From<u128>,
173	AccountId: Into<[u8; 32]>,
174	ConvertAssetId: MaybeConvert<TokenId, Location>,
175	EthereumUniversalLocation: Get<InteriorLocation>,
176	GlobalAssetHubLocation: Get<Location>,
177{
178	type Balance = Balance;
179	type AccountId = AccountId;
180
181	fn convert(
182		message_id: H256,
183		message: VersionedMessage,
184	) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
185		use Command::*;
186		use VersionedMessage::*;
187		match message {
188			V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
189				Ok(Self::convert_register_token(message_id, chain_id, token, fee)),
190			V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
191				Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)),
192			V1(MessageV1 {
193				chain_id,
194				command: SendNativeToken { token_id, destination, amount, fee },
195			}) => Self::convert_send_native_token(
196				message_id,
197				chain_id,
198				token_id,
199				destination,
200				amount,
201				fee,
202			),
203		}
204	}
205}
206
207impl<
208		CreateAssetCall,
209		CreateAssetDeposit,
210		InboundQueuePalletInstance,
211		AccountId,
212		Balance,
213		ConvertAssetId,
214		EthereumUniversalLocation,
215		GlobalAssetHubLocation,
216	>
217	MessageToXcm<
218		CreateAssetCall,
219		CreateAssetDeposit,
220		InboundQueuePalletInstance,
221		AccountId,
222		Balance,
223		ConvertAssetId,
224		EthereumUniversalLocation,
225		GlobalAssetHubLocation,
226	>
227where
228	CreateAssetCall: Get<CallIndex>,
229	CreateAssetDeposit: Get<u128>,
230	InboundQueuePalletInstance: Get<u8>,
231	Balance: BalanceT + From<u128>,
232	AccountId: Into<[u8; 32]>,
233	ConvertAssetId: MaybeConvert<TokenId, Location>,
234	EthereumUniversalLocation: Get<InteriorLocation>,
235	GlobalAssetHubLocation: Get<Location>,
236{
237	fn convert_register_token(
238		message_id: H256,
239		chain_id: u64,
240		token: H160,
241		fee: u128,
242	) -> (Xcm<()>, Balance) {
243		let network = Ethereum { chain_id };
244		let xcm_fee: Asset = (Location::parent(), fee).into();
245		let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into();
246
247		let total_amount = fee + CreateAssetDeposit::get();
248		let total: Asset = (Location::parent(), total_amount).into();
249
250		let bridge_location = Location::new(2, GlobalConsensus(network));
251
252		let owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&chain_id);
253		let asset_id = Self::convert_token_address(network, token);
254		let create_call_index: [u8; 2] = CreateAssetCall::get();
255		let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
256
257		let xcm: Xcm<()> = vec![
258			// Teleport required fees.
259			ReceiveTeleportedAsset(total.into()),
260			// Pay for execution.
261			BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
262			// Fund the snowbridge sovereign with the required deposit for creation.
263			DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() },
264			// This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be
265			// deposited to snowbridge sovereign, instead of being trapped, regardless of
266			// `Transact` success or not.
267			SetAppendix(Xcm(vec![
268				RefundSurplus,
269				DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
270			])),
271			// Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`.
272			DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
273			// Change origin to the bridge.
274			UniversalOrigin(GlobalConsensus(network)),
275			// Call create_asset on foreign assets pallet.
276			Transact {
277				origin_kind: OriginKind::Xcm,
278				fallback_max_weight: Some(Weight::from_parts(400_000_000, 8_000)),
279				call: (
280					create_call_index,
281					asset_id,
282					MultiAddress::<[u8; 32], ()>::Id(owner),
283					MINIMUM_DEPOSIT,
284				)
285					.encode()
286					.into(),
287			},
288			// Forward message id to Asset Hub
289			SetTopic(message_id.into()),
290			// Once the program ends here, appendix program will run, which will deposit any
291			// leftover fee to snowbridge sovereign.
292		]
293		.into();
294
295		(xcm, total_amount.into())
296	}
297
298	fn convert_send_token(
299		message_id: H256,
300		chain_id: u64,
301		token: H160,
302		destination: Destination,
303		amount: u128,
304		asset_hub_fee: u128,
305	) -> (Xcm<()>, Balance) {
306		let network = Ethereum { chain_id };
307		let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
308		let asset: Asset = (Self::convert_token_address(network, token), amount).into();
309
310		let (dest_para_id, beneficiary, dest_para_fee) = match destination {
311			// Final destination is a 32-byte account on AssetHub
312			Destination::AccountId32 { id } =>
313				(None, Location::new(0, [AccountId32 { network: None, id }]), 0),
314			// Final destination is a 32-byte account on a sibling of AssetHub
315			Destination::ForeignAccountId32 { para_id, id, fee } => (
316				Some(para_id),
317				Location::new(0, [AccountId32 { network: None, id }]),
318				// Total fee needs to cover execution on AssetHub and Sibling
319				fee,
320			),
321			// Final destination is a 20-byte account on a sibling of AssetHub
322			Destination::ForeignAccountId20 { para_id, id, fee } => (
323				Some(para_id),
324				Location::new(0, [AccountKey20 { network: None, key: id }]),
325				// Total fee needs to cover execution on AssetHub and Sibling
326				fee,
327			),
328		};
329
330		let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
331		let total_fee_asset: Asset = (Location::parent(), total_fees).into();
332		let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
333
334		let mut instructions = vec![
335			ReceiveTeleportedAsset(total_fee_asset.into()),
336			BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
337			DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
338			UniversalOrigin(GlobalConsensus(network)),
339			ReserveAssetDeposited(asset.clone().into()),
340			ClearOrigin,
341		];
342
343		match dest_para_id {
344			Some(dest_para_id) => {
345				let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();
346				let bridge_location = Location::new(2, GlobalConsensus(network));
347
348				instructions.extend(vec![
349					// After program finishes deposit any leftover assets to the snowbridge
350					// sovereign.
351					SetAppendix(Xcm(vec![DepositAsset {
352						assets: Wild(AllCounted(2)),
353						beneficiary: bridge_location,
354					}])),
355					// Perform a deposit reserve to send to destination chain.
356					DepositReserveAsset {
357						// Send over assets and unspent fees, XCM delivery fee will be charged from
358						// here.
359						assets: Wild(AllCounted(2)),
360						dest: Location::new(1, [Parachain(dest_para_id)]),
361						xcm: vec![
362							// Buy execution on target.
363							BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
364							// Deposit assets to beneficiary.
365							DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
366							// Forward message id to destination parachain.
367							SetTopic(message_id.into()),
368						]
369						.into(),
370					},
371				]);
372			},
373			None => {
374				instructions.extend(vec![
375					// Deposit both asset and fees to beneficiary so the fees will not get
376					// trapped. Another benefit is when fees left more than ED on AssetHub could be
377					// used to create the beneficiary account in case it does not exist.
378					DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
379				]);
380			},
381		}
382
383		// Forward message id to Asset Hub.
384		instructions.push(SetTopic(message_id.into()));
385
386		// The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since
387		// they are teleported within `instructions`).
388		(instructions.into(), total_fees.into())
389	}
390
391	// Convert ERC20 token address to a location that can be understood by Assets Hub.
392	fn convert_token_address(network: NetworkId, token: H160) -> Location {
393		if token == H160([0; 20]) {
394			Location::new(2, [GlobalConsensus(network)])
395		} else {
396			Location::new(
397				2,
398				[GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
399			)
400		}
401	}
402
403	/// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign
404	/// account of the Gateway contract and either deposits those assets into a recipient account or
405	/// forwards the assets to another parachain.
406	fn convert_send_native_token(
407		message_id: H256,
408		chain_id: u64,
409		token_id: TokenId,
410		destination: Destination,
411		amount: u128,
412		asset_hub_fee: u128,
413	) -> Result<(Xcm<()>, Balance), ConvertMessageError> {
414		let network = Ethereum { chain_id };
415		let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
416
417		let beneficiary = match destination {
418			// Final destination is a 32-byte account on AssetHub
419			Destination::AccountId32 { id } =>
420				Ok(Location::new(0, [AccountId32 { network: None, id }])),
421			// Forwarding to a destination parachain is not allowed for PNA and is validated on the
422			// Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224
423			_ => Err(ConvertMessageError::InvalidDestination),
424		}?;
425
426		let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
427
428		let asset_loc =
429			ConvertAssetId::maybe_convert(token_id).ok_or(ConvertMessageError::InvalidToken)?;
430
431		let mut reanchored_asset_loc = asset_loc.clone();
432		reanchored_asset_loc
433			.reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get())
434			.map_err(|_| ConvertMessageError::CannotReanchor)?;
435
436		let asset: Asset = (reanchored_asset_loc, amount).into();
437
438		let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
439
440		let instructions = vec![
441			ReceiveTeleportedAsset(total_fee_asset.clone().into()),
442			BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
443			DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
444			UniversalOrigin(GlobalConsensus(network)),
445			WithdrawAsset(asset.clone().into()),
446			// Deposit both asset and fees to beneficiary so the fees will not get
447			// trapped. Another benefit is when fees left more than ED on AssetHub could be
448			// used to create the beneficiary account in case it does not exist.
449			DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
450			SetTopic(message_id.into()),
451		];
452
453		// `total_fees` to burn on this chain when sending `instructions` to run on AH (which also
454		// teleport fees)
455		Ok((instructions.into(), asset_hub_fee.into()))
456	}
457}
458
459#[cfg(test)]
460mod tests {
461	use crate::{
462		v1::{Command, ConvertMessage, Destination, MessageToXcm, MessageV1, VersionedMessage},
463		CallIndex, EthereumLocationsConverterFor,
464	};
465	use frame_support::{assert_ok, parameter_types};
466	use hex_literal::hex;
467	use snowbridge_test_utils::mock_converter::{
468		add_location_override, reanchor_to_ethereum, LocationIdConvert,
469	};
470	use sp_core::H160;
471	use sp_runtime::{
472		traits::{IdentifyAccount, Verify},
473		MultiSignature,
474	};
475	use xcm::prelude::*;
476	use xcm_executor::traits::ConvertLocation;
477
478	pub const CHAIN_ID: u64 = 1;
479	const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID };
480
481	parameter_types! {
482		pub EthereumNetwork: NetworkId = NETWORK;
483		pub const CreateAssetCall: CallIndex = [1, 1];
484		pub const CreateAssetExecutionFee: u128 = 123;
485		pub const CreateAssetDeposit: u128 = 891;
486		pub const SendTokenExecutionFee: u128 = 592;
487		pub const InboundQueuePalletInstance: u8 = 80;
488		pub EthereumUniversalLocation: InteriorLocation =
489			[GlobalConsensus(NETWORK)].into();
490		pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Polkadot),Parachain(1000)]);
491		pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
492		pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Polkadot),Parachain(1002)].into();
493	}
494
495	type AccountId = <<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId;
496	type Balance = u128;
497
498	pub type MessageConverter = MessageToXcm<
499		CreateAssetCall,
500		CreateAssetDeposit,
501		InboundQueuePalletInstance,
502		AccountId,
503		Balance,
504		LocationIdConvert,
505		EthereumUniversalLocation,
506		AssetHubFromEthereum,
507	>;
508
509	#[test]
510	fn test_contract_location_with_network_converts_successfully() {
511		let expected_account: [u8; 32] =
512			hex!("204dfe37731e8e2b4866ad0da9a17c49f434542c3477c5f914a3349acd88ba1a");
513		let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]);
514
515		let account =
516			EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location)
517				.unwrap();
518		assert_eq!(account, expected_account);
519	}
520
521	#[test]
522	fn test_contract_location_with_incorrect_location_fails_convert() {
523		let contract_location = Location::new(2, [GlobalConsensus(Polkadot), Parachain(1000)]);
524
525		assert_eq!(
526			EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location),
527			None,
528		);
529	}
530
531	#[test]
532	fn test_reanchor_all_assets() {
533		let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into();
534		let ethereum = Location::new(2, ethereum_context.clone());
535		let ah_context: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into();
536		let global_ah = Location::new(1, ah_context.clone());
537		let assets = vec![
538			// DOT
539			Location::new(1, []),
540			// GLMR (Some Polkadot parachain currency)
541			Location::new(1, [Parachain(2004)]),
542			// AH asset
543			Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
544			// KSM
545			Location::new(2, [GlobalConsensus(Kusama)]),
546			// KAR (Some Kusama parachain currency)
547			Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
548		];
549		for asset in assets.iter() {
550			// reanchor logic in pallet_xcm on AH
551			let mut reanchored_asset = asset.clone();
552			assert_ok!(reanchored_asset.reanchor(&ethereum, &ah_context));
553			// reanchor back to original location in context of Ethereum
554			let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
555			assert_ok!(
556				reanchored_asset_with_ethereum_context.reanchor(&global_ah, &ethereum_context)
557			);
558			assert_eq!(reanchored_asset_with_ethereum_context, asset.clone());
559		}
560	}
561
562	#[test]
563	fn test_convert_send_weth() {
564		const WETH: H160 = H160([0xff; 20]);
565		const AMOUNT: u128 = 1_000_000;
566		const FEE: u128 = 1_000;
567		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
568		const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
569			chain_id: CHAIN_ID,
570			command: Command::SendToken {
571				token: WETH,
572				destination: Destination::AccountId32 { id: ACCOUNT_ID },
573				amount: AMOUNT,
574				fee: FEE,
575			},
576		});
577		let result = MessageConverter::convert([1; 32].into(), MESSAGE);
578		assert_ok!(&result);
579		let (xcm, fee) = result.unwrap();
580		assert_eq!(FEE, fee);
581
582		let expected_assets = ReserveAssetDeposited(
583			vec![Asset {
584				id: AssetId(Location {
585					parents: 2,
586					interior: Junctions::X2(
587						[
588							GlobalConsensus(NETWORK),
589							AccountKey20 { network: None, key: WETH.into() },
590						]
591						.into(),
592					),
593				}),
594				fun: Fungible(AMOUNT),
595			}]
596			.into(),
597		);
598		let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
599		assert_eq!(actual_assets, Some(expected_assets))
600	}
601
602	#[test]
603	fn test_convert_send_eth() {
604		const ETH: H160 = H160([0x00; 20]);
605		const AMOUNT: u128 = 1_000_000;
606		const FEE: u128 = 1_000;
607		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
608		const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
609			chain_id: CHAIN_ID,
610			command: Command::SendToken {
611				token: ETH,
612				destination: Destination::AccountId32 { id: ACCOUNT_ID },
613				amount: AMOUNT,
614				fee: FEE,
615			},
616		});
617		let result = MessageConverter::convert([1; 32].into(), MESSAGE);
618		assert_ok!(&result);
619		let (xcm, fee) = result.unwrap();
620		assert_eq!(FEE, fee);
621
622		let expected_assets = ReserveAssetDeposited(
623			vec![Asset {
624				id: AssetId(Location {
625					parents: 2,
626					interior: Junctions::X1([GlobalConsensus(NETWORK)].into()),
627				}),
628				fun: Fungible(AMOUNT),
629			}]
630			.into(),
631		);
632		let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
633		assert_eq!(actual_assets, Some(expected_assets))
634	}
635
636	#[test]
637	fn test_convert_send_dot() {
638		let dot_location = Location::parent();
639		let (token_id, _) = reanchor_to_ethereum(
640			dot_location.clone(),
641			EthereumLocation::get(),
642			BridgeHubContext::get(),
643		);
644		add_location_override(
645			dot_location.clone(),
646			EthereumLocation::get(),
647			BridgeHubContext::get(),
648		);
649		const AMOUNT: u128 = 1_000_000;
650		const FEE: u128 = 1_000;
651		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
652		let message: VersionedMessage = VersionedMessage::V1(MessageV1 {
653			chain_id: CHAIN_ID,
654			command: Command::SendNativeToken {
655				token_id,
656				destination: Destination::AccountId32 { id: ACCOUNT_ID },
657				amount: AMOUNT,
658				fee: FEE,
659			},
660		});
661
662		let result = MessageConverter::convert([1; 32].into(), message);
663		assert_ok!(&result);
664		let (xcm, fee) = result.unwrap();
665		assert_eq!(FEE, fee);
666
667		let expected_assets = WithdrawAsset(
668			vec![Asset { id: AssetId(Location::parent()), fun: Fungible(AMOUNT) }].into(),
669		);
670		let actual_assets = xcm.into_iter().find(|x| matches!(x, WithdrawAsset(..)));
671		assert_eq!(actual_assets, Some(expected_assets))
672	}
673}