diff --git a/parachain/pallets/inbound-queue/src/lib.rs b/parachain/pallets/inbound-queue/src/lib.rs index 6cdef8eb29..2d9e421cae 100644 --- a/parachain/pallets/inbound-queue/src/lib.rs +++ b/parachain/pallets/inbound-queue/src/lib.rs @@ -42,7 +42,7 @@ use envelope::{Envelope, Log}; use frame_support::{ traits::{ fungible::{Inspect, Mutate}, - tokens::Preservation, + tokens::{Fortitude, Precision, Preservation}, }, weights::WeightToFee, PalletError, @@ -113,7 +113,10 @@ pub mod pallet { type GatewayAddress: Get; /// Convert inbound message to XCM - type MessageConverter: ConvertMessage; + type MessageConverter: ConvertMessage< + AccountId = Self::AccountId, + Balance = BalanceOf, + >; type WeightInfo: WeightInfo; @@ -249,10 +252,15 @@ pub mod pallet { )?; // Decode message into XCM - let xcm = match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) { - Ok(message) => Self::do_convert(envelope.message_id, message)?, - Err(_) => return Err(Error::::InvalidPayload.into()), - }; + let (xcm, fee) = + match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) { + Ok(message) => Self::do_convert(envelope.message_id, message)?, + Err(_) => return Err(Error::::InvalidPayload.into()), + }; + + // We embed fees for xcm execution inside the xcm program using teleports + // so we must burn the amount of the fee embedded into the program. + T::Token::burn_from(&sovereign_account, fee, Precision::Exact, Fortitude::Polite)?; // Attempt to send XCM to a dest parachain let message_id = Self::send_xcm(xcm, envelope.dest)?; @@ -284,12 +292,12 @@ pub mod pallet { pub fn do_convert( message_id: H256, message: inbound::VersionedMessage, - ) -> Result, Error> { - let mut xcm = + ) -> Result<(Xcm<()>, BalanceOf), Error> { + let (mut xcm, fee) = T::MessageConverter::convert(message).map_err(|e| Error::::ConvertMessage(e))?; // Append the message id as an XCM topic xcm.inner_mut().extend(vec![SetTopic(message_id.into())]); - Ok(xcm) + Ok((xcm, fee)) } pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result> { diff --git a/parachain/pallets/inbound-queue/src/test.rs b/parachain/pallets/inbound-queue/src/test.rs index e7b3224b65..673218577f 100644 --- a/parachain/pallets/inbound-queue/src/test.rs +++ b/parachain/pallets/inbound-queue/src/test.rs @@ -44,6 +44,8 @@ parameter_types! { pub const BlockHashCount: u64 = 250; } +type Balance = u128; + impl frame_system::Config for Test { type BaseCallFilter = Everything; type BlockWeights = (); @@ -74,7 +76,7 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; - type Balance = u128; + type Balance = Balance; type RuntimeEvent = RuntimeEvent; type DustRemoval = (); type ExistentialDeposit = ConstU128<1>; @@ -132,6 +134,7 @@ parameter_types! { pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); pub const CreateAssetCall: [u8;2] = [53, 0]; pub const CreateAssetExecutionFee: u128 = 2_000_000_000; + pub const CreateAssetDeposit: u128 = 100_000_000_000; pub const SendTokenExecutionFee: u128 = 1_000_000_000; pub const InitialFund: u128 = 1_000_000_000_000; } @@ -177,8 +180,14 @@ impl inbound_queue::Config for Test { type XcmSender = MockXcmSender; type WeightInfo = (); type GatewayAddress = GatewayAddress; - type MessageConverter = - MessageToXcm; + type MessageConverter = MessageToXcm< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + >; #[cfg(feature = "runtime-benchmarks")] type Helper = Test; type WeightToFee = IdentityFee; @@ -240,10 +249,6 @@ const BAD_OUTBOUND_QUEUE_EVENT_LOG: [u8; 254] = hex!( " ); -const XCM_HASH: [u8; 32] = [ - 201, 101, 244, 67, 153, 61, 253, 203, 92, 23, 197, 172, 112, 209, 53, 248, 118, 25, 253, 110, - 168, 201, 60, 156, 227, 26, 55, 145, 5, 177, 78, 189, -]; const ASSET_HUB_PARAID: u32 = 1000u32; const TEMPLATE_PARAID: u32 = 1001u32; @@ -271,7 +276,10 @@ fn test_submit_happy_path() { expect_events(vec![InboundQueueEvent::MessageReceived { dest: ASSET_HUB_PARAID.into(), nonce: 1, - message_id: XCM_HASH, + message_id: [ + 3, 29, 43, 131, 7, 80, 47, 2, 238, 64, 45, 200, 64, 1, 46, 74, 121, 211, 8, 178, + 198, 26, 230, 13, 180, 78, 164, 58, 22, 133, 206, 83, + ], } .into()]); }); diff --git a/parachain/primitives/router/src/inbound/mod.rs b/parachain/primitives/router/src/inbound/mod.rs index 81ca55c3b4..1d42a445f6 100644 --- a/parachain/primitives/router/src/inbound/mod.rs +++ b/parachain/primitives/router/src/inbound/mod.rs @@ -1,9 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Ethereum to XCM messages + +#[cfg(test)] +mod tests; + use codec::{Decode, Encode}; use core::marker::PhantomData; -use frame_support::{traits::ContainsPair, weights::Weight, PalletError}; +use frame_support::{ + traits::{tokens::Balance as BalanceT, ContainsPair}, + weights::Weight, + PalletError, +}; use scale_info::TypeInfo; use sp_core::{Get, RuntimeDebug, H160}; use sp_io::hashing::blake2_256; @@ -65,13 +73,28 @@ pub enum Destination { ForeignAccountId20 { para_id: u32, id: [u8; 20] }, } -pub struct MessageToXcm -where +pub struct MessageToXcm< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, +> where CreateAssetCall: Get, CreateAssetExecutionFee: Get, + CreateAssetDeposit: Get, SendTokenExecutionFee: Get, + Balance: BalanceT, { - _phantom: PhantomData<(CreateAssetCall, CreateAssetExecutionFee, SendTokenExecutionFee)>, + _phantom: PhantomData<( + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + )>, } /// Reason why a message conversion failed. @@ -83,141 +106,194 @@ pub enum ConvertMessageError { /// convert the inbound message to xcm which will be forwarded to the destination chain pub trait ConvertMessage { + type Balance: BalanceT + From; + type AccountId; /// Converts a versioned message into an XCM message and an optional topicID - fn convert(message: VersionedMessage) -> Result, ConvertMessageError>; + fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; } pub type CallIndex = [u8; 2]; -impl ConvertMessage - for MessageToXcm -where +impl< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + > ConvertMessage + for MessageToXcm< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + > where CreateAssetCall: Get, CreateAssetExecutionFee: Get, + CreateAssetDeposit: Get, SendTokenExecutionFee: Get, + Balance: BalanceT + From, + AccountId: Into<[u8; 32]>, { - fn convert(message: VersionedMessage) -> Result, ConvertMessageError> { - match message { - VersionedMessage::V1(MessageV1 { chain_id, command }) => { - let network = Ethereum { chain_id }; - let buy_execution_fee_amount = match command { - Command::RegisterToken { .. } => CreateAssetExecutionFee::get(), - Command::SendToken { .. } => SendTokenExecutionFee::get(), - }; - let buy_execution_fee = MultiAsset { - id: Concrete(MultiLocation::parent()), - fun: Fungible(buy_execution_fee_amount), - }; - - let mut instructions = vec![ - UniversalOrigin(GlobalConsensus(network)), - WithdrawAsset(buy_execution_fee.clone().into()), - BuyExecution { fees: buy_execution_fee, weight_limit: Unlimited }, - SetAppendix( - vec![ - RefundSurplus, - DepositAsset { - assets: Wild(AllCounted(1)), - beneficiary: (Parent, Parent, GlobalConsensus(network)).into(), - }, - ] - .into(), - ), - ]; + type Balance = Balance; + type AccountId = AccountId; - let xcm = match command { - Command::RegisterToken { token, .. } => { - let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id( - &chain_id, - ); - - let asset_id = Self::convert_token_address(network, token); - - let create_call_index: [u8; 2] = CreateAssetCall::get(); - instructions.extend(vec![ - Transact { - origin_kind: OriginKind::Xcm, - require_weight_at_most: Weight::from_parts(400_000_000, 8_000), - call: ( - create_call_index, - asset_id, - MultiAddress::<[u8; 32], ()>::Id(owner), - MINIMUM_DEPOSIT, - ) - .encode() - .into(), - }, - ExpectTransactStatus(MaybeErrorCode::Success), - ]); - instructions.into() - }, - Command::SendToken { token, destination, amount, .. } => { - let asset = - MultiAsset::from((Self::convert_token_address(network, token), amount)); - - instructions.extend(vec![ - ReserveAssetDeposited(vec![asset.clone()].into()), - ClearOrigin, - ]); - - let (dest_para_id, beneficiary) = match destination { - Destination::AccountId32 { id } => ( - None, - MultiLocation { - parents: 0, - interior: X1(AccountId32 { network: None, id }), - }, - ), - Destination::ForeignAccountId32 { para_id, id } => ( - Some(para_id), - MultiLocation { - parents: 0, - interior: X1(AccountId32 { network: None, id }), - }, - ), - Destination::ForeignAccountId20 { para_id, id } => ( - Some(para_id), - MultiLocation { - parents: 0, - interior: X1(AccountKey20 { network: None, key: id }), - }, - ), - }; - - let assets = Definite(vec![asset].into()); - - let mut fragment: Vec> = match dest_para_id { - Some(dest_para_id) => { - vec![DepositReserveAsset { - assets: assets.clone(), - dest: MultiLocation { - parents: 1, - interior: X1(Parachain(dest_para_id)), - }, - xcm: vec![DepositAsset { assets, beneficiary }].into(), - }] - }, - None => { - vec![DepositAsset { assets, beneficiary }] - }, - }; - instructions.append(&mut fragment); - instructions.into() - }, - }; - Ok(xcm) - }, + fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { + use Command::*; + use VersionedMessage::*; + match message { + V1(MessageV1 { chain_id, command: RegisterToken { token } }) => + Ok(Self::convert_register_token(chain_id, token)), + V1(MessageV1 { chain_id, command: SendToken { token, destination, amount } }) => + Ok(Self::convert_send_token(chain_id, token, destination, amount)), } } } -impl - MessageToXcm -where +impl< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + > + MessageToXcm< + CreateAssetCall, + CreateAssetExecutionFee, + CreateAssetDeposit, + SendTokenExecutionFee, + AccountId, + Balance, + > where CreateAssetCall: Get, CreateAssetExecutionFee: Get, + CreateAssetDeposit: Get, SendTokenExecutionFee: Get, + Balance: BalanceT + From, + AccountId: Into<[u8; 32]>, { + fn convert_register_token(chain_id: u64, token: H160) -> (Xcm<()>, Balance) { + let network = Ethereum { chain_id }; + let fee: MultiAsset = (MultiLocation::parent(), CreateAssetExecutionFee::get()).into(); + let deposit: MultiAsset = (MultiLocation::parent(), CreateAssetDeposit::get()).into(); + + let total_amount = CreateAssetExecutionFee::get() + CreateAssetDeposit::get(); + let total: MultiAsset = (MultiLocation::parent(), total_amount).into(); + + let bridge_location: MultiLocation = (Parent, Parent, GlobalConsensus(network)).into(); + + let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id(&chain_id); + let asset_id = Self::convert_token_address(network, token); + let create_call_index: [u8; 2] = CreateAssetCall::get(); + + let xcm: Xcm<()> = vec![ + // Teleport required fees. + ReceiveTeleportedAsset(total.into()), + // Pay for execution. + BuyExecution { fees: fee, weight_limit: Unlimited }, + // Fund the snowbridge sovereign with the required deposit for creation. + DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location }, + // Change origin to the bridge. + UniversalOrigin(GlobalConsensus(network)), + // Call create_asset on foreign assets pallet. + Transact { + origin_kind: OriginKind::Xcm, + require_weight_at_most: Weight::from_parts(400_000_000, 8_000), + call: ( + create_call_index, + asset_id, + MultiAddress::<[u8; 32], ()>::Id(owner), + MINIMUM_DEPOSIT, + ) + .encode() + .into(), + }, + RefundSurplus, + // Clear the origin so that remaining assets in holding + // are claimable by the physical origin (BridgeHub) + ClearOrigin, + ] + .into(); + + (xcm, total_amount.into()) + } + + fn convert_send_token( + chain_id: u64, + token: H160, + destination: Destination, + amount: u128, + ) -> (Xcm<()>, Balance) { + let network = Ethereum { chain_id }; + let fee_amount = SendTokenExecutionFee::get(); + let fee: MultiAsset = (MultiLocation::parent(), fee_amount).into(); + let asset: MultiAsset = (Self::convert_token_address(network, token), amount).into(); + + let (dest_para_id, beneficiary, total_fee_amount) = match destination { + // Final destination is a 32-byte account on AssetHub + Destination::AccountId32 { id } => ( + None, + MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) }, + // Total fee needs to cover execution only on AssetHub + fee_amount, + ), + // Final destination is a 32-byte account on a sibling of AssetHub + Destination::ForeignAccountId32 { para_id, id } => ( + Some(para_id), + MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) }, + // Total fee needs to cover execution on AssetHub and Sibling + fee_amount * 2, + ), + // Final destination is a 20-byte account on a sibling of AssetHub + Destination::ForeignAccountId20 { para_id, id } => ( + Some(para_id), + MultiLocation { parents: 0, interior: X1(AccountKey20 { network: None, key: id }) }, + // Total fee needs to cover execution on AssetHub and Sibling + fee_amount * 2, + ), + }; + + let total_fee: MultiAsset = (MultiLocation::parent(), total_fee_amount).into(); + + let mut instructions = vec![ + ReceiveTeleportedAsset(total_fee.into()), + BuyExecution { fees: fee.clone(), weight_limit: Unlimited }, + UniversalOrigin(GlobalConsensus(network)), + ReserveAssetDeposited(asset.clone().into()), + ClearOrigin, + ]; + + match dest_para_id { + Some(dest_para_id) => { + instructions.extend(vec![ + // Perform a deposit reserve to send to destination chain. + DepositReserveAsset { + assets: Definite(vec![fee.clone(), asset.clone()].into()), + dest: MultiLocation { parents: 1, interior: X1(Parachain(dest_para_id)) }, + xcm: vec![ + // Buy execution on target. + BuyExecution { fees: fee, weight_limit: Unlimited }, + // Deposit asset to beneficiary. + DepositAsset { assets: Definite(asset.into()), beneficiary }, + ] + .into(), + }, + ]); + }, + None => { + instructions.extend(vec![ + // Deposit asset to beneficiary. + DepositAsset { assets: Definite(asset.into()), beneficiary }, + ]); + }, + } + + (instructions.into(), total_fee_amount.into()) + } + // Convert ERC20 token address to a Multilocation that can be understood by Assets Hub. fn convert_token_address(network: NetworkId, token: H160) -> MultiLocation { MultiLocation { @@ -261,98 +337,3 @@ impl GlobalConsensusEthereumConvertsFor { (b"ethereum-chain", chain_id).using_encoded(blake2_256) } } - -#[cfg(test)] -mod tests { - use super::{FromEthereumGlobalConsensus, GlobalConsensusEthereumConvertsFor}; - use frame_support::{parameter_types, traits::ContainsPair}; - use hex_literal::hex; - use sp_core::crypto::Ss58Codec; - use xcm::v3::prelude::*; - use xcm_executor::traits::ConvertLocation; - - const NETWORK: NetworkId = Ethereum { chain_id: 15 }; - const SS58_FORMAT: u16 = 2; - const EXPECTED_SOVEREIGN_KEY: [u8; 32] = - hex!("da4d66c3651dc151264eee5460493210338e41a7bbfca91a520e438daf180bf5"); - const EXPECTED_SOVEREIGN_ADDRESS: &'static str = - "HWYx2xgcdpSjJQicUUZFRR1EJNPVEQoUDSUB29rfxF617nv"; - - parameter_types! { - pub EthereumNetwork: NetworkId = NETWORK; - pub EthereumLocation: MultiLocation = MultiLocation::new(2, X1(GlobalConsensus(EthereumNetwork::get()))); - } - - #[test] - fn test_contract_location_without_network_converts_successfully() { - let contract_location = - MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; - - let account = - GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location) - .unwrap(); - let address = frame_support::sp_runtime::AccountId32::new(account) - .to_ss58check_with_version(SS58_FORMAT.into()); - - println!("SS58: {}\nBytes: {:?}", address, account); - - assert_eq!(account, EXPECTED_SOVEREIGN_KEY); - assert_eq!(address, EXPECTED_SOVEREIGN_ADDRESS); - } - - #[test] - fn test_contract_location_with_network_converts_successfully() { - let contract_location = - MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; - - let account = - GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location) - .unwrap(); - let address = frame_support::sp_runtime::AccountId32::new(account) - .to_ss58check_with_version(SS58_FORMAT.into()); - assert_eq!(account, EXPECTED_SOVEREIGN_KEY); - assert_eq!(address, EXPECTED_SOVEREIGN_ADDRESS); - - println!("SS58: {}\nBytes: {:?}", address, account); - } - - #[test] - fn test_contract_location_with_incorrect_location_fails_convert() { - let contract_location = - MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; - - assert_eq!( - GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location), - None, - ); - } - - #[test] - fn test_from_ethereum_global_consensus_with_containing_asset_yields_true() { - let origin = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; - let asset = MultiLocation { - parents: 2, - interior: X2(GlobalConsensus(NETWORK), AccountKey20 { network: None, key: [0; 20] }), - }; - assert!(FromEthereumGlobalConsensus::::contains(&asset, &origin)); - } - - #[test] - fn test_from_ethereum_global_consensus_without_containing_asset_yields_false() { - let origin = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; - let asset = - MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; - assert!(!FromEthereumGlobalConsensus::::contains(&asset, &origin)); - } - - #[test] - fn test_from_ethereum_global_consensus_without_bridge_origin_yields_false() { - let origin = - MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; - let asset = MultiLocation { - parents: 2, - interior: X2(GlobalConsensus(NETWORK), AccountKey20 { network: None, key: [0; 20] }), - }; - assert!(!FromEthereumGlobalConsensus::::contains(&asset, &origin)); - } -} diff --git a/parachain/primitives/router/src/inbound/tests.rs b/parachain/primitives/router/src/inbound/tests.rs new file mode 100644 index 0000000000..c1b2a44372 --- /dev/null +++ b/parachain/primitives/router/src/inbound/tests.rs @@ -0,0 +1,94 @@ +use super::{FromEthereumGlobalConsensus, GlobalConsensusEthereumConvertsFor}; +use crate::inbound::CallIndex; +use frame_support::{parameter_types, traits::ContainsPair}; +use hex_literal::hex; +use sp_core::crypto::Ss58Codec; +use xcm::v3::prelude::*; +use xcm_executor::traits::ConvertLocation; + +const NETWORK: NetworkId = Ethereum { chain_id: 15 }; +const SS58_FORMAT: u16 = 2; +const EXPECTED_SOVEREIGN_KEY: [u8; 32] = + hex!("da4d66c3651dc151264eee5460493210338e41a7bbfca91a520e438daf180bf5"); +const EXPECTED_SOVEREIGN_ADDRESS: &'static str = "HWYx2xgcdpSjJQicUUZFRR1EJNPVEQoUDSUB29rfxF617nv"; + +parameter_types! { + pub EthereumNetwork: NetworkId = NETWORK; + pub EthereumLocation: MultiLocation = MultiLocation::new(2, X1(GlobalConsensus(EthereumNetwork::get()))); + + pub const CreateAssetCall: CallIndex = [1, 1]; + pub const CreateAssetExecutionFee: u128 = 123; + pub const CreateAssetDeposit: u128 = 891; + pub const SendTokenExecutionFee: u128 = 592; +} + +#[test] +fn test_contract_location_without_network_converts_successfully() { + let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; + + let account = + GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location) + .unwrap(); + let address = frame_support::sp_runtime::AccountId32::new(account) + .to_ss58check_with_version(SS58_FORMAT.into()); + + println!("SS58: {}\nBytes: {:?}", address, account); + + assert_eq!(account, EXPECTED_SOVEREIGN_KEY); + assert_eq!(address, EXPECTED_SOVEREIGN_ADDRESS); +} + +#[test] +fn test_contract_location_with_network_converts_successfully() { + let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; + + let account = + GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location) + .unwrap(); + let address = frame_support::sp_runtime::AccountId32::new(account) + .to_ss58check_with_version(SS58_FORMAT.into()); + assert_eq!(account, EXPECTED_SOVEREIGN_KEY); + assert_eq!(address, EXPECTED_SOVEREIGN_ADDRESS); + + println!("SS58: {}\nBytes: {:?}", address, account); +} + +#[test] +fn test_contract_location_with_incorrect_location_fails_convert() { + let contract_location = + MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; + + assert_eq!( + GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location), + None, + ); +} + +#[test] +fn test_from_ethereum_global_consensus_with_containing_asset_yields_true() { + let origin = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; + let asset = MultiLocation { + parents: 2, + interior: X2(GlobalConsensus(NETWORK), AccountKey20 { network: None, key: [0; 20] }), + }; + assert!(FromEthereumGlobalConsensus::::contains(&asset, &origin)); +} + +#[test] +fn test_from_ethereum_global_consensus_without_containing_asset_yields_false() { + let origin = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) }; + let asset = + MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; + assert!(!FromEthereumGlobalConsensus::::contains(&asset, &origin)); +} + +#[test] +fn test_from_ethereum_global_consensus_without_bridge_origin_yields_false() { + let origin = + MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) }; + let asset = MultiLocation { + parents: 2, + interior: X2(GlobalConsensus(NETWORK), AccountKey20 { network: None, key: [0; 20] }), + }; + assert!(!FromEthereumGlobalConsensus::::contains(&asset, &origin)); +} diff --git a/parachain/primitives/router/src/outbound/mod.rs b/parachain/primitives/router/src/outbound/mod.rs index 5dd5e5145b..01c7be52a2 100644 --- a/parachain/primitives/router/src/outbound/mod.rs +++ b/parachain/primitives/router/src/outbound/mod.rs @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork //! Converts XCM messages into simpler commands that can be processed by the Gateway contract + +#[cfg(test)] +mod tests; + use core::slice::Iter; use codec::{Decode, Encode}; @@ -272,1052 +276,3 @@ impl<'a, Call> XcmConverter<'a, Call> { } } } - -#[cfg(test)] -mod tests { - use frame_support::parameter_types; - use hex_literal::hex; - use snowbridge_core::outbound::{Fee, SendError, SendMessageFeeProvider}; - - use xcm::v3::prelude::SendError as XcmSendError; - use xcm_builder::{DescribeAllTerminal, DescribeFamily, HashedDescription}; - - pub type AgentIdOf = HashedDescription>; - - use super::*; - - parameter_types! { - const MaxMessageSize: u32 = u32::MAX; - const RelayNetwork: NetworkId = Polkadot; - const UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(1013)); - const BridgedNetwork: NetworkId = Ethereum{ chain_id: 1 }; - const NonBridgedNetwork: NetworkId = Ethereum{ chain_id: 2 }; - } - - struct MockOkOutboundQueue; - impl SendMessage for MockOkOutboundQueue { - type Ticket = (); - - fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> { - Ok(((), Fee { local: 1, remote: 1 })) - } - - fn deliver(_: Self::Ticket) -> Result { - Ok(H256::zero()) - } - } - - impl SendMessageFeeProvider for MockOkOutboundQueue { - type Balance = u128; - - fn local_fee() -> Self::Balance { - 1 - } - } - struct MockErrOutboundQueue; - impl SendMessage for MockErrOutboundQueue { - type Ticket = (); - - fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> { - Err(SendError::MessageTooLarge) - } - - fn deliver(_: Self::Ticket) -> Result { - Err(SendError::MessageTooLarge) - } - } - - impl SendMessageFeeProvider for MockErrOutboundQueue { - type Balance = u128; - - fn local_fee() -> Self::Balance { - 1 - } - } - - #[test] - fn exporter_validate_with_unknown_network_yields_not_applicable() { - let network = Ethereum { chain_id: 1337 }; - let channel: u32 = 0; - let mut universal_source: Option = None; - let mut destination: Option = None; - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::NotApplicable)); - } - - #[test] - fn exporter_validate_with_invalid_destination_yields_missing_argument() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = None; - let mut destination: Option = None; - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::MissingArgument)); - } - - #[test] - fn exporter_validate_with_x8_destination_yields_not_applicable() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = None; - let mut destination: Option = Some(X8( - OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, - )); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::NotApplicable)); - } - - #[test] - fn exporter_validate_without_universal_source_yields_missing_argument() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = None; - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::MissingArgument)); - } - - #[test] - fn exporter_validate_without_global_universal_location_yields_unroutable() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = Here.into(); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::Unroutable)); - } - - #[test] - fn exporter_validate_without_global_bridge_location_yields_not_applicable() { - let network = NonBridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = Here.into(); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::NotApplicable)); - } - - #[test] - fn exporter_validate_with_remote_universal_source_yields_not_applicable() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = - Some(X2(GlobalConsensus(Kusama), Parachain(1000))); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::NotApplicable)); - } - - #[test] - fn exporter_validate_without_para_id_in_source_yields_missing_argument() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = - Some(X1(GlobalConsensus(Polkadot))); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::MissingArgument)); - } - - #[test] - fn exporter_validate_complex_para_id_in_source_yields_missing_argument() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = - Some(X3(GlobalConsensus(Polkadot), Parachain(1000), PalletInstance(12))); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::MissingArgument)); - } - - #[test] - fn exporter_validate_without_xcm_message_yields_missing_argument() { - let network = BridgedNetwork::get(); - let channel: u32 = 0; - let mut universal_source: Option = - Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); - let mut destination: Option = Here.into(); - let mut message: Option> = None; - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - assert_eq!(result, Err(XcmSendError::MissingArgument)); - } - - #[test] - fn exporter_validate_with_max_target_fee_yields_unroutable() { - let network = BridgedNetwork::get(); - let mut destination: Option = Here.into(); - - let mut universal_source: Option = - Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let channel: u32 = 0; - let fee = MultiAsset { id: Concrete(Here.into()), fun: Fungible(1000) }; - let fees: MultiAssets = vec![fee.clone()].into(); - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let mut message: Option> = Some( - vec![ - WithdrawAsset(fees), - BuyExecution { fees: fee, weight_limit: Unlimited }, - WithdrawAsset(assets), - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { - network: Some(network), - key: beneficiary_address, - }) - .into(), - }, - SetTopic([0; 32]), - ] - .into(), - ); - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - - assert_eq!(result, Err(XcmSendError::Unroutable)); - } - - #[test] - fn exporter_validate_with_unparsable_xcm_yields_unroutable() { - let network = BridgedNetwork::get(); - let mut destination: Option = Here.into(); - - let mut universal_source: Option = - Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); - - let channel: u32 = 0; - let fee = MultiAsset { id: Concrete(Here.into()), fun: Fungible(1000) }; - let fees: MultiAssets = vec![fee.clone()].into(); - - let mut message: Option> = Some( - vec![WithdrawAsset(fees), BuyExecution { fees: fee, weight_limit: Unlimited }].into(), - ); - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - - assert_eq!(result, Err(XcmSendError::Unroutable)); - } - - #[test] - fn exporter_validate_xcm_success_case_1() { - let network = BridgedNetwork::get(); - let mut destination: Option = Here.into(); - - let mut universal_source: Option = - Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let channel: u32 = 0; - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let fee = assets.clone().get(0).unwrap().clone(); - let filter: MultiAssetFilter = assets.clone().into(); - - let mut message: Option> = Some( - vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }) - .into(), - }, - SetTopic([0; 32]), - ] - .into(), - ); - - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockOkOutboundQueue, - AgentIdOf, - >::validate( - network, channel, &mut universal_source, &mut destination, &mut message - ); - - assert!(result.is_ok()); - } - - #[test] - fn exporter_deliver_with_submit_failure_yields_unroutable() { - let result = EthereumBlobExporter::< - UniversalLocation, - BridgedNetwork, - MockErrOutboundQueue, - AgentIdOf, - >::deliver((hex!("deadbeef").to_vec(), XcmHash::default())); - assert_eq!(result, Err(XcmSendError::Transport("other transport error"))) - } - - #[test] - fn xcm_converter_convert_success() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); - } - - #[test] - fn xcm_converter_convert_without_buy_execution_yields_success() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); - } - - #[test] - fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(All); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); - } - - #[test] - fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); - let fee_asset = MultiAsset { id: Concrete(asset_location), fun: Fungible(500) }; - - let assets: MultiAssets = - vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); - - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); - } - - #[test] - fn xcm_converter_convert_without_set_topic_yields_set_topic_expected() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - ClearTopic, - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::SetTopicExpected)); - } - - #[test] - fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let message: Xcm<()> = vec![WithdrawAsset(assets)].into(); - - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); - } - - #[test] - fn xcm_converter_with_different_fee_asset_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); - let fee_asset = MultiAsset { - id: Concrete(MultiLocation { parents: 0, interior: Here.into() }), - fun: Fungible(1000), - }; - - let assets: MultiAssets = - vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); - - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); - } - - #[test] - fn xcm_converter_with_fees_greater_than_reserve_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); - let fee_asset = MultiAsset { id: Concrete(asset_location), fun: Fungible(1001) }; - - let assets: MultiAssets = - vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); - - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); - } - - #[test] - fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() { - let network = BridgedNetwork::get(); - - let message: Xcm<()> = vec![].into(); - - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); - } - - #[test] - fn xcm_converter_convert_with_extra_instructions_yields_end_of_xcm_message_expected() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ClearError, - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::EndOfXcmMessageExpected)); - } - - #[test] - fn xcm_converter_convert_without_withdraw_asset_yields_withdraw_expected() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::WithdrawAssetExpected)); - } - - #[test] - fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::DepositAssetExpected)); - } - - #[test] - fn xcm_converter_convert_without_assets_yields_no_reserve_assets() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![].into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let fee = MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }; - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::NoReserveAssets)); - } - - #[test] - fn xcm_converter_convert_with_two_assets_yields_too_many_assets() { - let network = BridgedNetwork::get(); - - let token_address_1: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let token_address_2: [u8; 20] = hex!("1100000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![ - MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address_1 }).into()), - fun: Fungible(1000), - }, - MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address_2 }).into()), - fun: Fungible(500), - }, - ] - .into(); - let filter: MultiAssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::TooManyAssets)); - } - - #[test] - fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume_all_assets() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(0)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::FilterDoesNotConsumeAllAssets)); - } - - #[test] - fn xcm_converter_convert_with_zero_amount_asset_yields_zero_asset_transfer() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(0), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::ZeroAssetTransfer)); - } - - #[test] - fn xcm_converter_convert_non_ethereum_asset_yields_asset_resolution_failed() { - let network = BridgedNetwork::get(); - - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X3(GlobalConsensus(Polkadot), Parachain(1000), GeneralIndex(0)).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); - } - - #[test] - fn xcm_converter_convert_non_ethereum_chain_asset_yields_asset_resolution_failed() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete( - X1(AccountKey20 { network: Some(Ethereum { chain_id: 2 }), key: token_address }) - .into(), - ), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); - } - - #[test] - fn xcm_converter_convert_non_ethereum_chain_yields_asset_resolution_failed() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete( - X1(AccountKey20 { network: Some(NonBridgedNetwork::get()), key: token_address }) - .into(), - ), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); - } - - #[test] - fn xcm_converter_convert_with_non_ethereum_beneficiary_yields_beneficiary_resolution_failed() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - - let beneficiary_address: [u8; 32] = - hex!("2000000000000000000000000000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X3( - GlobalConsensus(Polkadot), - Parachain(1000), - AccountId32 { network: Some(Polkadot), id: beneficiary_address }, - ) - .into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed)); - } - - #[test] - fn xcm_converter_convert_with_non_ethereum_chain_beneficiary_yields_beneficiary_resolution_failed( - ) { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: MultiAssets = vec![MultiAsset { - id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), - fun: Fungible(1000), - }] - .into(); - let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: X1(AccountKey20 { - network: Some(Ethereum { chain_id: 2 }), - key: beneficiary_address, - }) - .into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed)); - } -} diff --git a/parachain/primitives/router/src/outbound/tests.rs b/parachain/primitives/router/src/outbound/tests.rs new file mode 100644 index 0000000000..0b24a6d4d3 --- /dev/null +++ b/parachain/primitives/router/src/outbound/tests.rs @@ -0,0 +1,1037 @@ +use frame_support::parameter_types; +use hex_literal::hex; +use snowbridge_core::outbound::{Fee, SendError, SendMessageFeeProvider}; + +use xcm::v3::prelude::SendError as XcmSendError; +use xcm_builder::{DescribeAllTerminal, DescribeFamily, HashedDescription}; + +pub type AgentIdOf = HashedDescription>; + +use super::*; + +parameter_types! { + const MaxMessageSize: u32 = u32::MAX; + const RelayNetwork: NetworkId = Polkadot; + const UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(1013)); + const BridgedNetwork: NetworkId = Ethereum{ chain_id: 1 }; + const NonBridgedNetwork: NetworkId = Ethereum{ chain_id: 2 }; +} + +struct MockOkOutboundQueue; +impl SendMessage for MockOkOutboundQueue { + type Ticket = (); + + fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> { + Ok(((), Fee { local: 1, remote: 1 })) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(H256::zero()) + } +} + +impl SendMessageFeeProvider for MockOkOutboundQueue { + type Balance = u128; + + fn local_fee() -> Self::Balance { + 1 + } +} +struct MockErrOutboundQueue; +impl SendMessage for MockErrOutboundQueue { + type Ticket = (); + + fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> { + Err(SendError::MessageTooLarge) + } + + fn deliver(_: Self::Ticket) -> Result { + Err(SendError::MessageTooLarge) + } +} + +impl SendMessageFeeProvider for MockErrOutboundQueue { + type Balance = u128; + + fn local_fee() -> Self::Balance { + 1 + } +} + +#[test] +fn exporter_validate_with_unknown_network_yields_not_applicable() { + let network = Ethereum { chain_id: 1337 }; + let channel: u32 = 0; + let mut universal_source: Option = None; + let mut destination: Option = None; + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::NotApplicable)); +} + +#[test] +fn exporter_validate_with_invalid_destination_yields_missing_argument() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = None; + let mut destination: Option = None; + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::MissingArgument)); +} + +#[test] +fn exporter_validate_with_x8_destination_yields_not_applicable() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = None; + let mut destination: Option = Some(X8( + OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, + )); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::NotApplicable)); +} + +#[test] +fn exporter_validate_without_universal_source_yields_missing_argument() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = None; + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::MissingArgument)); +} + +#[test] +fn exporter_validate_without_global_universal_location_yields_unroutable() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = Here.into(); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::Unroutable)); +} + +#[test] +fn exporter_validate_without_global_bridge_location_yields_not_applicable() { + let network = NonBridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = Here.into(); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::NotApplicable)); +} + +#[test] +fn exporter_validate_with_remote_universal_source_yields_not_applicable() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = + Some(X2(GlobalConsensus(Kusama), Parachain(1000))); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::NotApplicable)); +} + +#[test] +fn exporter_validate_without_para_id_in_source_yields_missing_argument() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = Some(X1(GlobalConsensus(Polkadot))); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::MissingArgument)); +} + +#[test] +fn exporter_validate_complex_para_id_in_source_yields_missing_argument() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = + Some(X3(GlobalConsensus(Polkadot), Parachain(1000), PalletInstance(12))); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::MissingArgument)); +} + +#[test] +fn exporter_validate_without_xcm_message_yields_missing_argument() { + let network = BridgedNetwork::get(); + let channel: u32 = 0; + let mut universal_source: Option = + Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); + let mut destination: Option = Here.into(); + let mut message: Option> = None; + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + assert_eq!(result, Err(XcmSendError::MissingArgument)); +} + +#[test] +fn exporter_validate_with_max_target_fee_yields_unroutable() { + let network = BridgedNetwork::get(); + let mut destination: Option = Here.into(); + + let mut universal_source: Option = + Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let channel: u32 = 0; + let fee = MultiAsset { id: Concrete(Here.into()), fun: Fungible(1000) }; + let fees: MultiAssets = vec![fee.clone()].into(); + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let mut message: Option> = Some( + vec![ + WithdrawAsset(fees), + BuyExecution { fees: fee, weight_limit: Unlimited }, + WithdrawAsset(assets), + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: Some(network), key: beneficiary_address }) + .into(), + }, + SetTopic([0; 32]), + ] + .into(), + ); + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + + assert_eq!(result, Err(XcmSendError::Unroutable)); +} + +#[test] +fn exporter_validate_with_unparsable_xcm_yields_unroutable() { + let network = BridgedNetwork::get(); + let mut destination: Option = Here.into(); + + let mut universal_source: Option = + Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); + + let channel: u32 = 0; + let fee = MultiAsset { id: Concrete(Here.into()), fun: Fungible(1000) }; + let fees: MultiAssets = vec![fee.clone()].into(); + + let mut message: Option> = + Some(vec![WithdrawAsset(fees), BuyExecution { fees: fee, weight_limit: Unlimited }].into()); + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + + assert_eq!(result, Err(XcmSendError::Unroutable)); +} + +#[test] +fn exporter_validate_xcm_success_case_1() { + let network = BridgedNetwork::get(); + let mut destination: Option = Here.into(); + + let mut universal_source: Option = + Some(X2(GlobalConsensus(Polkadot), Parachain(1000))); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let channel: u32 = 0; + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let fee = assets.clone().get(0).unwrap().clone(); + let filter: MultiAssetFilter = assets.clone().into(); + + let mut message: Option> = Some( + vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: fee, weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(), + ); + + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockOkOutboundQueue, + AgentIdOf, + >::validate( + network, channel, &mut universal_source, &mut destination, &mut message + ); + + assert!(result.is_ok()); +} + +#[test] +fn exporter_deliver_with_submit_failure_yields_unroutable() { + let result = EthereumBlobExporter::< + UniversalLocation, + BridgedNetwork, + MockErrOutboundQueue, + AgentIdOf, + >::deliver((hex!("deadbeef").to_vec(), XcmHash::default())); + assert_eq!(result, Err(XcmSendError::Transport("other transport error"))) +} + +#[test] +fn xcm_converter_convert_success() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let expected_payload = AgentExecuteCommand::TransferToken { + token: token_address.into(), + recipient: beneficiary_address.into(), + amount: 1000, + }; + let result = converter.convert(); + assert_eq!(result, Ok((expected_payload, [0; 32]))); +} + +#[test] +fn xcm_converter_convert_without_buy_execution_yields_success() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let expected_payload = AgentExecuteCommand::TransferToken { + token: token_address.into(), + recipient: beneficiary_address.into(), + amount: 1000, + }; + let result = converter.convert(); + assert_eq!(result, Ok((expected_payload, [0; 32]))); +} + +#[test] +fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(All); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let expected_payload = AgentExecuteCommand::TransferToken { + token: token_address.into(), + recipient: beneficiary_address.into(), + amount: 1000, + }; + let result = converter.convert(); + assert_eq!(result, Ok((expected_payload, [0; 32]))); +} + +#[test] +fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); + let fee_asset = MultiAsset { id: Concrete(asset_location), fun: Fungible(500) }; + + let assets: MultiAssets = + vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); + + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let expected_payload = AgentExecuteCommand::TransferToken { + token: token_address.into(), + recipient: beneficiary_address.into(), + amount: 1000, + }; + let result = converter.convert(); + assert_eq!(result, Ok((expected_payload, [0; 32]))); +} + +#[test] +fn xcm_converter_convert_without_set_topic_yields_set_topic_expected() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + ClearTopic, + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::SetTopicExpected)); +} + +#[test] +fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let message: Xcm<()> = vec![WithdrawAsset(assets)].into(); + + let mut converter = XcmConverter::new(&message, &network); + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); +} + +#[test] +fn xcm_converter_with_different_fee_asset_fails() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); + let fee_asset = MultiAsset { + id: Concrete(MultiLocation { parents: 0, interior: Here.into() }), + fun: Fungible(1000), + }; + + let assets: MultiAssets = + vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); + + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); +} + +#[test] +fn xcm_converter_with_fees_greater_than_reserve_fails() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let asset_location = X1(AccountKey20 { network: None, key: token_address }).into(); + let fee_asset = MultiAsset { id: Concrete(asset_location), fun: Fungible(1001) }; + + let assets: MultiAssets = + vec![MultiAsset { id: Concrete(asset_location), fun: Fungible(1000) }].into(); + + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); +} + +#[test] +fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() { + let network = BridgedNetwork::get(); + + let message: Xcm<()> = vec![].into(); + + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); +} + +#[test] +fn xcm_converter_convert_with_extra_instructions_yields_end_of_xcm_message_expected() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ClearError, + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::EndOfXcmMessageExpected)); +} + +#[test] +fn xcm_converter_convert_without_withdraw_asset_yields_withdraw_expected() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::WithdrawAssetExpected)); +} + +#[test] +fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::DepositAssetExpected)); +} + +#[test] +fn xcm_converter_convert_without_assets_yields_no_reserve_assets() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![].into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let fee = MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }; + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: fee, weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::NoReserveAssets)); +} + +#[test] +fn xcm_converter_convert_with_two_assets_yields_too_many_assets() { + let network = BridgedNetwork::get(); + + let token_address_1: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let token_address_2: [u8; 20] = hex!("1100000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![ + MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address_1 }).into()), + fun: Fungible(1000), + }, + MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address_2 }).into()), + fun: Fungible(500), + }, + ] + .into(); + let filter: MultiAssetFilter = assets.clone().into(); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::TooManyAssets)); +} + +#[test] +fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume_all_assets() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(0)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::FilterDoesNotConsumeAllAssets)); +} + +#[test] +fn xcm_converter_convert_with_zero_amount_asset_yields_zero_asset_transfer() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(0), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::ZeroAssetTransfer)); +} + +#[test] +fn xcm_converter_convert_non_ethereum_asset_yields_asset_resolution_failed() { + let network = BridgedNetwork::get(); + + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X3(GlobalConsensus(Polkadot), Parachain(1000), GeneralIndex(0)).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); +} + +#[test] +fn xcm_converter_convert_non_ethereum_chain_asset_yields_asset_resolution_failed() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete( + X1(AccountKey20 { network: Some(Ethereum { chain_id: 2 }), key: token_address }).into(), + ), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); +} + +#[test] +fn xcm_converter_convert_non_ethereum_chain_yields_asset_resolution_failed() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete( + X1(AccountKey20 { network: Some(NonBridgedNetwork::get()), key: token_address }).into(), + ), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { network: None, key: beneficiary_address }).into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed)); +} + +#[test] +fn xcm_converter_convert_with_non_ethereum_beneficiary_yields_beneficiary_resolution_failed() { + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + + let beneficiary_address: [u8; 32] = + hex!("2000000000000000000000000000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X3( + GlobalConsensus(Polkadot), + Parachain(1000), + AccountId32 { network: Some(Polkadot), id: beneficiary_address }, + ) + .into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed)); +} + +#[test] +fn xcm_converter_convert_with_non_ethereum_chain_beneficiary_yields_beneficiary_resolution_failed() +{ + let network = BridgedNetwork::get(); + + let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + let assets: MultiAssets = vec![MultiAsset { + id: Concrete(X1(AccountKey20 { network: None, key: token_address }).into()), + fun: Fungible(1000), + }] + .into(); + let filter: MultiAssetFilter = Wild(WildMultiAsset::AllCounted(1)); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: X1(AccountKey20 { + network: Some(Ethereum { chain_id: 2 }), + key: beneficiary_address, + }) + .into(), + }, + SetTopic([0; 32]), + ] + .into(); + let mut converter = XcmConverter::new(&message, &network); + + let result = converter.convert(); + assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed)); +} diff --git a/polkadot-sdk b/polkadot-sdk index 5e361f463a..cd3fe506a5 160000 --- a/polkadot-sdk +++ b/polkadot-sdk @@ -1 +1 @@ -Subproject commit 5e361f463accaf4f4d0aba46695db4b22141dd8e +Subproject commit cd3fe506a56118963ded71cce138f435823eec30 diff --git a/web/packages/test/scripts/configure-bridgehub.sh b/web/packages/test/scripts/configure-bridgehub.sh index c43625d42c..5c32c8ebed 100755 --- a/web/packages/test/scripts/configure-bridgehub.sh +++ b/web/packages/test/scripts/configure-bridgehub.sh @@ -28,7 +28,6 @@ fund_accounts() { transfer_balance $relaychain_ws_url "//Charlie" 1013 1000000000000000 $template_sovereign_account transfer_balance $relaychain_ws_url "//Charlie" 1013 1000000000000000 $beacon_relayer_pub_key transfer_balance $relaychain_ws_url "//Charlie" 1013 1000000000000000 $execution_relayer_pub_key - transfer_balance $relaychain_ws_url "//Charlie" 1000 1000000000000000 $snowbridge_sovereign_account } open_hrmp_channel() diff --git a/web/packages/test/scripts/set-env.sh b/web/packages/test/scripts/set-env.sh index 21585af512..c86f6892ee 100755 --- a/web/packages/test/scripts/set-env.sh +++ b/web/packages/test/scripts/set-env.sh @@ -60,8 +60,6 @@ template_sovereign_account="${TEMPLATE_SOVEREIGN_ACCOUNT:-0x7369626ce90300000000 beacon_relayer_pub_key="${BEACON_RELAYER_PUB_KEY:-0xc46e141b5083721ad5f5056ba1cded69dce4a65f027ed3362357605b1687986a}" # Execution relay account (//ExecutionRelay 5CFNWKMFPsw5Cs2Teo6Pvg7rWyjKiFfqPZs8U4MZXzMYFwXL in testnet) execution_relayer_pub_key="${EXECUTION_RELAYER_PUB_KEY:-0x08228efd065c58a043da95c8bf177659fc587643e71e7ed1534666177730196f}" -# Snowbridge account (HWYx2xgcdpSjJQicUUZFRR1EJNPVEQoUDSUB29rfxF617nv in testnet) -snowbridge_sovereign_account="${SNOWBRIDGE_SOVEREIGN_ACCOUNT:-0xda4d66c3651dc151264eee5460493210338e41a7bbfca91a520e438daf180bf5}" # Config for deploying contracts