Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

[PoC] pallet_xcm::reserve_transfer_assets with DestinationFees handling #7456

Closed
wants to merge 14 commits into from
Closed
1 change: 1 addition & 0 deletions runtime/kusama/src/xcm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ impl pallet_xcm::Config for Runtime {
type MaxRemoteLockConsumers = ConstU32<0>;
type RemoteLockConsumerIdentifier = ();
type WeightInfo = crate::weights::pallet_xcm::WeightInfo<Runtime>;
type DestinationFeesManager = ();
#[cfg(feature = "runtime-benchmarks")]
type ReachableDest = ReachableDest;
type AdminOrigin = EnsureRoot<AccountId>;
Expand Down
1 change: 1 addition & 0 deletions runtime/polkadot/src/xcm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ impl pallet_xcm::Config for Runtime {
type MaxRemoteLockConsumers = ConstU32<0>;
type RemoteLockConsumerIdentifier = ();
type WeightInfo = crate::weights::pallet_xcm::WeightInfo<Runtime>;
type DestinationFeesManager = ();
#[cfg(feature = "runtime-benchmarks")]
type ReachableDest = ReachableDest;
type AdminOrigin = EnsureRoot<AccountId>;
Expand Down
1 change: 1 addition & 0 deletions runtime/rococo/src/xcm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ impl pallet_xcm::Config for Runtime {
type MaxRemoteLockConsumers = ConstU32<0>;
type RemoteLockConsumerIdentifier = ();
type WeightInfo = crate::weights::pallet_xcm::WeightInfo<Runtime>;
type DestinationFeesManager = ();
#[cfg(feature = "runtime-benchmarks")]
type ReachableDest = ReachableDest;
type AdminOrigin = EnsureRoot<AccountId>;
Expand Down
1 change: 1 addition & 0 deletions runtime/test-runtime/src/xcm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ impl pallet_xcm::Config for crate::Runtime {
type MaxRemoteLockConsumers = frame_support::traits::ConstU32<0>;
type RemoteLockConsumerIdentifier = ();
type WeightInfo = pallet_xcm::TestWeightInfo;
type DestinationFeesManager = ();
#[cfg(feature = "runtime-benchmarks")]
type ReachableDest = ReachableDest;
type AdminOrigin = EnsureRoot<crate::AccountId>;
Expand Down
1 change: 1 addition & 0 deletions runtime/westend/src/xcm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ impl pallet_xcm::Config for Runtime {
type MaxRemoteLockConsumers = ConstU32<0>;
type RemoteLockConsumerIdentifier = ();
type WeightInfo = crate::weights::pallet_xcm::WeightInfo<Runtime>;
type DestinationFeesManager = ();
#[cfg(feature = "runtime-benchmarks")]
type ReachableDest = ReachableDest;
type AdminOrigin = EnsureRoot<AccountId>;
Expand Down
3 changes: 2 additions & 1 deletion xcm/pallet-xcm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", default-features
sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }

xcm = { path = "..", default-features = false }
xcm-builder = { path = "../xcm-builder", default-features = false }
xcm-executor = { path = "../xcm-executor", default-features = false }

[dev-dependencies]
pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
polkadot-runtime-parachains = { path = "../../runtime/parachains" }
polkadot-parachain = { path = "../../parachain" }
xcm-builder = { path = "../xcm-builder" }

[features]
default = ["std"]
Expand All @@ -44,6 +44,7 @@ std = [
"frame-support/std",
"frame-system/std",
"xcm/std",
"xcm-builder/std",
"xcm-executor/std",
]
runtime-benchmarks = [
Expand Down
77 changes: 77 additions & 0 deletions xcm/pallet-xcm/src/destination_fees.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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 <http://www.gnu.org/licenses/>.

//! Utilities for handling fees and `BuyExecution` on destination.

use xcm::prelude::*;

/// Describes how to handle `BuyExecution` on destination chains,
/// useful for handling foreign asset transfers.
pub enum DestinationFeesSetup {
/// Fees are directly paid using Origin's indicated asset.
ByOrigin,
/// `UniversalLocation` does some conversion from origin's indicated asset.
/// E.g. swap origin's fee and/or `BuyExecution` on destination using different asset.
ByUniversalLocation {
/// Local account where we want to place additional withdrawn assets from origin
/// (e.g. treasury, staking pot, BH...).
local_account: MultiLocation,
},
}

/// Pair of local and remote assets (equivalent in economic value).
pub struct DestinationFees {
/// Assets withdrawn on **source** chain.
pub proportional_amount_to_withdraw: MultiAsset,

/// Assets used for `BuyExecution` on **destination** chain.
pub proportional_amount_to_buy_execution: MultiAsset,
}

/// Manages fees handling.
pub trait DestinationFeesManager {
/// Decide how to handle `BuyExecution` based on `destination` and `requested_fee_asset_id`.
fn decide_for(
destination: &MultiLocation,
requested_fee_asset_id: &AssetId,
) -> DestinationFeesSetup;

/// Estimate destination fees.
fn estimate_fee_for(
destination: &MultiLocation,
requested_fee_asset_id: &AssetId,
weight: &WeightLimit,
) -> Option<DestinationFees>;
}

/// Implementation handles setup as `DestinationFeesSetup::Origin`
impl DestinationFeesManager for () {
fn decide_for(
_destination: &MultiLocation,
_desired_fee_asset_id: &AssetId,
) -> DestinationFeesSetup {
DestinationFeesSetup::ByOrigin
}

fn estimate_fee_for(
_destination: &MultiLocation,
_desired_fee_asset_id: &AssetId,
_weight: &WeightLimit,
) -> Option<DestinationFees> {
// dont do any conversion or whatever, handle what origin wants on input
None
}
}
204 changes: 178 additions & 26 deletions xcm/pallet-xcm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod mock;
#[cfg(test)]
mod tests;

pub mod destination_fees;
pub mod migration;

use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
Expand All @@ -42,6 +43,7 @@ use sp_std::{boxed::Box, marker::PhantomData, prelude::*, result::Result, vec};
use xcm::{latest::QueryResponseInfo, prelude::*};
use xcm_executor::traits::{ConvertOrigin, Properties};

use crate::destination_fees::{DestinationFees, DestinationFeesManager, DestinationFeesSetup};
use frame_support::{
dispatch::{Dispatchable, GetDispatchInfo},
pallet_prelude::*,
Expand All @@ -50,6 +52,7 @@ use frame_support::{
};
use frame_system::pallet_prelude::*;
pub use pallet::*;
use xcm_builder::ensure_is_remote;
use xcm_executor::{
traits::{
CheckSuspension, ClaimAssets, ConvertLocation, DropAssets, MatchesFungible, OnResponse,
Expand Down Expand Up @@ -148,6 +151,7 @@ impl WeightInfo for TestWeightInfo {
#[frame_support::pallet]
pub mod pallet {
use super::*;
use crate::destination_fees::DestinationFeesManager;
use frame_support::{
dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo},
parameter_types,
Expand Down Expand Up @@ -261,6 +265,9 @@ pub mod pallet {
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;

/// How to handle `BuyExecution` on destination chains, useful for handling foreign asset transfers.
type DestinationFeesManager: DestinationFeesManager;

/// 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.
Expand Down Expand Up @@ -1187,6 +1194,76 @@ impl<T: Config> QueryHandler for Pallet<T> {
}

impl<T: Config> Pallet<T> {
fn estimate_reserve_transfer_assets_remote_weight(
dest: MultiLocation,
beneficiary: MultiLocation,
assets: &[MultiAsset],
origin_fees: &MultiAsset,
destination_fees_setup: &DestinationFeesSetup,
) -> Result<WeightLimit, DispatchError> {
// TODO: estimate fees using local weigher,
// or add some `T::BuyExecutionSetupResolver::max_limit_for(&dest, &fees.id)`?
let max_assets = assets.len() as u32;
let assets: MultiAssets = assets.to_vec().into();
let context = T::UniversalLocation::get();
let weight_limit = Limited(Weight::zero());
let fees = origin_fees
.clone()
.reanchored(&dest, context)
.map_err(|_| Error::<T>::CannotReanchor)?;

let buy_execution_on_dest = match destination_fees_setup {
DestinationFeesSetup::ByOrigin => {
vec![BuyExecution { fees, weight_limit }]
},
DestinationFeesSetup::ByUniversalLocation { .. } => {
let sovereign_account_on_destination = T::UniversalLocation::get()
.invert_target(&dest)
.map_err(|_| Error::<T>::DestinationNotInvertible)?;
vec![
// Change origin to sovereign account of `T::UniversalLocation` on destination.
AliasOrigin(sovereign_account_on_destination),
// Withdraw fees (do not use those in `ReserveAssetDeposited`)
WithdrawAsset(MultiAssets::from(fees.clone())),
ClearOrigin,
BuyExecution { fees: fees.clone(), weight_limit },
RefundSurplus,
// deposit unspent `fees` back to `sovereign_account`
DepositAsset {
assets: MultiAssetFilter::from(MultiAssets::from(fees)),
beneficiary: sovereign_account_on_destination,
},
]
},
};

let mut remote_message = Xcm(vec![ReserveAssetDeposited(assets), ClearOrigin]);
remote_message.0.extend_from_slice(&buy_execution_on_dest);
remote_message
.0
.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });

// if message is going to different consensus, also include `UniversalOrigin/DescendOrigin`
if ensure_is_remote(T::UniversalLocation::get(), dest).is_ok() {
let (local_net, local_sub) = T::UniversalLocation::get()
.split_global()
.map_err(|_| Error::<T>::BadLocation)?;
remote_message.0.insert(0, UniversalOrigin(GlobalConsensus(local_net)));
if local_sub != Here {
remote_message.0.insert(1, DescendOrigin(local_sub));
}
}

// TODO: can we leave it here by default or should we add according to some configuration?
// TODO: think about some `trait RemoteMessageEstimator` stuff, where runtime can customize this?
remote_message.0.push(SetTopic([1; 32]));

// use local weight for remote message and hope for the best.
let remote_weight =
T::Weigher::weight(&mut remote_message).map_err(|()| Error::<T>::UnweighableMessage)?;
Ok(Limited(remote_weight))
}

fn do_reserve_transfer_assets(
origin: OriginFor<T>,
dest: Box<VersionedMultiLocation>,
Expand All @@ -1206,38 +1283,113 @@ impl<T: Config> Pallet<T> {
ensure!(T::XcmReserveTransferFilter::contains(&value), Error::<T>::Filtered);
let (origin_location, assets) = value;
let context = T::UniversalLocation::get();
let fees = assets
.get(fee_asset_item as usize)
.ok_or(Error::<T>::Empty)?
.clone()
.reanchored(&dest, context)
.map_err(|_| Error::<T>::CannotReanchor)?;
// origin's desired fee asset, means origin wants to be charged in these assets
let fees = assets.get(fee_asset_item as usize).ok_or(Error::<T>::Empty)?.clone();

// resolve `BuyExecution`-on-destination strategy for `(dest, fees)`
let destination_fees_setup = T::DestinationFeesManager::decide_for(&dest, &fees.id);

// resolve weight_limit
let weight_limit = maybe_weight_limit.ok_or(()).or_else(|()| {
Self::estimate_reserve_transfer_assets_remote_weight(
dest,
beneficiary,
&assets,
&fees,
&destination_fees_setup,
)
})?;

let max_assets = assets.len() as u32;
let assets: MultiAssets = assets.into();
let weight_limit = match maybe_weight_limit {
Some(weight_limit) => weight_limit,
None => {
let fees = fees.clone();
let mut remote_message = Xcm(vec![
ReserveAssetDeposited(assets.clone()),
ClearOrigin,
BuyExecution { fees, weight_limit: Limited(Weight::zero()) },
// handle `BuyExecution` on target chain
let mut message = match destination_fees_setup {
DestinationFeesSetup::ByOrigin => {
// reanchor fees to `dest`
let fees =
fees.reanchored(&dest, context).map_err(|_| Error::<T>::CannotReanchor)?;

// no change, origin asset is used to `BuyExecution`
let xcm = Xcm(vec![
BuyExecution { fees, weight_limit },
DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
]);
// use local weight for remote message and hope for the best.
let remote_weight = T::Weigher::weight(&mut remote_message)
.map_err(|()| Error::<T>::UnweighableMessage)?;
Limited(remote_weight)
Xcm(vec![
SetFeesMode { jit_withdraw: true },
TransferReserveAsset { assets, dest, xcm },
])
},
DestinationFeesSetup::ByUniversalLocation { local_account } => {
// estimate how much to use for `BuyExecution` and proportional amount to withdraw from origin's assets
let DestinationFees {
proportional_amount_to_withdraw,
proportional_amount_to_buy_execution,
} = T::DestinationFeesManager::estimate_fee_for(&dest, &fees.id, &weight_limit)
.ok_or(Error::<T>::UnweighableMessage)?;

// split origin's assets
let original_assets = assets.clone();
let assets_for_reserve: MultiAssets = xcm_executor::Assets::from(assets)
.checked_sub(proportional_amount_to_withdraw.clone())
.map_err(|_| Error::<T>::InvalidAsset)?
.into();
let assets_to_withdraw = MultiAssets::from(proportional_amount_to_withdraw);
// ensure that origin's assets amount to assets_for_reserve + assets_to_withdraw
{
let mut collected_assets =
xcm_executor::Assets::from(assets_for_reserve.clone());
collected_assets
.subsume_assets(xcm_executor::Assets::from(assets_to_withdraw.clone()));
ensure!(
original_assets == MultiAssets::from(collected_assets),
Error::<T>::InvalidAsset
);
}

// reanchor fees to `dest`
let proportional_amount_to_buy_execution = proportional_amount_to_buy_execution
.reanchored(&dest, context)
.map_err(|_| Error::<T>::CannotReanchor)?;

// reanchor paying account to `dest`
let sovereign_account_on_destination = T::UniversalLocation::get()
.invert_target(&dest)
.map_err(|_| Error::<T>::DestinationNotInvertible)?;

let xcm = Xcm(vec![
// Change origin to sovereign account of `T::UniversalLocation` on destination.
AliasOrigin(sovereign_account_on_destination),
// Withdraw `fees` (do not use those in `ReserveAssetDeposited`)
WithdrawAsset(MultiAssets::from(proportional_amount_to_buy_execution.clone())),
ClearOrigin,
// Use just those `fees`
BuyExecution {
fees: proportional_amount_to_buy_execution.clone(),
weight_limit,
},
RefundSurplus,
// deposit unspent `fees` back to `sovereign_account`
DepositAsset {
assets: MultiAssetFilter::from(MultiAssets::from(
proportional_amount_to_buy_execution,
)),
beneficiary: sovereign_account_on_destination,
},
// deposit `assets` to beneficiary
DepositAsset {
assets: Wild(AllCounted(assets_for_reserve.len() as u32)),
beneficiary,
},
]);
Xcm(vec![
SetFeesMode { jit_withdraw: true },
TransferAsset { assets: assets_to_withdraw, beneficiary: local_account },
TransferReserveAsset { assets: assets_for_reserve, dest, xcm },
])
},
};
let xcm = Xcm(vec![
BuyExecution { fees, weight_limit },
DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
]);
let mut message = Xcm(vec![
SetFeesMode { jit_withdraw: true },
TransferReserveAsset { assets, dest, xcm },
]);

// execute XCM
let weight =
T::Weigher::weight(&mut message).map_err(|()| Error::<T>::UnweighableMessage)?;
let hash = message.using_encoded(sp_io::hashing::blake2_256);
Expand Down
Loading
Loading