Skip to content

Commit

Permalink
pallet-xcm: add new flexible transfer_assets() call/extrinsic (pari…
Browse files Browse the repository at this point in the history
…tytech#2388)

# Motivation (+testing)

### Enable easy `ForeignAssets` transfers using `pallet-xcm` 

We had just previously added capabilities to teleport fees during
reserve-based transfers, but what about reserve-transferring fees when
needing to teleport some non-fee asset?

This PR aligns everything under either explicit reserve-transfer,
explicit teleport, or this new flexible `transfer_assets()` which can
mix and match as needed with fewer artificial constraints imposed to the
user.

This will enable, for example, a (non-system) parachain to teleport
their `ForeignAssets` assets to AssetHub while using DOT to pay fees.
(the assets are teleported - as foreign assets should from their owner
chain - while DOT used for fees can only be reserve-based transferred
between said parachain and AssetHub).

Added `xcm-emulator` tests for this scenario ^.

# Description

Reverts `(limited_)reserve_transfer_assets` to only allow reserve-based
transfers for all `assets` including fees.

Similarly `(limited_)teleport_assets` only allows teleports for all
`assets` including fees.
    
For complex combinations of asset transfers where assets and fees may
have different reserves or different reserve/teleport trust
configurations, users can use the newly added `transfer_assets()`
extrinsic which is more flexible in allowing more complex scenarios.

`assets` (excluding `fees`) must have same reserve location or otherwise
be teleportable to `dest`.
No limitations imposed on `fees`.

- for 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`.
- for 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`.
- for 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`.
- for teleports: burn local assets and forward XCM to `dest` chain to
mint/teleport assets and deposit them to `beneficiary`.

## Review notes

Only around 500 lines are prod code (see `pallet_xcm/src/lib.rs`), the
rest of the PR is new tests and improving existing tests.

---------

Co-authored-by: command-bot <>
  • Loading branch information
acatangiu authored Dec 6, 2023
1 parent 066bad6 commit e7651cf
Show file tree
Hide file tree
Showing 38 changed files with 3,322 additions and 957 deletions.
2 changes: 0 additions & 2 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 @@ -15,6 +15,7 @@

mod genesis;
pub use genesis::{genesis, ED, PARA_ID_A, PARA_ID_B};
pub use penpal_runtime::xcm_config::{LocalTeleportableToAssetHub, XcmConfig};

// Substrate
use frame_support::traits::OnInitialize;
Expand Down Expand Up @@ -67,6 +68,7 @@ decl_test_parachains! {

// Penpal implementation
impl_accounts_helpers_for_parachain!(PenpalA);
impl_accounts_helpers_for_parachain!(PenpalB);
impl_assets_helpers_for_parachain!(PenpalA, Rococo);
impl_assets_helpers_for_parachain!(PenpalB, Westend);
impl_assert_events_helpers_for_parachain!(PenpalA);
Expand Down
99 changes: 99 additions & 0 deletions cumulus/parachains/integration-tests/emulated/common/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,102 @@ macro_rules! test_parachain_is_trusted_teleporter {
}
};
}

#[macro_export]
macro_rules! include_penpal_create_foreign_asset_on_asset_hub {
( $penpal:ident, $asset_hub:ident, $relay_ed:expr, $weight_to_fee:expr) => {
$crate::impls::paste::paste! {
pub fn penpal_create_foreign_asset_on_asset_hub(
asset_id_on_penpal: u32,
foreign_asset_at_asset_hub: MultiLocation,
ah_as_seen_by_penpal: MultiLocation,
is_sufficient: bool,
asset_owner: AccountId,
prefund_amount: u128,
) {
use frame_support::weights::WeightToFee;
let ah_check_account = $asset_hub::execute_with(|| {
<$asset_hub as [<$asset_hub Pallet>]>::PolkadotXcm::check_account()
});
let penpal_check_account =
$penpal::execute_with(|| <$penpal as [<$penpal Pallet>]>::PolkadotXcm::check_account());
let penpal_as_seen_by_ah = $asset_hub::sibling_location_of($penpal::para_id());

// prefund SA of Penpal on AssetHub with enough native tokens to pay for creating
// new foreign asset, also prefund CheckingAccount with ED, because teleported asset
// itself might not be sufficient and CheckingAccount cannot be created otherwise
let sov_penpal_on_ah = $asset_hub::sovereign_account_id_of(penpal_as_seen_by_ah);
$asset_hub::fund_accounts(vec![
(sov_penpal_on_ah.clone().into(), $relay_ed * 100_000_000_000),
(ah_check_account.clone().into(), $relay_ed * 1000),
]);

// prefund SA of AssetHub on Penpal with native asset
let sov_ah_on_penpal = $penpal::sovereign_account_id_of(ah_as_seen_by_penpal);
$penpal::fund_accounts(vec![
(sov_ah_on_penpal.into(), $relay_ed * 1_000_000_000),
(penpal_check_account.clone().into(), $relay_ed * 1000),
]);

// Force create asset on $penpal and prefund [<$penpal Sender>]
$penpal::force_create_and_mint_asset(
asset_id_on_penpal,
ASSET_MIN_BALANCE,
is_sufficient,
asset_owner,
None,
prefund_amount,
);

let require_weight_at_most = Weight::from_parts(1_100_000_000_000, 30_000);
// `OriginKind::Xcm` required by ForeignCreators pallet-assets origin filter
let origin_kind = OriginKind::Xcm;
let call_create_foreign_assets =
<$asset_hub as Chain>::RuntimeCall::ForeignAssets(pallet_assets::Call::<
<$asset_hub as Chain>::Runtime,
pallet_assets::Instance2,
>::create {
id: foreign_asset_at_asset_hub,
min_balance: ASSET_MIN_BALANCE,
admin: sov_penpal_on_ah.into(),
})
.encode();
let buy_execution_fee_amount = $weight_to_fee::weight_to_fee(
&Weight::from_parts(10_100_000_000_000, 300_000),
);
let buy_execution_fee = MultiAsset {
id: Concrete(MultiLocation { parents: 1, interior: Here }),
fun: Fungible(buy_execution_fee_amount),
};
let xcm = VersionedXcm::from(Xcm(vec![
WithdrawAsset { 0: vec![buy_execution_fee.clone()].into() },
BuyExecution { fees: buy_execution_fee.clone(), weight_limit: Unlimited },
Transact { require_weight_at_most, origin_kind, call: call_create_foreign_assets.into() },
ExpectTransactStatus(MaybeErrorCode::Success),
RefundSurplus,
DepositAsset { assets: All.into(), beneficiary: penpal_as_seen_by_ah },
]));
// Send XCM message from penpal => asset_hub
let sudo_penpal_origin = <$penpal as Chain>::RuntimeOrigin::root();
$penpal::execute_with(|| {
assert_ok!(<$penpal as [<$penpal Pallet>]>::PolkadotXcm::send(
sudo_penpal_origin.clone(),
bx!(ah_as_seen_by_penpal.into()),
bx!(xcm),
));
type RuntimeEvent = <$penpal as Chain>::RuntimeEvent;
assert_expected_events!(
$penpal,
vec![
RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { .. }) => {},
]
);
});
$asset_hub::execute_with(|| {
type ForeignAssets = <$asset_hub as [<$asset_hub Pallet>]>::ForeignAssets;
assert!(ForeignAssets::asset_exists(foreign_asset_at_asset_hub));
});
}
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,17 @@ pub fn xcm_transact_unpaid_execution(
Transact { require_weight_at_most, origin_kind, call },
]))
}

/// Helper method to get the non-fee asset used in multiple assets transfer
pub fn non_fee_asset(assets: &MultiAssets, fee_idx: usize) -> Option<(MultiLocation, u128)> {
let asset = assets.inner().into_iter().enumerate().find(|a| a.0 != fee_idx)?.1.clone();
let asset_id = match asset.id {
Concrete(id) => id,
_ => return None,
};
let asset_amount = match asset.fun {
Fungible(amount) => amount,
_ => return None,
};
Some((asset_id, asset_amount))
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,4 @@ asset-test-utils = { path = "../../../../../runtimes/assets/test-utils" }
parachains-common = { path = "../../../../../../parachains/common" }
asset-hub-rococo-runtime = { path = "../../../../../runtimes/assets/asset-hub-rococo" }
emulated-integration-tests-common = { path = "../../../common", default-features = false }
penpal-runtime = { path = "../../../../../runtimes/testing/penpal" }
rococo-system-emulated-network = { path = "../../../networks/rococo-system" }
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ mod send;
mod set_xcm_versions;
mod swap;
mod teleport;

use crate::*;
emulated_integration_tests_common::include_penpal_create_foreign_asset_on_asset_hub!(
PenpalA,
AssetHubRococo,
ROCOCO_ED,
parachains_common::rococo::fee::WeightToFee
);
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@

use crate::*;
use asset_hub_rococo_runtime::xcm_config::XcmConfig as AssetHubRococoXcmConfig;
use penpal_runtime::xcm_config::XcmConfig as PenpalRococoXcmConfig;
use rococo_runtime::xcm_config::XcmConfig as RococoXcmConfig;
use rococo_system_emulated_network::penpal_emulated_chain::XcmConfig as PenpalRococoXcmConfig;

fn relay_to_para_sender_assertions(t: RelayToParaTest) {
type RuntimeEvent = <Rococo as Chain>::RuntimeEvent;

Rococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(864_610_000, 8_799)));

assert_expected_events!(
Rococo,
vec![
Expand All @@ -42,12 +40,10 @@ fn relay_to_para_sender_assertions(t: RelayToParaTest) {

fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;

AssetHubRococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(
864_610_000,
8_799,
)));

assert_expected_events!(
AssetHubRococo,
vec![
Expand Down Expand Up @@ -80,9 +76,7 @@ fn para_receiver_assertions<Test>(_: Test) {

fn para_to_system_para_sender_assertions(t: ParaToSystemParaTest) {
type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;

PenpalA::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(864_610_000, 8_799)));

assert_expected_events!(
PenpalA,
vec![
Expand All @@ -99,15 +93,13 @@ fn para_to_system_para_sender_assertions(t: ParaToSystemParaTest) {

fn para_to_system_para_receiver_assertions(t: ParaToSystemParaTest) {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;

let sov_penpal_on_ahr = AssetHubRococo::sovereign_account_id_of(
AssetHubRococo::sibling_location_of(PenpalA::para_id()),
);

assert_expected_events!(
AssetHubRococo,
vec![
// Amount to reserve transfer is transferred to Parachain's Sovereign account
// Amount to reserve transfer is withdrawn from Parachain's Sovereign account
RuntimeEvent::Balances(
pallet_balances::Event::Withdraw { who, amount }
) => {
Expand All @@ -124,12 +116,10 @@ fn para_to_system_para_receiver_assertions(t: ParaToSystemParaTest) {

fn system_para_to_para_assets_sender_assertions(t: SystemParaToParaTest) {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;

AssetHubRococo::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts(
864_610_000,
8799,
)));

assert_expected_events!(
AssetHubRococo,
vec![
Expand Down Expand Up @@ -162,7 +152,7 @@ fn system_para_to_para_assets_receiver_assertions<Test>(_: Test) {
);
}

fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
fn relay_to_para_reserve_transfer_assets(t: RelayToParaTest) -> DispatchResult {
<Rococo as RococoPallet>::XcmPallet::limited_reserve_transfer_assets(
t.signed_origin,
bx!(t.args.dest.into()),
Expand All @@ -173,7 +163,7 @@ fn relay_to_para_limited_reserve_transfer_assets(t: RelayToParaTest) -> Dispatch
)
}

fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
fn system_para_to_para_reserve_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
<AssetHubRococo as AssetHubRococoPallet>::PolkadotXcm::limited_reserve_transfer_assets(
t.signed_origin,
bx!(t.args.dest.into()),
Expand All @@ -184,7 +174,7 @@ fn system_para_to_para_limited_reserve_transfer_assets(t: SystemParaToParaTest)
)
}

fn para_to_system_para_limited_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
<PenpalA as PenpalAPallet>::PolkadotXcm::limited_reserve_transfer_assets(
t.signed_origin,
bx!(t.args.dest.into()),
Expand Down Expand Up @@ -285,7 +275,7 @@ fn reserve_transfer_native_asset_from_relay_to_para() {

test.set_assertion::<Rococo>(relay_to_para_sender_assertions);
test.set_assertion::<PenpalA>(para_receiver_assertions);
test.set_dispatchable::<Rococo>(relay_to_para_limited_reserve_transfer_assets);
test.set_dispatchable::<Rococo>(relay_to_para_reserve_transfer_assets);
test.assert();

let delivery_fees = Rococo::execute_with(|| {
Expand Down Expand Up @@ -329,7 +319,7 @@ fn reserve_transfer_native_asset_from_system_para_to_para() {

test.set_assertion::<AssetHubRococo>(system_para_to_para_sender_assertions);
test.set_assertion::<PenpalA>(para_receiver_assertions);
test.set_dispatchable::<AssetHubRococo>(system_para_to_para_limited_reserve_transfer_assets);
test.set_dispatchable::<AssetHubRococo>(system_para_to_para_reserve_transfer_assets);
test.assert();

let sender_balance_after = test.sender.balance;
Expand Down Expand Up @@ -379,7 +369,7 @@ fn reserve_transfer_native_asset_from_para_to_system_para() {

test.set_assertion::<PenpalA>(para_to_system_para_sender_assertions);
test.set_assertion::<AssetHubRococo>(para_to_system_para_receiver_assertions);
test.set_dispatchable::<PenpalA>(para_to_system_para_limited_reserve_transfer_assets);
test.set_dispatchable::<PenpalA>(para_to_system_para_reserve_transfer_assets);
test.assert();

let sender_balance_after = test.sender.balance;
Expand Down Expand Up @@ -474,7 +464,7 @@ fn reserve_transfer_assets_from_system_para_to_para() {

test.set_assertion::<AssetHubRococo>(system_para_to_para_assets_sender_assertions);
test.set_assertion::<PenpalA>(system_para_to_para_assets_receiver_assertions);
test.set_dispatchable::<AssetHubRococo>(system_para_to_para_limited_reserve_transfer_assets);
test.set_dispatchable::<AssetHubRococo>(system_para_to_para_reserve_transfer_assets);
test.assert();

let sender_balance_after = test.sender.balance;
Expand Down
Loading

0 comments on commit e7651cf

Please sign in to comment.