From 40096aa7a50cb7de35293a97e1e73cf65539d71c Mon Sep 17 00:00:00 2001 From: Ron Date: Wed, 26 Mar 2025 17:45:25 +0800 Subject: [PATCH] BACKPORT-CONFLICT --- Cargo.lock | 5 + .../bridges/bridge-hub-westend/Cargo.toml | 51 +++ .../src/tests/snowbridge_edge_case.rs | 301 ++++++++++++++++++ .../bridge-hub-westend/src/xcm_config.rs | 20 +- 4 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_edge_case.rs diff --git a/Cargo.lock b/Cargo.lock index 233fd51741179..142a218c8fd4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2832,6 +2832,11 @@ name = "bridge-hub-westend-integration-tests" version = "1.0.0" dependencies = [ "asset-hub-westend-runtime", +<<<<<<< HEAD +======= + "bp-asset-hub-westend", + "bridge-hub-common", +>>>>>>> d91e29d (Snowbridge - Deny ExportMessage from everywhere except for root of AH (#8037)) "bridge-hub-westend-runtime", "cumulus-pallet-parachain-system 0.19.0-rc1", "cumulus-pallet-xcmp-queue 0.20.0-rc1", 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 68987ceefff40..22a2da1efc2c8 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 @@ -15,6 +15,7 @@ codec = { workspace = true } hex-literal = { workspace = true, default-features = true } log = { workspace = true } scale-info = { workspace = true } +<<<<<<< HEAD frame-support.workspace = true pallet-asset-conversion.workspace = true pallet-assets.workspace = true @@ -43,3 +44,53 @@ snowbridge-pallet-inbound-queue-fixtures.workspace = true snowbridge-pallet-outbound-queue.workspace = true snowbridge-pallet-system.workspace = true snowbridge-router-primitives.workspace = true +======= + +# Substrate +frame-support = { workspace = true } +pallet-asset-conversion = { workspace = true } +pallet-assets = { workspace = true } +pallet-balances = { workspace = true } +pallet-message-queue = { workspace = true, default-features = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } + +# Polkadot +pallet-xcm = { workspace = true } +xcm = { workspace = true } +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } +xcm-runtime-apis = { workspace = true } + +# Bridges +pallet-bridge-messages = { workspace = true } +pallet-bridge-relayers = { workspace = true } + +# Cumulus +asset-hub-westend-runtime = { workspace = true } +bp-asset-hub-westend = { workspace = true } +bridge-hub-common = { workspace = true } +bridge-hub-westend-runtime = { workspace = true } +cumulus-pallet-parachain-system = { workspace = true } +cumulus-pallet-xcmp-queue = { workspace = true } +emulated-integration-tests-common = { workspace = true } +parachains-common = { workspace = true, default-features = true } +rococo-westend-system-emulated-network = { workspace = true } +testnet-parachains-constants = { features = [ + "rococo", + "westend", +], workspace = true, default-features = true } + +# Snowbridge +snowbridge-core = { workspace = true } +snowbridge-inbound-queue-primitives = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } +snowbridge-pallet-inbound-queue = { workspace = true } +snowbridge-pallet-inbound-queue-fixtures = { workspace = true } +snowbridge-pallet-inbound-queue-v2 = { workspace = true } +snowbridge-pallet-outbound-queue = { workspace = true } +snowbridge-pallet-outbound-queue-v2 = { workspace = true } +snowbridge-pallet-system = { workspace = true } +snowbridge-pallet-system-v2 = { workspace = true } +>>>>>>> d91e29d (Snowbridge - Deny ExportMessage from everywhere except for root of AH (#8037)) diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_edge_case.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_edge_case.rs new file mode 100644 index 0000000000000..6541b6f4e0ec6 --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_edge_case.rs @@ -0,0 +1,301 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + imports::*, + tests::{ + snowbridge::{CHAIN_ID, WETH}, + snowbridge_common::*, + }, +}; +use bridge_hub_westend_runtime::xcm_config::LocationToAccountId; +use snowbridge_core::AssetMetadata; +use snowbridge_pallet_system::Error; +use testnet_parachains_constants::westend::snowbridge::EthereumNetwork; +use xcm_executor::traits::ConvertLocation; + +// The user origin should be banned in ethereum_blob_exporter with error logs +// xcm::ethereum_blob_exporter: could not get parachain id from universal source +// 'X2([Parachain(1000), AccountId32 {...}])' +#[test] +fn user_send_message_directly_bypass_exporter_from_ah_will_fail() { + fund_on_bh(); + register_assets_on_ah(); + fund_on_ah(); + create_pools_on_ah(); + + let sov_account_for_sender = LocationToAccountId::convert_location(&Location::new( + 1, + [ + Parachain(AssetHubWestend::para_id().into()), + AccountId32 { + network: Some(ByGenesis(WESTEND_GENESIS_HASH)), + id: AssetHubWestendSender::get().into(), + }, + ], + )) + .unwrap(); + BridgeHubWestend::fund_accounts(vec![(sov_account_for_sender, INITIAL_FUND)]); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(1_000_000_000_000) }; + + let weth_location_reanchored = + Location::new(0, [AccountKey20 { network: None, key: WETH.into() }]); + + let weth_asset = Asset { + id: AssetId(weth_location_reanchored.clone()), + fun: Fungible(TOKEN_AMOUNT * 1_000_000_000), + }; + + assert_ok!(::PolkadotXcm::send( + RuntimeOrigin::signed(AssetHubWestendSender::get()), + bx!(VersionedLocation::from(bridge_hub())), + bx!(VersionedXcm::from(Xcm(vec![ + WithdrawAsset(local_fee_asset.clone().into()), + BuyExecution { fees: local_fee_asset.clone(), weight_limit: Unlimited }, + ExportMessage { + network: Ethereum { chain_id: CHAIN_ID }, + destination: Here, + xcm: Xcm(vec![ + WithdrawAsset(weth_asset.clone().into()), + DepositAsset { assets: Wild(All), beneficiary: beneficiary() }, + SetTopic([0; 32]), + ]), + }, + ]))), + )); + + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent{ .. }) => {},] + ); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success:false, .. }) => {},] + ); + }); +} + +// ENA is not allowed to be registered as PNA +#[test] +fn test_register_ena_on_bh_will_fail() { + BridgeHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + type Runtime = ::Runtime; + + assert_ok!(::Balances::force_set_balance( + RuntimeOrigin::root(), + sp_runtime::MultiAddress::Id(BridgeHubWestendSender::get()), + INITIAL_FUND * 10, + )); + + assert_err!( + ::EthereumSystem::register_token( + RuntimeOrigin::root(), + Box::new(VersionedLocation::from(weth_location())), + AssetMetadata { + name: "weth".as_bytes().to_vec().try_into().unwrap(), + symbol: "weth".as_bytes().to_vec().try_into().unwrap(), + decimals: 18, + }, + ), + Error::::LocationConversionFailed + ); + }); +} + +#[test] +fn user_exploit_with_arbitrary_message_will_fail() { + fund_on_bh(); + register_assets_on_ah(); + fund_on_ah(); + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let remote_fee_asset_location: Location = + Location::new(2, [EthereumNetwork::get().into()]).into(); + + let remote_fee_asset: Asset = (remote_fee_asset_location.clone(), 1).into(); + + let assets = VersionedAssets::from(vec![remote_fee_asset]); + + let exploited_weth = Asset { + id: AssetId(Location::new(0, [AccountKey20 { network: None, key: WETH.into() }])), + // A big amount without burning + fun: Fungible(TOKEN_AMOUNT * 1_000_000_000), + }; + + assert_ok!(::PolkadotXcm::transfer_assets_using_type_and_then( + RuntimeOrigin::signed(AssetHubWestendSender::get()), + bx!(VersionedLocation::from(ethereum())), + bx!(assets), + bx!(TransferType::DestinationReserve), + bx!(VersionedAssetId::from(remote_fee_asset_location.clone())), + bx!(TransferType::DestinationReserve), + // exploited_weth here is far more than the burnt, which means instructions inner + // are user provided and untrustworthy/dangerous! + // Currently it depends on EthereumBlobExporter on BH to check the message is legal + // and convert to Ethereum command. + bx!(VersionedXcm::from(Xcm(vec![ + WithdrawAsset(exploited_weth.clone().into()), + DepositAsset { assets: Wild(All), beneficiary: beneficiary() }, + SetTopic([0; 32]), + ]))), + Unlimited + )); + + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent{ .. }) => {},] + ); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success:false, .. }) => {},] + ); + }); +} + +#[test] +fn export_from_system_parachain_but_not_root_will_fail() { + fund_on_bh(); + register_assets_on_ah(); + fund_on_ah(); + create_pools_on_ah(); + + let sub_location = PalletInstance(100); + let assethub_pallet_sovereign = BridgeHubWestend::sovereign_account_id_of(Location::new( + 1, + [Parachain(AssetHubWestend::para_id().into()), sub_location], + )); + BridgeHubWestend::fund_accounts(vec![(assethub_pallet_sovereign.clone(), INITIAL_FUND)]); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(1_000_000_000_000) }; + + let weth_location_reanchored = + Location::new(0, [AccountKey20 { network: None, key: WETH.into() }]); + + let weth_asset = Asset { + id: AssetId(weth_location_reanchored.clone()), + fun: Fungible(TOKEN_AMOUNT * 1_000_000_000), + }; + + assert_ok!(::PolkadotXcm::send( + RuntimeOrigin::root(), + bx!(VersionedLocation::from(bridge_hub())), + bx!(VersionedXcm::from(Xcm(vec![ + DescendOrigin(sub_location.into()), + WithdrawAsset(local_fee_asset.clone().into()), + BuyExecution { fees: local_fee_asset.clone(), weight_limit: Unlimited }, + ExportMessage { + network: Ethereum { chain_id: CHAIN_ID }, + destination: Here, + xcm: Xcm(vec![ + WithdrawAsset(weth_asset.clone().into()), + DepositAsset { assets: Wild(All), beneficiary: beneficiary() }, + SetTopic([0; 32]), + ]), + }, + ]))), + )); + + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent{ .. }) => {},] + ); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success:false, .. }) => {},] + ); + }); +} + +#[test] +fn export_from_non_system_parachain_will_fail() { + let penpal_sovereign = BridgeHubWestend::sovereign_account_id_of(Location::new( + 1, + [Parachain(PenpalB::para_id().into())], + )); + BridgeHubWestend::fund_accounts(vec![(penpal_sovereign.clone(), INITIAL_FUND)]); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::here()), fun: Fungible(1_000_000_000_000) }; + + let weth_location_reanchored = + Location::new(0, [AccountKey20 { network: None, key: WETH.into() }]); + + let weth_asset = + Asset { id: AssetId(weth_location_reanchored.clone()), fun: Fungible(TOKEN_AMOUNT) }; + + assert_ok!(::PolkadotXcm::send( + RuntimeOrigin::root(), + bx!(VersionedLocation::from(bridge_hub())), + bx!(VersionedXcm::from(Xcm(vec![ + WithdrawAsset(local_fee_asset.clone().into()), + BuyExecution { fees: local_fee_asset.clone(), weight_limit: Unlimited }, + ExportMessage { + network: Ethereum { chain_id: CHAIN_ID }, + destination: Here, + xcm: Xcm(vec![ + WithdrawAsset(weth_asset.clone().into()), + DepositAsset { assets: Wild(All), beneficiary: beneficiary() }, + SetTopic([0; 32]), + ]), + }, + ]))), + )); + + assert_expected_events!( + PenpalB, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent{ .. }) => {},] + ); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success:false, origin, .. }) => { + origin: *origin == bridge_hub_common::AggregateMessageOrigin::Sibling(PenpalB::para_id()), + },] + ); + }); +} 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 4b6132c5be6c6..b525dc8bd5100 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 @@ -19,9 +19,17 @@ use super::{ ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, TransactionByteFee, WeightToFee, XcmOverBridgeHubRococo, XcmpQueue, }; +<<<<<<< HEAD +======= +use crate::bridge_to_ethereum_config::{AssetHubLocation, SnowbridgeFrontendLocation}; +use bridge_hub_common::DenyExportMessageFrom; +>>>>>>> d91e29d (Snowbridge - Deny ExportMessage from everywhere except for root of AH (#8037)) use frame_support::{ parameter_types, - traits::{tokens::imbalance::ResolveTo, ConstU32, Contains, Equals, Everything, Nothing}, + traits::{ + tokens::imbalance::ResolveTo, ConstU32, Contains, Equals, Everything, EverythingBut, + Nothing, + }, }; use frame_system::EnsureRoot; use pallet_collator_selection::StakingPotAccountId; @@ -130,7 +138,15 @@ impl Contains for ParentOrParentsPlurality { pub type Barrier = TrailingSetTopicAsId< DenyThenTry< - DenyRecursively, + ( + DenyRecursively, + DenyRecursively< + DenyExportMessageFrom< + EverythingBut>, + Equals, + >, + >, + ), ( // Allow local users to buy weight credit. TakeWeightCredit,