Skip to content

Commit

Permalink
[xcm-v5] implement RFC#100: add new InitiateTransfer instruction (#5876)
Browse files Browse the repository at this point in the history
A new instruction `InitiateTransfer` is introduced that initiates an
assets transfer from the chain it is executed on, to another chain. The
executed transfer is point-to-point (chain-to-chain) with all of the
transfer properties specified in the instruction parameters. The
instruction also allows specifying another XCM program to be executed on
the remote chain.
If a transfer requires going through multiple hops, an XCM program can
compose this instruction to be used at every chain along the path, on
each hop describing that specific leg of the transfer.

**Note:** Transferring assets that require different paths (chains along
the way) is _not supported within same XCM_ because of the async nature
of cross chain messages. This new instruction, however, enables
initiating transfers for multiple assets that take the same path even if
they require different transfer types along that path.

The usage and composition model of `InitiateTransfer` is the same as
with existing `DepositReserveAsset`, `InitiateReserveWithdraw` and
`InitiateTeleport` instructions. The main difference comes from the
ability to handle assets that have different point-to-point transfer
type between A and B. The other benefit is that it also allows
specifying remote fee payment and transparently appends the required
remote fees logic to the remote XCM.

We can specify the desired transfer type for some asset(s) using:

```rust
/// Specify which type of asset transfer is required for a particular `(asset, dest)` combination.
pub enum AssetTransferFilter {
	/// teleport assets matching `AssetFilter` to `dest`
	Teleport(AssetFilter),
	/// reserve-transfer assets matching `AssetFilter` to `dest`, using the local chain as reserve
	ReserveDeposit(AssetFilter),
	/// reserve-transfer assets matching `AssetFilter` to `dest`, using `dest` as reserve
	ReserveWithdraw(AssetFilter),
}
```

This PR adds 1 new XCM instruction:
```rust
/// Cross-chain transfer matching `assets` in the holding register as follows:
///
/// Assets in the holding register are matched using the given list of `AssetTransferFilter`s,
/// they are then transferred based on their specified transfer type:
///
/// - teleport: burn local assets and append a `ReceiveTeleportedAsset` XCM instruction to
///   the XCM program to be sent onward to the `dest` location,
///
/// - reserve deposit: place assets under the ownership of `dest` within this consensus system
///   (i.e. its sovereign account), and append a `ReserveAssetDeposited` XCM instruction
///   to the XCM program to be sent onward to the `dest` location,
///
/// - reserve withdraw: burn local assets and append a `WithdrawAsset` XCM instruction
///   to the XCM program to be sent onward to the `dest` location,
///
/// The onward XCM is then appended a `ClearOrigin` to allow safe execution of any following
/// custom XCM instructions provided in `remote_xcm`.
///
/// The onward XCM also potentially contains a `BuyExecution` instruction based on the presence
/// of the `remote_fees` parameter (see below).
///
/// If a transfer requires going through multiple hops, an XCM program can compose this instruction
/// to be used at every chain along the path, describing that specific leg of the transfer.
///
/// Parameters:
/// - `dest`: The location of the transfer next hop.
/// - `remote_fees`: If set to `Some(asset_xfer_filter)`, the single asset matching
///   `asset_xfer_filter` in the holding register will be transferred first in the remote XCM
///   program, followed by a `BuyExecution(fee)`, then rest of transfers follow.
///   This guarantees `remote_xcm` will successfully pass a `AllowTopLevelPaidExecutionFrom` barrier.
/// - `remote_xcm`: Custom instructions that will be executed on the `dest` chain. Note that
///   these instructions will be executed after a `ClearOrigin` so their origin will be `None`.
///
/// Safety: No concerns.
///
/// Kind: *Command*.
///
InitiateTransfer {
	destination: Location,
	remote_fees: Option<AssetTransferFilter>,
	assets: Vec<AssetTransferFilter>,
	remote_xcm: Xcm<()>,
}
```

An `InitiateTransfer { .. }` instruction shall transfer to `dest`, all
assets in the `holding` register that match the provided `assets` and
`remote_fees` filters.
These filters identify the assets to be transferred as well as the
transfer type to be used for transferring them.
It shall handle the local side of the transfer, then forward an onward
XCM to `dest` for handling the remote side of the transfer.

It should do so using same mechanisms as existing `DepositReserveAsset`,
`InitiateReserveWithdraw`, `InitiateTeleport` instructions but
practically combining all required XCM instructions to be remotely
executed into a _single_ remote XCM program to be sent over to `dest`.

Furthermore, through `remote_fees: Option<AssetTransferFilter>`, it
shall allow specifying a single asset to be used for fees on `dest`
chain. This single asset shall be remotely handled/received by the
**first instruction** in the onward XCM and shall be followed by a
`BuyExecution` instruction using it.
If `remote_fees` is set to `None`, the **first instruction** in the
onward XCM shall be a `UnpaidExecution` instruction. The rest of the
assets shall be handled by subsequent instructions, thus also finally
allowing [single asset buy
execution](#2423)
barrier security recommendation.

The `BuyExecution` appended to the onward XCM specifies
`WeightLimit::Unlimited`, thus being limited only by the `remote_fees`
asset "amount". This is a deliberate decision for enhancing UX - in
practice, people/dApps care about limiting the amount of fee asset used
and not the actually used weight.

The onward XCM, following the assets transfers instructions,
`ClearOrigin` or `DescendOrigin` instructions shall be appended to stop
acting on behalf of the source chain, then the caller-provided
`remote_xcm` shall also be appended, allowing the caller to control what
to do with the transferred assets.

Closes #5209

---------

Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: command-bot <>
  • Loading branch information
acatangiu and franciscoaguirre authored Oct 23, 2024
1 parent 4a6e85c commit 8fe7700
Show file tree
Hide file tree
Showing 38 changed files with 1,760 additions and 295 deletions.
9 changes: 3 additions & 6 deletions bridges/snowbridge/runtime/test-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ use snowbridge_pallet_ethereum_client_fixtures::*;
use sp_core::{Get, H160, U256};
use sp_keyring::AccountKeyring::*;
use sp_runtime::{traits::Header, AccountId32, DigestItem, SaturatedConversion, Saturating};
use xcm::{
latest::prelude::*,
v3::Error::{self, Barrier},
};
use xcm::latest::prelude::*;
use xcm_executor::XcmExecutor;

type RuntimeHelper<Runtime, AllPalletsWithoutSystem = ()> =
Expand Down Expand Up @@ -374,7 +371,7 @@ pub fn send_unpaid_transfer_token_message<Runtime, XcmConfig>(
Weight::zero(),
);
// check error is barrier
assert_err!(outcome.ensure_complete(), Barrier);
assert_err!(outcome.ensure_complete(), XcmError::Barrier);
});
}

Expand All @@ -388,7 +385,7 @@ pub fn send_transfer_token_message_failure<Runtime, XcmConfig>(
weth_contract_address: H160,
destination_address: H160,
fee_amount: u128,
expected_error: Error,
expected_error: XcmError,
) where
Runtime: frame_system::Config
+ pallet_balances::Config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use sp_keyring::Sr25519Keyring as Keyring;

// Cumulus
use emulated_integration_tests_common::{
accounts, build_genesis_storage, collators, PenpalSiblingSovereignAccount,
PenpalTeleportableAssetLocation, RESERVABLE_ASSET_ID, SAFE_XCM_VERSION, USDT_ID,
accounts, build_genesis_storage, collators, PenpalASiblingSovereignAccount,
PenpalATeleportableAssetLocation, PenpalBSiblingSovereignAccount,
PenpalBTeleportableAssetLocation, RESERVABLE_ASSET_ID, SAFE_XCM_VERSION, USDT_ID,
};
use parachains_common::{AccountId, Balance};

Expand Down Expand Up @@ -77,10 +78,17 @@ pub fn genesis() -> Storage {
},
foreign_assets: asset_hub_rococo_runtime::ForeignAssetsConfig {
assets: vec![
// Penpal's teleportable asset representation
// PenpalA's teleportable asset representation
(
PenpalTeleportableAssetLocation::get(),
PenpalSiblingSovereignAccount::get(),
PenpalATeleportableAssetLocation::get(),
PenpalASiblingSovereignAccount::get(),
false,
ED,
),
// PenpalB's teleportable asset representation
(
PenpalBTeleportableAssetLocation::get(),
PenpalBSiblingSovereignAccount::get(),
false,
ED,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use sp_keyring::Sr25519Keyring as Keyring;

// Cumulus
use emulated_integration_tests_common::{
accounts, build_genesis_storage, collators, PenpalBSiblingSovereignAccount,
PenpalBTeleportableAssetLocation, PenpalSiblingSovereignAccount,
PenpalTeleportableAssetLocation, RESERVABLE_ASSET_ID, SAFE_XCM_VERSION, USDT_ID,
accounts, build_genesis_storage, collators, PenpalASiblingSovereignAccount,
PenpalATeleportableAssetLocation, PenpalBSiblingSovereignAccount,
PenpalBTeleportableAssetLocation, RESERVABLE_ASSET_ID, SAFE_XCM_VERSION, USDT_ID,
};
use parachains_common::{AccountId, Balance};

Expand Down Expand Up @@ -75,10 +75,10 @@ pub fn genesis() -> Storage {
},
foreign_assets: asset_hub_westend_runtime::ForeignAssetsConfig {
assets: vec![
// Penpal's teleportable asset representation
// PenpalA's teleportable asset representation
(
PenpalTeleportableAssetLocation::get(),
PenpalSiblingSovereignAccount::get(),
PenpalATeleportableAssetLocation::get(),
PenpalASiblingSovereignAccount::get(),
false,
ED,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,26 @@ pub const TELEPORTABLE_ASSET_ID: u32 = 2;
// USDT registered on AH as (trust-backed) Asset and reserve-transferred between Parachain and AH
pub const USDT_ID: u32 = 1984;

pub const PENPAL_ID: u32 = 2000;
pub const PENPAL_A_ID: u32 = 2000;
pub const PENPAL_B_ID: u32 = 2001;
pub const ASSETS_PALLET_ID: u8 = 50;

parameter_types! {
pub PenpalTeleportableAssetLocation: xcm::v5::Location
pub PenpalATeleportableAssetLocation: xcm::v5::Location
= xcm::v5::Location::new(1, [
xcm::v5::Junction::Parachain(PENPAL_ID),
xcm::v5::Junction::Parachain(PENPAL_A_ID),
xcm::v5::Junction::PalletInstance(ASSETS_PALLET_ID),
xcm::v5::Junction::GeneralIndex(TELEPORTABLE_ASSET_ID.into()),
]
);
pub PenpalSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_ID).into_account_truncating();
pub PenpalBTeleportableAssetLocation: xcm::v5::Location
= xcm::v5::Location::new(1, [
xcm::v5::Junction::Parachain(PENPAL_B_ID),
xcm::v5::Junction::PalletInstance(ASSETS_PALLET_ID),
xcm::v5::Junction::GeneralIndex(TELEPORTABLE_ASSET_ID.into()),
]
);
pub PenpalASiblingSovereignAccount: AccountId = Sibling::from(PENPAL_A_ID).into_account_truncating();
pub PenpalBSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_B_ID).into_account_truncating();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
imports::*,
tests::teleport::do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using_xt,
};
use xcm::latest::AssetTransferFilter;

fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
Expand Down Expand Up @@ -819,3 +820,86 @@ fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
// should be non-zero
assert!(receiver_assets_after < receiver_assets_before + amount_to_send);
}

// ==============================================================================================
// ==== Bidirectional Transfer - Native + Teleportable Foreign Assets - Parachain<->AssetHub ====
// ==============================================================================================
/// Transfers of native asset plus teleportable foreign asset from Parachain to AssetHub and back
/// with fees paid using native asset.
#[test]
fn bidirectional_transfer_multiple_assets_between_penpal_and_asset_hub() {
fn execute_xcm_penpal_to_asset_hub(t: ParaToSystemParaTest) -> DispatchResult {
let all_assets = t.args.assets.clone().into_inner();
let mut assets = all_assets.clone();
let mut fees = assets.remove(t.args.fee_asset_item as usize);
// TODO(https://github.com/paritytech/polkadot-sdk/issues/6197): dry-run to get exact fees.
// For now just use half the fees locally, half on dest
if let Fungible(fees_amount) = fees.fun {
fees.fun = Fungible(fees_amount / 2);
}
// xcm to be executed at dest
let xcm_on_dest = Xcm(vec![
// since this is the last hop, we don't need to further use any assets previously
// reserved for fees (there are no further hops to cover transport fees for); we
// RefundSurplus to get back any unspent fees
RefundSurplus,
DepositAsset { assets: Wild(All), beneficiary: t.args.beneficiary },
]);
let xcm = Xcm::<()>(vec![
WithdrawAsset(all_assets.into()),
PayFees { asset: fees.clone() },
InitiateTransfer {
destination: t.args.dest,
remote_fees: Some(AssetTransferFilter::ReserveWithdraw(fees.into())),
assets: vec![AssetTransferFilter::Teleport(assets.into())],
remote_xcm: xcm_on_dest,
},
]);
<PenpalA as PenpalAPallet>::PolkadotXcm::execute(
t.signed_origin,
bx!(xcm::VersionedXcm::from(xcm.into())),
Weight::MAX,
)
.unwrap();
Ok(())
}
fn execute_xcm_asset_hub_to_penpal(t: SystemParaToParaTest) -> DispatchResult {
let all_assets = t.args.assets.clone().into_inner();
let mut assets = all_assets.clone();
let mut fees = assets.remove(t.args.fee_asset_item as usize);
// TODO(https://github.com/paritytech/polkadot-sdk/issues/6197): dry-run to get exact fees.
// For now just use half the fees locally, half on dest
if let Fungible(fees_amount) = fees.fun {
fees.fun = Fungible(fees_amount / 2);
}
// xcm to be executed at dest
let xcm_on_dest = Xcm(vec![
// since this is the last hop, we don't need to further use any assets previously
// reserved for fees (there are no further hops to cover transport fees for); we
// RefundSurplus to get back any unspent fees
RefundSurplus,
DepositAsset { assets: Wild(All), beneficiary: t.args.beneficiary },
]);
let xcm = Xcm::<()>(vec![
WithdrawAsset(all_assets.into()),
PayFees { asset: fees.clone() },
InitiateTransfer {
destination: t.args.dest,
remote_fees: Some(AssetTransferFilter::ReserveDeposit(fees.into())),
assets: vec![AssetTransferFilter::Teleport(assets.into())],
remote_xcm: xcm_on_dest,
},
]);
<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::execute(
t.signed_origin,
bx!(xcm::VersionedXcm::from(xcm.into())),
Weight::MAX,
)
.unwrap();
Ok(())
}
do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using_xt(
execute_xcm_penpal_to_asset_hub,
execute_xcm_asset_hub_to_penpal,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,6 @@ fn ah_to_penpal_foreign_assets_sender_assertions(t: SystemParaToParaTest) {
assert_expected_events!(
AssetHubWestend,
vec![
// native asset used for fees is transferred to Parachain's Sovereign account as reserve
RuntimeEvent::Balances(
pallet_balances::Event::Transfer { from, to, amount }
) => {
from: *from == t.sender.account_id,
to: *to == AssetHubWestend::sovereign_account_id_of(
t.args.dest.clone()
),
amount: *amount == t.args.amount,
},
// foreign asset is burned locally as part of teleportation
RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
asset_id: *asset_id == expected_foreign_asset_id,
Expand Down Expand Up @@ -283,13 +273,13 @@ pub fn do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using
ah_to_para_dispatchable: fn(SystemParaToParaTest) -> DispatchResult,
) {
// Init values for Parachain
let fee_amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 100;
let fee_amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 1000;
let asset_location_on_penpal = PenpalLocalTeleportableToAssetHub::get();
let asset_id_on_penpal = match asset_location_on_penpal.last() {
Some(Junction::GeneralIndex(id)) => *id as u32,
_ => unreachable!(),
};
let asset_amount_to_send = ASSET_HUB_WESTEND_ED * 100;
let asset_amount_to_send = ASSET_HUB_WESTEND_ED * 1000;
let asset_owner = PenpalAssetOwner::get();
let system_para_native_asset_location = RelayLocation::get();
let sender = PenpalASender::get();
Expand Down Expand Up @@ -318,7 +308,7 @@ pub fn do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using
<PenpalA as Chain>::RuntimeOrigin::signed(asset_owner.clone()),
asset_id_on_penpal,
sender.clone(),
asset_amount_to_send,
asset_amount_to_send * 2,
);
// fund Parachain's check account to be able to teleport
PenpalA::fund_accounts(vec![(
Expand Down Expand Up @@ -433,6 +423,10 @@ pub fn do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using
));
});

// Only send back half the amount.
let asset_amount_to_send = asset_amount_to_send / 2;
let fee_amount_to_send = fee_amount_to_send / 2;

let ah_to_penpal_beneficiary_id = PenpalAReceiver::get();
let penpal_as_seen_by_ah = AssetHubWestend::sibling_location_of(PenpalA::para_id());
let ah_assets: Assets = vec![
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ mod imports {
penpal_emulated_chain::{
penpal_runtime::xcm_config::{
CustomizableAssetFromSystemAssetHub as PenpalCustomizableAssetFromSystemAssetHub,
LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub,
UniversalLocation as PenpalUniversalLocation,
},
PenpalAssetOwner, PenpalBParaPallet as PenpalBPallet,
Expand Down
Loading

0 comments on commit 8fe7700

Please sign in to comment.