diff --git a/Cargo.lock b/Cargo.lock index 85986af3674ae..ef2b4ee085d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5561,6 +5561,7 @@ dependencies = [ "sp-keyring", "sp-runtime 40.1.0", "staging-xcm", + "staging-xcm-executor", "xcm-emulator", "xcm-simulator", ] @@ -13303,6 +13304,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex-literal", "pallet-assets", "pallet-balances", "parity-scale-codec", diff --git a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml index 2473ad9261bb8..3b018098427b1 100644 --- a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml @@ -43,6 +43,7 @@ polkadot-runtime-parachains.default-features = true xcm.workspace = true xcm.default-features = true pallet-xcm = { features = ["test-utils"], workspace = true, default-features = true } +xcm-executor = { workspace = true, default-features = true } parachains-common.workspace = true parachains-common.default-features = true cumulus-primitives-core.workspace = true diff --git a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs index 9b28ce418648e..3f1c8ac7d9c50 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs @@ -26,12 +26,15 @@ pub use pallet_xcm; pub use xcm::{ prelude::{ AccountId32, All, Asset, AssetId, BuyExecution, DepositAsset, ExpectTransactStatus, - Fungible, Here, Location, MaybeErrorCode, OriginKind, RefundSurplus, Transact, Unlimited, - VersionedAssets, VersionedXcm, WeightLimit, WithdrawAsset, Xcm, + Fungible, Here, Junction, Location, MaybeErrorCode, OriginKind, Parent, RefundSurplus, + Transact, Unlimited, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, + WeightLimit, WithdrawAsset, Xcm, }, v3::Location as V3Location, }; +pub use xcm_executor::traits::TransferType; + // Cumulus pub use asset_test_utils; pub use cumulus_pallet_xcmp_queue; @@ -653,17 +656,21 @@ macro_rules! test_dry_run_transfer_across_pk_bridge { // Give some initial funds. >::set_balance(&who, initial_balance); - let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::transfer_assets { - dest: Box::new(VersionedLocation::from($destination)), - beneficiary: Box::new(VersionedLocation::from(Junction::AccountId32 { + let beneficiary: $crate::macros::Location = $crate::macros::Junction::AccountId32 { id: who.clone().into(), network: None, - })), - assets: Box::new(VersionedAssets::from(vec![ - (Parent, transfer_amount).into(), + }.into(); + + let call = RuntimeCall::PolkadotXcm($crate::macros::pallet_xcm::Call::transfer_assets_using_type_and_then { + dest: Box::new($crate::macros::VersionedLocation::from($destination)), + assets: Box::new($crate::macros::VersionedAssets::from(vec![ + ($crate::macros::Parent, transfer_amount).into(), ])), - fee_asset_item: 0, - weight_limit: Unlimited, + assets_transfer_type: Box::new($crate::macros::TransferType::LocalReserve), + remote_fees_id: Box::new($crate::macros::VersionedAssetId::from($crate::macros::Parent)), + fees_transfer_type: Box::new($crate::macros::TransferType::LocalReserve), + custom_xcm_on_dest: Box::new($crate::macros::VersionedXcm::<()>::from($crate::macros::Xcm::<()>::builder_unsafe().deposit_asset(AllCounted(1), beneficiary).build())), + weight_limit: $crate::macros::Unlimited, }); let result = Runtime::dry_run_call(OriginCaller::system(RawOrigin::Signed(who)), call, XCM_VERSION).unwrap(); // We assert the dry run succeeds and sends only one message to the local bridge hub. diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs index 7fde929c0dcbe..6e9b050f72581 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/teleport.rs @@ -179,23 +179,57 @@ fn system_para_limited_teleport_assets(t: SystemParaToRelayTest) -> DispatchResu } fn para_to_system_para_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult { - ::PolkadotXcm::transfer_assets( + type Runtime = ::Runtime; + let remote_fee_id: AssetId = t + .args + .assets + .clone() + .into_inner() + .get(t.args.fee_asset_item as usize) + .ok_or(pallet_xcm::Error::::Empty)? + .clone() + .id; + + ::PolkadotXcm::transfer_assets_using_type_and_then( t.signed_origin, bx!(t.args.dest.into()), - bx!(t.args.beneficiary.into()), bx!(t.args.assets.into()), - t.args.fee_asset_item, + bx!(TransferType::Teleport), + bx!(remote_fee_id.into()), + bx!(TransferType::DestinationReserve), + bx!(VersionedXcm::from( + Xcm::<()>::builder_unsafe() + .deposit_asset(AllCounted(2), t.args.beneficiary) + .build() + )), t.args.weight_limit, ) } fn system_para_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult { - ::PolkadotXcm::transfer_assets( + type Runtime = ::Runtime; + let remote_fee_id: AssetId = t + .args + .assets + .clone() + .into_inner() + .get(t.args.fee_asset_item as usize) + .ok_or(pallet_xcm::Error::::Empty)? + .clone() + .id; + + ::PolkadotXcm::transfer_assets_using_type_and_then( t.signed_origin, bx!(t.args.dest.into()), - bx!(t.args.beneficiary.into()), bx!(t.args.assets.into()), - t.args.fee_asset_item, + bx!(TransferType::Teleport), + bx!(remote_fee_id.into()), + bx!(TransferType::LocalReserve), + bx!(VersionedXcm::from( + Xcm::<()>::builder_unsafe() + .deposit_asset(AllCounted(2), t.args.beneficiary) + .build() + )), t.args.weight_limit, ) } diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs index 55c3b9ed27393..6f304bb45603e 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs @@ -24,6 +24,7 @@ mod set_xcm_versions; mod swap; mod teleport; mod transact; +mod transfer_assets_validation; mod treasury; mod xcm_fee_estimation; diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs index 0897c187e7cbc..172bfbd482f1b 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/teleport.rs @@ -169,23 +169,57 @@ fn system_para_limited_teleport_assets(t: SystemParaToRelayTest) -> DispatchResu } fn para_to_system_para_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult { - ::PolkadotXcm::transfer_assets( + type Runtime = ::Runtime; + let remote_fee_id: AssetId = t + .args + .assets + .clone() + .into_inner() + .get(t.args.fee_asset_item as usize) + .ok_or(pallet_xcm::Error::::Empty)? + .clone() + .id; + + ::PolkadotXcm::transfer_assets_using_type_and_then( t.signed_origin, bx!(t.args.dest.into()), - bx!(t.args.beneficiary.into()), bx!(t.args.assets.into()), - t.args.fee_asset_item, + bx!(TransferType::Teleport), + bx!(remote_fee_id.into()), + bx!(TransferType::DestinationReserve), + bx!(VersionedXcm::from( + Xcm::<()>::builder_unsafe() + .deposit_asset(AllCounted(2), t.args.beneficiary) + .build() + )), t.args.weight_limit, ) } fn system_para_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult { - ::PolkadotXcm::transfer_assets( + type Runtime = ::Runtime; + let remote_fee_id: AssetId = t + .args + .assets + .clone() + .into_inner() + .get(t.args.fee_asset_item as usize) + .ok_or(pallet_xcm::Error::::Empty)? + .clone() + .id; + + ::PolkadotXcm::transfer_assets_using_type_and_then( t.signed_origin, bx!(t.args.dest.into()), - bx!(t.args.beneficiary.into()), bx!(t.args.assets.into()), - t.args.fee_asset_item, + bx!(TransferType::Teleport), + bx!(remote_fee_id.into()), + bx!(TransferType::LocalReserve), + bx!(VersionedXcm::from( + Xcm::<()>::builder_unsafe() + .deposit_asset(AllCounted(2), t.args.beneficiary) + .build() + )), t.args.weight_limit, ) } diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/transfer_assets_validation.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/transfer_assets_validation.rs new file mode 100644 index 0000000000000..6db851daaeff8 --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/transfer_assets_validation.rs @@ -0,0 +1,264 @@ +// 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. + +//! Tests for the validation of `pallet_xcm::Pallet::::transfer_assets`. +//! See the `pallet_xcm::transfer_assets_validation` module for more information. + +use crate::imports::*; +use frame_support::{assert_err, assert_ok}; +use sp_runtime::DispatchError; + +// ================================================================================== +// ============================== PenpalA <-> Westend =============================== +// ================================================================================== + +/// Test that `transfer_assets` fails when doing reserve transfer of WND from PenpalA to Westend. +/// This fails because PenpalA's IsReserve config considers Westend as the reserve for WND, +/// so transfer_assets automatically chooses reserve transfer, which we block. +#[test] +fn transfer_assets_wnd_reserve_transfer_para_to_relay_fails() { + let destination = PenpalA::parent_location(); + let beneficiary: Location = + AccountId32Junction { network: None, id: WestendReceiver::get().into() }.into(); + let amount_to_send: Balance = WESTEND_ED * 1000; + let assets: Assets = (Parent, amount_to_send).into(); + + // Mint WND on PenpalA for testing. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + PenpalASender::get(), + amount_to_send * 2, + ); + + // Fund PenpalA's sovereign account on Westend with the reserve WND. + let penpal_location_as_seen_by_relay = Westend::child_location_of(PenpalA::para_id()); + let sov_penpal_on_relay = Westend::sovereign_account_id_of(penpal_location_as_seen_by_relay); + Westend::fund_accounts(vec![(sov_penpal_on_relay.into(), amount_to_send * 2)]); + + PenpalA::execute_with(|| { + let result = ::PolkadotXcm::transfer_assets( + ::RuntimeOrigin::signed(PenpalASender::get()), + bx!(destination.into()), + bx!(beneficiary.into()), + bx!(assets.into()), + 0, + WeightLimit::Unlimited, + ); + + // This should fail because WND reserve transfer is blocked. + assert_err!( + result, + DispatchError::Module(sp_runtime::ModuleError { + index: 31, + error: [21, 0, 0, 0], // InvalidAssetUnknownReserve. + message: Some("InvalidAssetUnknownReserve") + }) + ); + }); +} + +/// Test that `transfer_assets` fails when doing reserve transfer of WND from Westend to PenpalA +/// This fails because Westend's configuration would make this a reserve transfer, which we block. +#[test] +fn transfer_assets_wnd_reserve_transfer_relay_to_para_fails() { + let destination = Westend::child_location_of(PenpalA::para_id()); + let beneficiary: Location = + AccountId32Junction { network: None, id: PenpalAReceiver::get().into() }.into(); + let amount_to_send: Balance = WESTEND_ED * 1000; + let assets: Assets = (Here, amount_to_send).into(); + + Westend::execute_with(|| { + let result = ::XcmPallet::transfer_assets( + ::RuntimeOrigin::signed(WestendSender::get()), + bx!(destination.into()), + bx!(beneficiary.into()), + bx!(assets.into()), + 0, + WeightLimit::Unlimited, + ); + + // This should fail because WND reserve transfer is blocked. + assert_err!( + result, + DispatchError::Module(sp_runtime::ModuleError { + index: 99, + error: [21, 0, 0, 0], // InvalidAssetUnknownReserve. + message: Some("InvalidAssetUnknownReserve") + }) + ); + }); +} + +// ================================================================================== +// ============================== PenpalA <-> PenpalB =============================== +// ================================================================================== + +/// Test that `transfer_assets` fails when doing reserve transfer of WND from PenpalA to PenpalB +#[test] +fn transfer_assets_wnd_reserve_transfer_para_to_para_fails() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let beneficiary: Location = + AccountId32Junction { network: None, id: PenpalBReceiver::get().into() }.into(); + let amount_to_send: Balance = WESTEND_ED * 1000; + let assets: Assets = (Parent, amount_to_send).into(); + + // Mint WND on PenpalA for testing + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + PenpalASender::get(), + amount_to_send * 2, + ); + + PenpalA::execute_with(|| { + let result = ::PolkadotXcm::transfer_assets( + ::RuntimeOrigin::signed(PenpalASender::get()), + bx!(destination.into()), + bx!(beneficiary.into()), + bx!(assets.into()), + 0, + WeightLimit::Unlimited, + ); + + // This should fail because WND reserve transfer is blocked + assert_err!( + result, + DispatchError::Module(sp_runtime::ModuleError { + index: 31, + error: [21, 0, 0, 0], // InvalidAssetUnknownReserve + message: Some("InvalidAssetUnknownReserve") + }) + ); + }); +} + +// ================================================================================== +// ============================== Mixed Assets and Fees ============================= +// ================================================================================== + +/// Test that `transfer_assets` fails when WND is used as fee asset in reserve transfer +#[test] +fn transfer_assets_wnd_as_fee_in_reserve_transfer_fails() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let beneficiary: Location = + AccountId32Junction { network: None, id: PenpalBReceiver::get().into() }.into(); + let asset_amount: Balance = 1_000_000_000_000; // A million USDT. + let fee_amount: Balance = WESTEND_ED * 100; + + // Create a foreign asset location (representing another asset). + let foreign_asset_location = Location::new( + 1, + [ + Parachain(AssetHubWestend::para_id().into()), + PalletInstance(ASSETS_PALLET_ID), + GeneralIndex(USDT_ID.into()), // USDT. + ], + ); + + // Mint both assets on PenpalA for testing. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + foreign_asset_location.clone(), + PenpalASender::get(), + asset_amount * 2, + ); + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + PenpalASender::get(), + fee_amount * 2, + ); + + // Transfer foreign asset, pay fees with WND. + let assets: Assets = vec![ + (foreign_asset_location, asset_amount).into(), + (Parent, fee_amount).into(), // WND as fee. + ] + .into(); + let fee_asset_item = 1; // WND is the fee asset. + + PenpalA::execute_with(|| { + let result = ::PolkadotXcm::transfer_assets( + ::RuntimeOrigin::signed(PenpalASender::get()), + bx!(destination.into()), + bx!(beneficiary.into()), + bx!(assets.into()), + fee_asset_item, + WeightLimit::Unlimited, + ); + + // This should fail because WND fee would be reserve transferred. + assert_err!( + result, + DispatchError::Module(sp_runtime::ModuleError { + index: 31, + error: [21, 0, 0, 0], // InvalidAssetUnknownReserve. + message: Some("InvalidAssetUnknownReserve") + }) + ); + }); +} + +/// Test that `transfer_assets` works when neither asset nor fee is WND. +#[test] +fn transfer_assets_non_native_assets_work() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let beneficiary: Location = + AccountId32Junction { network: None, id: PenpalBReceiver::get().into() }.into(); + let amount: Balance = 1_000_000_000_000; // A million USDT. + + // Create foreign asset locations (both non-native). + let asset_location = Location::new( + 1, + [ + Parachain(AssetHubWestend::para_id().into()), + PalletInstance(ASSETS_PALLET_ID), + GeneralIndex(USDT_ID.into()), // USDT. + ], + ); + + // Mint both USDT and WND on PenpalA, one for sending, the other for paying delivery fees. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + PenpalASender::get(), + amount * 2, + ); + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + asset_location.clone(), + PenpalASender::get(), + amount * 2, + ); + + // Transfer non-native assets. + let assets: Assets = (asset_location, amount).into(); + let fee_asset_item = 0; + + PenpalA::execute_with(|| { + let result = ::PolkadotXcm::transfer_assets( + ::RuntimeOrigin::signed(PenpalASender::get()), + bx!(destination.into()), + bx!(beneficiary.into()), + bx!(assets.into()), + fee_asset_item, + WeightLimit::Unlimited, + ); + + // This should succeed because neither asset is WND. + assert_ok!(result); + }); +} diff --git a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/tests.rs b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/tests.rs index b7fdaa34ec8ca..76c721566c86b 100644 --- a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/tests.rs +++ b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/tests.rs @@ -63,7 +63,7 @@ fn reserve_asset_transfers_work() { // If we don't specify anything, it will be a `u64`, which the conversion // will turn into a non-fungible token instead of a fungible one. let assets: Assets = (Here, 50u128 * CENTS as u128).into(); - assert_ok!(relay_chain::XcmPallet::transfer_assets( + assert_ok!(relay_chain::XcmPallet::limited_reserve_transfer_assets( relay_chain::RuntimeOrigin::signed(ALICE), Box::new(VersionedLocation::from(destination.clone())), Box::new(VersionedLocation::from(beneficiary)), @@ -99,7 +99,7 @@ fn reserve_asset_transfers_work() { // This chain doesn't have a token of its own, so we always refer to this token, // and we do so by the Location of the Relay Chain. let assets: Assets = (Parent, 25u128 * CENTS as u128).into(); - assert_ok!(parachain::XcmPallet::transfer_assets( + assert_ok!(parachain::XcmPallet::limited_reserve_transfer_assets( parachain::RuntimeOrigin::signed(BOB), Box::new(VersionedLocation::from(destination)), Box::new(VersionedLocation::from(beneficiary)), diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml index 8eb55aacf7753..f8d7dad5bd491 100644 --- a/polkadot/xcm/pallet-xcm/Cargo.toml +++ b/polkadot/xcm/pallet-xcm/Cargo.toml @@ -25,6 +25,7 @@ xcm-executor.workspace = true xcm-builder.workspace = true xcm-runtime-apis.workspace = true frame-benchmarking = { optional = true, workspace = true } +hex-literal = { workspace = true, default-features = false } pallet-balances = { optional = true, workspace = true } [dev-dependencies] diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs index 3b7d1712ba01d..87074e9bfb7f7 100644 --- a/polkadot/xcm/pallet-xcm/src/lib.rs +++ b/polkadot/xcm/pallet-xcm/src/lib.rs @@ -24,6 +24,7 @@ pub mod benchmarking; mod mock; #[cfg(test)] mod tests; +mod transfer_assets_validation; pub mod migration; #[cfg(any(test, feature = "test-utils"))] @@ -1291,6 +1292,16 @@ pub mod pallet { let (fees_transfer_type, assets_transfer_type) = Self::find_fee_and_assets_transfer_types(&assets, fee_asset_item, &dest)?; + // We check for network native asset reserve transfers in preparation for the Asset Hub + // Migration. This check will be removed after the migration and the determined + // reserve location adjusted accordingly. For more information, see https://github.com/paritytech/polkadot-sdk/issues/9054. + Self::ensure_network_asset_reserve_transfer_allowed( + &assets, + fee_asset_item, + &assets_transfer_type, + &fees_transfer_type, + )?; + Self::do_transfer_assets( origin, dest, diff --git a/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs b/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs new file mode 100644 index 0000000000000..75aa91508a539 --- /dev/null +++ b/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs @@ -0,0 +1,163 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Validation of the `transfer_assets` call. +//! This validation is a temporary patch in preparation for the Asset Hub Migration (AHM). +//! This module will be removed after the migration and the determined +//! reserve location will be adjusted accordingly to be Asset Hub. +//! For more information, see . + +use crate::{Config, Error, Pallet}; +use alloc::vec::Vec; +use hex_literal::hex; +use sp_core::Get; +use xcm::prelude::*; +use xcm_executor::traits::TransferType; + +/// The genesis hash of the Paseo Relay Chain. Used to identify it. +const PASEO_GENESIS_HASH: [u8; 32] = + hex!["77afd6190f1554ad45fd0d31aee62aacc33c6db0ea801129acb813f913e0764f"]; + +impl Pallet { + /// Check if network native asset reserve transfers should be blocked during Asset Hub + /// Migration. + /// + /// During the Asset Hub Migration (AHM), the native network asset's reserve will move + /// from the Relay Chain to Asset Hub. The `transfer_assets` function automatically determines + /// reserves based on asset ID location, which would incorrectly assume Relay Chain as the + /// reserve. + /// + /// This function blocks native network asset reserve transfers to prevent issues during + /// the migration. + /// Users should use `limited_reserve_transfer_assets`, `transfer_assets_using_type_and_then` or + /// `execute` instead, which allows explicit reserve specification. + pub(crate) fn ensure_network_asset_reserve_transfer_allowed( + assets: &Vec, + fee_asset_index: usize, + assets_transfer_type: &TransferType, + fees_transfer_type: &TransferType, + ) -> Result<(), Error> { + // Extract fee asset and check both assets and fees separately. + let mut remaining_assets = assets.clone(); + if fee_asset_index >= remaining_assets.len() { + return Err(Error::::Empty); + } + let fee_asset = remaining_assets.remove(fee_asset_index); + + // Check remaining assets with their transfer type. + Self::ensure_one_transfer_type_allowed(&remaining_assets, &assets_transfer_type)?; + + // Check fee asset with its transfer type. + Self::ensure_one_transfer_type_allowed(&[fee_asset], &fees_transfer_type)?; + + Ok(()) + } + + /// Checks that the transfer of `assets` is allowed. + /// + /// Returns an error if `transfer_type` is a reserve transfer and the network's native asset is + /// being transferred. Allows the transfer otherwise. + fn ensure_one_transfer_type_allowed( + assets: &[Asset], + transfer_type: &TransferType, + ) -> Result<(), Error> { + // Check if any reserve transfer (LocalReserve, DestinationReserve, or RemoteReserve) + // is being attempted. + let is_reserve_transfer = matches!( + transfer_type, + TransferType::LocalReserve | + TransferType::DestinationReserve | + TransferType::RemoteReserve(_) + ); + + if !is_reserve_transfer { + // If not a reserve transfer (e.g., teleport), allow it. + return Ok(()); + } + + // Check if any asset is a network native asset. + for asset in assets { + if Self::is_network_native_asset(&asset.id) { + tracing::debug!( + target: "xcm::pallet_xcm::transfer_assets", + asset_id = ?asset.id, ?transfer_type, + "Network native asset reserve transfer blocked during Asset Hub Migration. Use `limited_reserve_transfer_assets` instead." + ); + // It's error-prone to try to determine the reserve in this circumstances. + return Err(Error::::InvalidAssetUnknownReserve); + } + } + + Ok(()) + } + + /// Check if the given asset ID represents a network native asset based on our + /// UniversalLocation. + /// + /// Returns true if the asset is a native network asset (DOT, KSM, WND, PAS) that should be + /// blocked during Asset Hub Migration. + fn is_network_native_asset(asset_id: &AssetId) -> bool { + let universal_location = T::UniversalLocation::get(); + let asset_location = &asset_id.0; + + match universal_location.len() { + // Case 1: We are on the Relay Chain itself. + // UniversalLocation: GlobalConsensus(Network). + // Network asset ID: Here. + 1 => { + if let Some(Junction::GlobalConsensus(network)) = universal_location.first() { + let is_target_network = match network { + NetworkId::Polkadot | NetworkId::Kusama => true, + NetworkId::ByGenesis(genesis_hash) => { + // Check if this is Westend by genesis hash + *genesis_hash == xcm::v5::WESTEND_GENESIS_HASH || + *genesis_hash == PASEO_GENESIS_HASH || + *genesis_hash == xcm::v5::ROCOCO_GENESIS_HASH // Used in tests. + }, + _ => false, + }; + is_target_network && asset_location.is_here() + } else { + false + } + }, + // Case 2: We are on a parachain within one of the specified networks. + // UniversalLocation: GlobalConsensus(Network)/Parachain(id). + // Network asset ID: Parent. + 2 => { + if let (Some(Junction::GlobalConsensus(network)), Some(Junction::Parachain(_))) = + (universal_location.first(), universal_location.last()) + { + let is_target_network = match network { + NetworkId::Polkadot | NetworkId::Kusama => true, + NetworkId::ByGenesis(genesis_hash) => { + // Check if this is Westend by genesis hash + *genesis_hash == xcm::v5::WESTEND_GENESIS_HASH || + *genesis_hash == PASEO_GENESIS_HASH || + *genesis_hash == xcm::v5::ROCOCO_GENESIS_HASH // Used in tests. + }, + _ => false, + }; + is_target_network && *asset_location == Location::parent() + } else { + false + } + }, + // Case 3: We are not on a relay or parachain. We return false. + _ => false, + } + } +} diff --git a/prdoc/pr_9137.prdoc b/prdoc/pr_9137.prdoc new file mode 100644 index 0000000000000..f8e296eef3e24 --- /dev/null +++ b/prdoc/pr_9137.prdoc @@ -0,0 +1,34 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Pallet XCM - transfer_assets pre-ahm patch + +doc: + - audience: Runtime User + description: | + Pallet XCM's `transfer_assets` extrinsic now returns an error when it determines that a + reserve transfer of DOT|KSM|WND|PAS has to be done. + This is a safeguard in preparation for the Asset Hub Migration (AHM), where the reserve + of DOT|KSM|WND|PAS will change from the Relay Chain to Asset Hub. + After the migration, another patch will remove this error case and use the correct reserve. + `limited_reserve_transfer_assets`, `transfer_assets_using_type_and_then` or `execute` should + be used instead, since they provide the ability to specify the reserve you want to use. + - audience: Runtime Dev + description: | + Pallet XCM's `transfer_assets` extrinsic now returns an error when it determines that a + reserve transfer of DOT|KSM|WND|PAS has to be done. + This is a safeguard in preparation for the Asset Hub Migration (AHM), where the reserve + of DOT|KSM|WND|PAS will change from the Relay Chain to Asset Hub. + After the migration, another patch will remove this error case and use the correct reserve. + The pallet uses the `UniversalLocation` configuration to figure out the correct asset + being transferred. It's very important to have that configuration correct. + +crates: + - name: pallet-xcm + bump: patch + - name: emulated-integration-tests-common + bump: patch + - name: xcm-docs + bump: patch + - name: pallet-scheduler + bump: patch diff --git a/substrate/frame/scheduler/src/tests.rs b/substrate/frame/scheduler/src/tests.rs index e0d40710f0ee7..ae48d569377bc 100644 --- a/substrate/frame/scheduler/src/tests.rs +++ b/substrate/frame/scheduler/src/tests.rs @@ -3038,7 +3038,7 @@ fn unavailable_call_is_detected() { #[test] fn service_task_fetched_weight_sane() { - use crate::{WeightInfo, weights::SubstrateWeight}; + use crate::{weights::SubstrateWeight, WeightInfo}; let w = SubstrateWeight::::service_task_fetched(0); assert!(w.proof_size() < 100 * 1024, "Proof size is less then 100 KiB");