1use 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#[derive(Clone, Encode, Decode, RuntimeDebug)]
22pub enum VersionedMessage {
23 V1(MessageV1),
24}
25
26#[derive(Clone, Encode, Decode, RuntimeDebug)]
29pub struct MessageV1 {
30 pub chain_id: u64,
32 pub command: Command,
34}
35
36#[derive(Clone, Encode, Decode, RuntimeDebug)]
37pub enum Command {
38 RegisterToken {
40 token: H160,
42 fee: u128,
44 },
45 SendToken {
47 token: H160,
49 destination: Destination,
51 amount: u128,
53 fee: u128,
55 },
56 SendNativeToken {
58 token_id: TokenId,
60 destination: Destination,
62 amount: u128,
64 fee: u128,
66 },
67}
68
69#[derive(Clone, Encode, Decode, RuntimeDebug)]
71pub enum Destination {
72 AccountId32 { id: [u8; 32] },
74 ForeignAccountId32 {
78 para_id: u32,
79 id: [u8; 32],
80 fee: u128,
82 },
83 ForeignAccountId20 {
87 para_id: u32,
88 id: [u8; 20],
89 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#[derive(
125 Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, RuntimeDebug,
126)]
127pub enum ConvertMessageError {
128 UnsupportedVersion,
130 InvalidDestination,
131 InvalidToken,
132 UnsupportedFeeAsset,
134 CannotReanchor,
135}
136
137pub trait ConvertMessage {
139 type Balance: BalanceT + From<u128>;
140 type AccountId;
141 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 ReceiveTeleportedAsset(total.into()),
260 BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
262 DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() },
264 SetAppendix(Xcm(vec![
268 RefundSurplus,
269 DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
270 ])),
271 DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
273 UniversalOrigin(GlobalConsensus(network)),
275 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 SetTopic(message_id.into()),
290 ]
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 Destination::AccountId32 { id } =>
313 (None, Location::new(0, [AccountId32 { network: None, id }]), 0),
314 Destination::ForeignAccountId32 { para_id, id, fee } => (
316 Some(para_id),
317 Location::new(0, [AccountId32 { network: None, id }]),
318 fee,
320 ),
321 Destination::ForeignAccountId20 { para_id, id, fee } => (
323 Some(para_id),
324 Location::new(0, [AccountKey20 { network: None, key: id }]),
325 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 SetAppendix(Xcm(vec![DepositAsset {
352 assets: Wild(AllCounted(2)),
353 beneficiary: bridge_location,
354 }])),
355 DepositReserveAsset {
357 assets: Wild(AllCounted(2)),
360 dest: Location::new(1, [Parachain(dest_para_id)]),
361 xcm: vec![
362 BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
364 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
366 SetTopic(message_id.into()),
368 ]
369 .into(),
370 },
371 ]);
372 },
373 None => {
374 instructions.extend(vec![
375 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
379 ]);
380 },
381 }
382
383 instructions.push(SetTopic(message_id.into()));
385
386 (instructions.into(), total_fees.into())
389 }
390
391 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 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 Destination::AccountId32 { id } =>
420 Ok(Location::new(0, [AccountId32 { network: None, id }])),
421 _ => 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 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
450 SetTopic(message_id.into()),
451 ];
452
453 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 Location::new(1, []),
540 Location::new(1, [Parachain(2004)]),
542 Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
544 Location::new(2, [GlobalConsensus(Kusama)]),
546 Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
548 ];
549 for asset in assets.iter() {
550 let mut reanchored_asset = asset.clone();
552 assert_ok!(reanchored_asset.reanchor(ðereum, &ah_context));
553 let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
555 assert_ok!(
556 reanchored_asset_with_ethereum_context.reanchor(&global_ah, ðereum_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}