From bcdccc2a8253228030aa76dbcad30a723820fbf0 Mon Sep 17 00:00:00 2001 From: Ron Date: Mon, 9 Jun 2025 19:19:34 +0800 Subject: [PATCH 1/4] BACKPORT-CONFLICT --- .../system-frontend/src/backend_weights.rs | 32 +++- .../system-frontend/src/benchmarking.rs | 11 +- .../pallets/system-frontend/src/lib.rs | 152 +++++++++++++-- .../pallets/system-frontend/src/mock.rs | 20 +- .../pallets/system-frontend/src/tests.rs | 176 +++++++++++++++++- .../pallets/system-v2/src/benchmarking.rs | 2 +- .../snowbridge/pallets/system-v2/src/lib.rs | 3 +- .../snowbridge/pallets/system-v2/src/tests.rs | 7 +- .../src/tests/snowbridge_v2_outbound.rs | 28 ++- .../tests/snowbridge_v2_outbound_edge_case.rs | 11 +- .../snowbridge_v2_outbound_from_rococo.rs | 3 + .../src/bridge_to_ethereum_config.rs | 71 ++++++- .../snowbridge_pallet_system_backend.rs | 29 +++ prdoc/pr_8725.prdoc | 15 ++ 14 files changed, 524 insertions(+), 36 deletions(-) create mode 100644 prdoc/pr_8725.prdoc diff --git a/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs b/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs index eeba6d31276ac..220e32ab71453 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs @@ -2,17 +2,47 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! XCM Execution weights for invoking the backend implementation -use frame_support::weights::Weight; +use frame_support::weights::{constants::RocksDbWeight, Weight}; /// XCM Execution weights for invoking the backend implementation pub trait BackendWeightInfo { /// Execution weight for remote xcm that dispatches `EthereumSystemCall::RegisterToken` /// using `Transact`. fn transact_register_token() -> Weight; +<<<<<<< HEAD +======= + fn transact_add_tip() -> Weight; + fn do_process_message() -> Weight; + fn commit_single() -> Weight; + fn submit_delivery_receipt() -> Weight; +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } impl BackendWeightInfo for () { fn transact_register_token() -> Weight { Weight::from_parts(100_000_000, 10000) } +<<<<<<< HEAD +======= + fn transact_add_tip() -> Weight { + Weight::from_parts(100_000_000, 10000) + } + fn do_process_message() -> Weight { + Weight::from_parts(39_000_000, 3485) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + fn commit_single() -> Weight { + Weight::from_parts(9_000_000, 1586) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + + fn submit_delivery_receipt() -> Weight { + Weight::from_parts(70_000_000, 0) + .saturating_add(Weight::from_parts(0, 3601)) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(2)) + } +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } diff --git a/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs b/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs index 3ce2a06c8f9dd..db46d1f96cd5c 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs @@ -6,6 +6,7 @@ use super::*; use crate::Pallet as SnowbridgeControlFrontend; use frame_benchmarking::v2::*; use xcm::prelude::{Location, *}; +use xcm_executor::traits::ConvertLocation; #[benchmarks] mod benchmarks { @@ -17,7 +18,11 @@ mod benchmarks { let asset_location: Location = Location::new(1, [Parachain(2000), GeneralIndex(1)]); let asset_id = Box::new(VersionedLocation::from(asset_location.clone())); - T::Helper::initialize_storage(asset_location, origin_location); + T::Helper::initialize_storage(asset_location, origin_location.clone()); + + let ether = T::EthereumLocation::get(); + let asset_owner = T::AccountIdConverter::convert_location(&origin_location).unwrap(); + T::Helper::setup_pools(asset_owner, ether.clone()); let asset_metadata = AssetMetadata { name: "pal".as_bytes().to_vec().try_into().unwrap(), @@ -25,8 +30,10 @@ mod benchmarks { decimals: 12, }; + let fee_asset = Asset::from((Location::parent(), 1_000_000u128)); + #[extrinsic_call] - _(origin as T::RuntimeOrigin, asset_id, asset_metadata); + _(origin as T::RuntimeOrigin, asset_id, asset_metadata, fee_asset); Ok(()) } diff --git a/bridges/snowbridge/pallets/system-frontend/src/lib.rs b/bridges/snowbridge/pallets/system-frontend/src/lib.rs index c60e57c7176df..f16d9203c0ca5 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/lib.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/lib.rs @@ -59,6 +59,7 @@ pub enum EthereumSystemCall { sender: Box, asset_id: Box, metadata: AssetMetadata, + amount: u128, }, } @@ -74,6 +75,7 @@ where #[frame_support::pallet] pub mod pallet { use super::*; + use xcm_executor::traits::ConvertLocation; #[pallet::pallet] pub struct Pallet(_); @@ -109,6 +111,8 @@ pub mod pallet { /// InteriorLocation of this pallet. type PalletLocation: Get; + type AccountIdConverter: ConvertLocation; + /// Weights for dispatching XCM to backend implementation of `register_token` type BackendWeightInfo: BackendWeightInfo; @@ -191,11 +195,15 @@ pub mod pallet { #[pallet::weight( T::WeightInfo::register_token() .saturating_add(T::BackendWeightInfo::transact_register_token()) + .saturating_add(T::BackendWeightInfo::do_process_message()) + .saturating_add(T::BackendWeightInfo::commit_single()) + .saturating_add(T::BackendWeightInfo::submit_delivery_receipt()) )] pub fn register_token( origin: OriginFor, asset_id: Box, metadata: AssetMetadata, + fee_asset: Asset, ) -> DispatchResult { ensure!(!Self::export_operating_mode().is_halted(), Error::::Halted); @@ -203,22 +211,46 @@ pub mod pallet { (*asset_id).try_into().map_err(|_| Error::::UnsupportedLocationVersion)?; let origin_location = T::RegisterTokenOrigin::ensure_origin(origin, &asset_location)?; - let dest = T::BridgeHubLocation::get(); - let call = - Self::build_register_token_call(origin_location.clone(), asset_location, metadata)?; - let remote_xcm = Self::build_remote_xcm(&call); - let message_id = Self::send_xcm(origin_location, dest.clone(), remote_xcm.clone()) - .map_err(|error| Error::::from(error))?; + let ether_gained = if origin_location.is_here() { + // Root origin/location does not pay any fees/tip. + 0 + } else { + Self::swap_fee_asset_and_burn(origin_location.clone(), fee_asset)? + }; - Self::deposit_event(Event::::MessageSent { - origin: T::PalletLocation::get().into(), - destination: dest, - message: remote_xcm, - message_id, - }); + let call = Self::build_register_token_call( + origin_location.clone(), + asset_location, + metadata, + ether_gained, + )?; - Ok(()) + Self::send_transact_call(origin_location, call) + } +<<<<<<< HEAD +======= + + /// Add an additional relayer tip for a committed message identified by `message_id`. + /// The tip asset will be swapped for ether. + #[pallet::call_index(2)] + #[pallet::weight( + T::WeightInfo::add_tip() + .saturating_add(T::BackendWeightInfo::transact_add_tip()) + )] + pub fn add_tip(origin: OriginFor, message_id: MessageId, asset: Asset) -> DispatchResult + where + ::AccountId: Into, + { + let who = ensure_signed(origin)?; + + let ether_gained = Self::swap_fee_asset_and_burn(who.clone().into(), asset)?; + + // Send the tip details to BH to be allocated to the reward in the Inbound/Outbound + // pallet + let call = Self::build_add_tip_call(who.clone(), message_id.clone(), ether_gained); + Self::send_transact_call(who.into(), call) } +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } impl Pallet { @@ -232,12 +264,52 @@ pub mod pallet { T::XcmSender::deliver(ticket) } +<<<<<<< HEAD +======= + /// Swaps a specified tip asset to Ether and then burns the resulting ether for + /// teleportation. Returns the amount of Ether gained if successful, or a DispatchError if + /// any step fails. + fn swap_and_burn( + origin: Location, + tip_asset_location: Location, + ether_location: Location, + tip_amount: u128, + ) -> Result { + // Swap tip asset to ether + let swap_path = vec![tip_asset_location.clone(), ether_location.clone()]; + let who = T::AccountIdConverter::convert_location(&origin) + .ok_or(Error::::LocationConversionFailed)?; + + let ether_gained = T::Swap::swap_exact_tokens_for_tokens( + who.clone(), + swap_path, + tip_amount, + None, // No minimum amount required + who, + true, + )?; + + // Burn the ether + let ether_asset = Asset::from((ether_location.clone(), ether_gained)); + + burn_for_teleport::(&origin, ðer_asset) + .map_err(|_| Error::::BurnError)?; + + Ok(ether_gained) + } + +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) // Build the call to dispatch the `EthereumSystem::register_token` extrinsic on BH fn build_register_token_call( sender: Location, asset: Location, metadata: AssetMetadata, +<<<<<<< HEAD ) -> Result> { +======= + amount: u128, + ) -> Result, Error> { +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) // reanchor locations relative to BH let sender = Self::reanchored(sender)?; let asset = Self::reanchored(asset)?; @@ -246,6 +318,7 @@ pub mod pallet { sender: Box::new(VersionedLocation::from(sender)), asset_id: Box::new(VersionedLocation::from(asset)), metadata, + amount, }); Ok(call) @@ -269,6 +342,59 @@ pub mod pallet { .reanchored(&T::BridgeHubLocation::get(), &T::UniversalLocation::get()) .map_err(|_| Error::::LocationConversionFailed) } + + fn swap_fee_asset_and_burn( + origin: Location, + fee_asset: Asset, + ) -> Result { + let ether_location = T::EthereumLocation::get(); + let (fee_asset_location, fee_amount) = match fee_asset { + Asset { id: AssetId(ref loc), fun: Fungible(amount) } => (loc, amount), + _ => { + tracing::debug!(target: LOG_TARGET, ?fee_asset, "error matching fee asset"); + return Err(Error::::UnsupportedAsset.into()) + }, + }; + if fee_amount == 0 { + return Ok(0) + } + + let ether_gained = if *fee_asset_location != ether_location { + Self::swap_and_burn( + origin.clone(), + fee_asset_location.clone(), + ether_location, + fee_amount, + ) + .inspect_err(|&e| { + tracing::debug!(target: LOG_TARGET, ?e, "error swapping asset"); + })? + } else { + burn_for_teleport::(&origin, &fee_asset) + .map_err(|_| Error::::BurnError)?; + fee_amount + }; + Ok(ether_gained) + } + + fn send_transact_call( + origin_location: Location, + call: BridgeHubRuntime, + ) -> DispatchResult { + let dest = T::BridgeHubLocation::get(); + let remote_xcm = Self::build_remote_xcm(&call); + let message_id = Self::send_xcm(origin_location, dest.clone(), remote_xcm.clone()) + .map_err(|error| Error::::from(error))?; + + Self::deposit_event(Event::::MessageSent { + origin: T::PalletLocation::get().into(), + destination: dest, + message: remote_xcm, + message_id, + }); + + Ok(()) + } } impl ExportPausedQuery for Pallet { diff --git a/bridges/snowbridge/pallets/system-frontend/src/mock.rs b/bridges/snowbridge/pallets/system-frontend/src/mock.rs index fd212ca2e5135..d1eb9400e6757 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/mock.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/mock.rs @@ -7,10 +7,15 @@ use frame_support::{ derive_impl, parameter_types, traits::{AsEnsureOriginWithArg, Everything}, }; +<<<<<<< HEAD +======= +use snowbridge_core::ParaId; +use snowbridge_test_utils::mock_swap_executor::SwapExecutor; +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) pub use snowbridge_test_utils::{mock_origin::pallet_xcm_origin, mock_xcm::*}; use sp_core::H256; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, AccountId32, BuildStorage, }; use xcm::prelude::*; @@ -72,6 +77,18 @@ parameter_types! { pub PalletLocation: InteriorLocation = [PalletInstance(36)].into(); } +pub struct AccountIdConverter; +impl xcm_executor::traits::ConvertLocation for AccountIdConverter { + fn convert_location(ml: &Location) -> Option { + match ml.unpack() { + (0, [Junction::AccountId32 { id, .. }]) => + Some(::decode(&mut &*id.to_vec()).unwrap()), + (1, [Parachain(id)]) => Some(ParaId::from(*id).into_account_truncating()), + _ => None, + } + } +} + impl crate::Config for Test { type RuntimeEvent = RuntimeEvent; type RegisterTokenOrigin = AsEnsureOriginWithArg>; @@ -86,6 +103,7 @@ impl crate::Config for Test { type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Helper = (); + type AccountIdConverter = AccountIdConverter; } // Build genesis storage according to the mock runtime. diff --git a/bridges/snowbridge/pallets/system-frontend/src/tests.rs b/bridges/snowbridge/pallets/system-frontend/src/tests.rs index 7031621125806..eb02d2e3ce2e8 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/tests.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/tests.rs @@ -23,10 +23,15 @@ fn register_token() { decimals: 12, }; + let ether_location = Ether::get(); + let fee_amount = 1000; + let asset = Asset::from((ether_location.clone(), fee_amount)); + assert_ok!(EthereumSystemFrontend::register_token( origin.clone(), asset_id.clone(), - asset_metadata.clone() + asset_metadata.clone(), + asset.clone(), )); }); } @@ -43,11 +48,14 @@ fn register_token_fails_delivery_fees_not_met() { symbol: "pal".as_bytes().to_vec().try_into().unwrap(), decimals: 12, }; + let ether_location = Ether::get(); + let fee_amount = 1000; + let asset = Asset::from((ether_location.clone(), fee_amount)); set_charge_fees_override(|_, _| Err(XcmError::FeesNotMet)); assert_err!( - EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata), + EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset.clone()), Error::::FeesNotMet, ); }); @@ -65,6 +73,9 @@ fn register_token_fails_unroutable() { symbol: "pal".as_bytes().to_vec().try_into().unwrap(), decimals: 12, }; + let ether_location = Ether::get(); + let fee_amount = 1000; + let asset = Asset::from((ether_location.clone(), fee_amount)); // Send XCM with overrides for `SendXcm` behavior to return `Unroutable` error on // validate @@ -76,7 +87,8 @@ fn register_token_fails_unroutable() { EthereumSystemFrontend::register_token( origin.clone(), asset_id.clone(), - asset_metadata.clone() + asset_metadata.clone(), + asset.clone(), ), Error::::SendFailure ); @@ -89,7 +101,7 @@ fn register_token_fails_unroutable() { ); assert_err!( - EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata), + EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset.clone()), Error::::SendFailure ); }); @@ -111,11 +123,15 @@ fn test_switch_operating_mode() { symbol: "pal".as_bytes().to_vec().try_into().unwrap(), decimals: 12, }; + let ether_location = Ether::get(); + let fee_amount = 1000; + let asset = Asset::from((ether_location.clone(), fee_amount)); assert_noop!( EthereumSystemFrontend::register_token( origin.clone(), asset_id.clone(), - asset_metadata.clone() + asset_metadata.clone(), + asset.clone(), ), crate::Error::::Halted ); @@ -123,6 +139,154 @@ fn test_switch_operating_mode() { RawOrigin::Root.into(), BasicOperatingMode::Normal, )); - assert_ok!(EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata),); + assert_ok!(EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset)); + }); +} +<<<<<<< HEAD +======= + +#[test] +fn add_tip_ether_asset_succeeds() { + new_test_ext().execute_with(|| { + let who: AccountId = Keyring::Alice.into(); + let message_id = MessageId::Inbound(1); + let ether_location = Ether::get(); + let tip_amount = 1000; + let asset = Asset::from((ether_location.clone(), tip_amount)); + + assert_ok!(EthereumSystemFrontend::add_tip( + RuntimeOrigin::signed(who.clone()), + message_id.clone(), + asset.clone() + )); + + let events = System::events(); + let event_record = events.last().expect("Expected at least one event").event.clone(); + + if !matches!( + event_record, + RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. }) + ) { + panic!("Expected MessageSent event, got: {:?}", event_record); + } + }); +} + +#[test] +fn add_tip_non_ether_asset_succeeds() { + new_test_ext().execute_with(|| { + let who: AccountId = Keyring::Alice.into(); + let message_id = MessageId::Outbound(2); + let non_ether_location = Location::new(1, [Parachain(3000)]); + let tip_amount = 2000; + let asset = Asset::from((non_ether_location.clone(), tip_amount)); + + assert_ok!(EthereumSystemFrontend::add_tip( + RuntimeOrigin::signed(who.clone()), + message_id.clone(), + asset.clone() + )); + + let events = System::events(); + let event_record = events.last().expect("Expected at least one event").event.clone(); + + if !matches!( + event_record, + RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. }) + ) { + panic!("Expected MessageSent event, got: {:?}", event_record); + } + }); +} + +#[test] +fn add_tip_unsupported_asset_fails() { + new_test_ext().execute_with(|| { + let who: AccountId = Keyring::Alice.into(); + let message_id = MessageId::Inbound(1); + let asset = Asset { + id: AssetId(Location::new(1, [Parachain(4000)])), + fun: Fungibility::NonFungible(AssetInstance::Array4([0u8; 4])), + }; + assert_noop!( + EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), + Error::::UnsupportedAsset + ); + }); +} + +#[test] +fn add_tip_send_xcm_failure() { + new_test_ext().execute_with(|| { + set_sender_override( + |_, _| Ok((Default::default(), Default::default())), + |_| Err(SendError::Unroutable), + ); + let who: AccountId = Keyring::Alice.into(); + let message_id = MessageId::Outbound(4); + let ether_location = Ether::get(); + let tip_amount = 3000; + let asset = Asset::from((ether_location.clone(), tip_amount)); + assert_noop!( + EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), + Error::::SendFailure + ); + }); +} + +#[test] +fn add_tip_origin_not_signed_fails() { + new_test_ext().execute_with(|| { + let message_id = MessageId::Inbound(5); + let ether_location = Ether::get(); + let tip_amount = 1500; + let asset = Asset::from((ether_location, tip_amount)); + assert_noop!( + EthereumSystemFrontend::add_tip(RuntimeOrigin::root(), message_id, asset), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn tip_fails_due_to_swap_error() { + new_test_ext().execute_with(|| { + let who: AccountId = Keyring::Alice.into(); + let message_id = MessageId::Inbound(6); + let non_ether_location = Location::new(1, [Parachain(3000)]); + // Use the special amount 12345 that will trigger a swap error in mock_swap_executor + let tip_amount = TRIGGER_SWAP_ERROR_AMOUNT; + let asset = Asset::from((non_ether_location.clone(), tip_amount)); + + assert_noop!( + EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), + Other("Swap failed for test") + ); + }); +} + +#[test] +fn register_token_with_non_ether_fee_asset_succeeds() { + new_test_ext().execute_with(|| { + let origin_location = Location::new(1, [Parachain(2000)]); + let origin = make_xcm_origin(origin_location.clone()); + let asset_location: Location = Location::new(1, [Parachain(2000), GeneralIndex(1)]); + let asset_id = Box::new(VersionedLocation::from(asset_location)); + let asset_metadata = AssetMetadata { + name: "pal".as_bytes().to_vec().try_into().unwrap(), + symbol: "pal".as_bytes().to_vec().try_into().unwrap(), + decimals: 12, + }; + + let fee_amount = 1000; + let fee_asset = Asset::from((Location::parent(), fee_amount)); + + assert_ok!(EthereumSystemFrontend::register_token( + origin.clone(), + asset_id.clone(), + asset_metadata.clone(), + fee_asset.clone(), + )); }); } +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) diff --git a/bridges/snowbridge/pallets/system-v2/src/benchmarking.rs b/bridges/snowbridge/pallets/system-v2/src/benchmarking.rs index b734755e3fc0f..bbbc784dcd725 100644 --- a/bridges/snowbridge/pallets/system-v2/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/system-v2/src/benchmarking.rs @@ -26,7 +26,7 @@ mod benchmarks { }; #[extrinsic_call] - _(origin as T::RuntimeOrigin, creator, asset, asset_metadata); + _(origin as T::RuntimeOrigin, creator, asset, asset_metadata, 1); Ok(()) } diff --git a/bridges/snowbridge/pallets/system-v2/src/lib.rs b/bridges/snowbridge/pallets/system-v2/src/lib.rs index 2d411c69197ee..29e88a4f0ccc4 100644 --- a/bridges/snowbridge/pallets/system-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/system-v2/src/lib.rs @@ -186,6 +186,7 @@ pub mod pallet { sender: Box, asset_id: Box, metadata: AssetMetadata, + amount: u128, ) -> DispatchResult { T::FrontendOrigin::ensure_origin(origin)?; @@ -210,7 +211,7 @@ pub mod pallet { }; let message_origin = Self::location_to_message_origin(sender_location)?; - Self::send(message_origin, command, 0)?; + Self::send(message_origin, command, amount)?; Self::deposit_event(Event::::RegisterToken { location: location.into(), diff --git a/bridges/snowbridge/pallets/system-v2/src/tests.rs b/bridges/snowbridge/pallets/system-v2/src/tests.rs index 53b80fa80f31b..621422ba0b596 100644 --- a/bridges/snowbridge/pallets/system-v2/src/tests.rs +++ b/bridges/snowbridge/pallets/system-v2/src/tests.rs @@ -16,6 +16,7 @@ fn register_tokens_succeeds() { Box::new(versioned_location.clone()), Box::new(versioned_location), Default::default(), + 1 )); }); } @@ -128,7 +129,8 @@ fn register_all_tokens_succeeds() { origin, Box::new(versioned_location.clone()), Box::new(versioned_location), - Default::default() + Default::default(), + 1 )); let reanchored_location = EthereumSystemV2::reanchor(tc.native.clone()).unwrap(); @@ -161,7 +163,8 @@ fn register_ethereum_native_token_fails() { origin, versioned_location.clone(), versioned_location.clone(), - Default::default() + Default::default(), + 1 ), Error::::LocationConversionFailed ); diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound.rs index e5eb1c450a36c..6b92b4a52c68e 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound.rs @@ -32,7 +32,7 @@ use xcm::v5::AssetTransferFilter; #[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)] pub enum EthereumSystemFrontendCall { #[codec(index = 1)] - RegisterToken { asset_id: Box, metadata: AssetMetadata }, + RegisterToken { asset_id: Box, metadata: AssetMetadata, fee_asset: Asset }, } #[allow(clippy::large_enum_variant)] @@ -131,6 +131,8 @@ pub fn register_relay_token_from_asset_hub_with_sudo() { AssetHubWestend::execute_with(|| { type RuntimeOrigin = ::RuntimeOrigin; + let fees_asset = Asset { id: AssetId(ethereum()), fun: Fungible(1) }; + assert_ok!( ::SnowbridgeSystemFrontend::register_token( RuntimeOrigin::root(), @@ -139,10 +141,19 @@ pub fn register_relay_token_from_asset_hub_with_sudo() { name: "wnd".as_bytes().to_vec().try_into().unwrap(), symbol: "wnd".as_bytes().to_vec().try_into().unwrap(), decimals: 12, - } + }, + fees_asset ) ); }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},] + ); + }); } #[test] @@ -150,8 +161,13 @@ pub fn register_usdt_from_owner_on_asset_hub() { fund_on_bh(); register_assets_on_ah(); fund_on_ah(); + set_up_eth_and_dot_pool(); AssetHubWestend::execute_with(|| { type RuntimeOrigin = ::RuntimeOrigin; + type RuntimeEvent = ::RuntimeEvent; + + let fees_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(1_000_000_000u128) }; assert_ok!( ::SnowbridgeSystemFrontend::register_token( @@ -161,9 +177,14 @@ pub fn register_usdt_from_owner_on_asset_hub() { name: "usdt".as_bytes().to_vec().try_into().unwrap(), symbol: "usdt".as_bytes().to_vec().try_into().unwrap(), decimals: 6, - } + }, + fees_asset ) ); + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::SwapExecuted { .. }) => {},] + ); }); BridgeHubWestend::execute_with(|| { @@ -589,6 +610,7 @@ fn register_token_from_penpal() { EthereumSystemFrontendCall::RegisterToken { asset_id: Box::new(VersionedLocation::from(foreign_asset_at_asset_hub)), metadata: Default::default(), + fee_asset: remote_fee_asset_on_ethereum.clone(), }, ); diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_edge_case.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_edge_case.rs index 001382ddf432f..3f1d06fdfc85e 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_edge_case.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_edge_case.rs @@ -66,6 +66,7 @@ fn register_penpal_a_asset_from_penpal_b_will_fail() { 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(), }, ); @@ -180,6 +181,9 @@ pub fn register_usdt_not_from_owner_on_asset_hub_will_fail() { AssetHubWestend::execute_with(|| { type RuntimeOrigin = ::RuntimeOrigin; + let fees_asset = + Asset { id: AssetId(ethereum()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + assert_noop!( ::SnowbridgeSystemFrontend::register_token( // The owner is Alice, while AssetHubWestendReceiver is Bob, so it should fail @@ -189,7 +193,8 @@ pub fn register_usdt_not_from_owner_on_asset_hub_will_fail() { name: "usdt".as_bytes().to_vec().try_into().unwrap(), symbol: "usdt".as_bytes().to_vec().try_into().unwrap(), decimals: 6, - } + }, + fees_asset ), BadOrigin ); @@ -204,6 +209,9 @@ pub fn register_relay_token_from_asset_hub_user_origin_will_fail() { AssetHubWestend::execute_with(|| { type RuntimeOrigin = ::RuntimeOrigin; + let fees_asset = + Asset { id: AssetId(ethereum()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + assert_noop!( ::SnowbridgeSystemFrontend::register_token( RuntimeOrigin::signed(AssetHubWestendSender::get()), @@ -213,6 +221,7 @@ pub fn register_relay_token_from_asset_hub_user_origin_will_fail() { symbol: "wnd".as_bytes().to_vec().try_into().unwrap(), decimals: 12, }, + fees_asset ), BadOrigin ); diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_from_rococo.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_from_rococo.rs index 47afad61952fe..a14762712d4b6 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_from_rococo.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_outbound_from_rococo.rs @@ -303,10 +303,13 @@ fn register_rococo_asset_on_ethereum_from_rah() { vec![], ); + let fee_asset = Asset { id: AssetId(ethereum()), fun: Fungible(REMOTE_FEE_AMOUNT_IN_ETHER) }; + let call = EthereumSystemFrontend::EthereumSystemFrontend(EthereumSystemFrontendCall::RegisterToken { asset_id: Box::new(VersionedLocation::from(bridged_asset_at_wah.clone())), metadata: Default::default(), + fee_asset, }) .encode(); diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs index 7d3f2346e1fa3..0cc6bdf1a7fcb 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs @@ -63,17 +63,77 @@ pub mod benchmark_helpers { 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(); + fn initialize_storage(asset_location: Location, asset_owner_location: Location) { + let asset_owner = LocationToAccountId::convert_location(&asset_owner_location).unwrap(); ForeignAssets::force_create( RuntimeOrigin::root(), asset_location, - asset_owner.into(), + asset_owner.clone().into(), + true, + 1, + ) + .unwrap(); + } +<<<<<<< HEAD +======= + + 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() + .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().into(), + 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.into(), + ) + .unwrap(); } +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } } @@ -120,7 +180,7 @@ impl snowbridge_pallet_system_frontend::Config for Runtime { #[cfg(not(feature = "runtime-benchmarks"))] type XcmSender = XcmRouter; #[cfg(feature = "runtime-benchmarks")] - type XcmSender = DoNothingRouter; + type XcmSender = benchmark_helpers::DoNothingRouter; type AssetTransactor = AssetTransactors; type EthereumLocation = FeeAsset; type XcmExecutor = XcmExecutor; @@ -128,4 +188,5 @@ impl snowbridge_pallet_system_frontend::Config for Runtime { type UniversalLocation = UniversalLocation; type PalletLocation = SystemFrontendPalletLocation; type BackendWeightInfo = weights::snowbridge_pallet_system_backend::WeightInfo; + type AccountIdConverter = xcm_config::LocationToAccountId; } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs index 4892f73159bb3..9a2c13a29db4e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs @@ -31,4 +31,33 @@ impl snowbridge_pallet_system_frontend::BackendWeightIn .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } +<<<<<<< HEAD +======= + + fn transact_add_tip() -> Weight { + Weight::from_parts(45_000_000, 6044) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn do_process_message() -> Weight { + Weight::from_parts(19_000_000, 0) + .saturating_add(Weight::from_parts(0, 1527)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(4)) + } + + fn commit_single() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(Weight::from_parts(0, 1620)) + .saturating_add(T::DbWeight::get().reads(1)) + } + + fn submit_delivery_receipt() -> Weight { + Weight::from_parts(68_000_000, 0) + .saturating_add(Weight::from_parts(0, 3785)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)) + } +>>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } diff --git a/prdoc/pr_8725.prdoc b/prdoc/pr_8725.prdoc new file mode 100644 index 0000000000000..0d1ecdef17d3e --- /dev/null +++ b/prdoc/pr_8725.prdoc @@ -0,0 +1,15 @@ +title: 'Snowbridge: register polkadot native asset with fee' +doc: +- audience: Runtime Dev + description: To enforce a fee for PNA registration. +crates: +- name: snowbridge-pallet-system-frontend + bump: patch + validate: false +- name: snowbridge-pallet-system-v2 + bump: patch + validate: false +- name: asset-hub-westend-runtime + bump: patch +- name: bridge-hub-westend-integration-tests + bump: none From dd8ed7fcef881f0400249b40986e68d4d1305ffa Mon Sep 17 00:00:00 2001 From: Ron Date: Wed, 11 Jun 2025 18:11:56 +0800 Subject: [PATCH 2/4] Resolve conflicts in #8797 (#8803) --- Cargo.lock | 2 + .../pallets/system-frontend/Cargo.toml | 6 +- .../system-frontend/src/backend_weights.rs | 10 -- .../system-frontend/src/benchmarking.rs | 2 +- .../pallets/system-frontend/src/lib.rs | 67 ++++----- .../pallets/system-frontend/src/mock.rs | 10 +- .../pallets/system-frontend/src/tests.rs | 127 +----------------- bridges/snowbridge/test-utils/Cargo.toml | 6 +- bridges/snowbridge/test-utils/src/lib.rs | 5 +- .../test-utils/src/mock_swap_executor.rs | 48 +++++++ .../src/bridge_to_ethereum_config.rs | 14 +- .../snowbridge_pallet_system_backend.rs | 9 -- 12 files changed, 103 insertions(+), 203 deletions(-) create mode 100644 bridges/snowbridge/test-utils/src/mock_swap_executor.rs diff --git a/Cargo.lock b/Cargo.lock index 21ef5b4ed5cad..b5d15f92cc1d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21709,6 +21709,7 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-asset-conversion", "pallet-balances", "pallet-xcm", "parity-scale-codec", @@ -21831,6 +21832,7 @@ dependencies = [ "frame-system", "hex", "log", + "pallet-asset-conversion", "pallet-xcm", "parity-scale-codec", "scale-info", diff --git a/bridges/snowbridge/pallets/system-frontend/Cargo.toml b/bridges/snowbridge/pallets/system-frontend/Cargo.toml index 3df6628be3a8a..e6a9ecb5095f9 100644 --- a/bridges/snowbridge/pallets/system-frontend/Cargo.toml +++ b/bridges/snowbridge/pallets/system-frontend/Cargo.toml @@ -25,6 +25,7 @@ frame-benchmarking = { optional = true, workspace = true } frame-support.workspace = true frame-system.workspace = true log = { workspace = true } +pallet-asset-conversion = { workspace = true } pallet-xcm.workspace = true scale-info = { features = ["derive"], workspace = true } snowbridge-core.workspace = true @@ -33,8 +34,8 @@ sp-io.workspace = true sp-runtime.workspace = true sp-std.workspace = true tracing = { workspace = true } -xcm-executor.workspace = true xcm.workspace = true +xcm-executor.workspace = true [dev-dependencies] pallet-balances = { default-features = true, workspace = true } @@ -48,6 +49,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-asset-conversion/std", "pallet-xcm/std", "scale-info/std", "snowbridge-core/std", @@ -63,6 +65,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-asset-conversion/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", @@ -74,6 +77,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-asset-conversion/try-runtime", "pallet-balances/try-runtime", "pallet-xcm/try-runtime", "sp-runtime/try-runtime", diff --git a/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs b/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs index 220e32ab71453..30b0a2f270580 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/backend_weights.rs @@ -9,24 +9,15 @@ pub trait BackendWeightInfo { /// Execution weight for remote xcm that dispatches `EthereumSystemCall::RegisterToken` /// using `Transact`. fn transact_register_token() -> Weight; -<<<<<<< HEAD -======= - fn transact_add_tip() -> Weight; fn do_process_message() -> Weight; fn commit_single() -> Weight; fn submit_delivery_receipt() -> Weight; ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } impl BackendWeightInfo for () { fn transact_register_token() -> Weight { Weight::from_parts(100_000_000, 10000) } -<<<<<<< HEAD -======= - fn transact_add_tip() -> Weight { - Weight::from_parts(100_000_000, 10000) - } fn do_process_message() -> Weight { Weight::from_parts(39_000_000, 3485) .saturating_add(RocksDbWeight::get().reads(4_u64)) @@ -44,5 +35,4 @@ impl BackendWeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2)) .saturating_add(RocksDbWeight::get().writes(2)) } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } diff --git a/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs b/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs index db46d1f96cd5c..d770fe1947f69 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/benchmarking.rs @@ -8,7 +8,7 @@ use frame_benchmarking::v2::*; use xcm::prelude::{Location, *}; use xcm_executor::traits::ConvertLocation; -#[benchmarks] +#[benchmarks(where ::AccountId: Into)] mod benchmarks { use super::*; #[benchmark] diff --git a/bridges/snowbridge/pallets/system-frontend/src/lib.rs b/bridges/snowbridge/pallets/system-frontend/src/lib.rs index f16d9203c0ca5..5d580b51dbd76 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/lib.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/lib.rs @@ -26,8 +26,10 @@ pub use backend_weights::*; use frame_support::{pallet_prelude::*, traits::EnsureOriginWithArg}; use frame_system::pallet_prelude::*; +use pallet_asset_conversion::Swap; use snowbridge_core::{ - operating_mode::ExportPausedQuery, AssetMetadata, BasicOperatingMode as OperatingMode, + burn_for_teleport, operating_mode::ExportPausedQuery, AssetMetadata, + BasicOperatingMode as OperatingMode, }; use sp_std::prelude::*; use xcm::{ @@ -40,7 +42,7 @@ use xcm_executor::traits::{FeeManager, FeeReason, TransactAsset}; use frame_support::traits::OriginTrait; pub use pallet::*; - +pub type AccountIdOf = ::AccountId; pub const LOG_TARGET: &str = "snowbridge-system-frontend"; /// Call indices within BridgeHub runtime for dispatchables within `snowbridge-pallet-system-v2` @@ -64,12 +66,13 @@ pub enum EthereumSystemCall { } #[cfg(feature = "runtime-benchmarks")] -pub trait BenchmarkHelper +pub trait BenchmarkHelper where O: OriginTrait, { fn make_xcm_origin(location: Location) -> O; fn initialize_storage(asset_location: Location, asset_owner: Location); + fn setup_pools(caller: AccountId, asset: Location); } #[frame_support::pallet] @@ -102,6 +105,9 @@ pub mod pallet { /// Fee asset for the execution cost on ethereum type EthereumLocation: Get; + /// To swap the provided tip asset for + type Swap: Swap; + /// Location of bridge hub type BridgeHubLocation: Get; @@ -121,7 +127,7 @@ pub mod pallet { /// A set of helper functions for benchmarking. #[cfg(feature = "runtime-benchmarks")] - type Helper: BenchmarkHelper; + type Helper: BenchmarkHelper; } #[pallet::event] @@ -155,6 +161,16 @@ pub mod pallet { /// The desired destination was unreachable, generally because there is a no way of routing /// to it. Unreachable, + /// The asset provided for the tip is unsupported. + UnsupportedAsset, + /// Unable to withdraw asset. + WithdrawError, + /// Account could not be converted to a location. + InvalidAccount, + /// Provided tip asset could not be swapped for ether. + SwapError, + /// Ether could not be burned. + BurnError, } impl From for Error { @@ -173,7 +189,10 @@ pub mod pallet { pub type ExportOperatingMode = StorageValue<_, OperatingMode, ValueQuery>; #[pallet::call] - impl Pallet { + impl Pallet + where + ::AccountId: Into, + { /// Set the operating mode for exporting messages to Ethereum. #[pallet::call_index(0)] #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] @@ -227,30 +246,6 @@ pub mod pallet { Self::send_transact_call(origin_location, call) } -<<<<<<< HEAD -======= - - /// Add an additional relayer tip for a committed message identified by `message_id`. - /// The tip asset will be swapped for ether. - #[pallet::call_index(2)] - #[pallet::weight( - T::WeightInfo::add_tip() - .saturating_add(T::BackendWeightInfo::transact_add_tip()) - )] - pub fn add_tip(origin: OriginFor, message_id: MessageId, asset: Asset) -> DispatchResult - where - ::AccountId: Into, - { - let who = ensure_signed(origin)?; - - let ether_gained = Self::swap_fee_asset_and_burn(who.clone().into(), asset)?; - - // Send the tip details to BH to be allocated to the reward in the Inbound/Outbound - // pallet - let call = Self::build_add_tip_call(who.clone(), message_id.clone(), ether_gained); - Self::send_transact_call(who.into(), call) - } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } impl Pallet { @@ -264,8 +259,6 @@ pub mod pallet { T::XcmSender::deliver(ticket) } -<<<<<<< HEAD -======= /// Swaps a specified tip asset to Ether and then burns the resulting ether for /// teleportation. Returns the amount of Ether gained if successful, or a DispatchError if /// any step fails. @@ -298,18 +291,13 @@ pub mod pallet { Ok(ether_gained) } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) // Build the call to dispatch the `EthereumSystem::register_token` extrinsic on BH fn build_register_token_call( sender: Location, asset: Location, metadata: AssetMetadata, -<<<<<<< HEAD - ) -> Result> { -======= amount: u128, - ) -> Result, Error> { ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) + ) -> Result> { // reanchor locations relative to BH let sender = Self::reanchored(sender)?; let asset = Self::reanchored(asset)?; @@ -377,10 +365,7 @@ pub mod pallet { Ok(ether_gained) } - fn send_transact_call( - origin_location: Location, - call: BridgeHubRuntime, - ) -> DispatchResult { + fn send_transact_call(origin_location: Location, call: BridgeHubRuntime) -> DispatchResult { let dest = T::BridgeHubLocation::get(); let remote_xcm = Self::build_remote_xcm(&call); let message_id = Self::send_xcm(origin_location, dest.clone(), remote_xcm.clone()) diff --git a/bridges/snowbridge/pallets/system-frontend/src/mock.rs b/bridges/snowbridge/pallets/system-frontend/src/mock.rs index d1eb9400e6757..d4f6562d6a33a 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/mock.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/mock.rs @@ -7,11 +7,8 @@ use frame_support::{ derive_impl, parameter_types, traits::{AsEnsureOriginWithArg, Everything}, }; -<<<<<<< HEAD -======= use snowbridge_core::ParaId; use snowbridge_test_utils::mock_swap_executor::SwapExecutor; ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) pub use snowbridge_test_utils::{mock_origin::pallet_xcm_origin, mock_xcm::*}; use sp_core::H256; use sp_runtime::{ @@ -21,7 +18,7 @@ use sp_runtime::{ use xcm::prelude::*; type Block = frame_system::mocking::MockBlock; -type AccountId = AccountId32; +pub type AccountId = AccountId32; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -55,12 +52,14 @@ impl pallet_xcm_origin::Config for Test { } #[cfg(feature = "runtime-benchmarks")] -impl BenchmarkHelper for () { +impl BenchmarkHelper for () { fn make_xcm_origin(location: Location) -> RuntimeOrigin { RuntimeOrigin::from(pallet_xcm_origin::Origin(location)) } fn initialize_storage(_: Location, _: Location) {} + + fn setup_pools(_: AccountId, _: Location) {} } parameter_types! { @@ -100,6 +99,7 @@ impl crate::Config for Test { type UniversalLocation = UniversalLocation; type PalletLocation = PalletLocation; type BackendWeightInfo = (); + type Swap = SwapExecutor; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Helper = (); diff --git a/bridges/snowbridge/pallets/system-frontend/src/tests.rs b/bridges/snowbridge/pallets/system-frontend/src/tests.rs index eb02d2e3ce2e8..5541f37e2bef9 100644 --- a/bridges/snowbridge/pallets/system-frontend/src/tests.rs +++ b/bridges/snowbridge/pallets/system-frontend/src/tests.rs @@ -5,8 +5,8 @@ use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::RawOrigin; use snowbridge_core::{AssetMetadata, BasicOperatingMode}; use xcm::{ - latest::{Assets, Error as XcmError, Location}, - prelude::{GeneralIndex, Parachain, SendError}, + latest::Error as XcmError, + prelude::{Asset, Assets, GeneralIndex, Location, Parachain, SendError}, VersionedLocation, }; @@ -142,128 +142,6 @@ fn test_switch_operating_mode() { assert_ok!(EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset)); }); } -<<<<<<< HEAD -======= - -#[test] -fn add_tip_ether_asset_succeeds() { - new_test_ext().execute_with(|| { - let who: AccountId = Keyring::Alice.into(); - let message_id = MessageId::Inbound(1); - let ether_location = Ether::get(); - let tip_amount = 1000; - let asset = Asset::from((ether_location.clone(), tip_amount)); - - assert_ok!(EthereumSystemFrontend::add_tip( - RuntimeOrigin::signed(who.clone()), - message_id.clone(), - asset.clone() - )); - - let events = System::events(); - let event_record = events.last().expect("Expected at least one event").event.clone(); - - if !matches!( - event_record, - RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. }) - ) { - panic!("Expected MessageSent event, got: {:?}", event_record); - } - }); -} - -#[test] -fn add_tip_non_ether_asset_succeeds() { - new_test_ext().execute_with(|| { - let who: AccountId = Keyring::Alice.into(); - let message_id = MessageId::Outbound(2); - let non_ether_location = Location::new(1, [Parachain(3000)]); - let tip_amount = 2000; - let asset = Asset::from((non_ether_location.clone(), tip_amount)); - - assert_ok!(EthereumSystemFrontend::add_tip( - RuntimeOrigin::signed(who.clone()), - message_id.clone(), - asset.clone() - )); - - let events = System::events(); - let event_record = events.last().expect("Expected at least one event").event.clone(); - - if !matches!( - event_record, - RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. }) - ) { - panic!("Expected MessageSent event, got: {:?}", event_record); - } - }); -} - -#[test] -fn add_tip_unsupported_asset_fails() { - new_test_ext().execute_with(|| { - let who: AccountId = Keyring::Alice.into(); - let message_id = MessageId::Inbound(1); - let asset = Asset { - id: AssetId(Location::new(1, [Parachain(4000)])), - fun: Fungibility::NonFungible(AssetInstance::Array4([0u8; 4])), - }; - assert_noop!( - EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), - Error::::UnsupportedAsset - ); - }); -} - -#[test] -fn add_tip_send_xcm_failure() { - new_test_ext().execute_with(|| { - set_sender_override( - |_, _| Ok((Default::default(), Default::default())), - |_| Err(SendError::Unroutable), - ); - let who: AccountId = Keyring::Alice.into(); - let message_id = MessageId::Outbound(4); - let ether_location = Ether::get(); - let tip_amount = 3000; - let asset = Asset::from((ether_location.clone(), tip_amount)); - assert_noop!( - EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), - Error::::SendFailure - ); - }); -} - -#[test] -fn add_tip_origin_not_signed_fails() { - new_test_ext().execute_with(|| { - let message_id = MessageId::Inbound(5); - let ether_location = Ether::get(); - let tip_amount = 1500; - let asset = Asset::from((ether_location, tip_amount)); - assert_noop!( - EthereumSystemFrontend::add_tip(RuntimeOrigin::root(), message_id, asset), - sp_runtime::DispatchError::BadOrigin - ); - }); -} - -#[test] -fn tip_fails_due_to_swap_error() { - new_test_ext().execute_with(|| { - let who: AccountId = Keyring::Alice.into(); - let message_id = MessageId::Inbound(6); - let non_ether_location = Location::new(1, [Parachain(3000)]); - // Use the special amount 12345 that will trigger a swap error in mock_swap_executor - let tip_amount = TRIGGER_SWAP_ERROR_AMOUNT; - let asset = Asset::from((non_ether_location.clone(), tip_amount)); - - assert_noop!( - EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset), - Other("Swap failed for test") - ); - }); -} #[test] fn register_token_with_non_ether_fee_asset_succeeds() { @@ -289,4 +167,3 @@ fn register_token_with_non_ether_fee_asset_succeeds() { )); }); } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) diff --git a/bridges/snowbridge/test-utils/Cargo.toml b/bridges/snowbridge/test-utils/Cargo.toml index 3adc7fedf84e7..4835072b2f070 100644 --- a/bridges/snowbridge/test-utils/Cargo.toml +++ b/bridges/snowbridge/test-utils/Cargo.toml @@ -23,6 +23,7 @@ frame-system.default-features = true frame-system.workspace = true hex = { workspace = true, default-features = true } log = { workspace = true, default-features = true } +pallet-asset-conversion = { workspace = true, default-features = true } pallet-xcm.default-features = true pallet-xcm.workspace = true scale-info = { features = ["derive"], workspace = true, default-features = true } @@ -34,18 +35,19 @@ sp-core.default-features = true sp-core.workspace = true sp-std.default-features = true sp-std.workspace = true +xcm.default-features = true +xcm.workspace = true xcm-builder.default-features = true xcm-builder.workspace = true xcm-executor.default-features = true xcm-executor.workspace = true -xcm.default-features = true -xcm.workspace = true [features] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-asset-conversion/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", "xcm-builder/runtime-benchmarks", diff --git a/bridges/snowbridge/test-utils/src/lib.rs b/bridges/snowbridge/test-utils/src/lib.rs index a62c76e39492a..fe6ef641b7a3f 100644 --- a/bridges/snowbridge/test-utils/src/lib.rs +++ b/bridges/snowbridge/test-utils/src/lib.rs @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork +pub mod mock_converter; pub mod mock_origin; - pub mod mock_outbound_queue; - -pub mod mock_converter; +pub mod mock_swap_executor; pub mod mock_xcm; diff --git a/bridges/snowbridge/test-utils/src/mock_swap_executor.rs b/bridges/snowbridge/test-utils/src/mock_swap_executor.rs new file mode 100644 index 0000000000000..0ad4f35facb3c --- /dev/null +++ b/bridges/snowbridge/test-utils/src/mock_swap_executor.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork + +use frame_support::pallet_prelude::DispatchError; +use pallet_asset_conversion::Swap; +use xcm::opaque::latest::Location; +pub struct SwapExecutor; + +pub const TRIGGER_SWAP_ERROR_AMOUNT: u128 = 12345; + +impl Swap for SwapExecutor +where + AccountId: AsRef<[u8; 32]>, +{ + type Balance = u128; + type AssetKind = Location; + + fn max_path_len() -> u32 { + 2 + } + + fn swap_exact_tokens_for_tokens( + _sender: AccountId, + _path: Vec, + amount_in: Self::Balance, + _amount_out_min: Option, + _send_to: AccountId, + _keep_alive: bool, + ) -> Result { + // Special case for testing SwapError: + // If amount_in is exactly 12345, return an error + if amount_in == TRIGGER_SWAP_ERROR_AMOUNT { + return Err(DispatchError::Other("Swap failed for test")); + } + Ok(1_000_000_000u128) + } + + fn swap_tokens_for_exact_tokens( + _sender: AccountId, + _path: Vec, + _amount_out: Self::Balance, + _amount_in_max: Option, + _send_to: AccountId, + _keep_alive: bool, + ) -> Result { + unimplemented!() + } +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs index 0cc6bdf1a7fcb..c7532af65a324 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs @@ -19,7 +19,7 @@ use crate::{ AssetTransactors, LocationToAccountId, TrustBackedAssetsPalletLocation, UniversalLocation, XcmConfig, }, - AccountId, Assets, ForeignAssets, Runtime, RuntimeEvent, + AccountId, AssetConversion, Assets, ForeignAssets, Runtime, RuntimeEvent, }; use assets_common::{matching::FromSiblingParachain, AssetIdForTrustBackedAssetsConvert}; use frame_support::{parameter_types, traits::EitherOf}; @@ -37,7 +37,11 @@ use benchmark_helpers::DoNothingRouter; #[cfg(feature = "runtime-benchmarks")] pub mod benchmark_helpers { - use crate::{xcm_config::LocationToAccountId, ForeignAssets, RuntimeOrigin}; + 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; @@ -58,7 +62,7 @@ pub mod benchmark_helpers { } } - impl snowbridge_pallet_system_frontend::BenchmarkHelper for () { + impl snowbridge_pallet_system_frontend::BenchmarkHelper for () { fn make_xcm_origin(location: Location) -> RuntimeOrigin { RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location)) } @@ -74,8 +78,6 @@ pub mod benchmark_helpers { ) .unwrap(); } -<<<<<<< HEAD -======= fn setup_pools(caller: AccountId, asset: Location) { // Prefund the caller's account with DOT @@ -133,7 +135,6 @@ pub mod benchmark_helpers { ) .unwrap(); } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } } @@ -187,6 +188,7 @@ impl snowbridge_pallet_system_frontend::Config for Runtime { type BridgeHubLocation = BridgeHubLocation; type UniversalLocation = UniversalLocation; type PalletLocation = SystemFrontendPalletLocation; + type Swap = AssetConversion; type BackendWeightInfo = weights::snowbridge_pallet_system_backend::WeightInfo; type AccountIdConverter = xcm_config::LocationToAccountId; } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs index 9a2c13a29db4e..213e50b816f2b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/snowbridge_pallet_system_backend.rs @@ -31,14 +31,6 @@ impl snowbridge_pallet_system_frontend::BackendWeightIn .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } -<<<<<<< HEAD -======= - - fn transact_add_tip() -> Weight { - Weight::from_parts(45_000_000, 6044) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)) - } fn do_process_message() -> Weight { Weight::from_parts(19_000_000, 0) @@ -59,5 +51,4 @@ impl snowbridge_pallet_system_frontend::BackendWeightIn .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(1)) } ->>>>>>> 36c3039 (Snowbridge: enforce fee when registering Polkadot native asset (#8725)) } From 9801e934914764a77b02a6a8366422e6cebdb435 Mon Sep 17 00:00:00 2001 From: Adrian Catangiu Date: Wed, 11 Jun 2025 13:58:00 +0300 Subject: [PATCH 3/4] Update bridge_to_ethereum_config.rs --- .../assets/asset-hub-westend/src/bridge_to_ethereum_config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs index c7532af65a324..3a869af272817 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/bridge_to_ethereum_config.rs @@ -32,8 +32,6 @@ use xcm_executor::XcmExecutor; #[cfg(not(feature = "runtime-benchmarks"))] use crate::xcm_config::XcmRouter; -#[cfg(feature = "runtime-benchmarks")] -use benchmark_helpers::DoNothingRouter; #[cfg(feature = "runtime-benchmarks")] pub mod benchmark_helpers { From cda4df301c7f4b959ad3c83b5990da9677c0b404 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:58:41 +0000 Subject: [PATCH 4/4] Update from github-actions[bot] running command 'fmt' --- bridges/snowbridge/pallets/system-frontend/Cargo.toml | 2 +- bridges/snowbridge/test-utils/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bridges/snowbridge/pallets/system-frontend/Cargo.toml b/bridges/snowbridge/pallets/system-frontend/Cargo.toml index e6a9ecb5095f9..67288a4e0a271 100644 --- a/bridges/snowbridge/pallets/system-frontend/Cargo.toml +++ b/bridges/snowbridge/pallets/system-frontend/Cargo.toml @@ -34,8 +34,8 @@ sp-io.workspace = true sp-runtime.workspace = true sp-std.workspace = true tracing = { workspace = true } -xcm.workspace = true xcm-executor.workspace = true +xcm.workspace = true [dev-dependencies] pallet-balances = { default-features = true, workspace = true } diff --git a/bridges/snowbridge/test-utils/Cargo.toml b/bridges/snowbridge/test-utils/Cargo.toml index 4835072b2f070..cf4bb2cf0e6fe 100644 --- a/bridges/snowbridge/test-utils/Cargo.toml +++ b/bridges/snowbridge/test-utils/Cargo.toml @@ -35,12 +35,12 @@ sp-core.default-features = true sp-core.workspace = true sp-std.default-features = true sp-std.workspace = true -xcm.default-features = true -xcm.workspace = true xcm-builder.default-features = true xcm-builder.workspace = true xcm-executor.default-features = true xcm-executor.workspace = true +xcm.default-features = true +xcm.workspace = true [features] runtime-benchmarks = [