Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1a06690
fix: disable native network reserve transfers via transfer_assets
franciscoaguirre Jul 8, 2025
c359890
fix: reuse existing error to not make this a minor bump
franciscoaguirre Jul 9, 2025
467fdf4
fix: fmt
franciscoaguirre Jul 9, 2025
f1604da
Merge branch 'master' into transfer-assets-pre-ahm-patch
franciscoaguirre Jul 9, 2025
7d60f7d
fix: handle assets and fees independently
franciscoaguirre Jul 9, 2025
08b80fe
doc: add small comment about Paseo
franciscoaguirre Jul 9, 2025
3e26090
fix: remove external chain case
franciscoaguirre Jul 10, 2025
7c83445
fix: fmt
franciscoaguirre Jul 10, 2025
6740faa
fix: bidirectional transfer test
franciscoaguirre Jul 10, 2025
f33e148
test: add tests for blocking transfer_assets
franciscoaguirre Jul 10, 2025
4040d78
doc: small test comment changes
franciscoaguirre Jul 11, 2025
09675ad
chore: move patch to its own mod
franciscoaguirre Jul 11, 2025
f90ff12
chore: rename test file
franciscoaguirre Jul 11, 2025
768d4f6
doc: add prdoc
franciscoaguirre Jul 11, 2025
af28268
Merge branch 'master' into transfer-assets-pre-ahm-patch
franciscoaguirre Jul 11, 2025
05d0458
fix: fmt
franciscoaguirre Jul 11, 2025
d2472ed
chore: don't clone fee asset
franciscoaguirre Jul 11, 2025
8f97548
fix: taplo
franciscoaguirre Jul 11, 2025
ad195c6
fix: doc issues
franciscoaguirre Jul 11, 2025
19c4a8c
fix: xcm-docs test
franciscoaguirre Jul 14, 2025
c219704
Merge branch 'master' into transfer-assets-pre-ahm-patch
franciscoaguirre Jul 14, 2025
bef14a8
fix: use transfer_assets_using_type_and_then on rococo
franciscoaguirre Jul 14, 2025
c3e393c
fix: test in bridge-hub-westend-integration-tests
franciscoaguirre Jul 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod set_xcm_versions;
mod swap;
mod teleport;
mod transact;
mod transfer_assets_validation;
mod treasury;
mod xcm_fee_estimation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,57 @@ fn relay_to_system_para_limited_teleport_assets(t: RelayToSystemParaTest) -> Dis
}

fn para_to_system_para_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
type Runtime = <PenpalA as Chain>::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::<Runtime>::Empty)?
.clone()
.id;

<PenpalA as PenpalAPallet>::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 {
<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::transfer_assets(
type Runtime = <AssetHubWestend as Chain>::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::<Runtime>::Empty)?
.clone()
.id;

<AssetHubWestend as AssetHubWestendPallet>::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,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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::<T>::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(
<PenpalA as Chain>::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 = <PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
<PenpalA as Chain>::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 = <Westend as WestendPallet>::XcmPallet::transfer_assets(
<Westend as Chain>::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(
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
RelayLocation::get(),
PenpalASender::get(),
amount_to_send * 2,
);

PenpalA::execute_with(|| {
let result = <PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
<PenpalA as Chain>::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(
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
foreign_asset_location.clone(),
PenpalASender::get(),
asset_amount * 2,
);
PenpalA::mint_foreign_asset(
<PenpalA as Chain>::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 = <PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
<PenpalA as Chain>::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(
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
RelayLocation::get(),
PenpalASender::get(),
amount * 2,
);
PenpalA::mint_foreign_asset(
<PenpalA as Chain>::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 = <PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
<PenpalA as Chain>::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);
});
}
1 change: 1 addition & 0 deletions polkadot/xcm/pallet-xcm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ xcm-runtime-apis = { workspace = true }

# marked optional, used in benchmarking
frame-benchmarking = { optional = true, workspace = true }
hex-literal = { workspace = true, default-features = false }
pallet-balances = { optional = true, workspace = true }

[dev-dependencies]
Expand Down
11 changes: 11 additions & 0 deletions polkadot/xcm/pallet-xcm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod mock;
pub mod precompiles;
#[cfg(test)]
mod tests;
mod transfer_assets_validation;

pub mod migration;
#[cfg(any(test, feature = "test-utils"))]
Expand Down Expand Up @@ -1489,6 +1490,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,
Expand Down
Loading
Loading