Skip to content

Commit

Permalink
pallet-xcm: enhance reserve_transfer_assets to support remote reser…
Browse files Browse the repository at this point in the history
…ves (#1672)

## Motivation

`pallet-xcm` is the main user-facing interface for XCM functionality,
including assets manipulation functions like `teleportAssets()` and
`reserve_transfer_assets()` calls.

While `teleportAsset()` works both ways, `reserve_transfer_assets()`
works only for sending reserve-based assets to a remote destination and
beneficiary when the reserve is the _local chain_.

## Solution

This PR enhances `pallet_xcm::(limited_)reserve_withdraw_assets` to
support transfers when reserves are other chains.
This will allow complete, **bi-directional** reserve-based asset
transfers user stories using `pallet-xcm`.

Enables following scenarios:
- transferring assets with local reserve (was previously supported iff
asset used as fee also had local reserve - now it works in all cases),
- transferring assets with reserve on destination,
- transferring assets with reserve on remote/third-party chain (iff
assets and fees have same remote reserve),
- transferring assets with reserve different than the reserve of the
asset to be used as fees - meaning can be used to transfer random asset
with local/dest reserve while using DOT for fees on all involved chains,
even if DOT local/dest reserve doesn't match asset reserve,
- transferring assets with any type of local/dest reserve while using
fees which can be teleported between involved chains.

All of the above is done by pallet inner logic without the user having
to specify which scenario/reserves/teleports/etc. The correct scenario
and corresponding XCM programs are identified, and respectively, built
automatically based on runtime configuration of trusted teleporters and
trusted reserves.

#### Current limitations:
- while `fees` and "non-fee" `assets` CAN have different reserves (or
fees CAN be teleported), the remaining "non-fee" `assets` CANNOT, among
themselves, have different reserve locations (this is also implicitly
enforced by `MAX_ASSETS_FOR_TRANSFER=2`, but this can be safely
increased in the future).
- `fees` and "non-fee" `assets` CANNOT have **different remote**
reserves (this could also be supported in the future, but adds even more
complexity while possibly not being worth it - we'll see what the future
holds).

Fixes paritytech/polkadot-sdk#1584
Fixes paritytech/polkadot-sdk#2055

---------

Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: Branislav Kontur <bkontur@gmail.com>
  • Loading branch information
3 people authored Nov 13, 2023
1 parent 47226e6 commit fef1319
Show file tree
Hide file tree
Showing 18 changed files with 2,401 additions and 535 deletions.
10 changes: 8 additions & 2 deletions pallet-xcm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ scale-info = { version = "2.10.0", default-features = false, features = ["derive
serde = { version = "1.0.188", optional = true, features = ["derive"] }
log = { version = "0.4.17", default-features = false }

frame-benchmarking = { path = "../../../substrate/frame/benchmarking", default-features = false, optional = true }
frame-support = { path = "../../../substrate/frame/support", default-features = false}
frame-system = { path = "../../../substrate/frame/system", default-features = false}
sp-core = { path = "../../../substrate/primitives/core", default-features = false}
Expand All @@ -25,8 +24,12 @@ xcm = { package = "staging-xcm", path = "..", default-features = false }
xcm-executor = { package = "staging-xcm-executor", path = "../xcm-executor", default-features = false }
xcm-builder = { package = "staging-xcm-builder", path = "../xcm-builder", default-features = false }

# marked optional, used in benchmarking
frame-benchmarking = { path = "../../../substrate/frame/benchmarking", default-features = false, optional = true }
pallet-balances = { path = "../../../substrate/frame/balances", default-features = false, optional = true }

[dev-dependencies]
pallet-balances = { path = "../../../substrate/frame/balances" }
pallet-assets = { path = "../../../substrate/frame/assets" }
polkadot-runtime-parachains = { path = "../../runtime/parachains" }
polkadot-parachain-primitives = { path = "../../parachain" }

Expand All @@ -39,6 +42,7 @@ std = [
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-balances/std",
"scale-info/std",
"serde",
"sp-core/std",
Expand All @@ -53,6 +57,7 @@ runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-assets/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"polkadot-parachain-primitives/runtime-benchmarks",
"polkadot-runtime-parachains/runtime-benchmarks",
Expand All @@ -63,6 +68,7 @@ runtime-benchmarks = [
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-assets/try-runtime",
"pallet-balances/try-runtime",
"polkadot-runtime-parachains/try-runtime",
"sp-runtime/try-runtime",
Expand Down
165 changes: 122 additions & 43 deletions pallet-xcm/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,68 +16,147 @@

use super::*;
use bounded_collections::{ConstU32, WeakBoundedVec};
use frame_benchmarking::{benchmarks, BenchmarkError, BenchmarkResult};
use frame_support::weights::Weight;
use frame_benchmarking::{benchmarks, whitelisted_caller, BenchmarkError, BenchmarkResult};
use frame_support::{traits::Currency, weights::Weight};
use frame_system::RawOrigin;
use sp_std::prelude::*;
use xcm::{latest::prelude::*, v2};

type RuntimeOrigin<T> = <T as frame_system::Config>::RuntimeOrigin;

// existential deposit multiplier
const ED_MULTIPLIER: u32 = 100;

/// Pallet we're benchmarking here.
pub struct Pallet<T: Config>(crate::Pallet<T>);

/// Trait that must be implemented by runtime to be able to benchmark pallet properly.
pub trait Config: crate::Config {
/// A `MultiLocation` that can be reached via `XcmRouter`. Used only in benchmarks.
///
/// If `None`, the benchmarks that depend on a reachable destination will be skipped.
fn reachable_dest() -> Option<MultiLocation> {
None
}

/// A `(MultiAsset, MultiLocation)` pair representing asset and the destination it can be
/// teleported to. Used only in benchmarks.
///
/// Implementation should also make sure `dest` is reachable/connected.
///
/// If `None`, the benchmarks that depend on this will be skipped.
fn teleportable_asset_and_dest() -> Option<(MultiAsset, MultiLocation)> {
None
}

/// A `(MultiAsset, MultiLocation)` pair representing asset and the destination it can be
/// reserve-transferred to. Used only in benchmarks.
///
/// Implementation should also make sure `dest` is reachable/connected.
///
/// If `None`, the benchmarks that depend on this will be skipped.
fn reserve_transferable_asset_and_dest() -> Option<(MultiAsset, MultiLocation)> {
None
}
}

benchmarks! {
where_clause {
where
T: pallet_balances::Config,
<T as pallet_balances::Config>::Balance: From<u128> + Into<u128>,
}
send {
let send_origin =
T::SendXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
if T::SendXcmOrigin::try_origin(send_origin.clone()).is_err() {
return Err(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))
}
let msg = Xcm(vec![ClearOrigin]);
let versioned_dest: VersionedMultiLocation = T::ReachableDest::get().ok_or(
let versioned_dest: VersionedMultiLocation = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?
.into();
let versioned_msg = VersionedXcm::from(msg);
}: _<RuntimeOrigin<T>>(send_origin, Box::new(versioned_dest), Box::new(versioned_msg))

teleport_assets {
let asset: MultiAsset = (Here, 10).into();
let send_origin =
T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let origin_location = T::ExecuteXcmOrigin::try_origin(send_origin.clone())
let (asset, destination) = T::teleportable_asset_and_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?;

let transferred_amount = match &asset.fun {
Fungible(amount) => *amount,
_ => return Err(BenchmarkError::Stop("Benchmark asset not fungible")),
}.into();
let assets: MultiAssets = asset.into();

let existential_deposit = T::ExistentialDeposit::get();
let caller = whitelisted_caller();

// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
assert!(balance >= transferred_amount);
let _ = <pallet_balances::Pallet<T> as Currency<_>>::make_free_balance_be(&caller, balance);
// verify initial balance
assert_eq!(pallet_balances::Pallet::<T>::free_balance(&caller), balance);

let send_origin = RawOrigin::Signed(caller.clone());
let origin_location = T::ExecuteXcmOrigin::try_origin(send_origin.clone().into())
.map_err(|_| BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?;
if !T::XcmTeleportFilter::contains(&(origin_location, vec![asset.clone()])) {
if !T::XcmTeleportFilter::contains(&(origin_location, assets.clone().into_inner())) {
return Err(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))
}

let recipient = [0u8; 32];
let versioned_dest: VersionedMultiLocation = T::ReachableDest::get().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?
.into();
let versioned_dest: VersionedMultiLocation = destination.into();
let versioned_beneficiary: VersionedMultiLocation =
AccountId32 { network: None, id: recipient.into() }.into();
let versioned_assets: VersionedMultiAssets = asset.into();
}: _<RuntimeOrigin<T>>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)
let versioned_assets: VersionedMultiAssets = assets.into();
}: _<RuntimeOrigin<T>>(send_origin.into(), Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)
verify {
// verify balance after transfer, decreased by transferred amount (+ maybe XCM delivery fees)
assert!(pallet_balances::Pallet::<T>::free_balance(&caller) <= balance - transferred_amount);
}

reserve_transfer_assets {
let asset: MultiAsset = (Here, 10).into();
let send_origin =
T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let origin_location = T::ExecuteXcmOrigin::try_origin(send_origin.clone())
let (asset, destination) = T::reserve_transferable_asset_and_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?;

let transferred_amount = match &asset.fun {
Fungible(amount) => *amount,
_ => return Err(BenchmarkError::Stop("Benchmark asset not fungible")),
}.into();
let assets: MultiAssets = asset.into();

let existential_deposit = T::ExistentialDeposit::get();
let caller = whitelisted_caller();

// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
assert!(balance >= transferred_amount);
let _ = <pallet_balances::Pallet<T> as Currency<_>>::make_free_balance_be(&caller, balance);
// verify initial balance
assert_eq!(pallet_balances::Pallet::<T>::free_balance(&caller), balance);

let send_origin = RawOrigin::Signed(caller.clone());
let origin_location = T::ExecuteXcmOrigin::try_origin(send_origin.clone().into())
.map_err(|_| BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?;
if !T::XcmReserveTransferFilter::contains(&(origin_location, vec![asset.clone()])) {
if !T::XcmReserveTransferFilter::contains(&(origin_location, assets.clone().into_inner())) {
return Err(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))
}

let recipient = [0u8; 32];
let versioned_dest: VersionedMultiLocation = T::ReachableDest::get().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?
.into();
let versioned_dest: VersionedMultiLocation = destination.into();
let versioned_beneficiary: VersionedMultiLocation =
AccountId32 { network: None, id: recipient.into() }.into();
let versioned_assets: VersionedMultiAssets = asset.into();
}: _<RuntimeOrigin<T>>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)
let versioned_assets: VersionedMultiAssets = assets.into();
}: _<RuntimeOrigin<T>>(send_origin.into(), Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)
verify {
// verify balance after transfer, decreased by transferred amount (+ maybe XCM delivery fees)
assert!(pallet_balances::Pallet::<T>::free_balance(&caller) <= balance - transferred_amount);
}

execute {
let execute_origin =
Expand All @@ -92,7 +171,7 @@ benchmarks! {
}: _<RuntimeOrigin<T>>(execute_origin, Box::new(versioned_msg), Weight::zero())

force_xcm_version {
let loc = T::ReachableDest::get().ok_or(
let loc = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?;
let xcm_version = 2;
Expand All @@ -101,18 +180,18 @@ benchmarks! {
force_default_xcm_version {}: _(RawOrigin::Root, Some(2))

force_subscribe_version_notify {
let versioned_loc: VersionedMultiLocation = T::ReachableDest::get().ok_or(
let versioned_loc: VersionedMultiLocation = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?
.into();
}: _(RawOrigin::Root, Box::new(versioned_loc))

force_unsubscribe_version_notify {
let loc = T::ReachableDest::get().ok_or(
let loc = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?;
let versioned_loc: VersionedMultiLocation = loc.into();
let _ = Pallet::<T>::request_version_notify(loc);
let _ = crate::Pallet::<T>::request_version_notify(loc);
}: _(RawOrigin::Root, Box::new(versioned_loc))

force_suspension {}: _(RawOrigin::Root, true)
Expand All @@ -122,38 +201,38 @@ benchmarks! {
let loc = VersionedMultiLocation::from(MultiLocation::from(Parent));
SupportedVersion::<T>::insert(old_version, loc, old_version);
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateSupportedVersion, Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateSupportedVersion, Weight::zero());
}

migrate_version_notifiers {
let old_version = XCM_VERSION - 1;
let loc = VersionedMultiLocation::from(MultiLocation::from(Parent));
VersionNotifiers::<T>::insert(old_version, loc, 0);
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateVersionNotifiers, Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateVersionNotifiers, Weight::zero());
}

already_notified_target {
let loc = T::ReachableDest::get().ok_or(
let loc = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(T::DbWeight::get().reads(1))),
)?;
let loc = VersionedMultiLocation::from(loc);
let current_version = T::AdvertisedXcmVersion::get();
VersionNotifyTargets::<T>::insert(current_version, loc, (0, Weight::zero(), current_version));
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::NotifyCurrentTargets(None), Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::NotifyCurrentTargets(None), Weight::zero());
}

notify_current_targets {
let loc = T::ReachableDest::get().ok_or(
let loc = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(T::DbWeight::get().reads_writes(1, 3))),
)?;
let loc = VersionedMultiLocation::from(loc);
let current_version = T::AdvertisedXcmVersion::get();
let old_version = current_version - 1;
VersionNotifyTargets::<T>::insert(current_version, loc, (0, Weight::zero(), old_version));
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::NotifyCurrentTargets(None), Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::NotifyCurrentTargets(None), Weight::zero());
}

notify_target_migration_fail {
Expand All @@ -167,7 +246,7 @@ benchmarks! {
let current_version = T::AdvertisedXcmVersion::get();
VersionNotifyTargets::<T>::insert(current_version, bad_loc, (0, Weight::zero(), current_version));
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
}

migrate_version_notify_targets {
Expand All @@ -176,33 +255,33 @@ benchmarks! {
let loc = VersionedMultiLocation::from(MultiLocation::from(Parent));
VersionNotifyTargets::<T>::insert(old_version, loc, (0, Weight::zero(), current_version));
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
}

migrate_and_notify_old_targets {
let loc = T::ReachableDest::get().ok_or(
let loc = T::reachable_dest().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(T::DbWeight::get().reads_writes(1, 3))),
)?;
let loc = VersionedMultiLocation::from(loc);
let old_version = T::AdvertisedXcmVersion::get() - 1;
VersionNotifyTargets::<T>::insert(old_version, loc, (0, Weight::zero(), old_version));
}: {
Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
crate::Pallet::<T>::check_xcm_version_change(VersionMigrationStage::MigrateAndNotifyOldTargets, Weight::zero());
}

new_query {
let responder = MultiLocation::from(Parent);
let timeout = 1u32.into();
let match_querier = MultiLocation::from(Here);
}: {
Pallet::<T>::new_query(responder, timeout, match_querier);
crate::Pallet::<T>::new_query(responder, timeout, match_querier);
}

take_response {
let responder = MultiLocation::from(Parent);
let timeout = 1u32.into();
let match_querier = MultiLocation::from(Here);
let query_id = Pallet::<T>::new_query(responder, timeout, match_querier);
let query_id = crate::Pallet::<T>::new_query(responder, timeout, match_querier);
let infos = (0 .. xcm::v3::MaxPalletsInfo::get()).map(|_| PalletInfo::new(
u32::MAX,
(0..xcm::v3::MaxPalletNameLen::get()).map(|_| 97u8).collect::<Vec<_>>().try_into().unwrap(),
Expand All @@ -211,10 +290,10 @@ benchmarks! {
u32::MAX,
u32::MAX,
).unwrap()).collect::<Vec<_>>();
Pallet::<T>::expect_response(query_id, Response::PalletsInfo(infos.try_into().unwrap()));
crate::Pallet::<T>::expect_response(query_id, Response::PalletsInfo(infos.try_into().unwrap()));

}: {
<Pallet::<T> as QueryHandler>::take_response(query_id);
<crate::Pallet::<T> as QueryHandler>::take_response(query_id);
}

impl_benchmark_test_suite!(
Expand Down
Loading

0 comments on commit fef1319

Please sign in to comment.