diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c01f5b60d..d5206b9c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - [#8021](https://github.com/paritytech/polkadot-sdk/pull/8021): XCMP: use batching when enqueuing inbound messages This PR implements batching for the XCMP inbound enqueueing logic, which leads to an about ~75x performance improvement for that specific code. - [#9202](https://github.com/paritytech/polkadot-sdk/pull/9202): `apply_authorized_force_set_current_code` does not need to consume the whole block +- Add Snowbridge V2 pallets, to enable Snowbridge V2 bridging: [polkadot-fellows/runtimes/pull/796](https://github.com/polkadot-fellows/runtimes/pull/796)) ## [1.6.1] 24.06.2025 diff --git a/Cargo.lock b/Cargo.lock index 90915e83a6..5d22970750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,6 +1064,9 @@ dependencies = [ "scale-info", "serde_json", "snowbridge-inbound-queue-primitives", + "snowbridge-outbound-queue-primitives", + "snowbridge-pallet-system-frontend", + "snowbridge-runtime-common", "sp-api", "sp-block-builder", "sp-consensus-aura", @@ -2086,6 +2089,7 @@ name = "bridge-hub-polkadot-integration-tests" version = "1.0.0" dependencies = [ "asset-hub-polkadot-runtime", + "bp-asset-hub-polkadot", "bp-bridge-hub-polkadot", "bp-messages", "bridge-hub-common", @@ -2101,6 +2105,7 @@ dependencies = [ "pallet-assets", "pallet-balances", "pallet-bridge-messages", + "pallet-bridge-relayers", "pallet-message-queue", "pallet-xcm", "parachains-common", @@ -2111,12 +2116,19 @@ dependencies = [ "snowbridge-core", "snowbridge-inbound-queue-primitives", "snowbridge-outbound-queue-primitives", + "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", + "snowbridge-pallet-inbound-queue-v2", "snowbridge-pallet-outbound-queue", + "snowbridge-pallet-outbound-queue-v2", "snowbridge-pallet-system", + "snowbridge-pallet-system-frontend", + "snowbridge-pallet-system-v2", "sp-core 38.0.0", + "sp-io", "sp-runtime", "staging-xcm", + "staging-xcm-builder", "staging-xcm-executor", "system-parachains-constants", "xcm-runtime-apis", @@ -2197,8 +2209,11 @@ dependencies = [ "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", + "snowbridge-pallet-inbound-queue-v2", "snowbridge-pallet-outbound-queue", + "snowbridge-pallet-outbound-queue-v2", "snowbridge-pallet-system", + "snowbridge-pallet-system-v2", "snowbridge-runtime-common", "snowbridge-runtime-test-common", "snowbridge-system-runtime-api", @@ -13890,6 +13905,50 @@ dependencies = [ "sp-std", ] +[[package]] +name = "snowbridge-pallet-inbound-queue-v2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fcbdf36c77e56c1192fcd3f45179d85ff6a7075dedf3bcb0f6abb46a3cf3c6" +dependencies = [ + "alloy-core", + "bp-relayers", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "snowbridge-beacon-primitives", + "snowbridge-core", + "snowbridge-inbound-queue-primitives", + "snowbridge-pallet-inbound-queue-v2-fixtures", + "sp-core 38.0.0", + "sp-io", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", + "tracing", +] + +[[package]] +name = "snowbridge-pallet-inbound-queue-v2-fixtures" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61d72022a1b4776a2394f324e031e7ce9e9bfacae28287f93c84e0a38714a2e8" +dependencies = [ + "hex-literal", + "snowbridge-beacon-primitives", + "snowbridge-core", + "snowbridge-inbound-queue-primitives", + "sp-core 38.0.0", + "sp-std", +] + [[package]] name = "snowbridge-pallet-outbound-queue" version = "0.15.0" @@ -13914,6 +13973,38 @@ dependencies = [ "sp-std", ] +[[package]] +name = "snowbridge-pallet-outbound-queue-v2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a1a8e50ccf34b2df404af49969f1ead3bd3a55faac8865dd8d8e4ce91c15d" +dependencies = [ + "alloy-core", + "bp-relayers", + "bridge-hub-common", + "ethabi-decode", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "parity-scale-codec", + "scale-info", + "serde", + "snowbridge-beacon-primitives", + "snowbridge-core", + "snowbridge-merkle-tree", + "snowbridge-outbound-queue-primitives", + "snowbridge-verification-primitives", + "sp-arithmetic", + "sp-core 38.0.0", + "sp-io", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "snowbridge-pallet-system" version = "0.15.0" @@ -13936,6 +14027,53 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "snowbridge-pallet-system-frontend" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823e590b3d44669b35848c7367dc92a98842e296a52b6aaba68f66d7492afb19" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-asset-conversion", + "parity-scale-codec", + "scale-info", + "snowbridge-core", + "sp-core 38.0.0", + "sp-io", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-executor", + "tracing", +] + +[[package]] +name = "snowbridge-pallet-system-v2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "651de16e94a248c933eaba05f4bf59777e5bbfc746f414518e2a82a163c3dc07" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "snowbridge-core", + "snowbridge-outbound-queue-primitives", + "snowbridge-pallet-system", + "sp-core 38.0.0", + "sp-io", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-executor", + "tracing", +] + [[package]] name = "snowbridge-runtime-common" version = "0.15.0" diff --git a/Cargo.toml b/Cargo.toml index 6bf7d7c7de..f50b26eb8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,10 +209,14 @@ snowbridge-outbound-queue-runtime-api = { version = "0.15.0", default-features = snowbridge-outbound-queue-primitives = { version = "0.4.0", default-features = false } snowbridge-pallet-ethereum-client = { version = "0.15.0", default-features = false } snowbridge-pallet-inbound-queue = { version = "0.15.0", default-features = false } +snowbridge-pallet-inbound-queue-v2 = { version = "0.4.0", default-features = false } snowbridge-pallet-inbound-queue-fixtures = { version = "0.23.0" } snowbridge-pallet-ethereum-client-fixtures = { version = "0.23.0" } snowbridge-pallet-outbound-queue = { version = "0.15.0", default-features = false } +snowbridge-pallet-outbound-queue-v2 = { version = "0.4.0", default-features = false } snowbridge-pallet-system = { version = "0.15.0", default-features = false } +snowbridge-pallet-system-v2 = { version = "0.4.0", default-features = false } +snowbridge-pallet-system-frontend = { version = "0.4.0", default-features = false } snowbridge-inbound-queue-primitives = { version = "0.4.0", default-features = false } snowbridge-runtime-common = { version = "0.15.0", default-features = false } snowbridge-runtime-test-common = { version = "0.17.0" } diff --git a/integration-tests/emulated/chains/parachains/assets/asset-hub-polkadot/src/lib.rs b/integration-tests/emulated/chains/parachains/assets/asset-hub-polkadot/src/lib.rs index 354abc4268..4818101918 100644 --- a/integration-tests/emulated/chains/parachains/assets/asset-hub-polkadot/src/lib.rs +++ b/integration-tests/emulated/chains/parachains/assets/asset-hub-polkadot/src/lib.rs @@ -48,6 +48,7 @@ decl_test_parachains! { ForeignAssets: asset_hub_polkadot_runtime::ForeignAssets, PoolAssets: asset_hub_polkadot_runtime::PoolAssets, AssetConversion: asset_hub_polkadot_runtime::AssetConversion, + SnowbridgeSystemFrontend: asset_hub_polkadot_runtime::SnowbridgeSystemFrontend, } }, } diff --git a/integration-tests/emulated/chains/parachains/bridges/bridge-hub-polkadot/src/lib.rs b/integration-tests/emulated/chains/parachains/bridges/bridge-hub-polkadot/src/lib.rs index cf50845853..278b7cef22 100644 --- a/integration-tests/emulated/chains/parachains/bridges/bridge-hub-polkadot/src/lib.rs +++ b/integration-tests/emulated/chains/parachains/bridges/bridge-hub-polkadot/src/lib.rs @@ -44,6 +44,8 @@ decl_test_parachains! { EthereumSystem: bridge_hub_polkadot_runtime::EthereumSystem, EthereumInboundQueue: bridge_hub_polkadot_runtime::EthereumInboundQueue, EthereumOutboundQueue: bridge_hub_polkadot_runtime::EthereumOutboundQueue, + EthereumSystemV2: bridge_hub_polkadot_runtime::EthereumSystemV2, + BridgeRelayers: bridge_hub_polkadot_runtime::BridgeRelayers, } }, } diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/Cargo.toml b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/Cargo.toml index 87aa55a607..9e171cb00f 100644 --- a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/Cargo.toml +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/Cargo.toml @@ -14,6 +14,7 @@ hex-literal = { workspace = true } # Substrate sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } frame-support = { workspace = true, default-features = true } pallet-balances = { workspace = true, default-features = true } @@ -26,6 +27,7 @@ xcm = { workspace = true, default-features = true } pallet-xcm = { workspace = true, default-features = true } xcm-executor = { workspace = true, default-features = true } xcm-runtime-apis = { workspace = true, default-features = true } +xcm-builder = { workspace = true } # Cumulus emulated-integration-tests-common = { workspace = true } @@ -36,10 +38,12 @@ cumulus-pallet-xcmp-queue = { workspace = true, default-features = true } # Bridges bp-messages = { workspace = true, default-features = true } pallet-bridge-messages = { workspace = true, default-features = true } +pallet-bridge-relayers = { workspace = true, default-features = true } # Local bp-bridge-hub-polkadot = { workspace = true, default-features = true } bridge-hub-common = { workspace = true, default-features = true } +bp-asset-hub-polkadot = { workspace = true, default-features = true } bridge-hub-polkadot-runtime = { workspace = true } asset-hub-polkadot-runtime = { workspace = true } integration-tests-helpers = { workspace = true } @@ -48,17 +52,23 @@ polkadot-system-emulated-network = { workspace = true } system-parachains-constants = { workspace = true, default-features = true } # Snowbridge -snowbridge-beacon-primitives = { workspace = true, default-features = true } -snowbridge-core = { workspace = true, default-features = true } -snowbridge-inbound-queue-primitives = { workspace = true, default-features = true } -snowbridge-pallet-system = { workspace = true, default-features = true } -snowbridge-pallet-outbound-queue = { workspace = true, default-features = true } -snowbridge-pallet-inbound-queue-fixtures = { workspace = true } +snowbridge-core = { workspace = true } +snowbridge-beacon-primitives = { 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-frontend = { workspace = true } +snowbridge-pallet-system-v2 = { workspace = true } [features] runtime-benchmarks = [ "asset-hub-polkadot-runtime/runtime-benchmarks", + "bp-asset-hub-polkadot/runtime-benchmarks", "bp-bridge-hub-polkadot/runtime-benchmarks", "bridge-hub-common/runtime-benchmarks", "bridge-hub-polkadot-runtime/runtime-benchmarks", @@ -71,6 +81,7 @@ runtime-benchmarks = [ "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bridge-messages/runtime-benchmarks", + "pallet-bridge-relayers/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", @@ -78,10 +89,17 @@ runtime-benchmarks = [ "snowbridge-core/runtime-benchmarks", "snowbridge-inbound-queue-primitives/runtime-benchmarks", "snowbridge-pallet-inbound-queue-fixtures/runtime-benchmarks", + "snowbridge-pallet-inbound-queue-v2/runtime-benchmarks", + "snowbridge-pallet-inbound-queue-v2/runtime-benchmarks", + "snowbridge-pallet-inbound-queue/runtime-benchmarks", + "snowbridge-pallet-outbound-queue-v2/runtime-benchmarks", "snowbridge-pallet-outbound-queue/runtime-benchmarks", + "snowbridge-pallet-system-frontend/runtime-benchmarks", + "snowbridge-pallet-system-v2/runtime-benchmarks", "snowbridge-pallet-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "system-parachains-constants/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", "xcm-runtime-apis/runtime-benchmarks", "xcm/runtime-benchmarks", diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/asset_transfers.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/asset_transfers.rs index 301fe6572d..e61a268261 100644 --- a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/asset_transfers.rs +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/asset_transfers.rs @@ -170,7 +170,7 @@ fn send_dot_usdt_and_weth_from_asset_hub_polkadot_to_asset_hub_kusama() { let bridged_weth_at_ah = weth_at_asset_hubs(); let bridged_weth_at_ah_latest: Location = bridged_weth_at_ah.clone(); - let snowbridge_sovereign: AccountId = + let ethereum_sovereign: AccountId = EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&Location::new( 2, [GlobalConsensus(EthereumNetwork::get())], @@ -186,7 +186,7 @@ fn send_dot_usdt_and_weth_from_asset_hub_polkadot_to_asset_hub_kusama() { amount * 2, ); AssetHubPolkadot::mint_foreign_asset( - ::RuntimeOrigin::signed(snowbridge_sovereign), + ::RuntimeOrigin::signed(ethereum_sovereign), bridged_weth_at_ah.clone(), sender.clone(), MIN_ETHER_BALANCE, diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/mod.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/mod.rs index 6486958d7a..7b872552d8 100644 --- a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/mod.rs +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/mod.rs @@ -21,6 +21,13 @@ mod claim_assets; mod register_bridged_assets; mod send_xcm; mod snowbridge; +mod snowbridge_common; +mod snowbridge_v2_config; +mod snowbridge_v2_inbound; +mod snowbridge_v2_inbound_to_kusama; +mod snowbridge_v2_outbound; +mod snowbridge_v2_outbound_edge_case; +mod snowbridge_v2_outbound_from_kusama; mod teleport; pub(crate) fn asset_hub_kusama_location() -> Location { diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge.rs index 225e6d59f2..407abfea33 100644 --- a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge.rs +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge.rs @@ -16,22 +16,20 @@ use crate::{ tests::{ assert_bridge_hub_kusama_message_received, assert_bridge_hub_polkadot_message_accepted, asset_hub_kusama_location, bridged_dot_at_ah_kusama, create_foreign_on_ah_kusama, - dot_at_ah_polkadot, + dot_at_ah_polkadot, snowbridge_common::*, }, *, }; -use asset_hub_polkadot_runtime::xcm_config::{ - bridging::to_ethereum::{BridgeHubEthereumBaseFee, EthereumNetwork}, - RelayTreasuryPalletAccount, +use asset_hub_polkadot_runtime::xcm_config::bridging::to_ethereum::{ + BridgeHubEthereumBaseFee, EthereumNetwork, }; use bp_bridge_hub_polkadot::snowbridge::CreateAssetCall; use bridge_hub_polkadot_runtime::{ - bridge_to_ethereum_config::EthereumGatewayAddress, EthereumBeaconClient, EthereumInboundQueue, - Runtime, RuntimeOrigin, + bridge_to_ethereum_config::EthereumGatewayAddress, xcm_config::RelayTreasuryPalletAccount, + EthereumBeaconClient, EthereumInboundQueue, Runtime, RuntimeOrigin, }; -use codec::{Decode, DecodeWithMemTracking, Encode}; +use codec::Encode; use emulated_integration_tests_common::{xcm_emulator::ConvertLocation, RESERVABLE_ASSET_ID}; -use frame_support::pallet_prelude::TypeInfo; use hex_literal::hex; use integration_tests_helpers::common::snowbridge::{MIN_ETHER_BALANCE, WETH}; use polkadot_system_emulated_network::{ @@ -47,7 +45,6 @@ use snowbridge_inbound_queue_primitives::{ v1::{Command, Destination, MessageV1, VersionedMessage}, EthereumLocationsConverterFor, EventFixture, EventProof, Log, Proof, }; -use snowbridge_outbound_queue_primitives::OperatingMode; use snowbridge_pallet_system::PricingParametersOf; use sp_core::{H160, H256, U256}; use sp_runtime::{DispatchError::Token, FixedU128, TokenError::FundsUnavailable}; @@ -64,21 +61,6 @@ const TOKEN_AMOUNT: u128 = 20_000_000_000_000; const AH_BASE_FEE: u128 = 2_750_872_500_000u128; const ETHER_TOKEN_ADDRESS: [u8; 20] = [0; 20]; -#[derive(Encode, Decode, DecodeWithMemTracking, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub enum ControlCall { - #[codec(index = 3)] - CreateAgent, - #[codec(index = 4)] - CreateChannel { mode: OperatingMode }, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Encode, Decode, DecodeWithMemTracking, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub enum SnowbridgeControl { - #[codec(index = 83)] - Control(ControlCall), -} - pub fn send_inbound_message(fixture: EventFixture) -> DispatchResult { EthereumBeaconClient::store_finalized_header( fixture.finalized_header, @@ -155,15 +137,7 @@ fn send_token_from_ethereum_to_penpal() { // Fund ethereum sovereign on AssetHub AssetHubPolkadot::fund_accounts(vec![(ethereum_sovereign_account(), INITIAL_FUND)]); - PenpalB::execute_with(|| { - assert_ok!(::System::set_storage( - ::RuntimeOrigin::root(), - vec![( - PenpalCustomizableAssetFromSystemAssetHub::key().to_vec(), - Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]).encode(), - )], - )); - }); + set_trust_reserve_on_penpal(); // Create asset on the Penpal parachain. PenpalB::execute_with(|| { diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_common.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_common.rs new file mode 100644 index 0000000000..1b6fcdd76d --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_common.rs @@ -0,0 +1,536 @@ +// 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::{tests::snowbridge::CHAIN_ID, *}; +use asset_hub_polkadot_runtime::xcm_config::{ + bridging::to_ethereum::BridgeHubEthereumBaseFeeV2, LocationToAccountId, +}; +use bp_bridge_hub_polkadot::snowbridge::EthereumNetwork; +use emulated_integration_tests_common::{ + create_pool_with_native_on, PenpalBTeleportableAssetLocation, +}; +use frame_support::traits::fungibles::Mutate; +use hex_literal::hex; +use polkadot_system_emulated_network::penpal_emulated_chain::{ + penpal_runtime::xcm_config::{CheckingAccount, TELEPORTABLE_ASSET_ID}, + PenpalAssetOwner, +}; +use snowbridge_core::AssetMetadata; +use sp_core::H160; +use xcm_builder::ExternalConsensusLocationsConverterFor; +use xcm_executor::traits::ConvertLocation; + +/// Initial fund in DOT to be used to prefund test and sovereign accounts. +pub const INITIAL_FUND: u128 = 50_000_000_000_000_000; +/// A beneficiary address on Ethereum. +pub const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); +/// Agent on Ethereum address. +pub const AGENT_ADDRESS: [u8; 20] = hex!("90A987B944Cb1dCcE5564e5FDeCD7a54D3de27Fe"); +/// A test ERC-20 token to be registered and sent. +pub const TOKEN_ID: [u8; 20] = hex!("8daebade922df735c38c80c7ebd708af50815faa"); +/// ERC-20 token amount to be transferred. +pub const TOKEN_AMOUNT: u128 = 10_000_000_000_000_000; +/// The fee in ether to be sent +pub const REMOTE_FEE_AMOUNT_IN_ETHER: u128 = 6_000_000_000_000_000; +/// Local execution fee in DOT. +pub const LOCAL_FEE_AMOUNT_IN_DOT: u128 = 80_000_000_000_000; +/// Execution weight provided as limited for XCM execute. +pub const EXECUTION_WEIGHT: u64 = 800_000_000_000; +/// The execution fee (in Ether) for execution on AssetHub. +pub const EXECUTION_IN_ETHER: u128 = 1_500_000_000_000; +/// The reward allocated to the relayer for relaying the message. +pub const RELAYER_REWARD_IN_ETHER: u128 = 1_500_000_000_000; +/// The base cost for transfers to Ethereum, for Snowbridge V2. +const AH_BASE_FEE_V2: u128 = 100_000_000_000; +/// Amount of native to be provided for pool creation. +const DOT_POOL_AMOUNT: u128 = 900_000_000_000; +/// Amount of ether to be provided for pool creation. +const ETH_POOL_AMOUNT: u128 = 100_000_000_000_000; + +pub fn beneficiary() -> Location { + Location::new(0, [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS }]) +} + +pub fn asset_hub() -> Location { + Location::new(1, Parachain(AssetHubPolkadot::para_id().into())) +} + +pub fn bridge_hub() -> Location { + Location::new(1, Parachain(BridgeHubPolkadot::para_id().into())) +} + +pub(crate) fn asset_hub_polkadot_location() -> Location { + Location::new( + 2, + [GlobalConsensus(NetworkId::Polkadot), Parachain(AssetHubPolkadot::para_id().into())], + ) +} +pub(crate) fn bridge_hub_polkadot_location() -> Location { + Location::new( + 2, + [GlobalConsensus(NetworkId::Polkadot), Parachain(BridgeHubPolkadot::para_id().into())], + ) +} + +pub fn fund_on_bh() { + let assethub_sovereign = BridgeHubPolkadot::sovereign_account_id_of(asset_hub()); + BridgeHubPolkadot::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); +} + +pub fn weth_location() -> Location { + erc20_token_location(WETH.into()) +} + +pub fn eth_location() -> Location { + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]) +} + +pub fn erc20_token_location(token_id: H160) -> Location { + Location::new( + 2, + [ + GlobalConsensus(EthereumNetwork::get()), + AccountKey20 { network: None, key: token_id.into() }, + ], + ) +} + +pub(crate) fn bridged_ksm_at_ah_polkadot() -> Location { + Location::new(2, [GlobalConsensus(Kusama)]) +} + +pub fn penpal_root_sovereign() -> sp_runtime::AccountId32 { + let penpal_root_sovereign: AccountId = PenpalB::execute_with(|| { + use polkadot_system_emulated_network::penpal_emulated_chain::penpal_runtime::xcm_config; + xcm_config::LocationToAccountId::convert_location(&xcm_config::RootLocation::get()).unwrap() + }); + penpal_root_sovereign +} + +pub fn ethereum_sovereign() -> sp_runtime::AccountId32 { + use asset_hub_polkadot_runtime::xcm_config::UniversalLocation as AssetHubPolkadotUniversalLocation; + AssetHubPolkadot::execute_with(|| { + ExternalConsensusLocationsConverterFor::< + AssetHubPolkadotUniversalLocation, + [u8; 32], + >::convert_location(&Location::new( + 2, + [GlobalConsensus(EthereumNetwork::get())], + )) + .unwrap() + .into() + }) +} + +/// Registers KSM as a native Polkadot asset on Snowbridge. +pub fn register_ksm_as_native_polkadot_asset_on_snowbridge() { + register_asset_native_polkadot_asset_on_snowbridge( + bridged_ksm_at_ah_polkadot(), + String::from("ksm"), + String::from("KSM"), + 12, + ); +} + +/// Registers DOT as a native Polkadot asset on Snowbridge. +pub fn register_relay_token_on_polkadot_bh() { + register_asset_native_polkadot_asset_on_snowbridge( + Location::parent(), + String::from("dot"), + String::from("DOT"), + 10, + ); +} + +/// Method to register a native asset on Snowbridge. +pub fn register_asset_native_polkadot_asset_on_snowbridge( + asset_location: Location, + name: String, + symbol: String, + decimals: u8, +) { + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::EthereumSystem::register_token( + RuntimeOrigin::root(), + Box::new(VersionedLocation::from(asset_location)), + AssetMetadata { + name: name.as_bytes().to_vec().try_into().unwrap(), + symbol: symbol.as_bytes().to_vec().try_into().unwrap(), + decimals, + }, + )); + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumSystem(snowbridge_pallet_system::Event::RegisterToken { .. }) => {},] + ); + }); +} + +/// Register Ether and Weth on Penpal B. +pub fn register_ethereum_assets_on_penpal() { + let ethereum_sovereign: AccountId = ethereum_sovereign(); + register_foreign_asset_on_penpal(weth_location(), ethereum_sovereign.clone(), true); + register_foreign_asset_on_penpal(eth_location(), ethereum_sovereign, true); +} + +/// Register a foreign asset on PenpalB. +pub fn register_foreign_asset_on_penpal(id: Location, owner: AccountId, sufficient: bool) { + PenpalB::force_create_foreign_asset(id, owner, sufficient, ASSET_MIN_BALANCE, vec![]); +} + +/// Registers a foreign asset on Polkadot AssetHub. +pub fn register_foreign_asset(id: Location, owner: AccountId, sufficient: bool) { + AssetHubPolkadot::force_create_foreign_asset(id, owner, sufficient, ASSET_MIN_BALANCE, vec![]); +} + +/// Create PAL (native asset for penpal) on AH. +pub fn register_pal_on_polkadot_asset_hub() { + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + let penpal_asset_id = Location::new(1, Parachain(PenpalB::para_id().into())); + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + penpal_asset_id.clone(), + PenpalAssetOwner::get().into(), + false, + 1_000_000, + )); + + assert!(::ForeignAssets::asset_exists( + penpal_asset_id.clone(), + )); + + assert_ok!(::ForeignAssets::mint_into( + penpal_asset_id.clone(), + &AssetHubPolkadotReceiver::get(), + TOKEN_AMOUNT, + )); + + assert_ok!(::ForeignAssets::mint_into( + penpal_asset_id.clone(), + &AssetHubPolkadotSender::get(), + TOKEN_AMOUNT, + )); + }); +} + +pub fn register_pal_on_polkadot_bh() { + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::EthereumSystem::register_token( + RuntimeOrigin::root(), + Box::new(VersionedLocation::from(PenpalBTeleportableAssetLocation::get())), + AssetMetadata { + name: "pal".as_bytes().to_vec().try_into().unwrap(), + symbol: "pal".as_bytes().to_vec().try_into().unwrap(), + decimals: 12, + }, + )); + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumSystem(snowbridge_pallet_system::Event::RegisterToken { .. }) => {},] + ); + }); +} + +/// Fund all the accounts that need to funded for tests, on Penpal B. +pub fn prefund_accounts_on_penpal_b() { + let sudo_account = penpal_root_sovereign(); + PenpalB::fund_accounts(vec![ + (PenpalBReceiver::get(), INITIAL_FUND), + (PenpalBSender::get(), INITIAL_FUND), + (CheckingAccount::get(), INITIAL_FUND), + (sudo_account.clone(), INITIAL_FUND), + ]); + PenpalB::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + Location::parent(), + &PenpalBReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + Location::parent(), + &PenpalBSender::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + Location::parent(), + &sudo_account, + INITIAL_FUND, + )); + assert_ok!(::Assets::mint_into( + TELEPORTABLE_ASSET_ID, + &PenpalBReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::Assets::mint_into( + TELEPORTABLE_ASSET_ID, + &PenpalBSender::get(), + INITIAL_FUND, + )); + assert_ok!(::Assets::mint_into( + TELEPORTABLE_ASSET_ID, + &sudo_account, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &PenpalBReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &PenpalBSender::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &sudo_account, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &PenpalBReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &PenpalBSender::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &sudo_account, + INITIAL_FUND, + )); + }); +} + +/// Fund all the accounts that need to funded for tests, on Polkadot AssetHub. +pub fn prefund_accounts_on_polkadot_asset_hub() { + AssetHubPolkadot::fund_accounts(vec![(AssetHubPolkadotSender::get(), INITIAL_FUND)]); + AssetHubPolkadot::fund_accounts(vec![(AssetHubPolkadotReceiver::get(), INITIAL_FUND)]); + + let penpal_sovereign_on_pah = AssetHubPolkadot::sovereign_account_id_of( + AssetHubPolkadot::sibling_location_of(PenpalB::para_id()), + ); + let penpal_user_sovereign_on_pah = LocationToAccountId::convert_location(&Location::new( + 1, + [ + Parachain(PenpalB::para_id().into()), + AccountId32 { network: Some(NetworkId::Polkadot), id: PenpalBSender::get().into() }, + ], + )) + .unwrap(); + + AssetHubPolkadot::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &penpal_sovereign_on_pah, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &penpal_user_sovereign_on_pah, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &AssetHubPolkadotReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + weth_location(), + &AssetHubPolkadotSender::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &penpal_sovereign_on_pah, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &penpal_user_sovereign_on_pah, + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &AssetHubPolkadotReceiver::get(), + INITIAL_FUND, + )); + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &AssetHubPolkadotSender::get(), + INITIAL_FUND, + )); + }); + + AssetHubPolkadot::fund_accounts(vec![(ethereum_sovereign(), INITIAL_FUND)]); + AssetHubPolkadot::fund_accounts(vec![(penpal_sovereign_on_pah.clone(), INITIAL_FUND)]); + AssetHubPolkadot::fund_accounts(vec![(penpal_user_sovereign_on_pah.clone(), INITIAL_FUND)]); +} + +/// Create a pool between DOT and ETH on Polkadot AssetHub to support paying for fees with ETH. +pub(crate) fn set_up_eth_and_dot_pool_on_polkadot_asset_hub() { + set_up_foreign_asset_and_dot_pool_on_polkadot_asset_hub(eth_location()); +} + +/// Create a pool between DOT and a foreign asset on Polkadot AssetHub. +pub(crate) fn set_up_foreign_asset_and_dot_pool_on_polkadot_asset_hub(asset: Location) { + let ethereum_sovereign = ethereum_sovereign(); + AssetHubPolkadot::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + AssetHubPolkadot::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + asset.clone(), + ðereum_sovereign.clone(), + INITIAL_FUND, + )); + }); + create_pool_with_native_on!( + AssetHubPolkadot, + asset, + true, + ethereum_sovereign.clone(), + DOT_POOL_AMOUNT, + ETH_POOL_AMOUNT + ); +} + +/// Create a pool between DOT and ETH on Penpal to support paying for fees with ETH. +pub(crate) fn set_up_eth_and_dot_pool_on_penpal() { + let ethereum_sovereign = ethereum_sovereign(); + PenpalB::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + PenpalB::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + ðereum_sovereign.clone(), + INITIAL_FUND, + )); + }); + create_pool_with_native_on!( + PenpalB, + eth_location(), + true, + ethereum_sovereign.clone(), + DOT_POOL_AMOUNT, + ETH_POOL_AMOUNT + ); +} + +/// Setup KSM and Ether pool on Kusama to pay for fees. +pub(crate) fn set_up_eth_and_ksm_pool_on_kusama_asset_hub() { + let sa_of_pah_on_kah = AssetHubKusama::sovereign_account_of_parachain_on_other_global_consensus( + NetworkId::Polkadot, + AssetHubPolkadot::para_id(), + ); + AssetHubKusama::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &sa_of_pah_on_kah.clone(), + INITIAL_FUND, + )); + }); + AssetHubKusama::fund_accounts(vec![(sa_of_pah_on_kah.clone(), INITIAL_FUND)]); + create_pool_with_native_on!( + AssetHubKusama, + eth_location(), + true, + sa_of_pah_on_kah.clone(), + DOT_POOL_AMOUNT, + ETH_POOL_AMOUNT + ); +} + +/// Set the BridgeHubEthereumBaseFeeV2 storage item in the Polkadot AssetHub xcm config. +/// This is the minimum fee to send transactions from Polkadot AH to Ethereum. +pub fn set_bridge_hub_ethereum_base_fee() { + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::System::set_storage( + RuntimeOrigin::root(), + vec![(BridgeHubEthereumBaseFeeV2::key().to_vec(), AH_BASE_FEE_V2.encode())], + )); + }); +} + +/// Set the PenpalCustomizableAssetFromSystemAssetHub storage item to trust assets from +/// Ethereum. +pub fn set_trust_reserve_on_penpal() { + PenpalB::execute_with(|| { + assert_ok!(::System::set_storage( + ::RuntimeOrigin::root(), + vec![( + PenpalCustomizableAssetFromSystemAssetHub::key().to_vec(), + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]).encode(), + )], + )); + }); +} + +/// Check that no assets were trapped on Polkadot AssetHub. +pub fn ensure_no_assets_trapped_on_pah() { + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = AssetHubPolkadot::events(); + assert!( + !events.iter().any(|event| matches!( + event, + RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. }) + )), + "Assets were trapped on Polkadot AssetHub, should not happen." + ); + }); +} + +/// Check that no assets were trapped on Kusama AssetHub. +pub fn ensure_no_assets_trapped_on_kah() { + AssetHubKusama::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = AssetHubKusama::events(); + assert!( + !events.iter().any(|event| matches!( + event, + RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. }) + )), + "Assets were trapped on Kusama AssetHub, should not happen." + ); + }); +} + +/// Check that no assets were trapped on Penpal B. +pub fn ensure_no_assets_trapped_on_penpal_b() { + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = PenpalB::events(); + assert!( + !events.iter().any(|event| matches!( + event, + RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. }) + )), + "Assets were trapped on PenpalB, should not happen." + ); + }); +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_config.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_config.rs new file mode 100644 index 0000000000..6c6e2f91c7 --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_config.rs @@ -0,0 +1,70 @@ +// 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 asset_hub_polkadot_runtime::Runtime as AhRuntime; +use bridge_hub_polkadot_runtime::Runtime as BhRuntime; +use snowbridge_pallet_outbound_queue_v2::WeightInfo as OutboundQueueWeightInfo; +use snowbridge_pallet_system_frontend::BackendWeightInfo; +use snowbridge_pallet_system_v2::WeightInfo as SystemWeightInfo; + +/// Verifies that the AssetHub backend weights are equal to or larger than the corresponding +/// BridgeHub extrinsic weight. If this test fails, please update +/// system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_backend.rs +/// with the corresponding weight value in the Polkadot Bridge Hub runtime. +#[test] +fn asset_hub_weights_should_be_equal_or_gte_bridge_hub_weights() { + let bh_register_token = + ::WeightInfo::register_token(); + let bh_add_tip = ::WeightInfo::add_tip(); + let bh_do_process_message = + ::WeightInfo::do_process_message( + ); + let bh_commit_single = + ::WeightInfo::commit_single(); + let bh_submit_delivery_receipt = ::WeightInfo::submit_delivery_receipt(); + + let ah_register_token = ::BackendWeightInfo::transact_register_token(); + let ah_add_tip = ::BackendWeightInfo::transact_add_tip(); + let ah_do_process_message = ::BackendWeightInfo::do_process_message(); + let ah_commit_single = + ::BackendWeightInfo::commit_single( + ); + let ah_submit_delivery_receipt = ::BackendWeightInfo::submit_delivery_receipt(); + + assert!( + ah_register_token.all_gte(bh_register_token), + "Asset Hub register_token weight ({ah_register_token:?}) should be >= Bridge Hub weight ({bh_register_token:?})" + ); + + assert!( + ah_add_tip.all_gte(bh_add_tip), + "Asset Hub add_tip weight ({ah_add_tip:?}) should be >= Bridge Hub weight ({bh_add_tip:?})" + ); + + assert!( + ah_do_process_message.all_gte(bh_do_process_message), + "Asset Hub do_process_message weight ({ah_do_process_message:?}) should be >= Bridge Hub weight ({bh_do_process_message:?})" + ); + + assert!( + ah_commit_single.all_gte(bh_commit_single), + "Asset Hub commit_single weight ({ah_commit_single:?}) should be >= Bridge Hub weight ({bh_commit_single:?})" + ); + + assert!( + ah_submit_delivery_receipt.all_gte(bh_submit_delivery_receipt), + "Asset Hub submit_delivery_receipt weight ({ah_submit_delivery_receipt:?}) should be >= Bridge Hub weight ({bh_submit_delivery_receipt:?})" + ); +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound.rs new file mode 100644 index 0000000000..f95ac88561 --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound.rs @@ -0,0 +1,969 @@ +// 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::{tests::snowbridge_common::*, *}; +use asset_hub_polkadot_runtime::ForeignAssets; +use bp_bridge_hub_polkadot::snowbridge::CreateAssetCall; +use bridge_hub_polkadot_runtime::{ + bridge_common_config::BridgeReward, bridge_to_ethereum_config::EthereumGatewayAddress, + EthereumInboundQueueV2, +}; +use codec::Encode; +use emulated_integration_tests_common::{PENPAL_B_ID, RESERVABLE_ASSET_ID}; +use frame_support::{assert_ok, traits::fungibles::Mutate, BoundedVec}; +use hex_literal::hex; +use polkadot_system_emulated_network::penpal_emulated_chain::PARA_ID_B; +use snowbridge_core::{reward::MessageId, TokenIdOf}; +use snowbridge_inbound_queue_primitives::v2::{ + EthereumAsset::{ForeignTokenERC20, NativeTokenERC20}, + Message, Network, XcmPayload, +}; +use sp_core::{H160, H256}; +use sp_io::hashing::blake2_256; +use sp_runtime::MultiAddress; +use xcm::opaque::latest::AssetTransferFilter::ReserveDeposit; +use xcm_executor::traits::ConvertLocation; + +#[test] +fn register_token_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + let receiver = AssetHubPolkadotReceiver::get(); + let bridge_owner = ethereum_sovereign(); + BridgeHubPolkadot::fund_accounts(vec![(relayer_account.clone(), INITIAL_FUND)]); + AssetHubPolkadot::fund_accounts(vec![(bridge_owner.clone(), INITIAL_FUND)]); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + + let claimer = Location::new(0, AccountId32 { network: None, id: receiver.clone().into() }); + let claimer_bytes = claimer.encode(); + + let token: H160 = TOKEN_ID.into(); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets: vec![], + xcm: XcmPayload::CreateAsset { token, network: Network::Polkadot }, + claimer: Some(claimer_bytes), + // Used to pay the asset creation deposit. + value: TOKEN_AMOUNT, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Check that the token was created as a foreign asset on AssetHub + RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { asset_id, owner, .. }) => { + asset_id: *asset_id == erc20_token_location(token), + owner: *owner == bridge_owner, + }, + // Check that excess fees were paid to the claimer + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == receiver.clone(), + }, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn send_token_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let receiver = AssetHubPolkadotReceiver::get(); + let claimer = Location::new(0, AccountId32 { network: None, id: receiver.clone().into() }); + let claimer_bytes = claimer.encode(); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let ethereum_sovereign = ethereum_sovereign(); + + // To satisfy ED + AssetHubPolkadot::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + INITIAL_FUND, + )]); + + register_foreign_asset(token_location.clone(), ethereum_sovereign.clone(), false); + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: token, value: TOKEN_AMOUNT }, + ]; + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + let topic_id = BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + RefundSurplus, + DepositAsset { + assets: Wild(AllOf { + id: AssetId(token_location.clone()), + fun: WildFungibility::Fungible, + }), + beneficiary, + }, + DepositAsset { + assets: Wild(AllOf { id: AssetId(eth_location()), fun: WildFungibility::Fungible }), + beneficiary: claimer, + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = H160::random(); + + let message = Message { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message.clone()).unwrap(); + + let topic_id = blake2_256(&("SnowbridgeInboundQueueV2", message.nonce).encode()); + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + RuntimeEvent::EthereumInboundQueueV2(snowbridge_pallet_inbound_queue_v2::Event::MessageReceived { message_id, .. }) => { + message_id: *message_id == topic_id, + }, + ] + ); + topic_id + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, id, .. } + ) => { + id: *id == topic_id.into(), + }, + // Check that the token was received and issued as a foreign asset on AssetHub + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location, + owner: *owner == beneficiary_acc_bytes.into(), + }, + // Check that excess fees were paid to the claimer, which was set by the UX + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == receiver.clone(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + TOKEN_AMOUNT + ); + // Claimer received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), receiver) > 0); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn send_weth_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer = Location::new(0, AccountId32 { network: None, id: claimer_acc_id.into() }); + let claimer_bytes = claimer.encode(); + + let token_transfer_value = TOKEN_AMOUNT; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: WETH.into(), value: token_transfer_value }, + ]; + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + RefundSurplus, + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: beneficiary.clone() }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Check that the token was received and issued as a foreign asset on AssetHub + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == weth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + // Check that excess fees were paid to the beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + + // Claimer received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(beneficiary_acc_bytes)) > 0); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn register_and_send_multiple_tokens_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let bridge_owner = ethereum_sovereign(); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + // To satisfy ED + AssetHubPolkadot::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + INITIAL_FUND, + )]); + + let claimer_acc_id = H256::random(); + let claimer = Location::new(0, AccountId32 { network: None, id: claimer_acc_id.into() }); + let claimer_bytes = claimer.encode(); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + + let token_transfer_value = TOKEN_AMOUNT; + let weth_transfer_value = TOKEN_AMOUNT; + let eth_token_deposit = 9_000_000_000_000u128; + + let dot_asset = Location::new(1, Here); + let dot_fee: Asset = + (dot_asset, bp_asset_hub_polkadot::CreateForeignAssetDeposit::get()).into(); + + // Used to pay the asset creation deposit. + let eth_asset_value = eth_token_deposit + TOKEN_AMOUNT; + let asset_deposit: Asset = (eth_location(), eth_token_deposit).into(); + + let assets = vec![ + NativeTokenERC20 { token_id: WETH.into(), value: weth_transfer_value }, + NativeTokenERC20 { token_id: token, value: token_transfer_value }, + ]; + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + ExchangeAsset { + give: asset_deposit.clone().into(), + want: dot_fee.clone().into(), + maximal: false, + }, + DepositAsset { assets: dot_fee.into(), beneficiary: bridge_owner.clone().into() }, + // register new token + Transact { + origin_kind: OriginKind::Xcm, + fallback_max_weight: None, + call: ( + CreateAssetCall::get(), + token_location.clone(), + MultiAddress::<[u8; 32], ()>::Id(bridge_owner.clone().into()), + 1u128, + ) + .encode() + .into(), + }, + ExpectTransactStatus(MaybeErrorCode::Success), + // deposit new token, weth and leftover ether fees to beneficiary. + RefundSurplus, + DepositAsset { assets: Wild(AllCounted(3)), beneficiary: beneficiary.clone() }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: eth_asset_value, + execution_fee: EXECUTION_IN_ETHER * 10, // Since this is a more expensive operation + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Check that the token was created as a foreign asset on AssetHub + RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location.clone(), + owner: *owner == bridge_owner.clone(), + }, + // Check that the token was received and issued as a foreign asset on AssetHub + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location, + owner: *owner == beneficiary_acc_bytes.into(), + }, + // Check that excess fees were paid to the beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + + // Beneficiary received the weth transfer value + assert!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc_bytes)) >= + weth_transfer_value + ); + + // Beneficiary received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(beneficiary_acc_bytes)) > 0); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn send_token_to_penpal_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + // To pay fees on Penpal. + let eth_fee_penpal_ah: Asset = (eth_location(), MIN_ETHER_BALANCE * 2).into(); + + let ethereum_sovereign = ethereum_sovereign(); + + // To satisfy ED + AssetHubPolkadot::fund_para_sovereign(PENPAL_B_ID.into(), 3_000_000_000_000); + + register_foreign_asset(token_location.clone(), ethereum_sovereign.clone(), false); + + AssetHubPolkadot::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + ðereum_sovereign, + INITIAL_FUND, + )); + }); + + // To satisfy ED + PenpalB::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + INITIAL_FUND, + )]); + + register_ethereum_assets_on_penpal(); + register_foreign_asset_on_penpal(token_location.clone(), ethereum_sovereign.clone(), true); + set_trust_reserve_on_penpal(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_penpal(); + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: token, value: TOKEN_AMOUNT }, + ]; + + let token_asset_ah: Asset = (token_location.clone(), TOKEN_AMOUNT).into(); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Send message to Penpal + InitiateTransfer { + // Penpal + destination: Location::new(1, [Parachain(PARA_ID_B)]), + remote_fees: Some(ReserveDeposit(Definite(vec![eth_fee_penpal_ah.clone()].into()))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![ReserveDeposit(Definite( + vec![token_asset_ah.clone()].into(), + ))]), + remote_xcm: vec![ + // Refund unspent fees + RefundSurplus, + // Deposit assets to beneficiary. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: beneficiary.clone() }, + SetTopic(H256::random().into()), + ] + .into(), + }, + RefundSurplus, + DepositAsset { + assets: Wild(AllOf { id: AssetId(eth_location()), fun: WildFungibility::Fungible }), + beneficiary, + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT * 2, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + let penpal_sov_on_ah = AssetHubPolkadot::sovereign_account_id_of(Location::new( + 1, + [Parachain(PenpalB::para_id().into())], + )); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Ether was issued to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == penpal_sov_on_ah, + }, + // Token was issued to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location, + owner: *owner == penpal_sov_on_ah, + }, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + PenpalB, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Token was issued to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location, + owner: *owner == beneficiary_acc_bytes.into(), + }, + // Leftover fees was deposited to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + TOKEN_AMOUNT + ); + }); + + ensure_no_assets_trapped_on_penpal_b(); +} + +#[test] +fn send_foreign_erc20_token_back_to_polkadot() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let claimer = AccountId32 { network: None, id: H256::random().into() }; + let claimer_bytes = claimer.encode(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: AssetHubPolkadotReceiver::get().into() }); + + let asset_id: Location = + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(RESERVABLE_ASSET_ID.into())].into(); + + let asset_id_in_bh: Location = Location::new( + 1, + [ + Parachain(AssetHubPolkadot::para_id().into()), + PalletInstance(ASSETS_PALLET_ID), + GeneralIndex(RESERVABLE_ASSET_ID.into()), + ], + ); + + let asset_id_after_reanchored = Location::new( + 1, + [GlobalConsensus(NetworkId::Polkadot), Parachain(AssetHubPolkadot::para_id().into())], + ) + .appended_with(asset_id.clone().interior) + .unwrap(); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + + register_asset_native_polkadot_asset_on_snowbridge( + asset_id_in_bh.clone(), + String::from("ah_asset"), + String::from("ah_asset"), + 12, + ); + + let ethereum_sovereign: AccountId = ethereum_sovereign(); + + AssetHubPolkadot::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + + // Mint the asset into the bridge sovereign account, to mimic locked funds + AssetHubPolkadot::mint_asset( + ::RuntimeOrigin::signed(AssetHubPolkadotAssetOwner::get()), + RESERVABLE_ASSET_ID, + ethereum_sovereign.clone(), + TOKEN_AMOUNT, + ); + + let token_id = TokenIdOf::convert_location(&asset_id_after_reanchored).unwrap(); + + let assets = vec![ + // the token being transferred + ForeignTokenERC20 { token_id, value: TOKEN_AMOUNT }, + ]; + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = + vec![RefundSurplus, DepositAsset { assets: Wild(AllCounted(2)), beneficiary }]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: 1_500_000_000_000u128, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::Assets(pallet_assets::Event::Burned{..}) => {},] + ); + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Check that the native token burnt from some reserved account + RuntimeEvent::Assets(pallet_assets::Event::Burned { owner, .. }) => { + owner: *owner == ethereum_sovereign.clone(), + }, + // Check that the token was minted to beneficiary + RuntimeEvent::Assets(pallet_assets::Event::Issued { owner, .. }) => { + owner: *owner == AssetHubPolkadotReceiver::get(), + }, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn invalid_xcm_traps_funds_on_ah() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let token: H160 = TOKEN_ID.into(); + let claimer = AccountId32 { network: None, id: H256::random().into() }; + let claimer_bytes = claimer.encode(); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + + let assets = vec![ + // to transfer assets + NativeTokenERC20 { token_id: WETH.into(), value: 2_800_000_000_000u128 }, + // the token being transferred + NativeTokenERC20 { token_id: token, value: 2_000_000_000_000u128 }, + ]; + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // invalid xcm + let instructions = hex!("02806c072d50e2c7cd6821d1f084cbb4"); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(instructions.to_vec()), + claimer: Some(claimer_bytes), + value: EXECUTION_IN_ETHER, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Assets are trapped + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. }) => {},] + ); + }); +} + +#[test] +fn invalid_claimer_does_not_fail_the_message() { + let relayer_account = BridgeHubPolkadotSender::get(); + + let beneficiary_acc: [u8; 32] = H256::random().into(); + let beneficiary = Location::new(0, AccountId32 { network: None, id: beneficiary_acc }); + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: WETH.into(), value: TOKEN_AMOUNT }, + ]; + + let origin = H160::random(); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + RefundSurplus, + // Deposit weth and leftover ether fees to beneficiary. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: beneficiary.clone() }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + + let message = Message { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + // Set an invalid claimer + claimer: Some(hex!("2b7ce7bc7e87e4d6619da21487c7a53f").to_vec()), + value: TOKEN_AMOUNT, + execution_fee: EXECUTION_IN_ETHER, + relayer_fee: RELAYER_REWARD_IN_ETHER, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == RELAYER_REWARD_IN_ETHER, + }, + ] + ); + }); + + // Message still processes successfully + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Token was issued to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == weth_location(), + owner: *owner == beneficiary_acc.into(), + }, + // Leftover fees deposited to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc.into(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc)), + TOKEN_AMOUNT + ); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn create_foreign_asset_deposit_is_equal_to_asset_hub_foreign_asset_pallet_deposit() { + let asset_hub_deposit = asset_hub_polkadot_runtime::ForeignAssetsAssetDeposit::get(); + let bridge_hub_deposit = bp_asset_hub_polkadot::CreateForeignAssetDeposit::get(); + assert!( + bridge_hub_deposit >= + asset_hub_deposit, + "The BridgeHub asset creation deposit must be equal to or larger than the asset creation deposit configured on BridgeHub" + ); +} + +#[test] +pub fn add_tip_from_asset_hub_user_origin() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + let relayer = AssetHubPolkadotSender::get(); + + // Add the tip to a nonce that has not been processed. + let tip_message_id = MessageId::Inbound(2); + + let dot = Location::new(1, Here); + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!( + ::SnowbridgeSystemFrontend::add_tip( + RuntimeOrigin::signed(relayer.clone()), + tip_message_id.clone(), + xcm::prelude::Asset::from((dot, 1_000_000_000u128)), + ) + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = BridgeHubPolkadot::events(); + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::EthereumSystemV2(snowbridge_pallet_system_v2::Event::TipProcessed { sender, message_id, success, ..}) + if *sender == relayer &&*message_id == tip_message_id.clone() && *success, // expect success + )), + "tip added event found" + ); + }); +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound_to_kusama.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound_to_kusama.rs new file mode 100644 index 0000000000..ac7785f0ab --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_inbound_to_kusama.rs @@ -0,0 +1,507 @@ +// 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::{ + tests::{ + assert_bridge_hub_kusama_message_received, assert_bridge_hub_polkadot_message_accepted, + asset_hub_kusama_location, create_foreign_on_ah_polkadot, snowbridge_common::*, + }, + *, +}; +use asset_hub_polkadot_runtime::ForeignAssets; +use bridge_hub_polkadot_runtime::{ + bridge_common_config::BridgeReward, bridge_to_ethereum_config::EthereumGatewayAddress, + EthereumInboundQueueV2, +}; +use codec::Encode; +use frame_support::BoundedVec; +use snowbridge_core::TokenIdOf; +use snowbridge_inbound_queue_primitives::v2::{ + EthereumAsset::{ForeignTokenERC20, NativeTokenERC20}, + Message, XcmPayload, +}; +use sp_core::{H160, H256}; +use xcm::opaque::latest::AssetTransferFilter::{ReserveDeposit, ReserveWithdraw}; +use xcm_executor::traits::ConvertLocation; + +#[test] +fn send_token_to_kusama_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + let relayer_reward = 1_500_000_000_000u128; + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + // set XCM versions + BridgeHubPolkadot::force_xcm_version(asset_hub_polkadot_location(), XCM_VERSION); + BridgeHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + AssetHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + + // To pay fees on Kusama. + let eth_fee_kusama_ah: Asset = (eth_location(), MIN_ETHER_BALANCE * 2).into(); + + // To satisfy ED + AssetHubKusama::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + 3_000_000_000_000, + )]); + BridgeHubPolkadot::fund_para_sovereign(AssetHubPolkadot::para_id(), INITIAL_FUND); + + // Register the token on AH Polkadot and Kusama + let ethereum_sovereign = ethereum_sovereign(); + AssetHubKusama::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + token_location.clone(), + ethereum_sovereign.clone().into(), + true, + 1000, + )); + + assert!(::ForeignAssets::asset_exists( + token_location.clone(), + )); + }); + + // To satisfy ED + let sov_ahw_on_ahr = AssetHubPolkadot::sovereign_account_of_parachain_on_other_global_consensus( + Kusama, + AssetHubKusama::para_id(), + ); + AssetHubPolkadot::fund_accounts(vec![(sov_ahw_on_ahr.clone(), INITIAL_FUND)]); + + register_foreign_asset(token_location.clone(), ethereum_sovereign, false); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_up_eth_and_ksm_pool_on_kusama_asset_hub(); + + let token_transfer_value = TOKEN_AMOUNT; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: token, value: token_transfer_value }, + ]; + + let token_asset_ah: Asset = (token_location.clone(), token_transfer_value).into(); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Send message to Kusama AH + InitiateTransfer { + // Kusama + destination: Location::new(2, [GlobalConsensus(Kusama), Parachain(1000u32)]), + remote_fees: Some(ReserveDeposit(Definite(vec![eth_fee_kusama_ah.clone()].into()))), + preserve_origin: false, + assets: BoundedVec::truncate_from(vec![ReserveDeposit(Definite( + vec![token_asset_ah.clone()].into(), + ))]), + remote_xcm: vec![ + // Refund unspent fees + RefundSurplus, + // Deposit assets to beneficiary. + DepositAsset { assets: Wild(AllCounted(3)), beneficiary: beneficiary.clone() }, + SetTopic(H256::random().into()), + ] + .into(), + }, + RefundSurplus, + DepositAsset { + assets: Wild(AllOf { id: AssetId(eth_location()), fun: WildFungibility::Fungible }), + beneficiary, + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT, + execution_fee: MIN_ETHER_BALANCE * 3, + relayer_fee: relayer_reward, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == relayer_reward, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); + assert_bridge_hub_polkadot_message_accepted(true); + assert_bridge_hub_kusama_message_received(); + + AssetHubKusama::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubKusama, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Token was issued to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == token_location, + owner: *owner == beneficiary_acc_bytes.into(), + }, + // Leftover fees was deposited to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + ] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + }); + + ensure_no_assets_trapped_on_pah(); +} + +#[test] +fn send_ether_to_kusama_v2() { + let relayer_account = BridgeHubPolkadotSender::get(); + let relayer_reward = 1_500_000_000_000u128; + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + // set XCM versions + BridgeHubPolkadot::force_xcm_version(asset_hub_polkadot_location(), XCM_VERSION); + BridgeHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + AssetHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + + // To pay fees on Kusama. + let eth_fee_kusama_ah: Asset = (eth_location(), MIN_ETHER_BALANCE).into(); + let ether_asset_ah: Asset = (eth_location(), TOKEN_AMOUNT).into(); + + BridgeHubPolkadot::fund_para_sovereign(AssetHubPolkadot::para_id(), INITIAL_FUND); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_up_eth_and_ksm_pool_on_kusama_asset_hub(); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Send message to Kusama AH + InitiateTransfer { + // Kusama + destination: Location::new(2, [GlobalConsensus(Kusama), Parachain(1000u32)]), + remote_fees: Some(ReserveDeposit(Definite(vec![eth_fee_kusama_ah.clone()].into()))), + preserve_origin: false, + assets: BoundedVec::truncate_from(vec![ReserveDeposit(Definite( + vec![ether_asset_ah.clone()].into(), + ))]), + remote_xcm: vec![ + // Refund unspent fees + RefundSurplus, + // Deposit assets to beneficiary. + DepositAsset { assets: Wild(AllCounted(3)), beneficiary: beneficiary.clone() }, + SetTopic(H256::random().into()), + ] + .into(), + }, + RefundSurplus, + DepositAsset { + assets: Wild(AllOf { id: AssetId(eth_location()), fun: WildFungibility::Fungible }), + beneficiary, + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets: vec![], + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT, + execution_fee: MIN_ETHER_BALANCE * 2, + relayer_fee: relayer_reward, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == relayer_reward, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); + assert_bridge_hub_polkadot_message_accepted(true); + assert_bridge_hub_kusama_message_received(); + + AssetHubKusama::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubKusama, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + // Ether was deposited to beneficiary + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => { + asset_id: *asset_id == eth_location(), + owner: *owner == beneficiary_acc_bytes.into(), + }, + ] + ); + }); + + ensure_no_assets_trapped_on_kah(); +} + +#[test] +fn send_ksm_from_ethereum_to_kusama() { + let initial_fund: u128 = 200_000_000_000_000; + let relayer_account = BridgeHubPolkadotSender::get(); + let relayer_reward = 1_500_000_000_000u128; + + let claimer = AccountId32 { network: None, id: H256::random().into() }; + let claimer_bytes = claimer.encode(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: AssetHubKusamaReceiver::get().into() }); + + BridgeHubPolkadot::fund_para_sovereign(AssetHubPolkadot::para_id(), INITIAL_FUND); + + let ethereum_sovereign: AccountId = ethereum_sovereign(); + let bridged_roc_at_asset_hub_polkadot = bridged_ksm_at_ah_polkadot(); + create_foreign_on_ah_polkadot(bridged_roc_at_asset_hub_polkadot.clone(), true, vec![]); + + BridgeHubKusama::fund_para_sovereign(AssetHubKusama::para_id(), initial_fund); + AssetHubKusama::fund_accounts(vec![(AssetHubKusamaSender::get(), initial_fund)]); + register_ksm_as_native_polkadot_asset_on_snowbridge(); + + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_up_eth_and_ksm_pool_on_kusama_asset_hub(); + + // set XCM versions + BridgeHubPolkadot::force_xcm_version(asset_hub_polkadot_location(), XCM_VERSION); + BridgeHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + AssetHubPolkadot::force_xcm_version(asset_hub_kusama_location(), XCM_VERSION); + + let eth_fee_kusama_ah: Asset = (eth_location(), MIN_ETHER_BALANCE).into(); + + let ksm = Location::new(1, [GlobalConsensus(Kusama)]); + let token_id = TokenIdOf::convert_location(&ksm).unwrap(); + + let ksm_reachored: Asset = + (Location::new(2, [GlobalConsensus(NetworkId::Kusama)]), TOKEN_AMOUNT).into(); + + let assets = vec![ + // the token being transferred + ForeignTokenERC20 { token_id, value: TOKEN_AMOUNT }, + ]; + + AssetHubPolkadot::execute_with(|| { + // Mint the asset into the bridge sovereign account, to mimic locked funds + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(AssetHubPolkadotAssetOwner::get()), + bridged_roc_at_asset_hub_polkadot.clone(), + ethereum_sovereign.clone().into(), + TOKEN_AMOUNT, + )); + }); + + // fund the AHP's SA on AHK with the KSM tokens held in reserve + let sov_ahw_on_ahr = AssetHubKusama::sovereign_account_of_parachain_on_other_global_consensus( + NetworkId::Polkadot, + AssetHubPolkadot::para_id(), + ); + AssetHubKusama::fund_accounts(vec![(sov_ahw_on_ahr.clone(), INITIAL_FUND)]); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Send message to Kusama AH + InitiateTransfer { + // Kusama + destination: Location::new(2, [GlobalConsensus(Kusama), Parachain(1000u32)]), + remote_fees: Some(ReserveDeposit(Definite(vec![eth_fee_kusama_ah.clone()].into()))), + preserve_origin: false, + assets: BoundedVec::truncate_from(vec![ReserveWithdraw(Definite( + vec![ksm_reachored.clone()].into(), + ))]), + remote_xcm: vec![ + // Refund unspent fees + RefundSurplus, + // Deposit assets and leftover fees to beneficiary. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: beneficiary.clone() }, + SetTopic(H256::random().into()), + ] + .into(), + }, + RefundSurplus, + DepositAsset { + assets: Wild(AllOf { id: AssetId(eth_location()), fun: WildFungibility::Fungible }), + beneficiary, + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + gateway: origin, + nonce: 1, + origin, + assets, + xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + claimer: Some(claimer_bytes), + value: TOKEN_AMOUNT, + execution_fee: MIN_ETHER_BALANCE * 2, + relayer_fee: relayer_reward, + }; + + EthereumInboundQueueV2::process_message(relayer_account.clone(), message).unwrap(); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + // Check that the relayer reward was registered. + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => { + relayer: *relayer == relayer_account, + reward_kind: *reward_kind == BridgeReward::Snowbridge, + reward_balance: *reward_balance == relayer_reward, + }, + ] + ); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubPolkadot, + vec![ + // Message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); + assert_bridge_hub_polkadot_message_accepted(true); + assert_bridge_hub_kusama_message_received(); + + AssetHubKusama::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubKusama, + vec![ + // ROC is withdrawn from AHW's SA on AHR + RuntimeEvent::Balances( + pallet_balances::Event::Burned { who, amount } + ) => { + who: *who == sov_ahw_on_ahr, + amount: *amount == TOKEN_AMOUNT, + }, + // ROCs deposited to beneficiary + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, .. }) => { + who: *who == AssetHubKusamaReceiver::get(), + }, + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + }); + + ensure_no_assets_trapped_on_pah(); +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound.rs new file mode 100644 index 0000000000..b699ca751c --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound.rs @@ -0,0 +1,1016 @@ +// 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::{ + tests::{snowbridge_common::*, usdt_at_ah_polkadot}, + *, +}; +use bridge_hub_polkadot_runtime::{ + bridge_to_ethereum_config::EthereumGatewayAddress, EthereumOutboundQueueV2, +}; +use emulated_integration_tests_common::{impls::Decode, PenpalBTeleportableAssetLocation}; +use frame_support::{assert_err_ignore_postinfo, pallet_prelude::TypeInfo, BoundedVec}; +use polkadot_system_emulated_network::penpal_emulated_chain::penpal_runtime::xcm_config::LocalTeleportableToAssetHub; +use snowbridge_core::{reward::MessageId, AssetMetadata, BasicOperatingMode}; +use snowbridge_outbound_queue_primitives::v2::{ContractCall, DeliveryReceipt}; +use snowbridge_pallet_outbound_queue_v2::Error; +use snowbridge_pallet_system_v2::LostTips; +use sp_core::H256; +use xcm::v5::AssetTransferFilter; + +#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)] +pub enum EthereumSystemFrontendCall { + #[codec(index = 1)] + RegisterToken { asset_id: Box, metadata: AssetMetadata, fee_asset: Asset }, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)] +pub enum EthereumSystemFrontend { + #[codec(index = 36)] + EthereumSystemFrontend(EthereumSystemFrontendCall), +} + +#[test] +fn send_weth_from_asset_hub_to_ethereum() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let reserve_asset = Asset { id: AssetId(weth_location()), fun: Fungible(TOKEN_AMOUNT) }; + + let assets = vec![reserve_asset.clone(), remote_fee_asset.clone(), local_fee_asset.clone()]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(reserve_asset.clone().into()), + )]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(AllCounted(2)), + beneficiary: beneficiary(), + }]), + }, + ])); + + // Send the Weth back to Ethereum + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotReceiver::get()), + bx!(xcm), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {}, + RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageAccepted{ .. }) => {}, + ] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { .. }) => {}, + ] + ); + }); +} + +#[test] +pub fn register_relay_token_from_asset_hub_with_sudo() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let fees_asset = Asset { id: AssetId(eth_location()), fun: Fungible(1) }; + + assert_ok!( + ::SnowbridgeSystemFrontend::register_token( + RuntimeOrigin::root(), + bx!(VersionedLocation::from(Location { parents: 1, interior: [].into() })), + AssetMetadata { + name: "dot".as_bytes().to_vec().try_into().unwrap(), + symbol: "dot".as_bytes().to_vec().try_into().unwrap(), + decimals: 12, + }, + fees_asset + ) + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); +} + +#[test] +pub fn register_usdt_from_owner_on_asset_hub() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let fees_asset = Asset { id: AssetId(eth_location()), fun: Fungible(1) }; + + assert_ok!( + ::SnowbridgeSystemFrontend::register_token( + RuntimeOrigin::signed(AssetHubPolkadotAssetOwner::get()), + bx!(VersionedLocation::from(usdt_at_ah_polkadot())), + AssetMetadata { + name: "usdt".as_bytes().to_vec().try_into().unwrap(), + symbol: "usdt".as_bytes().to_vec().try_into().unwrap(), + decimals: 6, + }, + fees_asset + ) + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); +} + +#[test] +pub fn add_tip_from_asset_hub_user_origin() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + let relayer = AssetHubPolkadotSender::get(); + + // Fund the relayer account to pay xcm delivery fees from AH -> BH. + AssetHubPolkadot::fund_accounts(vec![(relayer.clone(), INITIAL_FUND)]); + + // Send a message from AH to Ethereum to increase the nonce + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + let reserve_asset = Asset { id: AssetId(weth_location()), fun: Fungible(TOKEN_AMOUNT) }; + let assets = vec![reserve_asset.clone(), remote_fee_asset.clone(), local_fee_asset.clone()]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(reserve_asset.clone().into()), + )]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(AllCounted(2)), + beneficiary: beneficiary(), + }]), + }, + ])); + + // Send the Weth back to Ethereum + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotReceiver::get()), + bx!(xcm), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + // Add the tip. + let tip_message_id = MessageId::Outbound(1); + + let dot = Location::new(1, Here); + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!( + ::SnowbridgeSystemFrontend::add_tip( + RuntimeOrigin::signed(relayer.clone()), + tip_message_id.clone(), + xcm::prelude::Asset::from((dot, 1_000_000_000u128)), + ) + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = BridgeHubPolkadot::events(); + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::EthereumSystemV2(snowbridge_pallet_system_v2::Event::TipProcessed { sender, message_id, success, ..}) + if *sender == relayer && *message_id == tip_message_id.clone() && *success, // expect success + )), + "tip added event found" + ); + }); +} + +#[test] +pub fn tip_to_invalid_nonce_is_added_to_lost_tips() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + let relayer = AssetHubPolkadotSender::get(); + + AssetHubPolkadot::fund_accounts(vec![(relayer.clone(), INITIAL_FUND)]); + + // A nonce that does not exist. + let tip_message_id = MessageId::Outbound(22); + + let dot = Location::new(1, Here); + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!( + ::SnowbridgeSystemFrontend::add_tip( + RuntimeOrigin::signed(relayer.clone()), + tip_message_id.clone(), + xcm::prelude::Asset::from((dot, 1_000_000_000u128)), + ) + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let events = BridgeHubPolkadot::events(); + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::EthereumSystemV2(snowbridge_pallet_system_v2::Event::TipProcessed { sender, message_id, success, ..}) + if *sender == relayer && *message_id == tip_message_id.clone() && !(*success), // expect a failure + )), + "tip added event found" + ); + + let relayer_lost_tip = LostTips::::get::< + sp_runtime::AccountId32, + >(relayer); + // Assert a tip was added to storage. + assert!(relayer_lost_tip > 0); + }); +} + +#[test] +fn transfer_relay_token_from_ah() { + let ethereum_sovereign: AccountId = ethereum_sovereign(); + fund_on_bh(); + // register token in either of the follow way should work + // a. register_relay_token_on_bh(); + // b. register_relay_token_from_asset_hub_with_sudo(); + // c. register_relay_token_from_asset_hub_user_origin(); + register_relay_token_on_polkadot_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + // Send token to Ethereum + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + type RuntimeEvent = ::RuntimeEvent; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let assets = vec![ + Asset { + id: AssetId(Location::parent()), + fun: Fungible(TOKEN_AMOUNT + LOCAL_FEE_AMOUNT_IN_DOT), + }, + remote_fee_asset.clone(), + ]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveDeposit( + Definite( + Asset { id: AssetId(Location::parent()), fun: Fungible(TOKEN_AMOUNT) } + .into(), + ), + )]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(AllCounted(2)), + beneficiary: beneficiary(), + }]), + }, + ])); + + // Send DOT to Ethereum + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotSender::get()), + bx!(xcm), + Weight::from(EXECUTION_WEIGHT), + )); + + // Check that the native asset transferred to some reserved account(sovereign of Ethereum) + let events = AssetHubPolkadot::events(); + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount}) + if *who == ethereum_sovereign.clone() && *amount == TOKEN_AMOUNT, + )), + "native token reserved to Ethereum sovereign account." + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { .. }) => {}, + ] + ); + }); +} + +#[test] +fn send_weth_and_dot_from_asset_hub_to_ethereum() { + fund_on_bh(); + register_relay_token_on_polkadot_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let weth_asset = Asset { id: weth_location().into(), fun: Fungible(TOKEN_AMOUNT) }; + + let dot_asset = Asset { id: AssetId(Location::parent()), fun: Fungible(TOKEN_AMOUNT) }; + + let assets = vec![ + weth_asset.clone(), + dot_asset.clone(), + local_fee_asset.clone(), + remote_fee_asset.clone(), + ]; + + let xcms = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![ + AssetTransferFilter::ReserveWithdraw(Definite(weth_asset.clone().into())), + AssetTransferFilter::ReserveDeposit(Definite(dot_asset.into())), + ]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(All), + beneficiary: beneficiary(), + }]), + }, + ])); + + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotReceiver::get()), + bx!(xcms), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { .. }) => {}, + ] + ); + }); +} + +#[test] +fn transact_with_agent_from_asset_hub() { + let weth_asset_location: Location = weth_location(); + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let reserve_asset = + Asset { id: AssetId(weth_asset_location.clone()), fun: Fungible(TOKEN_AMOUNT) }; + + let assets = vec![reserve_asset.clone(), local_fee_asset.clone(), remote_fee_asset.clone()]; + + let beneficiary = Location::new(0, [AccountKey20 { network: None, key: AGENT_ADDRESS }]); + + let transact_info = ContractCall::V1 { + target: Default::default(), + calldata: vec![], + gas: 40000, + // value should be less than the transfer amount, require validation on BH Exporter + value: 4 * (TOKEN_AMOUNT - REMOTE_FEE_AMOUNT_IN_ETHER) / 5, + }; + + let xcms = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(reserve_asset.clone().into()), + )]), + remote_xcm: Xcm(vec![ + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + Transact { + origin_kind: OriginKind::SovereignAccount, + fallback_max_weight: None, + call: transact_info.encode().into(), + }, + ]), + }, + ])); + + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotSender::get()), + bx!(xcms), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { .. }) => {}, + ] + ); + }); +} + +#[test] +fn transact_with_agent_from_asset_hub_without_any_asset_transfer() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let assets = vec![local_fee_asset.clone(), remote_fee_asset.clone()]; + + let beneficiary = Location::new(0, [AccountKey20 { network: None, key: AGENT_ADDRESS }]); + + let transact_info = + ContractCall::V1 { target: Default::default(), calldata: vec![], gas: 40000, value: 0 }; + + let xcms = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::new(), + remote_xcm: Xcm(vec![ + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + Transact { + origin_kind: OriginKind::SovereignAccount, + fallback_max_weight: None, + call: transact_info.encode().into(), + }, + ]), + }, + ])); + + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotSender::get()), + bx!(xcms), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + success: true, + topic: Default::default(), + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { .. }) => {}, + ] + ); + }); +} + +#[test] +fn register_token_from_penpal() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + set_trust_reserve_on_penpal(); + register_ethereum_assets_on_penpal(); + prefund_accounts_on_penpal_b(); + set_up_eth_and_dot_pool_on_penpal(); + + let penpal_user_location = Location::new( + 1, + [ + Parachain(PenpalB::para_id().into()), + AccountId32 { network: Some(NetworkId::Polkadot), id: PenpalBSender::get().into() }, + ], + ); + let asset_location_on_penpal = PenpalBTeleportableAssetLocation::get(); + let foreign_asset_at_asset_hub = + Location::new(1, [Junction::Parachain(PenpalB::para_id().into())]) + .appended_with(asset_location_on_penpal) + .unwrap(); + PenpalB::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset_on_penpal = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset_on_ah = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let remote_fee_asset_on_ethereum = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let call = EthereumSystemFrontend::EthereumSystemFrontend( + EthereumSystemFrontendCall::RegisterToken { + asset_id: Box::new(VersionedLocation::from(foreign_asset_at_asset_hub)), + metadata: Default::default(), + fee_asset: remote_fee_asset_on_ethereum.clone(), + }, + ); + + let assets = vec![ + local_fee_asset_on_penpal.clone(), + remote_fee_asset_on_ah.clone(), + remote_fee_asset_on_ethereum.clone(), + ]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset_on_penpal.clone() }, + InitiateTransfer { + destination: asset_hub(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset_on_ah.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(remote_fee_asset_on_ethereum.clone().into()), + )]), + remote_xcm: Xcm(vec![ + DepositAsset { assets: Wild(All), beneficiary: penpal_user_location }, + Transact { + origin_kind: OriginKind::Xcm, + call: call.encode().into(), + fallback_max_weight: None, + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]), + }, + ])); + + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::root(), + bx!(xcm.clone()), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { .. }) => {},] + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 1, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + // Submit a delivery receipt + assert_ok!(EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt)); + + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageDelivered { .. }) => {}, + ] + ); + }); +} + +fn send_message_from_penpal_to_ethereum(sudo: bool) { + // bh + fund_on_bh(); + // ah + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + register_pal_on_polkadot_asset_hub(); + register_pal_on_polkadot_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + // penpal + set_trust_reserve_on_penpal(); + register_ethereum_assets_on_penpal(); + prefund_accounts_on_penpal_b(); + + PenpalB::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset_on_penpal = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset_on_ah = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let remote_fee_asset_on_ethereum = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let pna = + Asset { id: AssetId(LocalTeleportableToAssetHub::get()), fun: Fungible(TOKEN_AMOUNT) }; + + let ena = Asset { id: AssetId(weth_location()), fun: Fungible(TOKEN_AMOUNT / 2) }; + + let transfer_asset_reanchor_on_ah = Asset { + id: AssetId(PenpalBTeleportableAssetLocation::get()), + fun: Fungible(TOKEN_AMOUNT), + }; + + let assets = vec![ + local_fee_asset_on_penpal.clone(), + remote_fee_asset_on_ah.clone(), + remote_fee_asset_on_ethereum.clone(), + pna.clone(), + ena.clone(), + ]; + + let transact_info = + ContractCall::V1 { target: Default::default(), calldata: vec![], gas: 40000, value: 0 }; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset_on_penpal.clone() }, + InitiateTransfer { + destination: asset_hub(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset_on_ah.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![ + AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset_on_ethereum.clone().into(), + )), + AssetTransferFilter::ReserveWithdraw(Definite(ena.clone().into())), + // Should use Teleport here because: + // a. Penpal is configured to allow teleport specific asset to AH + // b. AH is configured to trust asset teleport from sibling chain + AssetTransferFilter::Teleport(Definite(pna.clone().into())), + ]), + remote_xcm: Xcm(vec![InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset_on_ethereum.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![ + // should use ReserveDeposit because Ethereum does not trust asset from + // penpal. transfer_asset should be reachored first on AH + AssetTransferFilter::ReserveDeposit(Definite( + transfer_asset_reanchor_on_ah.clone().into(), + )), + AssetTransferFilter::ReserveWithdraw(Definite(ena.clone().into())), + ]), + remote_xcm: Xcm(vec![ + DepositAsset { assets: Wild(All), beneficiary: beneficiary() }, + Transact { + origin_kind: OriginKind::SovereignAccount, + fallback_max_weight: None, + call: transact_info.encode().into(), + }, + ]), + }]), + }, + ])); + + if sudo { + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::root(), + bx!(xcm.clone()), + Weight::from(EXECUTION_WEIGHT), + )); + } else { + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::signed(PenpalBSender::get()), + bx!(xcm.clone()), + Weight::from(EXECUTION_WEIGHT), + )); + } + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::SwapCreditExecuted { .. }) => {},] + ); + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); +} + +#[test] +fn send_message_from_penpal_to_ethereum_with_sudo() { + send_message_from_penpal_to_ethereum(true) +} + +#[test] +fn send_message_from_penpal_to_ethereum_with_user_origin() { + send_message_from_penpal_to_ethereum(false) +} + +#[test] +fn invalid_nonce_for_delivery_receipt_fails() { + BridgeHubPolkadot::execute_with(|| { + type Runtime = ::Runtime; + + let relayer = BridgeHubPolkadotSender::get(); + let reward_account = AssetHubPolkadotReceiver::get(); + let receipt = DeliveryReceipt { + gateway: EthereumGatewayAddress::get(), + nonce: 0, + reward_address: reward_account.into(), + topic: H256::zero(), + success: true, + }; + + assert_err!( + EthereumOutboundQueueV2::process_delivery_receipt(relayer, receipt), + Error::::InvalidPendingNonce + ); + }); +} + +#[test] +fn export_message_from_asset_hub_to_ethereum_is_banned_when_set_operating_mode_is_halted() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + assert_ok!( + ::SnowbridgeSystemFrontend::set_operating_mode( + RuntimeOrigin::root(), + BasicOperatingMode::Halted)); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + type Runtime = ::Runtime; + + let local_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let reserve_asset = Asset { id: AssetId(weth_location()), fun: Fungible(TOKEN_AMOUNT) }; + + let assets = vec![reserve_asset.clone(), remote_fee_asset.clone(), local_fee_asset.clone()]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(reserve_asset.clone().into()), + )]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(AllCounted(2)), + beneficiary: beneficiary(), + }]), + }, + ])); + + // Send the Weth back to Ethereum + assert_err_ignore_postinfo!( + ::PolkadotXcm::execute( + RuntimeOrigin::signed(AssetHubPolkadotReceiver::get()), + bx!(xcm), + Weight::from(EXECUTION_WEIGHT), + ), + pallet_xcm::Error::::LocalExecutionIncompleteWithError { + index: 2, + error: XcmError::Unroutable.into() + } + ); + }); +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_edge_case.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_edge_case.rs new file mode 100644 index 0000000000..016162f0e8 --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_edge_case.rs @@ -0,0 +1,279 @@ +// 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::{ + tests::{ + snowbridge::CHAIN_ID, + snowbridge_common::*, + snowbridge_v2_outbound::{EthereumSystemFrontend, EthereumSystemFrontendCall}, + usdt_at_ah_polkadot, + }, + *, +}; +use asset_hub_polkadot_runtime::xcm_config::bridging::to_ethereum::BridgeHubEthereumBaseFee; +use frame_support::{assert_noop, BoundedVec}; +use kusama_polkadot_system_emulated_network::asset_hub_kusama_emulated_chain::genesis::PenpalATeleportableAssetLocation; +use snowbridge_core::AssetMetadata; +use sp_runtime::DispatchError::BadOrigin; +use xcm::v5::AssetTransferFilter; + +#[test] +fn register_penpal_a_asset_from_penpal_b_will_fail() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + set_up_eth_and_dot_pool_on_polkadot_asset_hub(); + set_trust_reserve_on_penpal(); + register_ethereum_assets_on_penpal(); + prefund_accounts_on_penpal_b(); + let penpal_user_location = Location::new( + 1, + [ + Parachain(PenpalB::para_id().into()), + AccountId32 { network: Some(Polkadot), id: PenpalBSender::get().into() }, + ], + ); + let asset_location_on_penpal = PenpalATeleportableAssetLocation::get(); + let penpal_a_asset_at_asset_hub = Location::new(1, [Parachain(PenpalA::para_id().into())]) + .appended_with(asset_location_on_penpal) + .unwrap(); + PenpalB::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let local_fee_asset_on_penpal = + Asset { id: AssetId(Location::parent()), fun: Fungible(LOCAL_FEE_AMOUNT_IN_DOT) }; + + let remote_fee_asset_on_ah = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let remote_fee_asset_on_ethereum = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let call = EthereumSystemFrontend::EthereumSystemFrontend( + EthereumSystemFrontendCall::RegisterToken { + asset_id: Box::new(VersionedLocation::from(penpal_a_asset_at_asset_hub)), + metadata: Default::default(), + fee_asset: remote_fee_asset_on_ethereum.clone(), + }, + ); + + let assets = vec![ + local_fee_asset_on_penpal.clone(), + remote_fee_asset_on_ah.clone(), + remote_fee_asset_on_ethereum.clone(), + ]; + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + PayFees { asset: local_fee_asset_on_penpal.clone() }, + InitiateTransfer { + destination: asset_hub(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + remote_fee_asset_on_ah.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveWithdraw( + Definite(remote_fee_asset_on_ethereum.clone().into()), + )]), + remote_xcm: Xcm(vec![ + DepositAsset { assets: Wild(All), beneficiary: penpal_user_location }, + Transact { + origin_kind: OriginKind::Xcm, + call: call.encode().into(), + fallback_max_weight: None, + }, + ]), + }, + ])); + + assert_ok!(::PolkadotXcm::execute( + RuntimeOrigin::root(), + bx!(xcm.clone()), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + AssetHubPolkadot, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { .. }) => {},] + ); + }); + + // No events should be emitted on the bridge hub + BridgeHubPolkadot::execute_with(|| { + assert_expected_events!(BridgeHubPolkadot, vec![]); + }); +} + +#[test] +fn export_from_non_system_parachain_will_fail() { + let penpal_location = Location::new(1, [Parachain(PenpalB::para_id().into())]); + let penpal_sovereign = BridgeHubPolkadot::sovereign_account_id_of(penpal_location.clone()); + BridgeHubPolkadot::fund_accounts(vec![(penpal_sovereign.clone(), INITIAL_FUND)]); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let relay_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 }]); + + 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(relay_fee_asset.clone().into()), + BuyExecution { fees: relay_fee_asset.clone(), weight_limit: Unlimited }, + ExportMessage { + network: Ethereum { chain_id: CHAIN_ID }, + destination: Here, + xcm: Xcm(vec![ + AliasOrigin(penpal_location), + 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{ .. }) => {},] + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success: false, .. }) => {},] + ); + }); +} + +#[test] +pub fn register_usdt_not_from_owner_on_asset_hub_will_fail() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let fees_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + assert_noop!( + ::SnowbridgeSystemFrontend::register_token( + // The owner is Alice, while AssetHubPolkadotReceiver is Bob, so it should fail + RuntimeOrigin::signed(AssetHubPolkadotReceiver::get()), + bx!(VersionedLocation::from(usdt_at_ah_polkadot())), + AssetMetadata { + name: "usdt".as_bytes().to_vec().try_into().unwrap(), + symbol: "usdt".as_bytes().to_vec().try_into().unwrap(), + decimals: 6, + }, + fees_asset + ), + BadOrigin + ); + }); +} + +#[test] +pub fn register_relay_token_from_asset_hub_user_origin_will_fail() { + fund_on_bh(); + prefund_accounts_on_polkadot_asset_hub(); + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + let fees_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + assert_noop!( + ::SnowbridgeSystemFrontend::register_token( + RuntimeOrigin::signed(AssetHubPolkadotSender::get()), + bx!(VersionedLocation::from(Location { parents: 1, interior: [].into() })), + AssetMetadata { + name: "dot".as_bytes().to_vec().try_into().unwrap(), + symbol: "DOT".as_bytes().to_vec().try_into().unwrap(), + decimals: 10, + }, + fees_asset, + ), + BadOrigin + ); + }); +} + +// A malicious user attempted to exploit the bridge by manually adding an AliasOrigin in the +// remoteXcm, successfully routing to the V2 path, but ultimately failing at the BH Exporter. +#[test] +pub fn exploit_v2_route_with_legacy_v1_transfer_will_fail() { + prefund_accounts_on_polkadot_asset_hub(); + set_bridge_hub_ethereum_base_fee(); + + // Set base transfer fee to Ethereum on AH. + AssetHubPolkadot::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::System::set_storage( + RuntimeOrigin::root(), + vec![(BridgeHubEthereumBaseFee::key().to_vec(), 1_000_000_000_u128.encode())], + )); + }); + + let remote_fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let reserve_asset = Asset { id: AssetId(eth_location()), fun: Fungible(TOKEN_AMOUNT) }; + + let assets = vec![reserve_asset.clone(), remote_fee_asset.clone()]; + + let custom_xcm_on_dest = Xcm::<()>(vec![ + AliasOrigin(Location::parent()), + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: beneficiary() }, + ]); + + assert_ok!(AssetHubPolkadot::execute_with(|| { + ::PolkadotXcm::transfer_assets_using_type_and_then( + ::RuntimeOrigin::signed(AssetHubPolkadotSender::get()), + bx!(eth_location().into()), + bx!(assets.into()), + bx!(TransferType::DestinationReserve), + bx!(AssetId(eth_location()).into()), + bx!(TransferType::DestinationReserve), + bx!(VersionedXcm::from(custom_xcm_on_dest)), + Unlimited, + ) + })); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![ + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed{ success: false, .. }) => {}, + ] + ); + }) +} diff --git a/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_from_kusama.rs b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_from_kusama.rs new file mode 100644 index 0000000000..8ab15b4cca --- /dev/null +++ b/integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/snowbridge_v2_outbound_from_kusama.rs @@ -0,0 +1,373 @@ +// 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::{ + tests::{ + snowbridge_common::*, + snowbridge_v2_outbound::{EthereumSystemFrontend, EthereumSystemFrontendCall}, + }, + *, +}; +use frame_support::{traits::fungibles::Mutate, BoundedVec}; +use xcm::latest::AssetTransferFilter; + +pub(crate) fn create_foreign_on_ah_polkadot(id: Location, sufficient: bool) { + let owner = AssetHubPolkadot::account_id_of(ALICE); + AssetHubPolkadot::force_create_foreign_asset(id, owner, sufficient, ASSET_MIN_BALANCE, vec![]); +} + +// set up pool +pub(crate) fn set_up_pool_with_dot_on_ah_polkadot( + asset: Location, + is_foreign: bool, + initial_fund: u128, + initial_liquidity: u128, +) { + let dot: Location = Parent.into(); + AssetHubPolkadot::fund_accounts(vec![(AssetHubPolkadotSender::get(), initial_fund)]); + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let owner = AssetHubPolkadotSender::get(); + let signed_owner = ::RuntimeOrigin::signed(owner.clone()); + + if is_foreign { + assert_ok!(::ForeignAssets::mint( + signed_owner.clone(), + asset.clone(), + owner.clone().into(), + initial_fund, + )); + } else { + let asset_id = match asset.interior.last() { + Some(GeneralIndex(id)) => *id as u32, + _ => unreachable!(), + }; + assert_ok!(::Assets::mint( + signed_owner.clone(), + asset_id.into(), + owner.clone().into(), + initial_fund, + )); + } + assert_ok!(::AssetConversion::create_pool( + signed_owner.clone(), + Box::new(dot.clone()), + Box::new(asset.clone()), + )); + assert_expected_events!( + AssetHubPolkadot, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + assert_ok!(::AssetConversion::add_liquidity( + signed_owner.clone(), + Box::new(dot), + Box::new(asset), + initial_liquidity, + initial_liquidity, + 1, + 1, + owner + )); + assert_expected_events!( + AssetHubPolkadot, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {..}) => {}, + ] + ); + }); +} + +pub(crate) fn assert_bridge_hub_kusama_message_accepted(expected_processed: bool) { + BridgeHubKusama::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + if expected_processed { + assert_expected_events!( + BridgeHubKusama, + vec![ + // pay for bridge fees + RuntimeEvent::Balances(pallet_balances::Event::Burned { .. }) => {}, + // message exported + RuntimeEvent::BridgePolkadotMessages( + pallet_bridge_messages::Event::MessageAccepted { .. } + ) => {}, + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + } else { + assert_expected_events!( + BridgeHubKusama, + vec![ + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { + success: false, + .. + }) => {}, + ] + ); + } + }); +} + +pub(crate) fn assert_bridge_hub_polkadot_message_received() { + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubPolkadot, + vec![ + // message sent to destination + RuntimeEvent::XcmpQueue( + cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. } + ) => {}, + ] + ); + }) +} + +#[test] +fn send_ksm_from_asset_hub_kusama_to_ethereum() { + let initial_fund: u128 = 20_000_000_000_000_000; + let initial_liquidity: u128 = initial_fund / 2; + let amount: u128 = initial_fund; + let ksm_fee_amount: u128 = initial_liquidity / 2; + let dot_amount_to_swap: u128 = initial_liquidity / 10; + let dot_fee_amount: u128 = dot_amount_to_swap / 10; + + let ether_fee_amount: u128 = MIN_ETHER_BALANCE * 2; + + let sender = AssetHubKusamaSender::get(); + let ksm_at_asset_hub_kusama = Location::parent(); + let bridged_ksm_at_asset_hub_polkadot = bridged_ksm_at_ah_polkadot(); + + set_bridge_hub_ethereum_base_fee(); + create_foreign_on_ah_polkadot(bridged_ksm_at_asset_hub_polkadot.clone(), true); + set_up_pool_with_dot_on_ah_polkadot( + bridged_ksm_at_asset_hub_polkadot.clone(), + true, + initial_fund, + initial_liquidity, + ); + let previous_owner = ethereum_sovereign(); + AssetHubPolkadot::execute_with(|| { + assert_ok!(::ForeignAssets::start_destroy( + ::RuntimeOrigin::signed(previous_owner), + eth_location() + )); + assert_ok!(::ForeignAssets::finish_destroy( + ::RuntimeOrigin::signed(AssetHubPolkadot::account_id_of( + ALICE + )), + eth_location() + )); + }); + create_foreign_on_ah_polkadot(eth_location(), true); + set_up_pool_with_dot_on_ah_polkadot(eth_location(), true, initial_fund, initial_liquidity); + BridgeHubKusama::fund_para_sovereign(AssetHubKusama::para_id(), initial_fund); + AssetHubKusama::fund_accounts(vec![(AssetHubKusamaSender::get(), initial_fund)]); + fund_on_bh(); + register_ksm_as_native_polkadot_asset_on_snowbridge(); + + // set XCM versions + AssetHubKusama::force_xcm_version(asset_hub_polkadot_location(), XCM_VERSION); + BridgeHubKusama::force_xcm_version(bridge_hub_polkadot_location(), XCM_VERSION); + + // send ROCs, use them for fees + let local_fee_asset: Asset = (ksm_at_asset_hub_kusama.clone(), ksm_fee_amount).into(); + let remote_fee_on_polkadot: Asset = (ksm_at_asset_hub_kusama.clone(), ksm_fee_amount).into(); + let assets: Assets = (ksm_at_asset_hub_kusama.clone(), amount).into(); + let reserved_asset_on_polkadot: Asset = + (ksm_at_asset_hub_kusama.clone(), amount - ksm_fee_amount * 2).into(); + let reserved_asset_on_polkadot_reanchored: Asset = + (bridged_ksm_at_asset_hub_polkadot.clone(), (amount - ksm_fee_amount * 2) / 2).into(); + + let xcm = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone()), + PayFees { asset: local_fee_asset.clone() }, + InitiateTransfer { + destination: asset_hub_polkadot_location(), + remote_fees: Some(AssetTransferFilter::ReserveDeposit(Definite( + remote_fee_on_polkadot.clone().into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveDeposit(Definite( + reserved_asset_on_polkadot.clone().into(), + ))]), + remote_xcm: Xcm(vec![ + // swap from ksm to dot + ExchangeAsset { + give: Definite(reserved_asset_on_polkadot_reanchored.clone().into()), + want: (Parent, dot_amount_to_swap).into(), + maximal: true, + }, + // swap some dot to ether + ExchangeAsset { + give: Definite((Parent, dot_amount_to_swap).into()), + want: (eth_location(), ether_fee_amount).into(), + maximal: true, + }, + PayFees { asset: (Parent, dot_fee_amount).into() }, + InitiateTransfer { + destination: eth_location(), + remote_fees: Some(AssetTransferFilter::ReserveWithdraw(Definite( + Asset { id: AssetId(eth_location()), fun: Fungible(ether_fee_amount) } + .into(), + ))), + preserve_origin: true, + assets: BoundedVec::truncate_from(vec![AssetTransferFilter::ReserveDeposit( + Definite(reserved_asset_on_polkadot_reanchored.clone().into()), + )]), + remote_xcm: Xcm(vec![DepositAsset { + assets: Wild(All), + beneficiary: beneficiary(), + }]), + }, + ]), + }, + ])); + + AssetHubKusama::execute_with(|| { + assert_ok!(::PolkadotXcm::execute( + ::RuntimeOrigin::signed(sender), + bx!(xcm), + Weight::from(EXECUTION_WEIGHT), + )); + }); + + assert_bridge_hub_kusama_message_accepted(true); + assert_bridge_hub_polkadot_message_received(); + + // verify expected events on final destination + AssetHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + AssetHubPolkadot, + vec![ + // message processed successfully + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); + }); + + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); +} + +#[test] +fn register_kusama_asset_on_ethereum_from_rah() { + const XCM_FEE: u128 = 4_000_000_000_000; + let sa_of_kah_on_pah = + AssetHubPolkadot::sovereign_account_of_parachain_on_other_global_consensus( + Kusama, + AssetHubKusama::para_id(), + ); + + // Kusama Asset Hub asset when bridged to Polkadot Asset Hub. + let bridged_asset_at_pah = Location::new( + 2, + [ + GlobalConsensus(Kusama), + Parachain(AssetHubKusama::para_id().into()), + PalletInstance(ASSETS_PALLET_ID), + GeneralIndex(ASSET_ID.into()), + ], + ); + + AssetHubPolkadot::force_create_foreign_asset( + bridged_asset_at_pah.clone(), + sa_of_kah_on_pah.clone(), + true, + ASSET_MIN_BALANCE, + vec![], + ); + + let fee_asset = + Asset { id: AssetId(eth_location()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + + let call = + EthereumSystemFrontend::EthereumSystemFrontend(EthereumSystemFrontendCall::RegisterToken { + asset_id: Box::new(VersionedLocation::from(bridged_asset_at_pah.clone())), + metadata: Default::default(), + fee_asset, + }) + .encode(); + + let origin_kind = OriginKind::Xcm; + let fee_amount = XCM_FEE; + let fees = (Parent, fee_amount).into(); + + let xcm = xcm_transact_paid_execution(call.into(), origin_kind, fees, sa_of_kah_on_pah.clone()); + + // SA-of-RAH-on-WAH needs to have balance to pay for fees and asset creation deposit + AssetHubPolkadot::execute_with(|| { + assert_ok!(::ForeignAssets::mint_into( + eth_location(), + &sa_of_kah_on_pah, + INITIAL_FUND, + )); + assert_ok!(::Balances::force_set_balance( + ::RuntimeOrigin::root(), + sa_of_kah_on_pah.into(), + INITIAL_FUND + )); + }); + + let destination = asset_hub_polkadot_location(); + + // fund the RAH's SA on RBH for paying bridge delivery fees + BridgeHubKusama::fund_para_sovereign(AssetHubKusama::para_id(), 10_000_000_000_000u128); + + // set XCM versions + AssetHubKusama::force_xcm_version(destination.clone(), XCM_VERSION); + BridgeHubKusama::force_xcm_version(bridge_hub_polkadot_location(), XCM_VERSION); + + let root_origin = ::RuntimeOrigin::root(); + AssetHubKusama::execute_with(|| { + assert_ok!(::PolkadotXcm::send( + root_origin, + bx!(destination.into()), + bx!(xcm), + )); + + AssetHubKusama::assert_xcm_pallet_sent(); + }); + + assert_bridge_hub_kusama_message_accepted(true); + assert_bridge_hub_polkadot_message_received(); + AssetHubPolkadot::execute_with(|| { + AssetHubPolkadot::assert_xcmp_queue_success(None); + }); + BridgeHubPolkadot::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the Ethereum message was queue in the Outbound Queue + assert_expected_events!( + BridgeHubPolkadot, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); +} diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/Cargo.toml b/system-parachains/asset-hubs/asset-hub-polkadot/Cargo.toml index 067a5692f2..53c939f983 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/Cargo.toml +++ b/system-parachains/asset-hubs/asset-hub-polkadot/Cargo.toml @@ -102,6 +102,9 @@ assets-common = { workspace = true } # Bridges pallet-xcm-bridge-hub-router = { workspace = true } snowbridge-inbound-queue-primitives = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } +snowbridge-pallet-system-frontend = { workspace = true } +snowbridge-runtime-common = { workspace = true } [dev-dependencies] asset-test-utils = { workspace = true } @@ -151,6 +154,8 @@ runtime-benchmarks = [ "polkadot-runtime-common/runtime-benchmarks", "polkadot-runtime-constants/runtime-benchmarks", "snowbridge-inbound-queue-primitives/runtime-benchmarks", + "snowbridge-pallet-system-frontend/runtime-benchmarks", + "snowbridge-runtime-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "system-parachains-constants/runtime-benchmarks", "xcm-builder/runtime-benchmarks", @@ -191,6 +196,8 @@ try-runtime = [ "parachain-info/try-runtime", "parachains-common/try-runtime", "polkadot-runtime-common/try-runtime", + "snowbridge-pallet-system-frontend/try-runtime", + "snowbridge-runtime-common/try-runtime", "sp-runtime/try-runtime", ] std = [ @@ -251,6 +258,9 @@ std = [ "scale-info/std", "serde_json/std", "snowbridge-inbound-queue-primitives/std", + "snowbridge-outbound-queue-primitives/std", + "snowbridge-pallet-system-frontend/std", + "snowbridge-runtime-common/std", "sp-api/std", "sp-block-builder/std", "sp-consensus-aura/std", diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/primitives/src/lib.rs b/system-parachains/asset-hubs/asset-hub-polkadot/primitives/src/lib.rs index 2a758892cc..1fb58c401a 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/primitives/src/lib.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/primitives/src/lib.rs @@ -51,6 +51,8 @@ frame_support::parameter_types! { /// Should match the `AssetDeposit` of the `ForeignAssets` pallet on Asset Hub. pub const CreateForeignAssetDeposit: u128 = system_para_deposit(1, 190); + /// The pallet index of the Ethereum system frontend pallet in the AssetHub Hub runtime. + pub const SystemFrontendPalletInstance: u8 = 36; } /// Builds an (un)congestion XCM program with the `report_bridge_status` call for diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/bridge_to_ethereum_config.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/bridge_to_ethereum_config.rs new file mode 100644 index 0000000000..befe8ae9fa --- /dev/null +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/bridge_to_ethereum_config.rs @@ -0,0 +1,202 @@ +// 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::{ + weights, xcm_config, + xcm_config::{ + bridging::SiblingBridgeHub, AssetTransactors, LocationToAccountId, RootLocation, + TrustBackedAssetsPalletLocation, UniversalLocation, XcmConfig, + }, + AccountId, AssetConversion, Assets, ForeignAssets, Runtime, RuntimeEvent, +}; +use assets_common::{matching::FromSiblingParachain, AssetIdForTrustBackedAssetsConvert}; +use bp_asset_hub_polkadot::SystemFrontendPalletInstance; +use bp_bridge_hub_polkadot::snowbridge::EthereumNetwork; +use frame_support::{parameter_types, traits::EitherOf}; +use frame_system::EnsureRootWithSuccess; +use parachains_common::AssetIdForTrustBackedAssets; +use snowbridge_runtime_common::{ForeignAssetOwner, LocalAssetOwner}; +use xcm::prelude::{InteriorLocation, Location, PalletInstance}; +use xcm_executor::XcmExecutor; + +parameter_types! { + /// Fee asset for the execution cost on ethereum + pub storage FeeAsset: Location = Location::new( + 2, + [ + EthereumNetwork::get().into(), + ], + ); + pub SystemFrontendPalletLocation: InteriorLocation = [PalletInstance(SystemFrontendPalletInstance::get())].into(); +} + +impl snowbridge_pallet_system_frontend::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = weights::snowbridge_pallet_system_frontend::WeightInfo; + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type RegisterTokenOrigin = EitherOf< + EitherOf< + LocalAssetOwner< + AssetIdForTrustBackedAssetsConvert, + Assets, + AccountId, + AssetIdForTrustBackedAssets, + Location, + >, + ForeignAssetOwner< + ( + FromSiblingParachain, Location>, + xcm_config::bridging::to_kusama::KusamaAssetFromAssetHubKusama, + ), + ForeignAssets, + AccountId, + LocationToAccountId, + Location, + >, + >, + EnsureRootWithSuccess, + >; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = xcm_config::XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = benchmark_helpers::DoNothingRouter; + type AssetTransactor = AssetTransactors; + type EthereumLocation = FeeAsset; + type XcmExecutor = XcmExecutor; + type BridgeHubLocation = SiblingBridgeHub; + type UniversalLocation = UniversalLocation; + type PalletLocation = SystemFrontendPalletLocation; + type Swap = AssetConversion; + type BackendWeightInfo = weights::snowbridge_pallet_system_backend::WeightInfo; + type AccountIdConverter = LocationToAccountId; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SnowbridgeSystemFrontend; + + #[test] + fn bridge_hub_inbound_queue_pallet_index_is_correct() { + assert_eq!( + SystemFrontendPalletInstance::get(), + ::index() as u8 + ); + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmark_helpers { + use crate::{ + xcm_config::LocationToAccountId, AccountId, AssetConversion, Balances, ForeignAssets, + RuntimeOrigin, + }; + use alloc::boxed::Box; + use codec::Encode; + use xcm::prelude::*; + use xcm_executor::traits::ConvertLocation; + + pub struct DoNothingRouter; + impl SendXcm for DoNothingRouter { + type Ticket = Xcm<()>; + + fn validate( + _dest: &mut Option, + xcm: &mut Option>, + ) -> SendResult { + Ok((xcm.clone().unwrap(), Assets::new())) + } + fn deliver(xcm: Xcm<()>) -> Result { + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + Ok(hash) + } + } + + impl snowbridge_pallet_system_frontend::BenchmarkHelper for () { + fn make_xcm_origin(location: Location) -> RuntimeOrigin { + RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location)) + } + + fn initialize_storage(asset_location: Location, asset_owner: Location) { + let asset_owner = LocationToAccountId::convert_location(&asset_owner).unwrap(); + ForeignAssets::force_create( + RuntimeOrigin::root(), + asset_location, + asset_owner.into(), + true, + 1, + ) + .unwrap() + } + + fn setup_pools(caller: AccountId, asset: Location) { + // Prefund the caller's account with DOT + Balances::force_set_balance( + RuntimeOrigin::root(), + caller.clone().into(), + 10_000_000_000_000, + ) + .unwrap(); + + let asset_owner = caller.clone(); + ForeignAssets::force_create( + RuntimeOrigin::root(), + asset.clone(), + asset_owner.clone().into(), + true, + 1, + ) + .unwrap(); + + let signed_owner = RuntimeOrigin::signed(asset_owner.clone()); + + // Prefund the asset owner's account with DOT and Ether to create the pools + ForeignAssets::mint( + signed_owner.clone(), + asset.clone(), + asset_owner.clone().into(), + 10_000_000_000_000, + ) + .unwrap(); + Balances::force_set_balance( + RuntimeOrigin::root(), + asset_owner.clone().into(), + 10_000_000_000_000, + ) + .unwrap(); + + // Create the pool so the swap will succeed + let native_asset: Location = Parent.into(); + AssetConversion::create_pool( + signed_owner.clone(), + Box::new(native_asset.clone()), + Box::new(asset.clone()), + ) + .unwrap(); + AssetConversion::add_liquidity( + signed_owner, + Box::new(native_asset), + Box::new(asset), + 1_000_000_000_000, + 2_000_000_000_000, + 0, + 0, + asset_owner, + ) + .unwrap(); + } + } +} diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs index a419e6073f..ab99944874 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs @@ -62,6 +62,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); extern crate alloc; // Genesis preset configurations. +pub mod bridge_to_ethereum_config; pub mod genesis_config_presets; mod impls; mod weights; @@ -1023,6 +1024,7 @@ construct_runtime!( // DmpQueue: cumulus_pallet_dmp_queue = 33, removed ToKusamaXcmRouter: pallet_xcm_bridge_hub_router:: = 34, MessageQueue: pallet_message_queue = 35, + SnowbridgeSystemFrontend: snowbridge_pallet_system_frontend = 36, // Handy utilities. Utility: pallet_utility = 40, @@ -1189,6 +1191,7 @@ mod benches { [pallet_xcm, PalletXcmExtrinsicsBenchmark::] // Bridges [pallet_xcm_bridge_hub_router, ToKusama] + [snowbridge_pallet_system_frontend, SnowbridgeSystemFrontend] // NOTE: Make sure you point to the individual modules below. [pallet_xcm_benchmarks::fungible, XcmBalances] [pallet_xcm_benchmarks::generic, XcmGeneric] diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/mod.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/mod.rs index 9d174b0bf1..ba05d5392a 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/mod.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/mod.rs @@ -41,6 +41,8 @@ pub mod pallet_xcm; pub mod pallet_xcm_bridge_hub_router; pub mod paritydb_weights; pub mod rocksdb_weights; +pub mod snowbridge_pallet_system_backend; +pub mod snowbridge_pallet_system_frontend; pub mod xcm; pub use block_weights::constants::BlockExecutionWeight; diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_backend.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_backend.rs new file mode 100644 index 0000000000..3879b77788 --- /dev/null +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_backend.rs @@ -0,0 +1,65 @@ +// 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. + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_system_frontend::BackendWeightInfo`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_system_frontend::BackendWeightInfo for WeightInfo { + /// Copy the weight generated for `fn register_token() -> Weight` from ../../../../bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs + fn transact_register_token() -> Weight { + Weight::from_parts(66_072_000, 0) + .saturating_add(Weight::from_parts(0, 4115)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + + /// Copy the weight generated for `fn add_tip() -> Weight` from ../../../../bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs + fn transact_add_tip() -> Weight { + Weight::from_parts(17_015_000, 0) + .saturating_add(Weight::from_parts(0, 3505)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + + /// Copy the weight generated for `fn do_process_message() -> Weight` from ../../../../bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs + fn do_process_message() -> Weight { + Weight::from_parts(33_317_000, 0) + .saturating_add(Weight::from_parts(0, 1527)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(4)) + } + + /// Copy the weight generated for `fn commit_single() -> Weight` from ../../../../bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs + fn commit_single() -> Weight { + Weight::from_parts(15_441_000, 0) + .saturating_add(Weight::from_parts(0, 1620)) + .saturating_add(T::DbWeight::get().reads(1)) + } + + /// Copy the weight generated for `fn submit_delivery_receipt() -> Weight` from ../../../../bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs + fn submit_delivery_receipt() -> Weight { + Weight::from_parts(106_996_000, 0) + .saturating_add(Weight::from_parts(0, 3762)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_frontend.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_frontend.rs new file mode 100644 index 0000000000..54241249f5 --- /dev/null +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/weights/snowbridge_pallet_system_frontend.rs @@ -0,0 +1,93 @@ +// Copyright (C) Parity Technologies and the various Polkadot contributors, see Contributions.md +// for a list of specific contributors. +// 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. + +//! Autogenerated weights for `snowbridge_pallet_system_frontend` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 50.0.0 +//! DATE: 2025-08-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `58bdc404b8a5`, CPU: `QEMU Virtual CPU version 2.5+` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --extrinsic=* +// --runtime=target/production/wbuild/asset-hub-polkadot-runtime/asset_hub_polkadot_runtime.wasm +// --pallet=snowbridge_pallet_system_frontend +// --header=/_work/fellowship-001/runtimes/runtimes/.github/scripts/cmd/file_header.txt +// --output=./system-parachains/asset-hubs/asset-hub-polkadot/src/weights +// --wasm-execution=compiled +// --steps=50 +// --repeat=20 +// --heap-pages=4096 + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_system_frontend`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_system_frontend::WeightInfo for WeightInfo { + /// Storage: `SnowbridgeSystemFrontend::ExportOperatingMode` (r:1 w:0) + /// Proof: `SnowbridgeSystemFrontend::ExportOperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `ForeignAssets::Asset` (r:2 w:1) + /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x1b38344db996cce0ee20ee2bfb1848ff` (r:1 w:0) + /// Proof: UNKNOWN KEY `0x1b38344db996cce0ee20ee2bfb1848ff` (r:1 w:0) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ForeignAssets::Account` (r:2 w:2) + /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn register_token() -> Weight { + // Proof Size summary in bytes: + // Measured: `1077` + // Estimated: `7556` + // Minimum execution time: 200_150_000 picoseconds. + Weight::from_parts(224_979_000, 0) + .saturating_add(Weight::from_parts(0, 7556)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(5)) + } + /// Storage: UNKNOWN KEY `0x1b38344db996cce0ee20ee2bfb1848ff` (r:1 w:0) + /// Proof: UNKNOWN KEY `0x1b38344db996cce0ee20ee2bfb1848ff` (r:1 w:0) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ForeignAssets::Asset` (r:1 w:1) + /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `ForeignAssets::Account` (r:2 w:2) + /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn add_tip() -> Weight { + // Proof Size summary in bytes: + // Measured: `603` + // Estimated: `7404` + // Minimum execution time: 177_870_000 picoseconds. + Weight::from_parts(185_560_000, 0) + .saturating_add(Weight::from_parts(0, 7404)) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + } +} diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs b/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs index b991518f62..515353b313 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/src/xcm_config.rs @@ -42,7 +42,7 @@ use parachains_common::xcm_config::{ }; use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_constants::system_parachain; -use snowbridge_inbound_queue_primitives::EthereumLocationsConverterFor; +use snowbridge_outbound_queue_primitives::v2::exporter::PausableExporter; use sp_runtime::traits::{AccountIdConversion, TryConvertInto}; use system_parachains_constants::TREASURY_PALLET_ID; use xcm::latest::prelude::*; @@ -50,15 +50,14 @@ use xcm_builder::{ AccountId32Aliases, AliasChildLocation, AliasOriginRootUsingFilter, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, - DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, - FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, - IsConcrete, LocalMint, MatchedConvertedConcreteId, NoChecking, ParentAsSuperuser, - ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, - SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, - SingleAssetExchangeAdapter, SovereignSignedViaLocation, StartsWith, - StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, - UnpaidRemoteExporter, UsingComponents, WeightInfoBounds, WithComputedOrigin, - WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, + DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor, + FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, HashedDescription, IsConcrete, + LocalMint, MatchedConvertedConcreteId, NoChecking, ParentAsSuperuser, ParentIsPreset, + RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SingleAssetExchangeAdapter, + SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, + TrailingSetTopicAsId, UnpaidRemoteExporter, UsingComponents, WeightInfoBounds, + WithComputedOrigin, WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{traits::ConvertLocation, XcmExecutor}; @@ -102,12 +101,8 @@ pub type LocationToAccountId = ( AccountId32Aliases, // Foreign locations alias into accounts according to a hash of their standard description. HashedDescription>, - // Different global consensus parachain sovereign account. - // (Used for over-bridge transfers and reserve processing) - GlobalConsensusParachainConvertsFor, - // Ethereum contract sovereign account. - // (Used to get convert ethereum contract locations to sovereign account) - EthereumLocationsConverterFor, + // Different global consensus locations sovereign accounts. + ExternalConsensusLocationsConverterFor, ); /// Means for transacting the native currency on this chain. @@ -513,10 +508,18 @@ pub type XcmRouter = WithUniqueTopic<( ToKusamaXcmRouter, // Router which wraps and sends xcm to BridgeHub to be delivered to the Ethereum // GlobalConsensus - UnpaidRemoteExporter< - bridging::to_ethereum::EthereumNetworkExportTable, - XcmpQueue, - UniversalLocation, + PausableExporter< + crate::SnowbridgeSystemFrontend, + ( + UnpaidRemoteExporter< + ( + bridging::to_ethereum::EthereumNetworkExportTableV2, + bridging::to_ethereum::EthereumNetworkExportTableV1, + ), + XcmpQueue, + UniversalLocation, + >, + ), >, )>; @@ -717,14 +720,18 @@ pub mod bridging { pub mod to_ethereum { use super::*; pub use bp_bridge_hub_polkadot::snowbridge::EthereumNetwork; - use bp_bridge_hub_polkadot::snowbridge::InboundQueuePalletInstance; + use bp_bridge_hub_polkadot::snowbridge::{ + InboundQueuePalletInstance, InboundQueueV2PalletInstance, + }; parameter_types! { /// User fee for transfers from Polkadot to Ethereum. /// The fee is set to max Balance to disable the bridge until a fee is set by /// governance. pub const DefaultBridgeHubEthereumBaseFee: Balance = Balance::MAX; + pub const DefaultBridgeHubEthereumBaseFeeV2: Balance = Balance::MAX; pub storage BridgeHubEthereumBaseFee: Balance = DefaultBridgeHubEthereumBaseFee::get(); + pub storage BridgeHubEthereumBaseFeeV2: Balance = DefaultBridgeHubEthereumBaseFeeV2::get(); pub SiblingBridgeHubWithEthereumInboundQueueInstance: Location = Location::new( 1, [ @@ -732,13 +739,20 @@ pub mod bridging { PalletInstance(InboundQueuePalletInstance::get()), ] ); + pub SiblingBridgeHubWithEthereumInboundQueueV2Instance: Location = Location::new( + 1, + [ + Parachain(SiblingBridgeHubParaId::get()), + PalletInstance(InboundQueueV2PalletInstance::get()), + ] + ); /// Set up exporters configuration. /// `Option` represents static "base fee" which is used for total delivery fee calculation. - pub BridgeTable: Vec = vec![ + pub EthereumBridgeTableV1: Vec = vec![ NetworkExportTableItem::new( EthereumNetwork::get(), - Some(vec![Junctions::Here]), + Some(vec![Here]), SiblingBridgeHub::get(), Some(( XcmBridgeHubRouterFeeAssetId::get(), @@ -747,15 +761,35 @@ pub mod bridging { ), ]; + pub EthereumBridgeTableV2: Vec = vec![ + NetworkExportTableItem::new( + EthereumNetwork::get(), + Some(vec![Here]), + SiblingBridgeHub::get(), + Some(( + XcmBridgeHubRouterFeeAssetId::get(), + BridgeHubEthereumBaseFeeV2::get(), + ).into()) + ), + ]; + /// Universal aliases pub UniversalAliases: BTreeSet<(Location, Junction)> = BTreeSet::from_iter( vec![ + (SiblingBridgeHubWithEthereumInboundQueueV2Instance::get(), GlobalConsensus(EthereumNetwork::get())), (SiblingBridgeHubWithEthereumInboundQueueInstance::get(), GlobalConsensus(EthereumNetwork::get())), ] ); } - pub type EthereumNetworkExportTable = xcm_builder::NetworkExportTable; + pub type EthereumNetworkExportTableV1 = + xcm_builder::NetworkExportTable; + + pub type EthereumNetworkExportTableV2 = + snowbridge_outbound_queue_primitives::v2::XcmFilterExporter< + xcm_builder::NetworkExportTable, + snowbridge_outbound_queue_primitives::v2::XcmForSnowbridgeV2, + >; pub type EthereumAssetFromEthereum = IsForeignConcreteAsset>; diff --git a/system-parachains/asset-hubs/asset-hub-polkadot/tests/snowbridge.rs b/system-parachains/asset-hubs/asset-hub-polkadot/tests/snowbridge.rs index 64c318293f..0cdb7f24e8 100644 --- a/system-parachains/asset-hubs/asset-hub-polkadot/tests/snowbridge.rs +++ b/system-parachains/asset-hubs/asset-hub-polkadot/tests/snowbridge.rs @@ -19,12 +19,15 @@ extern crate alloc; use alloc::{vec, vec::Vec}; use asset_hub_polkadot_runtime::xcm_config::bridging::{ - to_ethereum::{BridgeHubEthereumBaseFee, BridgeTable, EthereumNetwork}, + to_ethereum::{ + BridgeHubEthereumBaseFee, EthereumNetwork, EthereumNetworkExportTableV1, + EthereumNetworkExportTableV2, + }, SiblingBridgeHub, XcmBridgeHubRouterFeeAssetId, }; use sp_core::H160; use xcm::latest::prelude::*; -use xcm_builder::{ExporterFor, NetworkExportTable}; +use xcm_builder::ExporterFor; #[test] fn network_export_table_works() { @@ -57,19 +60,29 @@ fn network_export_table_works() { None, ), // From Ethereum with the Sepolia chain ID instead of Mainnet, not matched. - (NetworkId::Ethereum { chain_id: 11155111 }, Here, None), + (Ethereum { chain_id: 11155111 }, Here, None), ]; for (network, remote_location, expected_result) in test_data { assert_eq!( - NetworkExportTable::::exporter_for( + EthereumNetworkExportTableV1::exporter_for( &network, &remote_location, &Xcm::default() ), expected_result, - "expected_result: {expected_result:?} not matched for network: {network:?} and remote_location: {remote_location:?}", - ) + "EthereumBridgeTableV1: expected_result: {expected_result:?} not matched for network: {network:?} and remote_location: {remote_location:?}", + ); + + assert_eq!( + EthereumNetworkExportTableV2::exporter_for( + &network, + &remote_location, + &Xcm::default() + ), + expected_result, + "EthereumBridgeTableV2: expected_result: {expected_result:?} not matched for network: {network:?} and remote_location: {remote_location:?}", + ); } }); } diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/Cargo.toml b/system-parachains/bridge-hubs/bridge-hub-polkadot/Cargo.toml index d9f982a827..6bdc38887b 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/Cargo.toml +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/Cargo.toml @@ -106,12 +106,15 @@ pallet-xcm-bridge-hub = { workspace = true } # Ethereum Bridge (Snowbridge) snowbridge-beacon-primitives = { workspace = true } snowbridge-pallet-system = { workspace = true } +snowbridge-pallet-system-v2 = { workspace = true } snowbridge-system-runtime-api = { workspace = true } snowbridge-core = { workspace = true } snowbridge-merkle-tree = { workspace = true } snowbridge-pallet-ethereum-client = { workspace = true } snowbridge-pallet-inbound-queue = { workspace = true } +snowbridge-pallet-inbound-queue-v2 = { workspace = true } snowbridge-pallet-outbound-queue = { workspace = true } +snowbridge-pallet-outbound-queue-v2 = { workspace = true } snowbridge-outbound-queue-primitives = { workspace = true } snowbridge-outbound-queue-runtime-api = { workspace = true } snowbridge-inbound-queue-primitives = { workspace = true } @@ -198,8 +201,11 @@ std = [ "snowbridge-outbound-queue-primitives/std", "snowbridge-outbound-queue-runtime-api/std", "snowbridge-pallet-ethereum-client/std", + "snowbridge-pallet-inbound-queue-v2/std", "snowbridge-pallet-inbound-queue/std", + "snowbridge-pallet-outbound-queue-v2/std", "snowbridge-pallet-outbound-queue/std", + "snowbridge-pallet-system-v2/std", "snowbridge-pallet-system/std", "snowbridge-runtime-common/std", "snowbridge-system-runtime-api/std", @@ -264,8 +270,11 @@ runtime-benchmarks = [ "snowbridge-inbound-queue-primitives/runtime-benchmarks", "snowbridge-pallet-ethereum-client-fixtures/runtime-benchmarks", "snowbridge-pallet-ethereum-client/runtime-benchmarks", + "snowbridge-pallet-inbound-queue-v2/runtime-benchmarks", "snowbridge-pallet-inbound-queue/runtime-benchmarks", + "snowbridge-pallet-outbound-queue-v2/runtime-benchmarks", "snowbridge-pallet-outbound-queue/runtime-benchmarks", + "snowbridge-pallet-system-v2/runtime-benchmarks", "snowbridge-pallet-system/runtime-benchmarks", "snowbridge-runtime-common/runtime-benchmarks", "snowbridge-runtime-test-common/runtime-benchmarks", @@ -306,8 +315,11 @@ try-runtime = [ "parachains-common/try-runtime", "polkadot-runtime-common/try-runtime", "snowbridge-pallet-ethereum-client/try-runtime", + "snowbridge-pallet-inbound-queue-v2/try-runtime", "snowbridge-pallet-inbound-queue/try-runtime", + "snowbridge-pallet-outbound-queue-v2/try-runtime", "snowbridge-pallet-outbound-queue/try-runtime", + "snowbridge-pallet-system-v2/try-runtime", "snowbridge-pallet-system/try-runtime", "snowbridge-runtime-common/try-runtime", "sp-runtime/try-runtime", diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/primitives/src/lib.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/primitives/src/lib.rs index 3727c98db2..4f8b37964c 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/primitives/src/lib.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/primitives/src/lib.rs @@ -166,8 +166,10 @@ pub mod snowbridge { parameter_types! { /// Should match the `ForeignAssets::create` index on Asset Hub. pub const CreateAssetCall: [u8;2] = [53, 0]; - /// The pallet index of the Ethereum inbound queue pallet in the Bridge Hub runtime. + /// The pallet index of the Ethereum inbound queue pallet in the BridgeHub runtime. pub const InboundQueuePalletInstance: u8 = 80; + /// The pallet index of the Ethereum inbound queue v2 pallet in the BridgeHub runtime. + pub const InboundQueueV2PalletInstance: u8 = 91; /// Default pricing parameters used to calculate bridging fees. Initialized to unit values, /// as it is intended that these parameters should be updated with more /// accurate values prior to bridge activation. This can be performed diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/bridge_to_ethereum_config.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/bridge_to_ethereum_config.rs index 01fa57c394..e6195f8fbf 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/bridge_to_ethereum_config.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/bridge_to_ethereum_config.rs @@ -15,23 +15,37 @@ // along with Cumulus. If not, see . use crate::{ - xcm_config::{self, RelayNetwork, RelayTreasuryPalletAccount, UniversalLocation}, - Balances, EthereumInboundQueue, EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, - RuntimeEvent, TransactionByteFee, + bridge_common_config::BridgeReward, + xcm_config::{self, RelayNetwork, RelayTreasuryPalletAccount, RootLocation, UniversalLocation}, + Balances, BridgeRelayers, EthereumBeaconClient, EthereumInboundQueue, EthereumInboundQueueV2, + EthereumOutboundQueue, EthereumOutboundQueueV2, EthereumSystem, EthereumSystemV2, MessageQueue, + Runtime, RuntimeEvent, TransactionByteFee, +}; +use bp_asset_hub_polkadot::SystemFrontendPalletInstance; +use bp_bridge_hub_polkadot::snowbridge::{ + CreateAssetCall, InboundQueuePalletInstance, InboundQueueV2PalletInstance, Parameters, }; -use bp_bridge_hub_polkadot::snowbridge::{CreateAssetCall, InboundQueuePalletInstance, Parameters}; pub use bp_bridge_hub_polkadot::snowbridge::{EthereumLocation, EthereumNetwork}; -use frame_support::{parameter_types, weights::ConstantMultiplier}; +use frame_support::{parameter_types, traits::Contains, weights::ConstantMultiplier}; +use frame_system::EnsureRootWithSuccess; +use hex_literal::hex; use pallet_xcm::EnsureXcm; use parachains_common::{AccountId, Balance}; +use polkadot_runtime_constants::system_parachain::AssetHubParaId; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::AllowSiblingsOnly; use snowbridge_inbound_queue_primitives::v1::MessageToXcm; -use snowbridge_outbound_queue_primitives::v1::{ConstantGasMeter, EthereumBlobExporter}; +use snowbridge_outbound_queue_primitives::{ + v1::{ConstantGasMeter, EthereumBlobExporter}, + v2::{ConstantGasMeter as ConstantGasMeterV2, EthereumBlobExporter as EthereumBlobExporterV2}, +}; use sp_core::H160; use sp_runtime::traits::{ConstU32, ConstU8, Keccak256}; use system_parachains_constants::polkadot::fee::WeightToFee; -use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, Parachain}; +use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, PalletInstance, Parachain}; +use xcm_executor::XcmExecutor; + +pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32; /// Exports message to the Ethereum Gateway contract. pub type SnowbridgeExporter = EthereumBlobExporter< @@ -42,11 +56,23 @@ pub type SnowbridgeExporter = EthereumBlobExporter< EthereumSystem, >; +pub type SnowbridgeExporterV2 = EthereumBlobExporterV2< + UniversalLocation, + EthereumNetwork, + EthereumOutboundQueueV2, + EthereumSystemV2, + AssetHubParaId, +>; + parameter_types! { // The gateway address is set by governance. pub storage EthereumGatewayAddress: H160 = H160::zero(); - pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(RelayNetwork::get()),Parachain(polkadot_runtime_constants::system_parachain::ASSET_HUB_ID)]); + pub AssetHubFromEthereum: Location = Location::new(1, [GlobalConsensus(RelayNetwork::get()),Parachain(polkadot_runtime_constants::system_parachain::ASSET_HUB_ID)]); pub EthereumUniversalLocation: InteriorLocation = [GlobalConsensus(EthereumNetwork::get())].into(); + pub AssetHubUniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Parachain(polkadot_runtime_constants::system_parachain::ASSET_HUB_ID)].into(); + pub InboundQueueV2Location: InteriorLocation = [PalletInstance(InboundQueueV2PalletInstance::get())].into(); + pub const SnowbridgeReward: BridgeReward = BridgeReward::Snowbridge; + pub SnowbridgeFrontendLocation: Location = Location::new(1, [Parachain(polkadot_runtime_constants::system_parachain::ASSET_HUB_ID), PalletInstance(SystemFrontendPalletInstance::get())]); } impl snowbridge_pallet_inbound_queue::Config for Runtime { @@ -79,6 +105,40 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type AssetTransactor = ::AssetTransactor; } +impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Verifier = EthereumBeaconClient; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = xcm_config::XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = benchmark_helpers::DoNothingRouter; + type GatewayAddress = EthereumGatewayAddress; + #[cfg(feature = "runtime-benchmarks")] + type Helper = Runtime; + type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; + type AssetHubParaId = AssetHubParaId; + type XcmExecutor = XcmExecutor; + type MessageConverter = snowbridge_inbound_queue_primitives::v2::MessageToXcm< + CreateAssetCall, + bp_asset_hub_polkadot::CreateForeignAssetDeposit, + EthereumNetwork, + InboundQueueV2Location, + EthereumSystem, + EthereumGatewayAddress, + EthereumUniversalLocation, + AssetHubFromEthereum, + AssetHubUniversalLocation, + AccountId, + >; + type AccountToLocation = xcm_builder::AliasesIntoAccountId32< + xcm_config::RelayNetwork, + ::AccountId, + >; + type RewardKind = BridgeReward; + type DefaultRewardKind = SnowbridgeReward; + type RewardPayment = BridgeRelayers; +} + impl snowbridge_pallet_outbound_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Hashing = Keccak256; @@ -94,31 +154,57 @@ impl snowbridge_pallet_outbound_queue::Config for Runtime { type Channels = EthereumSystem; } +impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Hashing = Keccak256; + type MessageQueue = MessageQueue; + // Maximum payload size for outbound messages. + type MaxMessagePayloadSize = ConstU32<2048>; + // Maximum number of outbound messages that can be committed per block. + // It's benchmarked, including the entire process flow(initialize,submit,commit) in the + // worst-case, Benchmark results in `../weights/snowbridge_pallet_outbound_queue_v2. + // rs` show that the `process` function consumes less than 1% of the block capacity, which is + // safe enough. + type MaxMessagesPerBlock = ConstU32<32>; + type GasMeter = ConstantGasMeterV2; + type Balance = Balance; + type WeightToFee = WeightToFee; + type Verifier = EthereumBeaconClient; + type GatewayAddress = EthereumGatewayAddress; + type WeightInfo = crate::weights::snowbridge_pallet_outbound_queue_v2::WeightInfo; + type EthereumNetwork = EthereumNetwork; + type RewardKind = BridgeReward; + type DefaultRewardKind = SnowbridgeReward; + type RewardPayment = BridgeRelayers; + #[cfg(feature = "runtime-benchmarks")] + type Helper = Runtime; +} + #[cfg(not(any(feature = "std", feature = "runtime-benchmarks", test)))] parameter_types! { pub const ChainForkVersions: ForkVersions = ForkVersions { genesis: Fork { - version: [0, 0, 0, 0], // 0x00000000 + version: hex!("00000000"), epoch: 0, }, altair: Fork { - version: [1, 0, 0, 0], // 0x01000000 + version: hex!("01000000"), epoch: 74240, }, bellatrix: Fork { - version: [2, 0, 0, 0], // 0x02000000 + version: hex!("02000000"), epoch: 144896, }, capella: Fork { - version: [3, 0, 0, 0], // 0x03000000 + version: hex!("03000000"), epoch: 194048, }, deneb: Fork { - version: [4, 0, 0, 0], // 0x04000000 + version: hex!("04000000"), epoch: 269568, }, electra: Fork { - version: [5, 0, 0, 0], // 0x05000000 + version: hex!("05000000"), epoch: 364032, }, }; @@ -128,34 +214,32 @@ parameter_types! { parameter_types! { pub const ChainForkVersions: ForkVersions = ForkVersions { genesis: Fork { - version: [0, 0, 0, 0], // 0x00000000 + version: hex!("00000000"), epoch: 0, }, altair: Fork { - version: [1, 0, 0, 0], // 0x01000000 + version: hex!("01000000"), epoch: 0, }, bellatrix: Fork { - version: [2, 0, 0, 0], // 0x02000000 + version: hex!("02000000"), epoch: 0, }, capella: Fork { - version: [3, 0, 0, 0], // 0x03000000 + version: hex!("03000000"), epoch: 0, }, deneb: Fork { - version: [4, 0, 0, 0], // 0x04000000 + version: hex!("04000000"), epoch: 0, }, electra: Fork { - version: [5, 0, 0, 0], // 0x05000000 + version: hex!("05000000"), epoch: 0, } }; } -pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32; - impl snowbridge_pallet_ethereum_client::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ForkVersions = ChainForkVersions; @@ -179,25 +263,89 @@ impl snowbridge_pallet_system::Config for Runtime { type EthereumLocation = EthereumLocation; } +pub struct AllowFromEthereumFrontend; +impl Contains for AllowFromEthereumFrontend { + fn contains(location: &Location) -> bool { + match location.unpack() { + (1, [Parachain(para_id), PalletInstance(index)]) => + *para_id == polkadot_runtime_constants::system_parachain::ASSET_HUB_ID && + *index == SystemFrontendPalletInstance::get(), + _ => false, + } + } +} + +impl snowbridge_pallet_system_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OutboundQueue = EthereumOutboundQueueV2; + type InboundQueue = EthereumInboundQueueV2; + type FrontendOrigin = EnsureXcm; + type WeightInfo = crate::weights::snowbridge_pallet_system_v2::WeightInfo; + type GovernanceOrigin = EnsureRootWithSuccess; + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); +} + #[cfg(feature = "runtime-benchmarks")] pub mod benchmark_helpers { use super::{EthereumGatewayAddress, RelayTreasuryPalletAccount, Runtime}; use crate::{Balances, EthereumBeaconClient, ExistentialDeposit, RuntimeOrigin}; use codec::Encode; - use frame_support::traits::fungible; + use frame_support::{parameter_types, traits::fungible}; use hex_literal::hex; use snowbridge_beacon_primitives::BeaconHeader; use snowbridge_pallet_inbound_queue::BenchmarkHelper; - use sp_core::H256; + use snowbridge_pallet_inbound_queue_v2::BenchmarkHelper as InboundQueueBenchmarkHelperV2; + use snowbridge_pallet_outbound_queue_v2::BenchmarkHelper as OutboundQueueBenchmarkHelperV2; + use sp_core::{H160, H256}; use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash}; + parameter_types! { + // The fixture data for benchmark tests in the Polkadot SDK relies on these gateway addresses, + // which is validated in the pallets. + pub EthereumGatewayAddressV1: H160 = hex!["eda338e4dc46038493b885327842fd3e301cab39"].into(); + pub EthereumGatewayAddressV2: H160 = hex!["b1185ede04202fe62d38f5db72f71e38ff3e8305"].into(); + } + impl BenchmarkHelper for Runtime { fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) { - EthereumBeaconClient::store_finalized_header(beacon_header, block_roots_root).unwrap(); - EthereumGatewayAddress::set(&hex!["EDa338E4dC46038493b885327842fD3E301CaB39"].into()); + initialize_storage_for_benchmarks( + EthereumGatewayAddressV1::get(), + beacon_header, + block_roots_root, + ); + } + } + + impl InboundQueueBenchmarkHelperV2 for Runtime { + fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) { + initialize_storage_for_benchmarks( + EthereumGatewayAddressV2::get(), + beacon_header, + block_roots_root, + ); } } + impl OutboundQueueBenchmarkHelperV2 for Runtime { + fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) { + initialize_storage_for_benchmarks( + EthereumGatewayAddressV2::get(), + beacon_header, + block_roots_root, + ); + } + } + + fn initialize_storage_for_benchmarks( + gateway_address: H160, + beacon_header: BeaconHeader, + block_roots_root: H256, + ) { + EthereumBeaconClient::store_finalized_header(beacon_header, block_roots_root).unwrap(); + EthereumGatewayAddress::set(&gateway_address); + } + pub struct DoNothingRouter; impl SendXcm for DoNothingRouter { type Ticket = Xcm<()>; @@ -225,6 +373,12 @@ pub mod benchmark_helpers { RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location)) } } + + impl snowbridge_pallet_system_v2::BenchmarkHelper for () { + fn make_xcm_origin(location: Location) -> RuntimeOrigin { + RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location)) + } + } } #[cfg(test)] @@ -238,4 +392,12 @@ mod tests { ::index() as u8 ); } + + #[test] + fn bridge_hub_inbound_v2_queue_pallet_index_is_correct() { + assert_eq!( + InboundQueueV2PalletInstance::get(), + ::index() as u8 + ); + } } diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/lib.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/lib.rs index b6175cab6d..5a7646768f 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/lib.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/lib.rs @@ -395,13 +395,14 @@ impl pallet_message_queue::Config for Runtime { type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor; #[cfg(not(all(not(feature = "std"), feature = "runtime-benchmarks")))] - type MessageProcessor = bridge_hub_common::BridgeHubMessageRouter< + type MessageProcessor = bridge_hub_common::BridgeHubDualMessageRouter< xcm_builder::ProcessXcmMessage< AggregateMessageOrigin, xcm_executor::XcmExecutor, RuntimeCall, >, EthereumOutboundQueue, + EthereumOutboundQueueV2, >; type Size = u32; // The XCMP queue pallet is only ever able to handle the `Sibling(ParaId)` origin: @@ -594,6 +595,11 @@ construct_runtime!( EthereumBeaconClient: snowbridge_pallet_ethereum_client = 82, EthereumSystem: snowbridge_pallet_system = 83, + // Ethereum bridge pallets V2. + EthereumSystemV2: snowbridge_pallet_system_v2 = 90, + EthereumInboundQueueV2: snowbridge_pallet_inbound_queue_v2 = 91, + EthereumOutboundQueueV2: snowbridge_pallet_outbound_queue_v2 = 92, + // Message Queue. Importantly, it is registered after Snowbridge pallets // so that messages are processed after the `on_initialize` hooks of bridging pallets. MessageQueue: pallet_message_queue = 175, @@ -639,6 +645,9 @@ mod benches { [snowbridge_pallet_outbound_queue, EthereumOutboundQueue] [snowbridge_pallet_system, EthereumSystem] [snowbridge_pallet_ethereum_client, EthereumBeaconClient] + [snowbridge_pallet_inbound_queue_v2, EthereumInboundQueueV2] + [snowbridge_pallet_outbound_queue_v2, EthereumOutboundQueueV2] + [snowbridge_pallet_system_v2, EthereumSystemV2] ); impl frame_system_benchmarking::Config for Runtime { diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/mod.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/mod.rs index 00015758bf..785b7c6628 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/mod.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/mod.rs @@ -47,8 +47,11 @@ pub mod paritydb_weights; pub mod rocksdb_weights; pub mod snowbridge_pallet_ethereum_client; pub mod snowbridge_pallet_inbound_queue; +pub mod snowbridge_pallet_inbound_queue_v2; pub mod snowbridge_pallet_outbound_queue; +pub mod snowbridge_pallet_outbound_queue_v2; pub mod snowbridge_pallet_system; +pub mod snowbridge_pallet_system_v2; pub mod xcm; pub use block_weights::constants::BlockExecutionWeight; diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_inbound_queue_v2.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_inbound_queue_v2.rs new file mode 100644 index 0000000000..4e73abcc5a --- /dev/null +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_inbound_queue_v2.rs @@ -0,0 +1,73 @@ +// Copyright (C) Parity Technologies and the various Polkadot contributors, see Contributions.md +// for a list of specific contributors. +// 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. + +//! Autogenerated weights for `snowbridge_pallet_inbound_queue` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 50.0.0 +//! DATE: 2025-08-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-3-187`, CPU: `Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// target/release/wbuild/bridge-hub-polkadot-runtime/bridge_hub_polkadot_runtime.wasm +// --pallet=snowbridge_pallet_inbound_queue_v2 +// --header=.github/scripts/cmd/file_header.txt +// --output=./system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_inbound_queue_v2.rs +// --wasm-execution=compiled +// --extrinsic=* + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_inbound_queue_v2`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_inbound_queue_v2::WeightInfo for WeightInfo { + /// Storage: `EthereumInboundQueueV2::OperatingMode` (r:1 w:0) + /// Proof: `EthereumInboundQueueV2::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0) + /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0) + /// Proof: `EthereumBeaconClient::FinalizedBeaconState` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) + /// Proof: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) + /// Storage: `EthereumInboundQueueV2::NonceBitmap` (r:1 w:1) + /// Proof: `EthereumInboundQueueV2::NonceBitmap` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `BridgeRelayers::RelayerRewards` (r:1 w:1) + /// Proof: `BridgeRelayers::RelayerRewards` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `239` + // Estimated: `3704` + // Minimum execution time: 121_390_000 picoseconds. + Weight::from_parts(125_900_000, 0) + .saturating_add(Weight::from_parts(0, 3704)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs new file mode 100644 index 0000000000..9dc4345ab4 --- /dev/null +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs @@ -0,0 +1,136 @@ +// Copyright (C) Parity Technologies and the various Polkadot contributors, see Contributions.md +// for a list of specific contributors. +// 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. + +//! Autogenerated weights for `snowbridge_pallet_outbound_queue_v2` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 50.0.0 +//! DATE: 2025-08-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-3-187`, CPU: `Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// target/release/wbuild/bridge-hub-polkadot-runtime/bridge_hub_polkadot_runtime.wasm +// --pallet=snowbridge_pallet_outbound_queue_v2 +// --header=.github/scripts/cmd/file_header.txt +// --output=./system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_outbound_queue_v2.rs +// --wasm-execution=compiled +// --extrinsic=* + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_outbound_queue_v2`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_outbound_queue_v2::WeightInfo for WeightInfo { + /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:1 w:1) + /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `EthereumOutboundQueueV2::Nonce` (r:1 w:1) + /// Proof: `EthereumOutboundQueueV2::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumOutboundQueueV2::Messages` (r:1 w:1) + /// Proof: `EthereumOutboundQueueV2::Messages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:0 w:1) + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + fn do_process_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `1527` + // Minimum execution time: 28_260_000 picoseconds. + Weight::from_parts(30_430_000, 0) + .saturating_add(Weight::from_parts(0, 1527)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:1 w:0) + /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn commit() -> Weight { + // Proof Size summary in bytes: + // Measured: `1128` + // Estimated: `2613` + // Minimum execution time: 40_910_000 picoseconds. + Weight::from_parts(42_980_000, 0) + .saturating_add(Weight::from_parts(0, 2613)) + .saturating_add(T::DbWeight::get().reads(1)) + } + /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:1 w:0) + /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn commit_single() -> Weight { + // Proof Size summary in bytes: + // Measured: `135` + // Estimated: `1620` + // Minimum execution time: 12_460_000 picoseconds. + Weight::from_parts(13_050_000, 0) + .saturating_add(Weight::from_parts(0, 1620)) + .saturating_add(T::DbWeight::get().reads(1)) + } + /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:0 w:1) + /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `EthereumOutboundQueueV2::Messages` (r:0 w:1) + /// Proof: `EthereumOutboundQueueV2::Messages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_820_000 picoseconds. + Weight::from_parts(2_390_000, 0) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `EthereumOutboundQueueV2::Nonce` (r:1 w:1) + /// Proof: `EthereumOutboundQueueV2::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:0 w:32) + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:0 w:1) + /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `EthereumOutboundQueueV2::Messages` (r:0 w:1) + /// Proof: `EthereumOutboundQueueV2::Messages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn process() -> Weight { + // Proof Size summary in bytes: + // Measured: `113` + // Estimated: `1493` + // Minimum execution time: 783_779_000 picoseconds. + Weight::from_parts(806_279_000, 0) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(35)) + } + /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0) + /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0) + /// Proof: `EthereumBeaconClient::FinalizedBeaconState` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) + /// Proof: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) + /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:1 w:1) + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + fn submit_delivery_receipt() -> Weight { + // Proof Size summary in bytes: + // Measured: `297` + // Estimated: `3762` + // Minimum execution time: 91_170_000 picoseconds. + Weight::from_parts(101_590_000, 0) + .saturating_add(Weight::from_parts(0, 3762)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs new file mode 100644 index 0000000000..ab7462a558 --- /dev/null +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs @@ -0,0 +1,117 @@ +// Copyright (C) Parity Technologies and the various Polkadot contributors, see Contributions.md +// for a list of specific contributors. +// 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. + +//! Autogenerated weights for `snowbridge_pallet_system_v2` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 50.0.0 +//! DATE: 2025-08-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-3-187`, CPU: `Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// target/release/wbuild/bridge-hub-polkadot-runtime/bridge_hub_polkadot_runtime.wasm +// --pallet=snowbridge_pallet_system_v2 +// --header=.github/scripts/cmd/file_header.txt +// --output=./system-parachains/bridge-hubs/bridge-hub-polkadot/src/weights/snowbridge_pallet_system_v2.rs +// --wasm-execution=compiled +// --extrinsic=* + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_system_v2`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_system_v2::WeightInfo for WeightInfo { + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::ForeignToNativeId` (r:1 w:1) + /// Proof: `EthereumSystem::ForeignToNativeId` (`max_values`: None, `max_size`: Some(650), added: 3125, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(65613), added: 68088, mode: `MaxEncodedLen`) + fn register_token() -> Weight { + // Proof Size summary in bytes: + // Measured: `112` + // Estimated: `4115` + // Minimum execution time: 48_830_000 picoseconds. + Weight::from_parts(51_731_000, 0) + .saturating_add(Weight::from_parts(0, 4115)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(65613), added: 68088, mode: `MaxEncodedLen`) + fn upgrade() -> Weight { + // Proof Size summary in bytes: + // Measured: `38` + // Estimated: `3601` + // Minimum execution time: 40_640_000 picoseconds. + Weight::from_parts(42_970_000, 0) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(3)) + } + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(65613), added: 68088, mode: `MaxEncodedLen`) + fn set_operating_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `38` + // Estimated: `3601` + // Minimum execution time: 34_410_000 picoseconds. + Weight::from_parts(35_978_000, 0) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(3)) + } + /// Storage: `EthereumInboundQueueV2::NonceBitmap` (r:1 w:0) + /// Proof: `EthereumInboundQueueV2::NonceBitmap` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// Storage: `EthereumInboundQueueV2::Tips` (r:1 w:1) + /// Proof: `EthereumInboundQueueV2::Tips` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + fn add_tip() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `3505` + // Minimum execution time: 12_019_000 picoseconds. + Weight::from_parts(13_130_000, 0) + .saturating_add(Weight::from_parts(0, 3497)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs index 22eccd80e0..b5cf637467 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs @@ -22,6 +22,7 @@ use super::{ ParachainSystem, PolkadotXcm, PriceForParentDelivery, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, WeightToFee, XcmpQueue, }; +use crate::bridge_to_ethereum_config::SnowbridgeFrontendLocation; use frame_support::{ parameter_types, traits::{ @@ -48,12 +49,12 @@ use xcm_builder::{ AccountId32Aliases, AliasChildLocation, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, DenyRecursively, DenyReserveTransferToRelayChain, DenyThenTry, - DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, - FungibleAdapter, HashedDescription, IsConcrete, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor, + FrameTransactionalProcessor, FungibleAdapter, HashedDescription, IsConcrete, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, + WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{traits::ConvertLocation, XcmExecutor}; @@ -90,6 +91,8 @@ pub type LocationToAccountId = ( AccountId32Aliases, // Foreign locations alias into accounts according to a hash of their standard description. HashedDescription>, + // Different global consensus locations sovereign accounts. + ExternalConsensusLocationsConverterFor, ); /// Means for transacting the native currency on this chain. @@ -182,6 +185,7 @@ pub type Barrier = TrailingSetTopicAsId< FellowsPlurality, Equals, Equals, + Equals, )>, // Subscriptions for version tracking are OK. AllowSubscriptionsFrom, @@ -251,8 +255,12 @@ impl xcm_executor::Config for XcmConfig { WaivedLocations, SendXcmFeeToAccount, >; - type MessageExporter = - (XcmOverBridgeHubKusama, crate::bridge_to_ethereum_config::SnowbridgeExporter); + type MessageExporter = ( + XcmOverBridgeHubKusama, + // `SnowbridgeExporterV2` should always be checked before legacy `SnowbridgeExporter` + crate::bridge_to_ethereum_config::SnowbridgeExporterV2, + crate::bridge_to_ethereum_config::SnowbridgeExporter, + ); type UniversalAliases = Nothing; type CallDispatcher = RuntimeCall; type SafeCallFilter = Everything; diff --git a/system-parachains/bridge-hubs/bridge-hub-polkadot/tests/snowbridge.rs b/system-parachains/bridge-hubs/bridge-hub-polkadot/tests/snowbridge.rs index db5d66a8d0..a60e568dc7 100644 --- a/system-parachains/bridge-hubs/bridge-hub-polkadot/tests/snowbridge.rs +++ b/system-parachains/bridge-hubs/bridge-hub-polkadot/tests/snowbridge.rs @@ -22,9 +22,9 @@ use bridge_hub_polkadot_runtime::{ bridge_to_ethereum_config::{EthereumGatewayAddress, EthereumNetwork}, bridge_to_kusama_config::OnBridgeHubPolkadotRefundBridgeHubKusamaMessages, xcm_config::{GovernanceLocation, UniversalLocation, XcmConfig}, - AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, Executive, - MessageQueueServiceWeight, Runtime, RuntimeCall, RuntimeEvent, SessionKeys, TxExtension, - UncheckedExtrinsic, + AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, EthereumBeaconClient, + Executive, MessageQueueServiceWeight, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + SessionKeys, TxExtension, UncheckedExtrinsic, }; use bridge_hub_test_utils::GovernanceOrigin; use codec::{Decode, Encode}; @@ -282,8 +282,8 @@ pub fn ethereum_extrinsic( let alice_account: ::AccountId = alice_account.into(); let balance_before = >::free_balance(&alice_account); - assert_ok!(>::force_checkpoint( - RuntimeHelper::::root_origin(), + assert_ok!(EthereumBeaconClient::force_checkpoint( + RuntimeOrigin::root(), initial_checkpoint.clone(), )); let balance_after_checkpoint =