1use super::{message::*, traits::*};
6use crate::{v2::LOG_TARGET, CallIndex};
7use codec::{Decode, DecodeLimit, Encode};
8use core::marker::PhantomData;
9use frame_support::ensure;
10use snowbridge_core::TokenId;
11use sp_core::{Get, RuntimeDebug, H160};
12use sp_io::hashing::blake2_256;
13use sp_runtime::{traits::MaybeConvert, MultiAddress};
14use sp_std::prelude::*;
15use xcm::{
16 prelude::{Junction::*, *},
17 MAX_XCM_DECODE_DEPTH,
18};
19use xcm_builder::ExternalConsensusLocationsConverterFor;
20use xcm_executor::traits::ConvertLocation;
21
22const MINIMUM_DEPOSIT: u128 = 1;
23
24const INBOUND_QUEUE_TOPIC_PREFIX: &str = "SnowbridgeInboundQueueV2";
26
27#[derive(Clone, RuntimeDebug, Encode)]
30pub struct PreparedMessage {
31 pub origin: H160,
33 pub claimer: Location,
35 pub assets: Vec<AssetTransfer>,
37 pub remote_xcm: Xcm<()>,
39 pub execution_fee: Asset,
41}
42
43#[derive(Clone, RuntimeDebug, Encode)]
45pub enum AssetTransfer {
46 ReserveDeposit(Asset),
47 ReserveWithdraw(Asset),
48}
49
50pub struct MessageToXcm<
52 CreateAssetCall,
53 CreateAssetDeposit,
54 EthereumNetwork,
55 InboundQueueLocation,
56 ConvertAssetId,
57 GatewayProxyAddress,
58 EthereumUniversalLocation,
59 AssetHubFromEthereum,
60 AssetHubUniversalLocation,
61 AccountId,
62> {
63 _phantom: PhantomData<(
64 CreateAssetCall,
65 CreateAssetDeposit,
66 EthereumNetwork,
67 InboundQueueLocation,
68 ConvertAssetId,
69 GatewayProxyAddress,
70 EthereumUniversalLocation,
71 AssetHubFromEthereum,
72 AssetHubUniversalLocation,
73 AccountId,
74 )>,
75}
76
77impl<
78 CreateAssetCall,
79 CreateAssetDeposit,
80 EthereumNetwork,
81 InboundQueueLocation,
82 ConvertAssetId,
83 GatewayProxyAddress,
84 EthereumUniversalLocation,
85 AssetHubFromEthereum,
86 AssetHubUniversalLocation,
87 AccountId,
88 >
89 MessageToXcm<
90 CreateAssetCall,
91 CreateAssetDeposit,
92 EthereumNetwork,
93 InboundQueueLocation,
94 ConvertAssetId,
95 GatewayProxyAddress,
96 EthereumUniversalLocation,
97 AssetHubFromEthereum,
98 AssetHubUniversalLocation,
99 AccountId,
100 >
101where
102 CreateAssetCall: Get<CallIndex>,
103 CreateAssetDeposit: Get<u128>,
104 EthereumNetwork: Get<NetworkId>,
105 InboundQueueLocation: Get<InteriorLocation>,
106 ConvertAssetId: MaybeConvert<TokenId, Location>,
107 GatewayProxyAddress: Get<H160>,
108 EthereumUniversalLocation: Get<InteriorLocation>,
109 AssetHubFromEthereum: Get<Location>,
110 AssetHubUniversalLocation: Get<InteriorLocation>,
111 AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone,
112{
113 fn prepare(message: Message) -> Result<PreparedMessage, ConvertMessageError> {
116 let ether_location = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
118 let bridge_owner = Self::bridge_owner()?;
119
120 let claimer = message
121 .claimer
122 .and_then(|claimer_bytes| Location::decode(&mut claimer_bytes.as_ref()).ok())
124 .unwrap_or_else(|| {
126 Location::new(0, [AccountId32 { network: None, id: bridge_owner.clone().into() }])
127 });
128
129 let mut remote_xcm: Xcm<()> = match &message.xcm {
130 XcmPayload::Raw(raw) => Self::decode_raw_xcm(raw),
131 XcmPayload::CreateAsset { token, network } => Self::make_create_asset_xcm(
132 token,
133 *network,
134 message.value,
135 bridge_owner,
136 claimer.clone(),
137 )?,
138 };
139
140 let execution_fee_asset: Asset = (ether_location.clone(), message.execution_fee).into();
142
143 let mut assets = vec![];
144
145 if message.value > 0 {
146 let remaining_ether_asset: Asset = (ether_location.clone(), message.value).into();
148 assets.push(AssetTransfer::ReserveDeposit(remaining_ether_asset));
149 }
150
151 for asset in &message.assets {
152 match asset {
153 EthereumAsset::NativeTokenERC20 { token_id, value } => {
154 ensure!(*token_id != H160::zero(), ConvertMessageError::InvalidAsset);
155 let token_location: Location = Location::new(
156 2,
157 [
158 GlobalConsensus(EthereumNetwork::get()),
159 AccountKey20 { network: None, key: (*token_id).into() },
160 ],
161 );
162 let asset: Asset = (token_location, *value).into();
163 assets.push(AssetTransfer::ReserveDeposit(asset));
164 },
165 EthereumAsset::ForeignTokenERC20 { token_id, value } => {
166 let asset_loc = ConvertAssetId::maybe_convert(*token_id)
167 .ok_or(ConvertMessageError::InvalidAsset)?;
168 let reanchored_asset_loc = asset_loc
169 .reanchored(&AssetHubFromEthereum::get(), &EthereumUniversalLocation::get())
170 .map_err(|_| ConvertMessageError::CannotReanchor)?;
171 let asset: Asset = (reanchored_asset_loc, *value).into();
172 assets.push(AssetTransfer::ReserveWithdraw(asset));
173 },
174 }
175 }
176
177 if !matches!(remote_xcm.0.last(), Some(SetTopic(_))) {
179 let topic = blake2_256(&(INBOUND_QUEUE_TOPIC_PREFIX, message.nonce).encode());
180 remote_xcm.0.push(SetTopic(topic));
181 }
182
183 let prepared_message = PreparedMessage {
184 origin: message.origin,
185 claimer,
186 assets,
187 remote_xcm,
188 execution_fee: execution_fee_asset,
189 };
190
191 Ok(prepared_message)
192 }
193
194 fn bridge_owner() -> Result<AccountId, ConvertMessageError> {
196 let account =
197 ExternalConsensusLocationsConverterFor::<AssetHubUniversalLocation, AccountId>::convert_location(
198 &Location::new(2, [GlobalConsensus(EthereumNetwork::get())]),
199 )
200 .ok_or(ConvertMessageError::CannotReanchor)?;
201
202 Ok(account)
203 }
204
205 fn make_create_asset_xcm(
208 token: &H160,
209 network: super::message::Network,
210 eth_value: u128,
211 bridge_owner: AccountId,
212 claimer: Location,
213 ) -> Result<Xcm<()>, ConvertMessageError> {
214 let dot_asset = Location::new(1, Here);
215 let dot_fee: xcm::prelude::Asset = (dot_asset, CreateAssetDeposit::get()).into();
216
217 let eth_asset: xcm::prelude::Asset =
218 (Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), eth_value).into();
219
220 let create_call_index: [u8; 2] = CreateAssetCall::get();
221
222 let asset_id = Location::new(
223 2,
224 [
225 GlobalConsensus(EthereumNetwork::get()),
226 AccountKey20 { network: None, key: (*token).into() },
227 ],
228 );
229
230 match network {
231 super::message::Network::Polkadot => Ok(Self::make_create_asset_xcm_for_polkadot(
232 create_call_index,
233 asset_id,
234 bridge_owner,
235 dot_fee,
236 eth_asset,
237 claimer,
238 )),
239 }
240 }
241
242 fn make_create_asset_xcm_for_polkadot(
244 create_call_index: [u8; 2],
245 asset_id: Location,
246 bridge_owner: AccountId,
247 dot_fee_asset: xcm::prelude::Asset,
248 eth_asset: xcm::prelude::Asset,
249 claimer: Location,
250 ) -> Xcm<()> {
251 let bridge_owner_bytes: [u8; 32] = bridge_owner.into();
252 vec![
253 ExchangeAsset {
255 give: eth_asset.into(),
256 want: dot_fee_asset.clone().into(),
257 maximal: false,
258 },
259 DepositAsset {
262 assets: dot_fee_asset.clone().into(),
263 beneficiary: bridge_owner_bytes.into(),
264 },
265 Transact {
267 origin_kind: OriginKind::Xcm,
268 fallback_max_weight: None,
269 call: (
270 create_call_index,
271 asset_id.clone(),
272 MultiAddress::<[u8; 32], ()>::Id(bridge_owner_bytes.into()),
273 MINIMUM_DEPOSIT,
274 )
275 .encode()
276 .into(),
277 },
278 RefundSurplus,
279 DepositAsset { assets: Wild(AllCounted(2)), beneficiary: claimer },
281 ]
282 .into()
283 }
284
285 fn decode_raw_xcm(raw: &[u8]) -> Xcm<()> {
290 let mut data = raw;
291 if let Ok(versioned_xcm) =
292 VersionedXcm::<()>::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data)
293 {
294 if let Ok(decoded_xcm) = versioned_xcm.try_into() {
295 return decoded_xcm;
296 }
297 }
298 Xcm::new()
300 }
301}
302
303impl<
304 CreateAssetCall,
305 CreateAssetDeposit,
306 EthereumNetwork,
307 InboundQueueLocation,
308 ConvertAssetId,
309 GatewayProxyAddress,
310 EthereumUniversalLocation,
311 AssetHubFromEthereum,
312 AssetHubUniversalLocation,
313 AccountId,
314 > ConvertMessage
315 for MessageToXcm<
316 CreateAssetCall,
317 CreateAssetDeposit,
318 EthereumNetwork,
319 InboundQueueLocation,
320 ConvertAssetId,
321 GatewayProxyAddress,
322 EthereumUniversalLocation,
323 AssetHubFromEthereum,
324 AssetHubUniversalLocation,
325 AccountId,
326 >
327where
328 CreateAssetCall: Get<CallIndex>,
329 CreateAssetDeposit: Get<u128>,
330 EthereumNetwork: Get<NetworkId>,
331 InboundQueueLocation: Get<InteriorLocation>,
332 ConvertAssetId: MaybeConvert<TokenId, Location>,
333 GatewayProxyAddress: Get<H160>,
334 EthereumUniversalLocation: Get<InteriorLocation>,
335 AssetHubFromEthereum: Get<Location>,
336 AssetHubUniversalLocation: Get<InteriorLocation>,
337 AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone,
338{
339 fn convert(message: Message) -> Result<Xcm<()>, ConvertMessageError> {
340 let message = Self::prepare(message)?;
341
342 log::trace!(target: LOG_TARGET, "prepared message: {:?}", message);
343
344 let mut instructions = vec![
345 DescendOrigin(InboundQueueLocation::get()),
346 UniversalOrigin(GlobalConsensus(EthereumNetwork::get())),
347 ReserveAssetDeposited(message.execution_fee.clone().into()),
348 ];
349
350 instructions.push(SetHints {
353 hints: vec![AssetClaimer { location: message.claimer }]
354 .try_into()
355 .expect("checked statically, qed"),
356 });
357
358 instructions.push(PayFees { asset: message.execution_fee.clone() });
359
360 let mut reserve_deposit_assets = vec![];
361 let mut reserve_withdraw_assets = vec![];
362
363 for asset in message.assets {
364 match asset {
365 AssetTransfer::ReserveDeposit(asset) => reserve_deposit_assets.push(asset),
366 AssetTransfer::ReserveWithdraw(asset) => reserve_withdraw_assets.push(asset),
367 };
368 }
369
370 if !reserve_deposit_assets.is_empty() {
371 instructions.push(ReserveAssetDeposited(reserve_deposit_assets.into()));
372 }
373 if !reserve_withdraw_assets.is_empty() {
374 instructions.push(WithdrawAsset(reserve_withdraw_assets.into()));
375 }
376
377 if message.origin != GatewayProxyAddress::get() {
381 instructions.push(DescendOrigin(
382 AccountKey20 { key: message.origin.into(), network: None }.into(),
383 ));
384 }
385
386 instructions.extend(message.remote_xcm.0);
388
389 Ok(instructions.into())
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 use codec::Encode;
398 use frame_support::{assert_err, assert_ok, parameter_types};
399 use hex_literal::hex;
400 use snowbridge_core::TokenId;
401 use snowbridge_test_utils::mock_converter::{
402 add_location_override, reanchor_to_ethereum, LocationIdConvert,
403 };
404 use sp_core::{H160, H256};
405 const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"];
406
407 parameter_types! {
408 pub const EthereumNetwork: xcm::v5::NetworkId = xcm::v5::NetworkId::Ethereum { chain_id: 1 };
409 pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
410 pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
411 pub EthereumUniversalLocation: InteriorLocation =
412 [GlobalConsensus(EthereumNetwork::get())].into();
413 pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Polkadot),Parachain(1000)]);
414 pub AssetHubUniversalLocation: InteriorLocation = [GlobalConsensus(Polkadot),Parachain(1000)].into();
415 pub const CreateAssetCall: [u8;2] = [53, 0];
416 pub const CreateAssetDeposit: u128 = 10_000_000_000u128;
417 pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
418 pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Polkadot),Parachain(1002)].into();
419 }
420
421 pub struct MockFailedTokenConvert;
422 impl MaybeConvert<TokenId, Location> for MockFailedTokenConvert {
423 fn maybe_convert(_id: TokenId) -> Option<Location> {
424 None
425 }
426 }
427
428 type Converter = MessageToXcm<
429 CreateAssetCall,
430 CreateAssetDeposit,
431 EthereumNetwork,
432 InboundQueueLocation,
433 LocationIdConvert,
434 GatewayAddress,
435 EthereumUniversalLocation,
436 AssetHubFromEthereum,
437 AssetHubUniversalLocation,
438 [u8; 32],
439 >;
440
441 type ConverterFailing = MessageToXcm<
442 CreateAssetCall,
443 CreateAssetDeposit,
444 EthereumNetwork,
445 InboundQueueLocation,
446 MockFailedTokenConvert,
447 GatewayAddress,
448 EthereumUniversalLocation,
449 AssetHubFromEthereum,
450 AssetHubUniversalLocation,
451 [u8; 32],
452 >;
453
454 #[test]
455 fn test_successful_message() {
456 sp_io::TestExternalities::default().execute_with(|| {
457 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
458 let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
459 let dot_location = Location::parent();
460 let (foreign_token_id, _) = reanchor_to_ethereum(
461 dot_location.clone(),
462 EthereumLocation::get(),
463 BridgeHubContext::get(),
464 );
465 add_location_override(dot_location, EthereumLocation::get(), BridgeHubContext::get());
466 let beneficiary: Location =
467 hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
468 let token_value = 3_000_000_000_000u128;
469 let assets = vec![
470 EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value },
471 EthereumAsset::ForeignTokenERC20 { token_id: foreign_token_id, value: token_value },
472 ];
473 let instructions = vec![DepositAsset {
474 assets: Wild(AllCounted(1).into()),
475 beneficiary: beneficiary.clone(),
476 }];
477 let xcm: Xcm<()> = instructions.into();
478 let versioned_xcm = VersionedXcm::V5(xcm);
479 let claimer_location =
480 Location::new(0, AccountId32 { network: None, id: H256::random().into() });
481 let claimer: Option<Vec<u8>> = Some(claimer_location.clone().encode());
482 let value = 6_000_000_000_000u128;
483 let execution_fee = 1_000_000_000_000u128;
484 let relayer_fee = 5_000_000_000_000u128;
485
486 let message = Message {
487 gateway: H160::zero(),
488 nonce: 0,
489 origin,
490 assets,
491 xcm: XcmPayload::Raw(versioned_xcm.encode()),
492 claimer,
493 value,
494 execution_fee,
495 relayer_fee,
496 };
497
498 let result = Converter::convert(message);
499
500 assert_ok!(result.clone());
501
502 let xcm = result.unwrap();
503
504 let instructions: Vec<_> = xcm.into_iter().collect();
506
507 let last_instruction =
509 instructions.last().expect("should have at least one instruction");
510 assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
511
512 let mut asset_claimer_found = false;
513 let mut pay_fees_found = false;
514 let mut descend_origin_found = 0;
515 let mut reserve_deposited_found = 0;
516 let mut withdraw_assets_found = 0;
517 let mut deposit_asset_found = 0;
518
519 for instruction in &instructions {
520 if let SetHints { ref hints } = instruction {
521 if let Some(AssetClaimer { ref location }) = hints.clone().into_iter().next() {
522 assert_eq!(claimer_location, location.clone());
523 asset_claimer_found = true;
524 }
525 }
526 if let DescendOrigin(ref location) = instruction {
527 descend_origin_found += 1;
528 if descend_origin_found == 2 {
530 let junctions: Junctions =
531 AccountKey20 { key: origin.into(), network: None }.into();
532 assert_eq!(junctions, location.clone());
533 }
534 }
535 if let PayFees { ref asset } = instruction {
536 let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
537 assert_eq!(asset.id, AssetId(fee_asset));
538 assert_eq!(asset.fun, Fungible(execution_fee));
539 pay_fees_found = true;
540 }
541 if let ReserveAssetDeposited(ref reserve_assets) = instruction {
542 reserve_deposited_found += 1;
543 if reserve_deposited_found == 1 {
544 let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
545 let fee: Asset = (fee_asset, execution_fee).into();
546 let fee_assets: Assets = fee.into();
547 assert_eq!(fee_assets, reserve_assets.clone());
548 }
549 if reserve_deposited_found == 2 {
550 let token_asset = Location::new(
551 2,
552 [
553 GlobalConsensus(EthereumNetwork::get()),
554 AccountKey20 { network: None, key: native_token_id.into() },
555 ],
556 );
557 let token: Asset = (token_asset, token_value).into();
558
559 let remaining_ether_asset: Asset =
560 (Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), value)
561 .into();
562
563 let expected_assets: Assets = vec![token, remaining_ether_asset].into();
564 assert_eq!(expected_assets, reserve_assets.clone());
565 }
566 }
567 if let WithdrawAsset(ref withdraw_assets) = instruction {
568 withdraw_assets_found += 1;
569 let token_asset = Location::new(1, Here);
570 let token: Asset = (token_asset, token_value).into();
571 let token_assets: Assets = token.into();
572 assert_eq!(token_assets, withdraw_assets.clone());
573 }
574 if let DepositAsset { ref assets, beneficiary: deposit_beneficiary } = instruction {
575 deposit_asset_found += 1;
576 if deposit_asset_found == 1 {
577 assert_eq!(AssetFilter::from(Wild(AllCounted(1).into())), assets.clone());
578 assert_eq!(*deposit_beneficiary, beneficiary);
579 }
580 }
581 }
582
583 assert!(asset_claimer_found);
585 assert!(pay_fees_found);
587 assert!(descend_origin_found == 2);
590 assert!(reserve_deposited_found == 2);
593 assert!(withdraw_assets_found == 1);
595 assert!(deposit_asset_found == 1);
597 });
598 }
599
600 #[test]
601 fn test_message_with_gateway_origin_does_not_descend_origin_into_sender() {
602 let origin: H160 = GatewayAddress::get();
603 let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
604 let beneficiary =
605 hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
606 let message_id: H256 =
607 hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into();
608 let token_value = 3_000_000_000_000u128;
609 let assets =
610 vec![EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value }];
611 let instructions = vec![
612 DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary },
613 SetTopic(message_id.into()),
614 ];
615 let xcm: Xcm<()> = instructions.into();
616 let versioned_xcm = VersionedXcm::V5(xcm);
617 let claimer_account = AccountId32 { network: None, id: H256::random().into() };
618 let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
619 let value = 6_000_000_000_000u128;
620 let execution_fee = 1_000_000_000_000u128;
621 let relayer_fee = 5_000_000_000_000u128;
622
623 let message = Message {
624 gateway: H160::zero(),
625 nonce: 0,
626 origin,
627 assets,
628 xcm: XcmPayload::Raw(versioned_xcm.encode()),
629 claimer,
630 value,
631 execution_fee,
632 relayer_fee,
633 };
634
635 let result = Converter::convert(message);
636
637 assert_ok!(result.clone());
638
639 let xcm = result.unwrap();
640
641 let mut instructions = xcm.into_iter();
642 let mut commands_found = 0;
643 while let Some(instruction) = instructions.next() {
644 if let DescendOrigin(ref _location) = instruction {
645 commands_found = commands_found + 1;
646 }
647 }
648 assert!(commands_found == 1);
650 }
651
652 #[test]
653 fn test_invalid_foreign_erc20() {
654 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
655 let token_id: H256 =
656 hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into();
657 let beneficiary =
658 hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
659 let message_id: H256 =
660 hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into();
661 let token_value = 3_000_000_000_000u128;
662 let assets = vec![EthereumAsset::ForeignTokenERC20 { token_id, value: token_value }];
663 let instructions = vec![
664 DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary },
665 SetTopic(message_id.into()),
666 ];
667 let xcm: Xcm<()> = instructions.into();
668 let versioned_xcm = VersionedXcm::V5(xcm);
669 let claimer_account = AccountId32 { network: None, id: H256::random().into() };
670 let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
671 let value = 0;
672 let execution_fee = 1_000_000_000_000u128;
673 let relayer_fee = 5_000_000_000_000u128;
674
675 let message = Message {
676 gateway: H160::zero(),
677 nonce: 0,
678 origin,
679 assets,
680 xcm: XcmPayload::Raw(versioned_xcm.encode()),
681 claimer,
682 value,
683 execution_fee,
684 relayer_fee,
685 };
686
687 assert_err!(ConverterFailing::convert(message), ConvertMessageError::InvalidAsset);
688 }
689
690 #[test]
691 fn test_invalid_claimer() {
692 sp_io::TestExternalities::default().execute_with(|| {
693 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
694 let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
695 let beneficiary =
696 hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
697 let token_value = 3_000_000_000_000u128;
698 let assets = vec![EthereumAsset::NativeTokenERC20 {
699 token_id: native_token_id,
700 value: token_value,
701 }];
702 let instructions =
703 vec![DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }];
704 let xcm: Xcm<()> = instructions.into();
705 let versioned_xcm = VersionedXcm::V5(xcm);
706 let claimer: Option<Vec<u8>> = Some(vec![]);
708 let value = 6_000_000_000_000u128;
709 let execution_fee = 1_000_000_000_000u128;
710 let relayer_fee = 5_000_000_000_000u128;
711
712 let message = Message {
713 gateway: H160::zero(),
714 nonce: 0,
715 origin,
716 assets,
717 xcm: XcmPayload::Raw(versioned_xcm.encode()),
718 claimer,
719 value,
720 execution_fee,
721 relayer_fee,
722 };
723
724 let result = Converter::convert(message.clone());
725
726 assert_ok!(result.clone());
728
729 let xcm = result.unwrap();
730 let instructions: Vec<_> = xcm.into_iter().collect();
731
732 let last_instruction =
734 instructions.last().expect("should have at least one instruction");
735 assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
736
737 let mut actual_claimer: Option<Location> = None;
738 for instruction in &instructions {
739 if let SetHints { ref hints } = instruction {
740 if let Some(AssetClaimer { location }) = hints.clone().into_iter().next() {
741 actual_claimer = Some(location);
742 break;
743 }
744 }
745 }
746
747 let bridge_owner = ExternalConsensusLocationsConverterFor::<
749 AssetHubUniversalLocation,
750 [u8; 32],
751 >::convert_location(&Location::new(
752 2,
753 [GlobalConsensus(EthereumNetwork::get())],
754 ))
755 .unwrap();
756 assert_eq!(
757 actual_claimer,
758 Some(Location::new(0, [AccountId32 { network: None, id: bridge_owner }]))
759 );
760 });
761 }
762
763 #[test]
764 fn test_invalid_xcm() {
765 sp_io::TestExternalities::default().execute_with(|| {
766 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
767 let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
768 let token_value = 3_000_000_000_000u128;
769 let assets = vec![EthereumAsset::NativeTokenERC20 {
770 token_id: native_token_id,
771 value: token_value,
772 }];
773 let versioned_xcm = hex!("8b69c7e376e28114618e829a7ec7").to_vec();
775 let claimer_account = AccountId32 { network: None, id: H256::random().into() };
776 let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
777 let value = 6_000_000_000_000u128;
778 let execution_fee = 1_000_000_000_000u128;
779 let relayer_fee = 5_000_000_000_000u128;
780
781 let message = Message {
782 gateway: H160::zero(),
783 nonce: 0,
784 origin,
785 assets,
786 xcm: XcmPayload::Raw(versioned_xcm),
787 claimer: Some(claimer.encode()),
788 value,
789 execution_fee,
790 relayer_fee,
791 };
792
793 let result = Converter::convert(message);
794
795 assert_ok!(result.clone());
797 });
798 }
799
800 #[test]
801 fn message_with_set_topic_respects_user_topic() {
802 sp_io::TestExternalities::default().execute_with(|| {
803 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
804
805 let user_topic: [u8; 32] =
807 hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
808
809 let instructions = vec![RefundSurplus, SetTopic(user_topic)];
811 let xcm: Xcm<()> = instructions.into();
812 let versioned_xcm = VersionedXcm::V5(xcm);
813
814 let execution_fee = 1_000_000_000_000u128;
815 let value = 0;
816
817 let message = Message {
818 gateway: H160::zero(),
819 nonce: 0,
820 origin,
821 assets: vec![],
822 xcm: XcmPayload::Raw(versioned_xcm.encode()),
823 claimer: None,
824 value,
825 execution_fee,
826 relayer_fee: 0,
827 };
828
829 let result = Converter::convert(message);
830 assert_ok!(result.clone());
831
832 let xcm = result.unwrap();
833 let instructions: Vec<_> = xcm.into_iter().collect();
834
835 let last_instruction =
837 instructions.last().expect("should have at least one instruction");
838 if let SetTopic(ref topic) = last_instruction {
839 assert_eq!(*topic, user_topic);
840 } else {
841 panic!("Last instruction should be SetTopic");
842 }
843 });
844 }
845
846 #[test]
847 fn message_with_generates_a_unique_topic_if_no_topic_is_present() {
848 sp_io::TestExternalities::default().execute_with(|| {
849 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
850
851 let execution_fee = 1_000_000_000_000u128;
852 let value = 0;
853
854 let message = Message {
855 gateway: H160::zero(),
856 nonce: 0,
857 origin,
858 assets: vec![],
859 xcm: XcmPayload::Raw(vec![]),
860 claimer: None,
861 value,
862 execution_fee,
863 relayer_fee: 0,
864 };
865
866 let result = Converter::convert(message);
867 assert_ok!(result.clone());
868
869 let xcm = result.unwrap();
870 let instructions: Vec<_> = xcm.into_iter().collect();
871
872 let last_instruction =
874 instructions.last().expect("should have at least one instruction");
875 assert!(matches!(last_instruction, SetTopic(_)));
876 });
877 }
878
879 #[test]
880 fn message_with_user_topic_not_last_instruction_gets_appended() {
881 sp_io::TestExternalities::default().execute_with(|| {
882 let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
883
884 let execution_fee = 1_000_000_000_000u128;
885 let value = 0;
886
887 let user_topic: [u8; 32] =
888 hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
889
890 let instructions = vec![SetTopic(user_topic), RefundSurplus];
892 let xcm: Xcm<()> = instructions.into();
893 let versioned_xcm = VersionedXcm::V5(xcm);
894
895 let message = Message {
896 gateway: H160::zero(),
897 nonce: 0,
898 origin,
899 assets: vec![],
900 xcm: XcmPayload::Raw(versioned_xcm.encode()),
901 claimer: None,
902 value,
903 execution_fee,
904 relayer_fee: 0,
905 };
906
907 let result = Converter::convert(message);
908 assert_ok!(result.clone());
909
910 let xcm = result.unwrap();
911 let instructions: Vec<_> = xcm.into_iter().collect();
912
913 let last_instruction =
918 instructions.last().expect("should have at least one instruction");
919
920 assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
922 });
923 }
924}