From d14d8dbd139b37c4d6d22a07afa043d3837bd05b Mon Sep 17 00:00:00 2001 From: Francisco Aguirre Date: Mon, 14 Jul 2025 16:28:37 -0300 Subject: [PATCH 1/2] Pallet XCM - `transfer_assets` pre-ahm patch (#9137) Addresses https://github.com/paritytech/polkadot-sdk/issues/9054 `transfer_assets` automatically figures out the reserve for a cross-chain transfer based on on-chain configurations like `IsReserve` and the asset ids. The Asset Hub Migration (AHM) will make it unable to return the correct reserve for the network native asset (DOT, KSM, WND, PAS) since its reserve will move from the Relay Chain to the Asset Hub. Before the migration, it'll be disabled to do network native reserve transfers via `transfer_assets`. After the migration, once everything is configured properly, it'll be patched to use the correct reserve. - [x] Patch - [x] Tests - [x] PRDoc (cherry picked from commit efa765b6d9fbab59dd9bab944f99b40a157d0d64) --- Cargo.lock | 2 + .../emulated/common/Cargo.toml | 2 + .../emulated/common/src/macros.rs | 27 +- .../asset-hub-rococo/src/tests/teleport.rs | 46 ++- .../assets/asset-hub-westend/src/tests/mod.rs | 1 + .../asset-hub-westend/src/tests/teleport.rs | 46 ++- .../src/tests/transfer_assets_validation.rs | 265 ++++++++++++++++++ .../cookbook/relay_token_transactor/tests.rs | 4 +- polkadot/xcm/pallet-xcm/Cargo.toml | 1 + polkadot/xcm/pallet-xcm/src/lib.rs | 11 + .../src/transfer_assets_validation.rs | 159 +++++++++++ prdoc/pr_9137.prdoc | 28 ++ 12 files changed, 568 insertions(+), 24 deletions(-) create mode 100644 cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/transfer_assets_validation.rs create mode 100644 polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs create mode 100644 prdoc/pr_9137.prdoc diff --git a/Cargo.lock b/Cargo.lock index e79213f8dc75c..624c442a19601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5162,6 +5162,7 @@ dependencies = [ "sp-core", "sp-runtime", "staging-xcm", + "staging-xcm-executor", "xcm-emulator", ] @@ -11983,6 +11984,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex-literal", "log", "pallet-assets", "pallet-balances", diff --git a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml index d250620903f07..d9f4070300d22 100644 --- a/cumulus/parachains/integration-tests/emulated/common/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/common/Cargo.toml @@ -42,6 +42,8 @@ xcm.workspace = true xcm.default-features = true pallet-xcm.workspace = true pallet-xcm.default-features = true +xcm-executor.workspace = true +xcm-executor.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 7b26713a09254..14adb17f9f265 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; @@ -429,17 +432,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::AssetId(Location::new(1, [])))), + 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 d6cf819118e90..0c4c21037da3b 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 a0c9902a7b537..8f347967db0bd 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 @@ -22,6 +22,7 @@ mod send; mod set_xcm_versions; mod swap; mod teleport; +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 ddb82a954a877..743666e19da7c 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 @@ -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/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..b39d2cccc97dd --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/transfer_assets_validation.rs @@ -0,0 +1,265 @@ +// 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 emulated_integration_tests_common::USDT_ID; +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 792cf6149e7cb..1731a24643e5a 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::V4(destination.clone())), Box::new(VersionedLocation::V4(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::V4(destination)), Box::new(VersionedLocation::V4(beneficiary)), diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml index c8b80006cadb7..6eb91c150a57e 100644 --- a/polkadot/xcm/pallet-xcm/Cargo.toml +++ b/polkadot/xcm/pallet-xcm/Cargo.toml @@ -26,6 +26,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 ab9780a3a6be1..8185229564def 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; @@ -1289,6 +1290,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..a69e53b535217 --- /dev/null +++ b/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs @@ -0,0 +1,159 @@ +// 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 | NetworkId::Westend | NetworkId::Rococo => true, + NetworkId::ByGenesis(genesis_hash) => { + // Check if this is Paseo by genesis hash + *genesis_hash == PASEO_GENESIS_HASH + }, + _ => 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 | NetworkId::Westend | NetworkId::Rococo => true, + NetworkId::ByGenesis(genesis_hash) => { + // Check if this is Paseo by genesis hash + *genesis_hash == PASEO_GENESIS_HASH + }, + _ => 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..182a10af31c49 --- /dev/null +++ b/prdoc/pr_9137.prdoc @@ -0,0 +1,28 @@ +# 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 From add7db9c42a92a4315e8cc752c96d70a0df79553 Mon Sep 17 00:00:00 2001 From: Francisco Aguirre Date: Wed, 16 Jul 2025 16:30:44 -0300 Subject: [PATCH 2/2] fix: fmt --- .../xcm/pallet-xcm/src/transfer_assets_validation.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs b/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs index a69e53b535217..dbc789e75870e 100644 --- a/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs +++ b/polkadot/xcm/pallet-xcm/src/transfer_assets_validation.rs @@ -120,7 +120,10 @@ impl Pallet { 1 => { if let Some(Junction::GlobalConsensus(network)) = universal_location.first() { let is_target_network = match network { - NetworkId::Polkadot | NetworkId::Kusama | NetworkId::Westend | NetworkId::Rococo => true, + NetworkId::Polkadot | + NetworkId::Kusama | + NetworkId::Westend | + NetworkId::Rococo => true, NetworkId::ByGenesis(genesis_hash) => { // Check if this is Paseo by genesis hash *genesis_hash == PASEO_GENESIS_HASH @@ -140,7 +143,10 @@ impl Pallet { (universal_location.first(), universal_location.last()) { let is_target_network = match network { - NetworkId::Polkadot | NetworkId::Kusama | NetworkId::Westend | NetworkId::Rococo => true, + NetworkId::Polkadot | + NetworkId::Kusama | + NetworkId::Westend | + NetworkId::Rococo => true, NetworkId::ByGenesis(genesis_hash) => { // Check if this is Paseo by genesis hash *genesis_hash == PASEO_GENESIS_HASH