Skip to main content

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, 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, Debug)]
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, Debug)]
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, Debug)]
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, Debug)]
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(Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, Debug)]
125pub enum ConvertMessageError {
126	/// The message version is not supported for conversion.
127	UnsupportedVersion,
128	InvalidDestination,
129	InvalidToken,
130	/// The fee asset is not supported for conversion.
131	UnsupportedFeeAsset,
132	CannotReanchor,
133}
134
135/// convert the inbound message to xcm which will be forwarded to the destination chain
136pub trait ConvertMessage {
137	type Balance: BalanceT + From<u128>;
138	type AccountId;
139	/// Converts a versioned message into an XCM message and an optional topicID
140	fn convert(
141		message_id: H256,
142		message: VersionedMessage,
143	) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
144}
145
146impl<
147		CreateAssetCall,
148		CreateAssetDeposit,
149		InboundQueuePalletInstance,
150		AccountId,
151		Balance,
152		ConvertAssetId,
153		EthereumUniversalLocation,
154		GlobalAssetHubLocation,
155	> ConvertMessage
156	for MessageToXcm<
157		CreateAssetCall,
158		CreateAssetDeposit,
159		InboundQueuePalletInstance,
160		AccountId,
161		Balance,
162		ConvertAssetId,
163		EthereumUniversalLocation,
164		GlobalAssetHubLocation,
165	>
166where
167	CreateAssetCall: Get<CallIndex>,
168	CreateAssetDeposit: Get<u128>,
169	InboundQueuePalletInstance: Get<u8>,
170	Balance: BalanceT + From<u128>,
171	AccountId: Into<[u8; 32]>,
172	ConvertAssetId: MaybeConvert<TokenId, Location>,
173	EthereumUniversalLocation: Get<InteriorLocation>,
174	GlobalAssetHubLocation: Get<Location>,
175{
176	type Balance = Balance;
177	type AccountId = AccountId;
178
179	fn convert(
180		message_id: H256,
181		message: VersionedMessage,
182	) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
183		use Command::*;
184		use VersionedMessage::*;
185		match message {
186			V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => {
187				Ok(Self::convert_register_token(message_id, chain_id, token, fee))
188			},
189			V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) => {
190				Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee))
191			},
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			},
315			// Final destination is a 32-byte account on a sibling of AssetHub
316			Destination::ForeignAccountId32 { para_id, id, fee } => (
317				Some(para_id),
318				Location::new(0, [AccountId32 { network: None, id }]),
319				// Total fee needs to cover execution on AssetHub and Sibling
320				fee,
321			),
322			// Final destination is a 20-byte account on a sibling of AssetHub
323			Destination::ForeignAccountId20 { para_id, id, fee } => (
324				Some(para_id),
325				Location::new(0, [AccountKey20 { network: None, key: id }]),
326				// Total fee needs to cover execution on AssetHub and Sibling
327				fee,
328			),
329		};
330
331		let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
332		let total_fee_asset: Asset = (Location::parent(), total_fees).into();
333		let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
334
335		let mut instructions = vec![
336			ReceiveTeleportedAsset(total_fee_asset.into()),
337			BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
338			DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
339			UniversalOrigin(GlobalConsensus(network)),
340			ReserveAssetDeposited(asset.clone().into()),
341			ClearOrigin,
342		];
343
344		match dest_para_id {
345			Some(dest_para_id) => {
346				let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();
347				let bridge_location = Location::new(2, GlobalConsensus(network));
348
349				instructions.extend(vec![
350					// After program finishes deposit any leftover assets to the snowbridge
351					// sovereign.
352					SetAppendix(Xcm(vec![DepositAsset {
353						assets: Wild(AllCounted(2)),
354						beneficiary: bridge_location,
355					}])),
356					// Perform a deposit reserve to send to destination chain.
357					DepositReserveAsset {
358						// Send over assets and unspent fees, XCM delivery fee will be charged from
359						// here.
360						assets: Wild(AllCounted(2)),
361						dest: Location::new(1, [Parachain(dest_para_id)]),
362						xcm: vec![
363							// Buy execution on target.
364							BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
365							// Deposit assets to beneficiary.
366							DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
367							// Forward message id to destination parachain.
368							SetTopic(message_id.into()),
369						]
370						.into(),
371					},
372				]);
373			},
374			None => {
375				instructions.extend(vec![
376					// Deposit both asset and fees to beneficiary so the fees will not get
377					// trapped. Another benefit is when fees left more than ED on AssetHub could be
378					// used to create the beneficiary account in case it does not exist.
379					DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
380				]);
381			},
382		}
383
384		// Forward message id to Asset Hub.
385		instructions.push(SetTopic(message_id.into()));
386
387		// The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since
388		// they are teleported within `instructions`).
389		(instructions.into(), total_fees.into())
390	}
391
392	// Convert ERC20 token address to a location that can be understood by Assets Hub.
393	fn convert_token_address(network: NetworkId, token: H160) -> Location {
394		if token == H160([0; 20]) {
395			Location::new(2, [GlobalConsensus(network)])
396		} else {
397			Location::new(
398				2,
399				[GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
400			)
401		}
402	}
403
404	/// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign
405	/// account of the Gateway contract and either deposits those assets into a recipient account or
406	/// forwards the assets to another parachain.
407	fn convert_send_native_token(
408		message_id: H256,
409		chain_id: u64,
410		token_id: TokenId,
411		destination: Destination,
412		amount: u128,
413		asset_hub_fee: u128,
414	) -> Result<(Xcm<()>, Balance), ConvertMessageError> {
415		let network = Ethereum { chain_id };
416		let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
417
418		let beneficiary = match destination {
419			// Final destination is a 32-byte account on AssetHub
420			Destination::AccountId32 { id } => {
421				Ok(Location::new(0, [AccountId32 { network: None, id }]))
422			},
423			// Forwarding to a destination parachain is not allowed for PNA and is validated on the
424			// Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224
425			_ => Err(ConvertMessageError::InvalidDestination),
426		}?;
427
428		let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
429
430		let asset_loc =
431			ConvertAssetId::maybe_convert(token_id).ok_or(ConvertMessageError::InvalidToken)?;
432
433		let mut reanchored_asset_loc = asset_loc.clone();
434		reanchored_asset_loc
435			.reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get())
436			.map_err(|_| ConvertMessageError::CannotReanchor)?;
437
438		let asset: Asset = (reanchored_asset_loc, amount).into();
439
440		let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
441
442		let instructions = vec![
443			ReceiveTeleportedAsset(total_fee_asset.clone().into()),
444			BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
445			DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
446			UniversalOrigin(GlobalConsensus(network)),
447			WithdrawAsset(asset.clone().into()),
448			// Deposit both asset and fees to beneficiary so the fees will not get
449			// trapped. Another benefit is when fees left more than ED on AssetHub could be
450			// used to create the beneficiary account in case it does not exist.
451			DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
452			SetTopic(message_id.into()),
453		];
454
455		// `total_fees` to burn on this chain when sending `instructions` to run on AH (which also
456		// teleport fees)
457		Ok((instructions.into(), asset_hub_fee.into()))
458	}
459}
460
461#[cfg(test)]
462mod tests {
463	use crate::{
464		v1::{Command, ConvertMessage, Destination, MessageToXcm, MessageV1, VersionedMessage},
465		CallIndex, EthereumLocationsConverterFor,
466	};
467	use frame_support::{assert_ok, parameter_types};
468	use hex_literal::hex;
469	use snowbridge_test_utils::mock_converter::{
470		add_location_override, reanchor_to_ethereum, LocationIdConvert,
471	};
472	use sp_core::H160;
473	use sp_runtime::{
474		traits::{IdentifyAccount, Verify},
475		MultiSignature,
476	};
477	use xcm::prelude::*;
478	use xcm_executor::traits::ConvertLocation;
479
480	pub const CHAIN_ID: u64 = 1;
481	const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID };
482
483	parameter_types! {
484		pub EthereumNetwork: NetworkId = NETWORK;
485		pub const CreateAssetCall: CallIndex = [1, 1];
486		pub const CreateAssetExecutionFee: u128 = 123;
487		pub const CreateAssetDeposit: u128 = 891;
488		pub const SendTokenExecutionFee: u128 = 592;
489		pub const InboundQueuePalletInstance: u8 = 80;
490		pub EthereumUniversalLocation: InteriorLocation =
491			[GlobalConsensus(NETWORK)].into();
492		pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Polkadot),Parachain(1000)]);
493		pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
494		pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Polkadot),Parachain(1002)].into();
495	}
496
497	type AccountId = <<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId;
498	type Balance = u128;
499
500	pub type MessageConverter = MessageToXcm<
501		CreateAssetCall,
502		CreateAssetDeposit,
503		InboundQueuePalletInstance,
504		AccountId,
505		Balance,
506		LocationIdConvert,
507		EthereumUniversalLocation,
508		AssetHubFromEthereum,
509	>;
510
511	#[test]
512	fn test_contract_location_with_network_converts_successfully() {
513		let expected_account: [u8; 32] =
514			hex!("204dfe37731e8e2b4866ad0da9a17c49f434542c3477c5f914a3349acd88ba1a");
515		let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]);
516
517		let account =
518			EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location)
519				.unwrap();
520		assert_eq!(account, expected_account);
521	}
522
523	#[test]
524	fn test_contract_location_with_incorrect_location_fails_convert() {
525		let contract_location = Location::new(2, [GlobalConsensus(Polkadot), Parachain(1000)]);
526
527		assert_eq!(
528			EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location),
529			None,
530		);
531	}
532
533	#[test]
534	fn test_reanchor_all_assets() {
535		let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into();
536		let ethereum = Location::new(2, ethereum_context.clone());
537		let ah_context: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into();
538		let global_ah = Location::new(1, ah_context.clone());
539		let assets = vec![
540			// DOT
541			Location::new(1, []),
542			// GLMR (Some Polkadot parachain currency)
543			Location::new(1, [Parachain(2004)]),
544			// AH asset
545			Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
546			// KSM
547			Location::new(2, [GlobalConsensus(Kusama)]),
548			// KAR (Some Kusama parachain currency)
549			Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
550		];
551		for asset in assets.iter() {
552			// reanchor logic in pallet_xcm on AH
553			let mut reanchored_asset = asset.clone();
554			assert_ok!(reanchored_asset.reanchor(&ethereum, &ah_context));
555			// reanchor back to original location in context of Ethereum
556			let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
557			assert_ok!(
558				reanchored_asset_with_ethereum_context.reanchor(&global_ah, &ethereum_context)
559			);
560			assert_eq!(reanchored_asset_with_ethereum_context, asset.clone());
561		}
562	}
563
564	#[test]
565	fn test_convert_send_weth() {
566		const WETH: H160 = H160([0xff; 20]);
567		const AMOUNT: u128 = 1_000_000;
568		const FEE: u128 = 1_000;
569		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
570		const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
571			chain_id: CHAIN_ID,
572			command: Command::SendToken {
573				token: WETH,
574				destination: Destination::AccountId32 { id: ACCOUNT_ID },
575				amount: AMOUNT,
576				fee: FEE,
577			},
578		});
579		let result = MessageConverter::convert([1; 32].into(), MESSAGE);
580		assert_ok!(&result);
581		let (xcm, fee) = result.unwrap();
582		assert_eq!(FEE, fee);
583
584		let expected_assets = ReserveAssetDeposited(
585			vec![Asset {
586				id: AssetId(Location {
587					parents: 2,
588					interior: Junctions::X2(
589						[
590							GlobalConsensus(NETWORK),
591							AccountKey20 { network: None, key: WETH.into() },
592						]
593						.into(),
594					),
595				}),
596				fun: Fungible(AMOUNT),
597			}]
598			.into(),
599		);
600		let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
601		assert_eq!(actual_assets, Some(expected_assets))
602	}
603
604	#[test]
605	fn test_convert_send_eth() {
606		const ETH: H160 = H160([0x00; 20]);
607		const AMOUNT: u128 = 1_000_000;
608		const FEE: u128 = 1_000;
609		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
610		const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
611			chain_id: CHAIN_ID,
612			command: Command::SendToken {
613				token: ETH,
614				destination: Destination::AccountId32 { id: ACCOUNT_ID },
615				amount: AMOUNT,
616				fee: FEE,
617			},
618		});
619		let result = MessageConverter::convert([1; 32].into(), MESSAGE);
620		assert_ok!(&result);
621		let (xcm, fee) = result.unwrap();
622		assert_eq!(FEE, fee);
623
624		let expected_assets = ReserveAssetDeposited(
625			vec![Asset {
626				id: AssetId(Location {
627					parents: 2,
628					interior: Junctions::X1([GlobalConsensus(NETWORK)].into()),
629				}),
630				fun: Fungible(AMOUNT),
631			}]
632			.into(),
633		);
634		let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
635		assert_eq!(actual_assets, Some(expected_assets))
636	}
637
638	#[test]
639	fn test_convert_send_dot() {
640		let dot_location = Location::parent();
641		let (token_id, _) = reanchor_to_ethereum(
642			dot_location.clone(),
643			EthereumLocation::get(),
644			BridgeHubContext::get(),
645		);
646		add_location_override(
647			dot_location.clone(),
648			EthereumLocation::get(),
649			BridgeHubContext::get(),
650		);
651		const AMOUNT: u128 = 1_000_000;
652		const FEE: u128 = 1_000;
653		const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
654		let message: VersionedMessage = VersionedMessage::V1(MessageV1 {
655			chain_id: CHAIN_ID,
656			command: Command::SendNativeToken {
657				token_id,
658				destination: Destination::AccountId32 { id: ACCOUNT_ID },
659				amount: AMOUNT,
660				fee: FEE,
661			},
662		});
663
664		let result = MessageConverter::convert([1; 32].into(), message);
665		assert_ok!(&result);
666		let (xcm, fee) = result.unwrap();
667		assert_eq!(FEE, fee);
668
669		let expected_assets = WithdrawAsset(
670			vec![Asset { id: AssetId(Location::parent()), fun: Fungible(AMOUNT) }].into(),
671		);
672		let actual_assets = xcm.into_iter().find(|x| matches!(x, WithdrawAsset(..)));
673		assert_eq!(actual_assets, Some(expected_assets))
674	}
675}