diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs index ddc36ce8cb61..90cec1ad8590 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs @@ -201,7 +201,7 @@ impl<'a, Call> XcmConverter<'a, Call> { let _ = self.next(); } - // Get the fee asset item from BuyExecution or continue parsing. + // Todo: Get the fee asset item from BuyExecution and verify against remote_fee. let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees); if fee_asset.is_some() { let _ = self.next(); @@ -259,6 +259,13 @@ impl<'a, Call> XcmConverter<'a, Call> { // transfer amount must be greater than 0. ensure!(amount > 0, ZeroAssetTransfer); + // Todo: Get the fee asset item from BurnAsset and verify against local_fee. + let local_assets = + match_expression!(self.peek(), Ok(BurnAsset(local_assets)), Some(local_assets)); + if local_assets.is_some() { + let _ = self.next(); + } + // Check if there is a SetTopic and skip over it if found. let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?; diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs index d2820f69441f..cb4cad4c1067 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -380,7 +380,6 @@ fn send_token_from_ethereum_to_penpal() { /// - returning the token to Ethereum #[test] fn send_weth_asset_from_asset_hub_to_ethereum() { - use asset_hub_rococo_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(Location::new( 1, [Parachain(AssetHubRococo::para_id().into())], @@ -397,6 +396,9 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { AssetHubRococo::fund_accounts(vec![(AssetHubRococoReceiver::get(), INITIAL_FUND)]); const WETH_AMOUNT: u128 = 1_000_000_000; + const WETH_FEE_AMOUNT: u128 = 1_000_000_000; + + const RELAY_TOKEN_FEE_AMOUNT: u128 = 1_000_000_000; BridgeHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; @@ -445,7 +447,19 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { )), fun: Fungible(WETH_AMOUNT), }]; - let multi_assets = VersionedAssets::V4(Assets::from(assets)); + let fees = vec![ + Asset { id: AssetId(Location::parent()), fun: Fungible(RELAY_TOKEN_FEE_AMOUNT) }, + Asset { + id: AssetId(Location::new( + 2, + [ + GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), + AccountKey20 { network: None, key: WETH }, + ], + )), + fun: Fungible(WETH_FEE_AMOUNT), + }, + ]; let destination = VersionedLocation::V4(Location::new( 2, @@ -461,12 +475,12 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { AssetHubRococoReceiver::get(), ); // Send the Weth back to Ethereum - ::PolkadotXcm::reserve_transfer_assets( + ::PolkadotXcm::reserve_transfer_ethereum_assets( RuntimeOrigin::signed(AssetHubRococoReceiver::get()), Box::new(destination), Box::new(beneficiary), - Box::new(multi_assets), - 0, + Box::new(VersionedAssets::V4(Assets::from(assets))), + Box::new(VersionedAssets::V4(Assets::from(fees))), ) .unwrap(); let free_balance_after = ::Balances::free_balance( @@ -474,7 +488,7 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { ); // Assert at least DefaultBridgeHubEthereumBaseFee charged from the sender let free_balance_diff = free_balance_before - free_balance_after; - assert!(free_balance_diff > DefaultBridgeHubEthereumBaseFee::get()); + assert!(free_balance_diff > 0); }); BridgeHubRococo::execute_with(|| { @@ -487,25 +501,6 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued {..}) => {}, ] ); - let events = BridgeHubRococo::events(); - // Check that the local fee was credited to the Snowbridge sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == TREASURY_ACCOUNT.into() && *amount == 16903333 - )), - "Snowbridge sovereign takes local fee." - ); - // Check that the remote fee was credited to the AssetHub sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == assethub_sovereign && *amount == 2680000000000, - )), - "AssetHub sovereign takes remote fee." - ); }); } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index 47c3ed368888..8ddd7ef68617 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -57,10 +57,9 @@ use xcm_builder::{ IsConcrete, LocalMint, NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, - SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, - StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, - WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, - XcmFeeToAccount, + SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, + TrailingSetTopicAsId, UnpaidRemoteExporter, UsingComponents, WeightInfoBounds, + WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, XcmFeeToAccount, }; use xcm_executor::{traits::WithOriginFilter, XcmExecutor}; @@ -657,7 +656,7 @@ pub type XcmRouter = WithUniqueTopic<( ToWestendXcmRouter, // Router which wraps and sends xcm to BridgeHub to be delivered to the Ethereum // GlobalConsensus - SovereignPaidRemoteExporter, + UnpaidRemoteExporter, )>; impl pallet_xcm::Config for Runtime { @@ -864,10 +863,7 @@ pub mod bridging { EthereumNetwork::get(), Some(sp_std::vec![Junctions::Here]), SiblingBridgeHub::get(), - Some(( - XcmBridgeHubRouterFeeAssetId::get(), - BridgeHubEthereumBaseFee::get(), - ).into()) + None, ), ]; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs index 8934ff9b2272..64911b9b5c28 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs @@ -48,7 +48,6 @@ use parachains_common::{ }; use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; -use snowbridge_runtime_common::XcmExportFeeToSibling; use sp_core::Get; use sp_runtime::traits::AccountIdConversion; use sp_std::marker::PhantomData; @@ -61,7 +60,8 @@ use xcm_builder::{ FungibleAdapter, HandleFee, IsConcrete, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, - UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeToAccount, + UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, + XcmFeeManagerFromComponents, XcmFeeToAccount, }; use xcm_executor::{ traits::{FeeManager, FeeReason, FeeReason::Export, TransactAsset, WithOriginFilter}, @@ -79,6 +79,7 @@ parameter_types! { pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating(); pub RelayTreasuryLocation: Location = (Parent, PalletInstance(rococo_runtime_constants::TREASURY_PALLET_ID)).into(); pub SiblingPeople: Location = (Parent, Parachain(rococo_runtime_constants::system_parachain::PEOPLE_ID)).into(); + pub SiblingAssetHub: Location = (Parent, Parachain(rococo_runtime_constants::system_parachain::ASSET_HUB_ID)).into(); } /// Type for specifying how a `Location` can be converted into an `AccountId`. This is used @@ -255,6 +256,7 @@ pub type Barrier = TrailingSetTopicAsId< ParentOrParentsPlurality, Equals, Equals, + Equals, )>, // Subscriptions for version tracking are OK. AllowSubscriptionsFrom, @@ -305,26 +307,9 @@ impl xcm_executor::Config for XcmConfig { type SubscriptionService = PolkadotXcm; type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; - type FeeManager = XcmFeeManagerFromComponentsBridgeHub< + type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, - ( - XcmExportFeeToRelayerRewardAccounts< - Self::AssetTransactor, - crate::bridge_to_westend_config::WestendGlobalConsensusNetwork, - crate::bridge_to_westend_config::AssetHubWestendParaId, - crate::bridge_to_westend_config::BridgeHubWestendChainId, - crate::bridge_to_westend_config::AssetHubRococoToAssetHubWestendMessagesLane, - >, - XcmExportFeeToSibling< - bp_rococo::Balance, - AccountId, - TokenLocation, - EthereumNetwork, - Self::AssetTransactor, - crate::EthereumOutboundQueue, - >, - XcmFeeToAccount, - ), + XcmFeeToAccount, >; type MessageExporter = ( crate::bridge_to_westend_config::ToBridgeHubWestendHaulBlobExporter, diff --git a/polkadot/xcm/pallet-xcm/src/ethereum.rs b/polkadot/xcm/pallet-xcm/src/ethereum.rs new file mode 100644 index 000000000000..227e9ae68caf --- /dev/null +++ b/polkadot/xcm/pallet-xcm/src/ethereum.rs @@ -0,0 +1,198 @@ +use crate::{ + vec, Box, Config, Error, Event, FeesHandling, Instruction::BurnAsset, Pallet, Vec, Xcm, +}; +use codec::Encode; +use frame_support::{ + dispatch::DispatchResult, + ensure, + traits::{Contains, EnsureOrigin}, +}; +use frame_system::pallet_prelude::OriginFor; +use sp_runtime::traits::Zero; +use xcm::{ + latest::{validate_send, Asset, Assets, Location, WeightLimit}, + prelude::{Fungible, Here}, + v4::{ExecuteXcm, SendXcm}, + VersionedAssets, VersionedLocation, +}; +use xcm_executor::traits::{TransferType, WeightBounds, XcmAssetTransfers}; + +impl Pallet { + pub fn do_reserve_transfer_ethereum_assets( + origin: OriginFor, + dest: Box, + beneficiary: Box, + assets: Box, + fees: Box, + weight_limit: WeightLimit, + ) -> DispatchResult { + let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?; + let dest = (*dest).try_into().map_err(|()| Error::::BadVersion)?; + let beneficiary: Location = + (*beneficiary).try_into().map_err(|()| Error::::BadVersion)?; + let assets: Assets = (*assets).try_into().map_err(|()| Error::::BadVersion)?; + log::debug!( + target: "xcm::pallet_xcm::do_reserve_transfer_assets", + "origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, fees {:?}", + origin_location, dest, beneficiary, assets, fees, + ); + + let value = (origin_location, assets.into_inner()); + ensure!(T::XcmReserveTransferFilter::contains(&value), Error::::Filtered); + let (origin, assets) = value; + + let fees: Assets = (*fees).try_into().map_err(|()| Error::::BadVersion)?; + ensure!(fees.len() == 2, Error::::FeesNotMet); + let fee_on_ethereum = fees.get(1).unwrap(); + // Find transfer types for fee and non-fee assets. + let (fees_transfer_type, assets_transfer_type) = + Self::find_ethereum_assets_transfer_types(&assets, fee_on_ethereum, &dest)?; + // Ensure assets (and fees according to check below) are not teleportable to `dest`. + ensure!(assets_transfer_type != TransferType::Teleport, Error::::Filtered); + // Ensure all assets (including fees) have same reserve location. + ensure!(assets_transfer_type == fees_transfer_type, Error::::TooManyReserves); + let (local_xcm, remote_xcm) = Self::build_ethereum_xcm_transfer( + origin.clone(), + dest.clone(), + beneficiary, + assets, + assets_transfer_type, + fees.clone().into_inner(), + weight_limit, + )?; + + Self::execute_ethereum_xcm_transfer(origin, dest, fees, local_xcm, remote_xcm) + } + + fn find_ethereum_assets_transfer_types( + assets: &[Asset], + fee: &Asset, + dest: &Location, + ) -> Result<(TransferType, TransferType), Error> { + let mut assets_transfer_type = None; + let fee_transfer_type = + T::XcmExecutor::determine_for(fee, dest).map_err(Error::::from)?; + + for (_, asset) in assets.iter().enumerate() { + if let Fungible(x) = asset.fun { + // If fungible asset, ensure non-zero amount. + ensure!(!x.is_zero(), Error::::Empty); + } + let transfer_type = + T::XcmExecutor::determine_for(&asset, dest).map_err(Error::::from)?; + if let Some(existing) = assets_transfer_type.as_ref() { + // Ensure transfer for multiple assets uses same transfer type (only fee may + // have different transfer type/path) + ensure!(existing == &transfer_type, Error::::TooManyReserves); + } else { + // asset reserve identified + assets_transfer_type = Some(transfer_type); + } + } + Ok((fee_transfer_type, assets_transfer_type.ok_or(Error::::Empty)?)) + } + + fn build_ethereum_xcm_transfer( + origin: Location, + dest: Location, + beneficiary: Location, + assets: Vec, + transfer_type: TransferType, + fees: Vec, + weight_limit: WeightLimit, + ) -> Result<(Xcm<::RuntimeCall>, Option>), Error> { + log::debug!( + target: "xcm::pallet_xcm::build_xcm_transfer_type", + "origin {:?}, dest {:?}, beneficiary {:?}, assets {:?}, transfer_type {:?}, \ + fees_handling {:?}, weight_limit: {:?}", + origin, dest, beneficiary, assets, transfer_type, fees, weight_limit, + ); + let fee_on_bridgehub = &fees[0]; + let fee_on_ethereum = &fees[1]; + let (local, mut remote) = match transfer_type { + TransferType::LocalReserve => { + let (local, remote) = Self::local_reserve_transfer_programs( + origin.clone(), + dest.clone(), + beneficiary, + assets, + FeesHandling::Batched { fees: fee_on_ethereum.clone() }, + weight_limit, + )?; + Some((local, remote)) + }, + TransferType::DestinationReserve => { + let (local, remote) = Self::destination_reserve_transfer_programs( + origin.clone(), + dest.clone(), + beneficiary, + assets, + FeesHandling::Batched { fees: fee_on_ethereum.clone() }, + weight_limit, + )?; + Some((local, remote)) + }, + _ => None, + } + .ok_or(Error::InvalidAssetUnsupportedReserve)?; + remote.inner_mut().push(BurnAsset(fee_on_bridgehub.clone().into())); + Ok((local, Some(remote))) + } + + fn execute_ethereum_xcm_transfer( + origin: Location, + dest: Location, + fees: Assets, + mut local_xcm: Xcm<::RuntimeCall>, + remote_xcm: Option>, + ) -> DispatchResult { + log::debug!( + target: "xcm::pallet_xcm::execute_xcm_transfer", + "origin {:?}, dest {:?}, local_xcm {:?}, remote_xcm {:?}", + origin, dest, local_xcm, remote_xcm, + ); + + let weight = + T::Weigher::weight(&mut local_xcm).map_err(|()| Error::::UnweighableMessage)?; + let mut hash = local_xcm.using_encoded(sp_io::hashing::blake2_256); + let outcome = T::XcmExecutor::prepare_and_execute( + origin.clone(), + local_xcm, + &mut hash, + weight, + weight, + ); + Self::deposit_event(Event::Attempted { outcome: outcome.clone() }); + outcome.ensure_complete().map_err(|error| { + log::error!( + target: "xcm::pallet_xcm::execute_xcm_transfer", + "XCM execution failed with error {:?}", error + ); + Error::::LocalExecutionIncomplete + })?; + + if let Some(remote_xcm) = remote_xcm { + let (ticket, price) = validate_send::(dest.clone(), remote_xcm.clone()) + .map_err(Error::::from)?; + if origin != Here.into_location() { + let total_fees: Assets = vec![fees, price] + .iter() + .flat_map(|s| s.clone().into_inner()) + .collect::>() + .into(); + Self::charge_fees(origin.clone(), total_fees).map_err(|error| { + log::error!( + target: "xcm::pallet_xcm::execute_xcm_transfer", + "Unable to charge fee with error {:?}", error + ); + Error::::FeesNotMet + })?; + } + let message_id = T::XcmRouter::deliver(ticket).map_err(Error::::from)?; + + let e = Event::Sent { origin, destination: dest, message: remote_xcm, message_id }; + Self::deposit_event(e); + } + Ok(()) + } +} diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs index 29b61988f73c..e2843dcae74b 100644 --- a/polkadot/xcm/pallet-xcm/src/lib.rs +++ b/polkadot/xcm/pallet-xcm/src/lib.rs @@ -25,6 +25,7 @@ mod mock; #[cfg(test)] mod tests; +mod ethereum; pub mod migration; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; @@ -1544,6 +1545,69 @@ pub mod pallet { >::send_blob(origin, dest, encoded_message)?; Ok(()) } + + /// Transfer some assets from the local chain to the destination chain through their local, + /// destination or remote reserve. + /// + /// `assets` must have same reserve location and may not be teleportable to `dest`. + /// - `assets` have local reserve: transfer assets to sovereign account of destination + /// chain and forward a notification XCM to `dest` to mint and deposit reserve-based + /// assets to `beneficiary`. + /// - `assets` have destination reserve: burn local assets and forward a notification to + /// `dest` chain to withdraw the reserve assets from this chain's sovereign account and + /// deposit them to `beneficiary`. + /// - `assets` have remote reserve: burn local assets, forward XCM to reserve chain to move + /// reserves from this chain's SA to `dest` chain's SA, and forward another XCM to `dest` + /// to mint and deposit reserve-based assets to `beneficiary`. + /// + /// **This function is deprecated: Use `limited_reserve_transfer_assets` instead.** + /// + /// Fee payment on the destination side is made from the asset in the `assets` vector of + /// index `fee_asset_item`. The weight limit for fees is not provided and thus is unlimited, + /// with all fees taken as needed from the asset. + /// + /// - `origin`: Must be capable of withdrawing the `assets` and executing XCM. + /// - `dest`: Destination context for the assets. Will typically be `[Parent, + /// Parachain(..)]` to send from parachain to parachain, or `[Parachain(..)]` to send from + /// relay to parachain. + /// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will + /// generally be an `AccountId32` value. + /// - `assets`: The assets to be withdrawn. + /// - `fees`: The assets used to pay fees. + #[pallet::call_index(15)] + #[pallet::weight({ + let maybe_assets: Result = (*assets.clone()).try_into(); + let maybe_dest: Result = (*dest.clone()).try_into(); + match (maybe_assets, maybe_dest) { + (Ok(assets), Ok(dest)) => { + use sp_std::vec; + // heaviest version of locally executed XCM program: equivalent in weight to + // transfer assets to SA, reanchor them, extend XCM program, and send onward XCM + let mut message = Xcm(vec![ + SetFeesMode { jit_withdraw: true }, + TransferReserveAsset { assets, dest, xcm: Xcm(vec![]) } + ]); + T::Weigher::weight(&mut message).map_or(Weight::MAX, |w| T::WeightInfo::reserve_transfer_assets().saturating_add(w)) + } + _ => Weight::MAX, + } + })] + pub fn reserve_transfer_ethereum_assets( + origin: OriginFor, + dest: Box, + beneficiary: Box, + assets: Box, + fees: Box, + ) -> DispatchResult { + Self::do_reserve_transfer_ethereum_assets( + origin, + dest, + beneficiary, + assets, + fees, + Unlimited, + ) + } } }