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 index a4fbdb255a14f..715a279daa973 100644 --- 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 @@ -23,6 +23,7 @@ use crate::{ 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 @@ -123,3 +124,176 @@ fn test_register_ena_on_bh_will_fail() { ); }); } + +#[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_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::ProcessingFailed{ .. }) => {},] + ); + }); +} + +#[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.clone()], + )); + 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, .. }) => {},] + ); + }); +} 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 7965aa8c01555..4b29aeb7bb791 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,10 +19,14 @@ use super::{ ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, TransactionByteFee, WeightToFee, XcmOverBridgeHubRococo, XcmpQueue, }; -use crate::bridge_to_ethereum_config::SnowbridgeFrontendLocation; +use crate::bridge_to_ethereum_config::{AssethubLocation, SnowbridgeFrontendLocation}; +use bridge_hub_common::DenyExportMessageFrom; 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 +134,15 @@ impl Contains for ParentOrParentsPlurality { pub type Barrier = TrailingSetTopicAsId< DenyThenTry< - DenyRecursively, + ( + DenyRecursively, + DenyRecursively< + DenyExportMessageFrom< + EverythingBut>, + Equals, + >, + >, + ), ( // Allow local users to buy weight credit. TakeWeightCredit,