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, 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, Debug)]
22pub enum VersionedMessage {
23 V1(MessageV1),
24}
25
26#[derive(Clone, Encode, Decode, Debug)]
29pub struct MessageV1 {
30 pub chain_id: u64,
32 pub command: Command,
34}
35
36#[derive(Clone, Encode, Decode, Debug)]
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, Debug)]
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(Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, Debug)]
125pub enum ConvertMessageError {
126 UnsupportedVersion,
128 InvalidDestination,
129 InvalidToken,
130 UnsupportedFeeAsset,
132 CannotReanchor,
133}
134
135pub trait ConvertMessage {
137 type Balance: BalanceT + From<u128>;
138 type AccountId;
139 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 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 },
315 Destination::ForeignAccountId32 { para_id, id, fee } => (
317 Some(para_id),
318 Location::new(0, [AccountId32 { network: None, id }]),
319 fee,
321 ),
322 Destination::ForeignAccountId20 { para_id, id, fee } => (
324 Some(para_id),
325 Location::new(0, [AccountKey20 { network: None, key: id }]),
326 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 SetAppendix(Xcm(vec![DepositAsset {
353 assets: Wild(AllCounted(2)),
354 beneficiary: bridge_location,
355 }])),
356 DepositReserveAsset {
358 assets: Wild(AllCounted(2)),
361 dest: Location::new(1, [Parachain(dest_para_id)]),
362 xcm: vec![
363 BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
365 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
367 SetTopic(message_id.into()),
369 ]
370 .into(),
371 },
372 ]);
373 },
374 None => {
375 instructions.extend(vec![
376 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
380 ]);
381 },
382 }
383
384 instructions.push(SetTopic(message_id.into()));
386
387 (instructions.into(), total_fees.into())
390 }
391
392 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 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 Destination::AccountId32 { id } => {
421 Ok(Location::new(0, [AccountId32 { network: None, id }]))
422 },
423 _ => 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 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
452 SetTopic(message_id.into()),
453 ];
454
455 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 Location::new(1, []),
542 Location::new(1, [Parachain(2004)]),
544 Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
546 Location::new(2, [GlobalConsensus(Kusama)]),
548 Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
550 ];
551 for asset in assets.iter() {
552 let mut reanchored_asset = asset.clone();
554 assert_ok!(reanchored_asset.reanchor(ðereum, &ah_context));
555 let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
557 assert_ok!(
558 reanchored_asset_with_ethereum_context.reanchor(&global_ah, ðereum_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}