// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Solidity ABI-encoding to XCM use super::{message::*, traits::*}; use crate::{v2::LOG_TARGET, CallIndex}; use codec::{Decode, DecodeLimit, Encode}; use core::marker::PhantomData; use frame_support::ensure; use snowbridge_core::{ParaId, TokenId}; use sp_core::{Get, RuntimeDebug, H160}; use sp_io::hashing::blake2_256; use sp_runtime::{traits::MaybeConvert, MultiAddress}; use sp_std::prelude::*; use xcm::{ prelude::{Junction::*, *}, MAX_XCM_DECODE_DEPTH, }; use xcm_builder::ExternalConsensusLocationsConverterFor; use xcm_executor::traits::ConvertLocation; /// Topic prefix used for generating unique identifiers for messages const INBOUND_QUEUE_TOPIC_PREFIX: &str = "SnowbridgeInboundQueueV2"; /// Representation of an intermediate parsed message, before final /// conversion to XCM. #[derive(Clone, RuntimeDebug, Encode)] pub struct PreparedMessage { /// Ethereum account that initiated this messaging operation pub origin: H160, /// The claimer in the case that funds get trapped. pub claimer: Location, /// The assets bridged from Ethereum pub assets: Vec, /// The XCM to execute on the destination pub remote_xcm: Xcm<()>, /// Fee in Ether to cover the xcm execution on AH. pub execution_fee: Asset, } /// An asset transfer instruction #[derive(Clone, RuntimeDebug, Encode)] pub enum AssetTransfer { ReserveDeposit(Asset), ReserveWithdraw(Asset), } #[derive(Clone, RuntimeDebug, Encode)] pub struct CreateAssetCallInfo { pub create_call: CallIndex, pub deposit: u128, pub min_balance: u128, pub set_reserves_call: CallIndex, } pub struct AssetHubUniversal( PhantomData<(LocalNetwork, AssetHubParaId)>, ); impl Get for AssetHubUniversal where LocalNetwork: Get, AssetHubParaId: Get, { fn get() -> InteriorLocation { [GlobalConsensus(LocalNetwork::get()), Teyrchain(AssetHubParaId::get().into())].into() } } /// Concrete implementation of `ConvertMessage` pub struct MessageToXcm< CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, > { _phantom: PhantomData<( CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, )>, } impl< CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, > MessageToXcm< CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, > where CreateAssetCall: Get, EthereumNetwork: Get, LocalNetwork: Get, GatewayProxyAddress: Get, InboundQueueLocation: Get, AssetHubParaId: Get, ConvertAssetId: MaybeConvert, AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone, { /// Parse the message into an intermediate form, with all fields decoded /// and prepared. fn prepare(message: Message) -> Result { // ETH "asset id" is the Ethereum root location. Same location used for the "bridge owner". let ether_location = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); let bridge_owner = Self::bridge_owner()?; let claimer = message .claimer // Get the claimer from the message, .and_then(|claimer_bytes| Location::decode(&mut claimer_bytes.as_ref()).ok()) // or use the Snowbridge sovereign on AH as the fallback claimer. .unwrap_or_else(|| { Location::new(0, [AccountId32 { network: None, id: bridge_owner.clone().into() }]) }); let mut remote_xcm: Xcm<()> = match &message.xcm { XcmPayload::Raw(raw) => Self::decode_raw_xcm(raw), XcmPayload::CreateAsset { token, network } => Self::make_create_asset_xcm( token, *network, message.value, bridge_owner, claimer.clone(), )?, }; // Asset to cover XCM execution fee let execution_fee_asset: Asset = (ether_location.clone(), message.execution_fee).into(); let mut assets = vec![]; if message.value > 0 { // Asset for remaining ether let remaining_ether_asset: Asset = (ether_location.clone(), message.value).into(); assets.push(AssetTransfer::ReserveDeposit(remaining_ether_asset)); } for asset in &message.assets { match asset { EthereumAsset::NativeTokenERC20 { token_id, value } => { ensure!(*token_id != H160::zero(), ConvertMessageError::InvalidAsset); let token_location: Location = Location::new( 2, [ GlobalConsensus(EthereumNetwork::get()), AccountKey20 { network: None, key: (*token_id).into() }, ], ); let asset: Asset = (token_location, *value).into(); assets.push(AssetTransfer::ReserveDeposit(asset)); }, EthereumAsset::ForeignTokenERC20 { token_id, value } => { let asset_location = ConvertAssetId::maybe_convert(*token_id) .ok_or(ConvertMessageError::InvalidAsset)?; let asset_hub_from_ethereum: Location = Location::new( 1, [ GlobalConsensus(LocalNetwork::get()), Teyrchain(AssetHubParaId::get().into()), ], ); let ethereum_universal: InteriorLocation = [GlobalConsensus(EthereumNetwork::get())].into(); let reanchored_asset_location = asset_location .reanchored(&asset_hub_from_ethereum, ðereum_universal) .map_err(|_| ConvertMessageError::CannotReanchor)?; let asset: Asset = (reanchored_asset_location, *value).into(); assets.push(AssetTransfer::ReserveWithdraw(asset)); }, } } // Add SetTopic instruction if not already present as the last instruction if !matches!(remote_xcm.0.last(), Some(SetTopic(_))) { let topic = blake2_256(&(INBOUND_QUEUE_TOPIC_PREFIX, message.nonce).encode()); remote_xcm.0.push(SetTopic(topic)); } let prepared_message = PreparedMessage { origin: message.origin, claimer, assets, remote_xcm, execution_fee: execution_fee_asset, }; Ok(prepared_message) } /// Get sovereign account of Ethereum on Asset Hub. fn bridge_owner() -> Result { let account = ExternalConsensusLocationsConverterFor::< AssetHubUniversal, AccountId, >::convert_location(&Location::new(2, [GlobalConsensus(EthereumNetwork::get())])) .ok_or(ConvertMessageError::CannotReanchor)?; Ok(account) } /// Construct the remote XCM needed to create a new asset in the `ForeignAssets` pallet /// on AssetHub. Pezkuwi is the only supported network at the moment. fn make_create_asset_xcm( token: &H160, network: super::message::Network, eth_value: u128, bridge_owner: AccountId, claimer: Location, ) -> Result, ConvertMessageError> { let dot_asset = Location::new(1, Here); let dot_fee: xcm::prelude::Asset = (dot_asset, CreateAssetCall::get().deposit).into(); let eth_asset: xcm::prelude::Asset = (Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), eth_value).into(); let create_call_index: [u8; 2] = CreateAssetCall::get().create_call; let create_min_blance: u128 = CreateAssetCall::get().min_balance; let set_reserves_call_index: [u8; 2] = CreateAssetCall::get().set_reserves_call; let asset_id = Location::new( 2, [ GlobalConsensus(EthereumNetwork::get()), AccountKey20 { network: None, key: (*token).into() }, ], ); match network { super::message::Network::Pezkuwi => Ok(Self::make_create_asset_xcm_for_pezkuwi( create_call_index, set_reserves_call_index, create_min_blance, asset_id, bridge_owner, dot_fee, eth_asset, claimer, )), } } /// Construct the asset creation XCM for the Polkdot network. fn make_create_asset_xcm_for_pezkuwi( create_call_index: [u8; 2], set_reserves_call_index: [u8; 2], create_min_blance: u128, asset_id: Location, bridge_owner: AccountId, dot_fee_asset: xcm::prelude::Asset, eth_asset: xcm::prelude::Asset, claimer: Location, ) -> Xcm<()> { let bridge_owner_bytes: [u8; 32] = bridge_owner.into(); let reserve_data = assets_common::local_and_foreign_assets::ForeignAssetReserveData { reserve: Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), teleportable: false, }; vec![ // Exchange eth for dot to pay the asset creation deposit. ExchangeAsset { give: eth_asset.into(), want: dot_fee_asset.clone().into(), maximal: false, }, // Deposit the dot deposit into the bridge sovereign account (where the asset // creation fee will be deducted from). DepositAsset { assets: dot_fee_asset.clone().into(), beneficiary: bridge_owner_bytes.into(), }, // Call to create the asset. Transact { origin_kind: OriginKind::Xcm, fallback_max_weight: None, call: ( create_call_index, asset_id.clone(), MultiAddress::<[u8; 32], ()>::Id(bridge_owner_bytes.into()), create_min_blance, ) .encode() .into(), }, // Call to set Ethereum as the asset's reserve. Transact { origin_kind: OriginKind::Xcm, fallback_max_weight: None, call: (set_reserves_call_index, asset_id, vec![reserve_data]).encode().into(), }, RefundSurplus, // Deposit leftover funds to Snowbridge sovereign DepositAsset { assets: Wild(AllCounted(2)), beneficiary: claimer }, ] .into() } /// Parse and (non-strictly) decode `raw` XCM bytes into a `Xcm<()>`. /// If decoding fails, return an empty `Xcm<()>`—thus allowing the message /// to proceed so assets can still be trapped on AH rather than the funds being locked on /// Ethereum but not accessible on AH. fn decode_raw_xcm(raw: &[u8]) -> Xcm<()> { let mut data = raw; if let Ok(versioned_xcm) = VersionedXcm::<()>::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data) { if let Ok(decoded_xcm) = versioned_xcm.try_into() { return decoded_xcm; } } // Decoding failed; allow an empty XCM so the message won't fail entirely. Xcm::new() } } impl< CreateAsset, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, > ConvertMessage for MessageToXcm< CreateAsset, EthereumNetwork, LocalNetwork, GatewayProxyAddress, InboundQueueLocation, AssetHubParaId, ConvertAssetId, AccountId, > where CreateAsset: Get, EthereumNetwork: Get, LocalNetwork: Get, GatewayProxyAddress: Get, InboundQueueLocation: Get, AssetHubParaId: Get, ConvertAssetId: MaybeConvert, AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone, { fn convert(message: Message) -> Result, ConvertMessageError> { let message = Self::prepare(message)?; tracing::trace!(target: LOG_TARGET, ?message, "prepared message"); let mut instructions = vec![ DescendOrigin(InboundQueueLocation::get()), UniversalOrigin(GlobalConsensus(EthereumNetwork::get())), ReserveAssetDeposited(message.execution_fee.clone().into()), ]; // Set claimer before PayFees, in case the fees are not enough. Then the claimer will be // able to claim the funds still. instructions.push(SetHints { hints: vec![AssetClaimer { location: message.claimer }] .try_into() .expect("checked statically, qed"), }); instructions.push(PayFees { asset: message.execution_fee.clone() }); let mut reserve_deposit_assets = vec![]; let mut reserve_withdraw_assets = vec![]; for asset in message.assets { match asset { AssetTransfer::ReserveDeposit(asset) => reserve_deposit_assets.push(asset), AssetTransfer::ReserveWithdraw(asset) => reserve_withdraw_assets.push(asset), }; } if !reserve_deposit_assets.is_empty() { instructions.push(ReserveAssetDeposited(reserve_deposit_assets.into())); } if !reserve_withdraw_assets.is_empty() { instructions.push(WithdrawAsset(reserve_withdraw_assets.into())); } // If the message origin is not the gateway proxy contract, set the origin to // the original sender on Ethereum. Important to be before the arbitrary XCM that is // appended to the message on the next line. if message.origin != GatewayProxyAddress::get() { instructions.push(DescendOrigin( AccountKey20 { key: message.origin.into(), network: None }.into(), )); } // Add the XCM sent in the message to the end of the xcm instruction instructions.extend(message.remote_xcm.0); Ok(instructions.into()) } } #[cfg(test)] mod tests { use super::*; use codec::Encode; use frame_support::{assert_err, assert_ok, parameter_types}; use hex_literal::hex; use snowbridge_core::TokenId; use snowbridge_test_utils::mock_converter::{ add_location_override, reanchor_to_ethereum, LocationIdConvert, }; use sp_core::{H160, H256}; const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"]; parameter_types! { pub const EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 1 }; pub const LocalNetwork: NetworkId = NetworkId::Pezkuwi; pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into(); pub EthereumUniversalLocation: InteriorLocation = [GlobalConsensus(EthereumNetwork::get())].into(); pub AssetHubParaId: ParaId = 1000.into(); pub const CreateAssetCallIndex: [u8;2] = [53, 0]; pub const SetReservesCallIndex: [u8;2] = [53, 33]; pub const CreateAssetDeposit: u128 = 10_000_000_000u128; pub const CreateAssetMinBalance: u128 = 1; pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get()); pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Pezkuwi),Teyrchain(1002)].into(); pub CreateAssetCall: CreateAssetCallInfo = CreateAssetCallInfo { create_call: CreateAssetCallIndex::get(), deposit: CreateAssetDeposit::get(), min_balance: CreateAssetMinBalance::get(), set_reserves_call: SetReservesCallIndex::get(), }; } pub struct MockFailedTokenConvert; impl MaybeConvert for MockFailedTokenConvert { fn maybe_convert(_id: TokenId) -> Option { None } } type Converter = MessageToXcm< CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayAddress, InboundQueueLocation, AssetHubParaId, LocationIdConvert, [u8; 32], >; type ConverterFailing = MessageToXcm< CreateAssetCall, EthereumNetwork, LocalNetwork, GatewayAddress, InboundQueueLocation, AssetHubParaId, MockFailedTokenConvert, [u8; 32], >; #[test] fn test_successful_message() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); let dot_location = Location::parent(); let (foreign_token_id, _) = reanchor_to_ethereum( dot_location.clone(), EthereumLocation::get(), BridgeHubContext::get(), ); add_location_override(dot_location, EthereumLocation::get(), BridgeHubContext::get()); let beneficiary: Location = hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); let token_value = 3_000_000_000_000u128; let assets = vec![ EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value }, EthereumAsset::ForeignTokenERC20 { token_id: foreign_token_id, value: token_value }, ]; let instructions = vec![DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary: beneficiary.clone(), }]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); let claimer_location = Location::new(0, AccountId32 { network: None, id: H256::random().into() }); let claimer: Option> = Some(claimer_location.clone().encode()); let value = 6_000_000_000_000u128; let execution_fee = 1_000_000_000_000u128; let relayer_fee = 5_000_000_000_000u128; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets, xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, relayer_fee, }; let result = Converter::convert(message); assert_ok!(result.clone()); let xcm = result.unwrap(); // Convert to vec for easier inspection let instructions: Vec<_> = xcm.into_iter().collect(); // Check last instruction is a SetTopic (automatically added) let last_instruction = instructions.last().expect("should have at least one instruction"); assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic"); let mut asset_claimer_found = false; let mut pay_fees_found = false; let mut descend_origin_found = 0; let mut reserve_deposited_found = 0; let mut withdraw_assets_found = 0; let mut deposit_asset_found = 0; for instruction in &instructions { if let SetHints { ref hints } = instruction { if let Some(AssetClaimer { ref location }) = hints.clone().into_iter().next() { assert_eq!(claimer_location, location.clone()); asset_claimer_found = true; } } if let DescendOrigin(ref location) = instruction { descend_origin_found += 1; // The second DescendOrigin should be the message.origin (sender) if descend_origin_found == 2 { let junctions: Junctions = AccountKey20 { key: origin.into(), network: None }.into(); assert_eq!(junctions, location.clone()); } } if let PayFees { ref asset } = instruction { let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); assert_eq!(asset.id, AssetId(fee_asset)); assert_eq!(asset.fun, Fungible(execution_fee)); pay_fees_found = true; } if let ReserveAssetDeposited(ref reserve_assets) = instruction { reserve_deposited_found += 1; if reserve_deposited_found == 1 { let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); let fee: Asset = (fee_asset, execution_fee).into(); let fee_assets: Assets = fee.into(); assert_eq!(fee_assets, reserve_assets.clone()); } if reserve_deposited_found == 2 { let token_asset = Location::new( 2, [ GlobalConsensus(EthereumNetwork::get()), AccountKey20 { network: None, key: native_token_id.into() }, ], ); let token: Asset = (token_asset, token_value).into(); let remaining_ether_asset: Asset = (Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), value) .into(); let expected_assets: Assets = vec![token, remaining_ether_asset].into(); assert_eq!(expected_assets, reserve_assets.clone()); } } if let WithdrawAsset(ref withdraw_assets) = instruction { withdraw_assets_found += 1; let token_asset = Location::new(1, Here); let token: Asset = (token_asset, token_value).into(); let token_assets: Assets = token.into(); assert_eq!(token_assets, withdraw_assets.clone()); } if let DepositAsset { ref assets, beneficiary: deposit_beneficiary } = instruction { deposit_asset_found += 1; if deposit_asset_found == 1 { assert_eq!(AssetFilter::from(Wild(AllCounted(1).into())), assets.clone()); assert_eq!(*deposit_beneficiary, beneficiary); } } } // SetAssetClaimer must be in the message. assert!(asset_claimer_found); // PayFees must be in the message. assert!(pay_fees_found); // The first DescendOrigin to descend into the InboundV2 pallet index and the // DescendOrigin into the message.origin assert!(descend_origin_found == 2); // Expecting two ReserveAssetDeposited instructions, one for the fee and one for the // token being transferred. assert!(reserve_deposited_found == 2); // Expecting one WithdrawAsset for the foreign ERC-20 assert!(withdraw_assets_found == 1); // Deposit asset added by user assert!(deposit_asset_found == 1); }); } #[test] fn test_message_with_gateway_origin_does_not_descend_origin_into_sender() { let origin: H160 = GatewayAddress::get(); let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); let beneficiary = hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); let message_id: H256 = hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); let token_value = 3_000_000_000_000u128; let assets = vec![EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value }]; let instructions = vec![ DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, SetTopic(message_id.into()), ]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); let claimer_account = AccountId32 { network: None, id: H256::random().into() }; let claimer: Option> = Some(claimer_account.clone().encode()); let value = 6_000_000_000_000u128; let execution_fee = 1_000_000_000_000u128; let relayer_fee = 5_000_000_000_000u128; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets, xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, relayer_fee, }; let result = Converter::convert(message); assert_ok!(result.clone()); let xcm = result.unwrap(); let mut instructions = xcm.into_iter(); let mut commands_found = 0; while let Some(instruction) = instructions.next() { if let DescendOrigin(ref _location) = instruction { commands_found = commands_found + 1; } } // There should only be 1 DescendOrigin in the message. assert!(commands_found == 1); } #[test] fn test_invalid_foreign_erc20() { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let token_id: H256 = hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); let beneficiary = hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); let message_id: H256 = hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); let token_value = 3_000_000_000_000u128; let assets = vec![EthereumAsset::ForeignTokenERC20 { token_id, value: token_value }]; let instructions = vec![ DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, SetTopic(message_id.into()), ]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); let claimer_account = AccountId32 { network: None, id: H256::random().into() }; let claimer: Option> = Some(claimer_account.clone().encode()); let value = 0; let execution_fee = 1_000_000_000_000u128; let relayer_fee = 5_000_000_000_000u128; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets, xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, relayer_fee, }; assert_err!(ConverterFailing::convert(message), ConvertMessageError::InvalidAsset); } #[test] fn test_invalid_claimer() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); let beneficiary = hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); let token_value = 3_000_000_000_000u128; let assets = vec![EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value, }]; let instructions = vec![DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); // Invalid claimer location, cannot be decoded into a Location let claimer: Option> = Some(vec![]); let value = 6_000_000_000_000u128; let execution_fee = 1_000_000_000_000u128; let relayer_fee = 5_000_000_000_000u128; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets, xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, relayer_fee, }; let result = Converter::convert(message.clone()); // Invalid claimer does not break the message conversion assert_ok!(result.clone()); let xcm = result.unwrap(); let instructions: Vec<_> = xcm.into_iter().collect(); // Check last instruction is a SetTopic (automatically added) let last_instruction = instructions.last().expect("should have at least one instruction"); assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic"); let mut actual_claimer: Option = None; for instruction in &instructions { if let SetHints { ref hints } = instruction { if let Some(AssetClaimer { location }) = hints.clone().into_iter().next() { actual_claimer = Some(location); break; } } } // actual claimer should default to Snowbridge sovereign account let bridge_owner = ExternalConsensusLocationsConverterFor::< AssetHubUniversal, [u8; 32], >::convert_location(&Location::new( 2, [GlobalConsensus(EthereumNetwork::get())], )) .unwrap(); assert_eq!( actual_claimer, Some(Location::new(0, [AccountId32 { network: None, id: bridge_owner }])) ); }); } #[test] fn test_invalid_xcm() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); let token_value = 3_000_000_000_000u128; let assets = vec![EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value, }]; // invalid xcm let versioned_xcm = hex!("8b69c7e376e28114618e829a7ec7").to_vec(); let claimer_account = AccountId32 { network: None, id: H256::random().into() }; let claimer: Option> = Some(claimer_account.clone().encode()); let value = 6_000_000_000_000u128; let execution_fee = 1_000_000_000_000u128; let relayer_fee = 5_000_000_000_000u128; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets, xcm: XcmPayload::Raw(versioned_xcm), claimer: Some(claimer.encode()), value, execution_fee, relayer_fee, }; let result = Converter::convert(message); // Invalid xcm does not break the message, allowing funds to be trapped on AH. assert_ok!(result.clone()); }); } #[test] fn message_with_set_topic_respects_user_topic() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); // Create a custom topic ID that the user specifies let user_topic: [u8; 32] = hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); // User's XCM with a SetTopic as the last instruction let instructions = vec![RefundSurplus, SetTopic(user_topic)]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); let execution_fee = 1_000_000_000_000u128; let value = 0; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets: vec![], xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer: None, value, execution_fee, relayer_fee: 0, }; let result = Converter::convert(message); assert_ok!(result.clone()); let xcm = result.unwrap(); let instructions: Vec<_> = xcm.into_iter().collect(); // The last instruction should be the user's SetTopic let last_instruction = instructions.last().expect("should have at least one instruction"); if let SetTopic(ref topic) = last_instruction { assert_eq!(*topic, user_topic); } else { panic!("Last instruction should be SetTopic"); } }); } #[test] fn message_with_generates_a_unique_topic_if_no_topic_is_present() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let execution_fee = 1_000_000_000_000u128; let value = 0; let message = Message { gateway: H160::zero(), nonce: 0, origin, assets: vec![], xcm: XcmPayload::Raw(vec![]), claimer: None, value, execution_fee, relayer_fee: 0, }; let result = Converter::convert(message); assert_ok!(result.clone()); let xcm = result.unwrap(); let instructions: Vec<_> = xcm.into_iter().collect(); // The last instruction should be a SetTopic let last_instruction = instructions.last().expect("should have at least one instruction"); assert!(matches!(last_instruction, SetTopic(_))); }); } #[test] fn message_with_user_topic_not_last_instruction_gets_appended() { sp_io::TestExternalities::default().execute_with(|| { let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); let execution_fee = 1_000_000_000_000u128; let value = 0; let user_topic: [u8; 32] = hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); // Add a set topic, but not as the last instruction. let instructions = vec![SetTopic(user_topic), RefundSurplus]; let xcm: Xcm<()> = instructions.into(); let versioned_xcm = VersionedXcm::V5(xcm); let message = Message { gateway: H160::zero(), nonce: 0, origin, assets: vec![], xcm: XcmPayload::Raw(versioned_xcm.encode()), claimer: None, value, execution_fee, relayer_fee: 0, }; let result = Converter::convert(message); assert_ok!(result.clone()); let xcm = result.unwrap(); let instructions: Vec<_> = xcm.into_iter().collect(); // Get the last instruction - should still be a SetTopic, but might not have the // original topic since for non-last-instruction topics, the filter_topic function // extracts it during prepare() and then the original value is later lost when we // append a new one let last_instruction = instructions.last().expect("should have at least one instruction"); // Check if the last instruction is a SetTopic (content isn't important) assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic"); }); } }