diff --git a/Cargo.lock b/Cargo.lock index 435b07328fef..cc5fe0c7ef94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,6 +2415,7 @@ dependencies = [ "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", + "snowbridge-pallet-outbound-queue-v2", "snowbridge-pallet-system", "snowbridge-router-primitives", "sp-core 28.0.0", @@ -21205,6 +21206,7 @@ dependencies = [ "sp-runtime 31.0.1", "sp-std 14.0.0", "staging-xcm", + "staging-xcm-builder", "staging-xcm-executor", ] diff --git a/bridges/snowbridge/primitives/router-v2/Cargo.toml b/bridges/snowbridge/primitives/router-v2/Cargo.toml index 10c03511c405..859ad244bfaf 100644 --- a/bridges/snowbridge/primitives/router-v2/Cargo.toml +++ b/bridges/snowbridge/primitives/router-v2/Cargo.toml @@ -23,6 +23,7 @@ sp-runtime = { workspace = true } sp-std = { workspace = true } xcm = { workspace = true } +xcm-builder = { workspace = true } xcm-executor = { workspace = true } snowbridge-core = { workspace = true } @@ -43,6 +44,7 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-builder/std", "xcm-executor/std", "xcm/std", ] @@ -50,5 +52,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", ] diff --git a/bridges/snowbridge/primitives/router-v2/src/outbound/mod.rs b/bridges/snowbridge/primitives/router-v2/src/outbound/mod.rs index 4df0b88ee4aa..a539f83b847d 100644 --- a/bridges/snowbridge/primitives/router-v2/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router-v2/src/outbound/mod.rs @@ -5,11 +5,15 @@ #[cfg(test)] mod tests; -use core::slice::Iter; - use codec::{Decode, Encode}; +use core::slice::Iter; +use sp_std::ops::ControlFlow; -use frame_support::{ensure, traits::Get, BoundedVec}; +use frame_support::{ + ensure, + traits::{Get, ProcessMessageError}, + BoundedVec, +}; use snowbridge_core::{ outbound_v2::{Command, Message, SendMessage}, AgentId, TokenId, TokenIdOf, @@ -18,8 +22,11 @@ use sp_core::{H160, H256}; use sp_runtime::traits::MaybeEquivalence; use sp_std::{iter::Peekable, marker::PhantomData, prelude::*}; use xcm::prelude::*; +use xcm_builder::{CreateMatcher, MatchXcm}; use xcm_executor::traits::{ConvertLocation, ExportXcm}; +const TARGET: &'static str = "xcm::ethereum_blob_exporter::v2"; + pub struct EthereumBlobExporter< UniversalLocation, EthereumNetwork, @@ -65,14 +72,14 @@ where let universal_location = UniversalLocation::get(); if network != expected_network { - log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}."); + log::trace!(target: TARGET, "skipped due to unmatched bridge network {network:?}."); return Err(SendError::NotApplicable) } // Cloning destination to avoid modifying the value so subsequent exporters can use it. let dest = destination.clone().take().ok_or(SendError::MissingArgument)?; if dest != Here { - log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}."); + log::trace!(target: TARGET, "skipped due to unmatched remote destination {dest:?}."); return Err(SendError::NotApplicable) } @@ -80,24 +87,24 @@ where let (local_net, local_sub) = universal_source.clone() .take() .ok_or_else(|| { - log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided."); + log::error!(target: TARGET, "universal source not provided."); SendError::MissingArgument })? .split_global() .map_err(|()| { - log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'."); + log::error!(target: TARGET, "could not get global consensus from universal source '{universal_source:?}'."); SendError::NotApplicable })?; if Ok(local_net) != universal_location.global_consensus() { - log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}."); + log::trace!(target: TARGET, "skipped due to unmatched relay network {local_net:?}."); return Err(SendError::NotApplicable) } let _para_id = match local_sub.as_slice() { [Parachain(para_id)] => *para_id, _ => { - log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'."); + log::error!(target: TARGET, "could not get parachain id from universal source '{local_sub:?}'."); return Err(SendError::NotApplicable) }, }; @@ -107,26 +114,39 @@ where let agent_id = match AgentHashedDescription::convert_location(&source_location) { Some(id) => id, None => { - log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'"); + log::error!(target: TARGET, "unroutable due to not being able to create agent id. '{source_location:?}'"); return Err(SendError::NotApplicable) }, }; - let message = message.take().ok_or_else(|| { - log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided."); + let message = message.clone().ok_or_else(|| { + log::error!(target: TARGET, "xcm message not provided."); SendError::MissingArgument })?; + // An workaround to inspect ExpectAsset as V2 message + let mut instructions = message.clone().0; + let result = instructions.matcher().match_next_inst_while( + |_| true, + |inst| { + return match inst { + ExpectAsset(..) => Err(ProcessMessageError::Unsupported), + _ => Ok(ControlFlow::Continue(())), + } + }, + ); + ensure!(result.is_err(), SendError::NotApplicable); + let mut converter = XcmConverter::::new(&message, expected_network, agent_id); - let message = converter.convert().map_err(|err|{ - log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'."); + let message = converter.convert().map_err(|err| { + log::error!(target: TARGET, "unroutable due to pattern matching error '{err:?}'."); SendError::Unroutable })?; // validate the message let (ticket, fee) = OutboundQueue::validate(&message).map_err(|err| { - log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}"); + log::error!(target: TARGET, "OutboundQueue validation of message failed. {err:?}"); SendError::Unroutable })?; @@ -139,16 +159,16 @@ where fn deliver(blob: (Vec, XcmHash)) -> Result { let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref()) .map_err(|_| { - log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error"); + log::trace!(target: TARGET, "undeliverable due to decoding error"); SendError::NotApplicable })?; let message_id = OutboundQueue::deliver(ticket).map_err(|_| { - log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed"); + log::error!(target: TARGET, "OutboundQueue submit of message failed"); SendError::Transport("other transport error") })?; - log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}."); + log::info!(target: TARGET, "message delivered {message_id:#?}."); Ok(message_id.into()) } } @@ -232,9 +252,17 @@ where let _ = self.next(); } - // Get the fee asset item from BuyExecution or continue parsing. - let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees); - if fee_asset.is_some() { + // Extract the fee asset item from BuyExecution + let fee_asset = match_expression!(self.next()?, BuyExecution { fees, .. }, fees) + .ok_or(InvalidFeeAsset)?; + let fee_amount = match fee_asset { + Asset { id: _, fun: Fungible(amount) } => Some(*amount), + _ => None, + } + .ok_or(AssetResolutionFailed)?; + + // Check if ExpectAsset exists and skip over it. + if match_expression!(self.peek(), Ok(ExpectAsset { .. }), ()).is_some() { let _ = self.next(); } @@ -268,14 +296,6 @@ where ensure!(reserve_assets.len() == 1, TooManyAssets); let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?; - // If there was a fee specified verify it. - if let Some(fee_asset) = fee_asset { - // The fee asset must be the same as the reserve asset. - if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun { - return Err(InvalidFeeAsset) - } - } - let (token, amount) = match reserve_asset { Asset { id: AssetId(inner_location), fun: Fungible(amount) } => match inner_location.unpack() { @@ -298,7 +318,7 @@ where // Todo: from XCMV5 AliasOrigin origin: H256::zero(), // Todo: from XCMV5 PayFees - fee: 0, + fee: fee_amount, commands: BoundedVec::try_from(vec![Command::UnlockNativeToken { agent_id: self.agent_id, token, diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs index efc1ef56f304..aafa191cba8d 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs @@ -112,7 +112,7 @@ where }, }; - let message = message.take().ok_or_else(|| { + let message = message.clone().ok_or_else(|| { log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided."); SendError::MissingArgument })?; diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml index 44121cbfdafb..d263fc9ac46d 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml @@ -50,3 +50,4 @@ snowbridge-pallet-system = { workspace = true } snowbridge-pallet-outbound-queue = { workspace = true } snowbridge-pallet-inbound-queue = { workspace = true } snowbridge-pallet-inbound-queue-fixtures = { workspace = true } +snowbridge-pallet-outbound-queue-v2 = { workspace = true } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs index 4e9dd5a77dd7..5fc252d05f1b 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs @@ -262,21 +262,12 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { vec![RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued{ .. }) => {},] ); let events = BridgeHubWestend::events(); - // Check that the local fee was credited to the Snowbridge sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == TreasuryAccount::get().into() && *amount == 5071000000 - )), - "Snowbridge sovereign takes local fee." - ); // Check that the remote fee was credited to the AssetHub sovereign account assert!( events.iter().any(|event| matches!( event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == assethub_sovereign && *amount == 2680000000000, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who,.. }) + if *who == assethub_sovereign )), "AssetHub sovereign takes remote fee." ); @@ -595,3 +586,134 @@ fn transfer_ah_token() { ); }); } + +#[test] +fn send_weth_from_asset_hub_to_ethereum_by_executing_raw_xcm() { + let assethub_location = BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()); + let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(assethub_location); + let weth_asset_location: Location = + (Parent, Parent, EthereumNetwork::get(), AccountKey20 { network: None, key: WETH }).into(); + + BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); + + AssetHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + assethub_sovereign.clone().into(), + false, + 1, + )); + + assert!(::ForeignAssets::asset_exists( + weth_asset_location.clone().try_into().unwrap(), + )); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let message = VersionedMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::SendToken { + token: WETH.into(), + destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() }, + amount: TOKEN_AMOUNT, + fee: XCM_FEE, + }, + }); + let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap(); + let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + // Check that the send token message was sent using xcm + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) =>{},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + // Check that AssetHub has issued the foreign asset + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + let local_fee_amount = 80_000_000_000; + let remote_fee_amount = 4_000_000_000; + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(local_fee_amount) }; + let remote_fee_asset = + Asset { id: AssetId(weth_asset_location.clone()), fun: Fungible(remote_fee_amount) }; + let reserve_asset = Asset { + id: AssetId(weth_asset_location.clone()), + fun: Fungible(TOKEN_AMOUNT - remote_fee_amount), + }; + let assets = vec![ + Asset { id: AssetId(weth_asset_location.clone()), fun: Fungible(TOKEN_AMOUNT) }, + local_fee_asset.clone(), + ]; + let destination = Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]); + + let beneficiary = Location::new( + 0, + [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], + ); + + // Internal xcm of InitiateReserveWithdraw, WithdrawAssets + ClearOrigin instructions will + // be appended to the front of the list by the xcm executor + let xcm_on_bh = Xcm(vec![ + BuyExecution { fees: remote_fee_asset.clone(), weight_limit: Unlimited }, + // ExpectAsset as a workaround before XCMv5 to differ Route V1 and V2 + ExpectAsset(vec![remote_fee_asset.clone()].into()), + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + ]); + + let xcms = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + // BuyExecution { fees: local_fee_asset.clone(), weight_limit: Unlimited }, + SetFeesMode { jit_withdraw: true }, + InitiateReserveWithdraw { + assets: Definite(reserve_asset.clone().into()), + // with reserve set to Ethereum destination, the ExportMessage will + // be appended to the front of the list by the SovereignPaidRemoteExporter + reserve: destination, + xcm: xcm_on_bh, + }, + ])); + + // Send the Weth back to Ethereum + ::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubWestendReceiver::get()), + bx!(xcms), + Weight::from(8_000_000_000), + ) + .unwrap(); + }); + + BridgeHubWestend::execute_with(|| { + use bridge_hub_westend_runtime::xcm_config::TreasuryAccount; + type RuntimeEvent = ::RuntimeEvent; + // Check that the transfer token back to Ethereum message was queue in the Ethereum + // Outbound Queue + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + let events = BridgeHubWestend::events(); + // Check that the remote fee was credited to the AssetHub sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) + if *who == assethub_sovereign && *amount == 2737194500000, + )), + "AssetHub sovereign takes remote fee." + ); + }); +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs index 29f9687fc880..8c71cf40a13d 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs @@ -219,8 +219,8 @@ impl xcm_executor::Config for XcmConfig { >; type MessageExporter = ( XcmOverBridgeHubRococo, - crate::bridge_to_ethereum_config::SnowbridgeExporter, crate::bridge_to_ethereum_config::SnowbridgeExporterV2, + crate::bridge_to_ethereum_config::SnowbridgeExporter, ); type UniversalAliases = Nothing; type CallDispatcher = RuntimeCall;