pallet-xcm::transfer_assets_using_type() supports custom actions on destination (#4260)

Change `transfer_assets_using_type()` to not assume `DepositAssets` as
the intended use of the assets on the destination.

Instead provides the caller with the ability to specify custom XCM that
be executed on `dest` chain as the last step of the transfer, thus
allowing custom usecases for the transferred assets. E.g. some are
used/swapped/etc there, while some are sent further to yet another
chain.

Note: this is a follow-up on
https://github.com/paritytech/polkadot-sdk/pull/3695, bringing in an API
change for `transfer_assets_using_type()`. This is ok as the previous
version has not been yet released. Thus, its first release will include
the new API proposed by this PR.

This allows usecases such as:
https://forum.polkadot.network/t/managing-sas-on-multiple-reserve-chains-for-same-asset/7538/4

BTW: all this pallet-xcm asset transfers code will be massively reduced
once we have https://github.com/paritytech/xcm-format/pull/54

---------

Signed-off-by: Adrian Catangiu <adrian@parity.io>
This commit is contained in:
Adrian Catangiu
2024-04-24 11:44:42 +03:00
committed by GitHub
parent 9a0049d0da
commit e0584a153d
12 changed files with 531 additions and 116 deletions
@@ -70,7 +70,9 @@ mod imports {
LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub,
LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub,
};
pub use rococo_runtime::xcm_config::XcmConfig as RococoXcmConfig;
pub use rococo_runtime::xcm_config::{
UniversalLocation as RococoUniversalLocation, XcmConfig as RococoXcmConfig,
};
pub const ASSET_ID: u32 = 3;
pub const ASSET_MIN_BALANCE: u128 = 1000;
@@ -83,6 +85,7 @@ mod imports {
pub type ParaToSystemParaTest = Test<PenpalA, AssetHubRococo>;
pub type ParaToParaThroughRelayTest = Test<PenpalA, PenpalB, Rococo>;
pub type ParaToParaThroughAHTest = Test<PenpalA, PenpalB, AssetHubRococo>;
pub type RelayToParaThroughAHTest = Test<Rococo, PenpalA, AssetHubRococo>;
}
#[cfg(test)]
@@ -54,14 +54,18 @@ fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) {
fn ah_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<AssetHubRococo as AssetHubRococoPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<AssetHubRococo as AssetHubRococoPallet>::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()),
bx!(TransferType::LocalReserve),
bx!(fee.id.into()),
bx!(TransferType::LocalReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -69,14 +73,18 @@ fn ah_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
fn para_to_ah_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::DestinationReserve),
bx!(fee.id.into()),
bx!(TransferType::DestinationReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -85,14 +93,18 @@ fn para_to_para_transfer_assets_through_ah(t: ParaToParaThroughAHTest) -> Dispat
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
let asset_hub_location: Location = PenpalA::sibling_location_of(AssetHubRococo::para_id());
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::RemoteReserve(asset_hub_location.clone().into())),
bx!(fee.id.into()),
bx!(TransferType::RemoteReserve(asset_hub_location.into())),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -100,14 +112,18 @@ fn para_to_para_transfer_assets_through_ah(t: ParaToParaThroughAHTest) -> Dispat
fn para_to_asset_hub_teleport_foreign_assets(t: ParaToSystemParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::DestinationReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -115,14 +131,18 @@ fn para_to_asset_hub_teleport_foreign_assets(t: ParaToSystemParaTest) -> Dispatc
fn asset_hub_to_para_teleport_foreign_assets(t: SystemParaToParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<AssetHubRococo as AssetHubRococoPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<AssetHubRococo as AssetHubRococoPallet>::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()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::LocalReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -626,3 +646,166 @@ fn bidirectional_teleport_foreign_asset_between_para_and_asset_hub_using_explici
asset_hub_to_para_teleport_foreign_assets,
);
}
// ===============================================================
// ===== Transfer - Native Asset - Relay->AssetHub->Parachain ====
// ===============================================================
/// Transfers of native asset Relay to Parachain (using AssetHub reserve). Parachains want to avoid
/// managing SAs on all system chains, thus want all their DOT-in-reserve to be held in their
/// Sovereign Account on Asset Hub.
#[test]
fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
// Init values for Relay
let destination = Rococo::child_location_of(PenpalA::para_id());
let sender = RococoSender::get();
let amount_to_send: Balance = ROCOCO_ED * 1000;
// Init values for Parachain
let relay_native_asset_location = RelayLocation::get();
let receiver = PenpalAReceiver::get();
// Init Test
let test_args = TestContext {
sender,
receiver: receiver.clone(),
args: TestArgs::new_relay(destination.clone(), receiver.clone(), amount_to_send),
};
let mut test = RelayToParaThroughAHTest::new(test_args);
let sov_penpal_on_ah = AssetHubRococo::sovereign_account_id_of(
AssetHubRococo::sibling_location_of(PenpalA::para_id()),
);
// Query initial balances
let sender_balance_before = test.sender.balance;
let sov_penpal_on_ah_before = AssetHubRococo::execute_with(|| {
<AssetHubRococo as AssetHubRococoPallet>::Balances::free_balance(sov_penpal_on_ah.clone())
});
let receiver_assets_before = PenpalA::execute_with(|| {
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &receiver)
});
fn relay_assertions(t: RelayToParaThroughAHTest) {
type RuntimeEvent = <Rococo as Chain>::RuntimeEvent;
Rococo::assert_xcm_pallet_attempted_complete(None);
assert_expected_events!(
Rococo,
vec![
// Amount to teleport is withdrawn from Sender
RuntimeEvent::Balances(pallet_balances::Event::Burned { who, amount }) => {
who: *who == t.sender.account_id,
amount: *amount == t.args.amount,
},
// Amount to teleport is deposited in Relay's `CheckAccount`
RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) => {
who: *who == <Rococo as RococoPallet>::XcmPallet::check_account(),
amount: *amount == t.args.amount,
},
]
);
}
fn asset_hub_assertions(_: RelayToParaThroughAHTest) {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
let sov_penpal_on_ah = AssetHubRococo::sovereign_account_id_of(
AssetHubRococo::sibling_location_of(PenpalA::para_id()),
);
assert_expected_events!(
AssetHubRococo,
vec![
// Deposited to receiver parachain SA
RuntimeEvent::Balances(
pallet_balances::Event::Minted { who, .. }
) => {
who: *who == sov_penpal_on_ah,
},
RuntimeEvent::MessageQueue(
pallet_message_queue::Event::Processed { success: true, .. }
) => {},
]
);
}
fn penpal_assertions(t: RelayToParaThroughAHTest) {
type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
let expected_id =
t.args.assets.into_inner().first().unwrap().id.0.clone().try_into().unwrap();
assert_expected_events!(
PenpalA,
vec![
RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => {
asset_id: *asset_id == expected_id,
owner: *owner == t.receiver.account_id,
},
]
);
}
fn transfer_assets_dispatchable(t: RelayToParaThroughAHTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
let asset_hub_location = Rococo::child_location_of(AssetHubRococo::para_id());
let context = RococoUniversalLocation::get();
// reanchor fees to the view of destination (Penpal)
let mut remote_fees = fee.clone().reanchored(&t.args.dest, &context).unwrap();
if let Fungible(ref mut amount) = remote_fees.fun {
// we already spent some fees along the way, just use half of what we started with
*amount = *amount / 2;
}
let xcm_on_final_dest = Xcm::<()>(vec![
BuyExecution { fees: remote_fees, weight_limit: t.args.weight_limit.clone() },
DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
},
]);
// reanchor final dest (Penpal) to the view of hop (Asset Hub)
let mut dest = t.args.dest.clone();
dest.reanchor(&asset_hub_location, &context).unwrap();
// on Asset Hub, forward assets to Penpal
let xcm_on_hop = Xcm::<()>(vec![DepositReserveAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
dest,
xcm: xcm_on_final_dest,
}]);
// First leg is a teleport, from there a local-reserve-transfer to final dest
<Rococo as RococoPallet>::XcmPallet::transfer_assets_using_type_and_then(
t.signed_origin,
bx!(asset_hub_location.into()),
bx!(t.args.assets.into()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::Teleport),
bx!(VersionedXcm::from(xcm_on_hop)),
t.args.weight_limit,
)
}
// Set assertions and dispatchables
test.set_assertion::<Rococo>(relay_assertions);
test.set_assertion::<AssetHubRococo>(asset_hub_assertions);
test.set_assertion::<PenpalA>(penpal_assertions);
test.set_dispatchable::<Rococo>(transfer_assets_dispatchable);
test.assert();
// Query final balances
let sender_balance_after = test.sender.balance;
let sov_penpal_on_ah_after = AssetHubRococo::execute_with(|| {
<AssetHubRococo as AssetHubRococoPallet>::Balances::free_balance(sov_penpal_on_ah)
});
let receiver_assets_after = PenpalA::execute_with(|| {
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location, &receiver)
});
// Sender's balance is reduced by amount sent plus delivery fees
assert!(sender_balance_after < sender_balance_before - amount_to_send);
// SA on AH balance is increased
assert!(sov_penpal_on_ah_after > sov_penpal_on_ah_before);
// Receiver's asset balance is increased
assert!(receiver_assets_after > receiver_assets_before);
// Receiver's asset balance increased by `amount_to_send - delivery_fees - bought_execution`;
// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
// should be non-zero
assert!(receiver_assets_after < receiver_assets_before + amount_to_send);
}
@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod foreign_assets_transfers;
mod hybrid_transfers;
mod reserve_transfer;
mod send;
mod set_xcm_versions;
@@ -574,7 +574,7 @@ fn reserve_transfer_native_asset_from_relay_to_para() {
let sender = RococoSender::get();
let amount_to_send: Balance = ROCOCO_ED * 1000;
// Init values fot Parachain
// Init values for Parachain
let relay_native_asset_location = RelayLocation::get();
let receiver = PenpalAReceiver::get();
@@ -74,7 +74,9 @@ mod imports {
LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub,
LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub,
};
pub use westend_runtime::xcm_config::XcmConfig as WestendXcmConfig;
pub use westend_runtime::xcm_config::{
UniversalLocation as WestendUniversalLocation, XcmConfig as WestendXcmConfig,
};
pub const ASSET_ID: u32 = 3;
pub const ASSET_MIN_BALANCE: u128 = 1000;
@@ -87,6 +89,7 @@ mod imports {
pub type ParaToSystemParaTest = Test<PenpalA, AssetHubWestend>;
pub type ParaToParaThroughRelayTest = Test<PenpalA, PenpalB, Westend>;
pub type ParaToParaThroughAHTest = Test<PenpalA, PenpalB, AssetHubWestend>;
pub type RelayToParaThroughAHTest = Test<Westend, PenpalA, AssetHubWestend>;
}
#[cfg(test)]
@@ -54,14 +54,18 @@ fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) {
fn ah_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::LocalReserve),
bx!(fee.id.into()),
bx!(TransferType::LocalReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -69,14 +73,18 @@ fn ah_to_para_transfer_assets(t: SystemParaToParaTest) -> DispatchResult {
fn para_to_ah_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::DestinationReserve),
bx!(fee.id.into()),
bx!(TransferType::DestinationReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -85,14 +93,18 @@ fn para_to_para_transfer_assets_through_ah(t: ParaToParaThroughAHTest) -> Dispat
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
let asset_hub_location: Location = PenpalA::sibling_location_of(AssetHubWestend::para_id());
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::RemoteReserve(asset_hub_location.clone().into())),
bx!(fee.id.into()),
bx!(TransferType::RemoteReserve(asset_hub_location.into())),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -100,14 +112,18 @@ fn para_to_para_transfer_assets_through_ah(t: ParaToParaThroughAHTest) -> Dispat
fn para_to_asset_hub_teleport_foreign_assets(t: ParaToSystemParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::DestinationReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -115,14 +131,18 @@ fn para_to_asset_hub_teleport_foreign_assets(t: ParaToSystemParaTest) -> Dispatc
fn asset_hub_to_para_teleport_foreign_assets(t: SystemParaToParaTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::transfer_assets_using_type(
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
}]);
<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()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::LocalReserve),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
t.args.weight_limit,
)
}
@@ -627,3 +647,166 @@ fn bidirectional_teleport_foreign_asset_between_para_and_asset_hub_using_explici
asset_hub_to_para_teleport_foreign_assets,
);
}
// ===============================================================
// ===== Transfer - Native Asset - Relay->AssetHub->Parachain ====
// ===============================================================
/// Transfers of native asset Relay to Parachain (using AssetHub reserve). Parachains want to avoid
/// managing SAs on all system chains, thus want all their DOT-in-reserve to be held in their
/// Sovereign Account on Asset Hub.
#[test]
fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
// Init values for Relay
let destination = Westend::child_location_of(PenpalA::para_id());
let sender = WestendSender::get();
let amount_to_send: Balance = WESTEND_ED * 1000;
// Init values for Parachain
let relay_native_asset_location = RelayLocation::get();
let receiver = PenpalAReceiver::get();
// Init Test
let test_args = TestContext {
sender,
receiver: receiver.clone(),
args: TestArgs::new_relay(destination.clone(), receiver.clone(), amount_to_send),
};
let mut test = RelayToParaThroughAHTest::new(test_args);
let sov_penpal_on_ah = AssetHubWestend::sovereign_account_id_of(
AssetHubWestend::sibling_location_of(PenpalA::para_id()),
);
// Query initial balances
let sender_balance_before = test.sender.balance;
let sov_penpal_on_ah_before = AssetHubWestend::execute_with(|| {
<AssetHubWestend as AssetHubWestendPallet>::Balances::free_balance(sov_penpal_on_ah.clone())
});
let receiver_assets_before = PenpalA::execute_with(|| {
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &receiver)
});
fn relay_assertions(t: RelayToParaThroughAHTest) {
type RuntimeEvent = <Westend as Chain>::RuntimeEvent;
Westend::assert_xcm_pallet_attempted_complete(None);
assert_expected_events!(
Westend,
vec![
// Amount to teleport is withdrawn from Sender
RuntimeEvent::Balances(pallet_balances::Event::Burned { who, amount }) => {
who: *who == t.sender.account_id,
amount: *amount == t.args.amount,
},
// Amount to teleport is deposited in Relay's `CheckAccount`
RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) => {
who: *who == <Westend as WestendPallet>::XcmPallet::check_account(),
amount: *amount == t.args.amount,
},
]
);
}
fn asset_hub_assertions(_: RelayToParaThroughAHTest) {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
let sov_penpal_on_ah = AssetHubWestend::sovereign_account_id_of(
AssetHubWestend::sibling_location_of(PenpalA::para_id()),
);
assert_expected_events!(
AssetHubWestend,
vec![
// Deposited to receiver parachain SA
RuntimeEvent::Balances(
pallet_balances::Event::Minted { who, .. }
) => {
who: *who == sov_penpal_on_ah,
},
RuntimeEvent::MessageQueue(
pallet_message_queue::Event::Processed { success: true, .. }
) => {},
]
);
}
fn penpal_assertions(t: RelayToParaThroughAHTest) {
type RuntimeEvent = <PenpalA as Chain>::RuntimeEvent;
let expected_id =
t.args.assets.into_inner().first().unwrap().id.0.clone().try_into().unwrap();
assert_expected_events!(
PenpalA,
vec![
RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => {
asset_id: *asset_id == expected_id,
owner: *owner == t.receiver.account_id,
},
]
);
}
fn transfer_assets_dispatchable(t: RelayToParaThroughAHTest) -> DispatchResult {
let fee_idx = t.args.fee_asset_item as usize;
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
let asset_hub_location = Westend::child_location_of(AssetHubWestend::para_id());
let context = WestendUniversalLocation::get();
// reanchor fees to the view of destination (Penpal)
let mut remote_fees = fee.clone().reanchored(&t.args.dest, &context).unwrap();
if let Fungible(ref mut amount) = remote_fees.fun {
// we already spent some fees along the way, just use half of what we started with
*amount = *amount / 2;
}
let xcm_on_final_dest = Xcm::<()>(vec![
BuyExecution { fees: remote_fees, weight_limit: t.args.weight_limit.clone() },
DepositAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
beneficiary: t.args.beneficiary,
},
]);
// reanchor final dest (Penpal) to the view of hop (Asset Hub)
let mut dest = t.args.dest.clone();
dest.reanchor(&asset_hub_location, &context).unwrap();
// on Asset Hub, forward assets to Penpal
let xcm_on_hop = Xcm::<()>(vec![DepositReserveAsset {
assets: Wild(AllCounted(t.args.assets.len() as u32)),
dest,
xcm: xcm_on_final_dest,
}]);
// First leg is a teleport, from there a local-reserve-transfer to final dest
<Westend as WestendPallet>::XcmPallet::transfer_assets_using_type_and_then(
t.signed_origin,
bx!(asset_hub_location.into()),
bx!(t.args.assets.into()),
bx!(TransferType::Teleport),
bx!(fee.id.into()),
bx!(TransferType::Teleport),
bx!(VersionedXcm::from(xcm_on_hop)),
t.args.weight_limit,
)
}
// Set assertions and dispatchables
test.set_assertion::<Westend>(relay_assertions);
test.set_assertion::<AssetHubWestend>(asset_hub_assertions);
test.set_assertion::<PenpalA>(penpal_assertions);
test.set_dispatchable::<Westend>(transfer_assets_dispatchable);
test.assert();
// Query final balances
let sender_balance_after = test.sender.balance;
let sov_penpal_on_ah_after = AssetHubWestend::execute_with(|| {
<AssetHubWestend as AssetHubWestendPallet>::Balances::free_balance(sov_penpal_on_ah)
});
let receiver_assets_after = PenpalA::execute_with(|| {
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location, &receiver)
});
// Sender's balance is reduced by amount sent plus delivery fees
assert!(sender_balance_after < sender_balance_before - amount_to_send);
// SA on AH balance is increased
assert!(sov_penpal_on_ah_after > sov_penpal_on_ah_before);
// Receiver's asset balance is increased
assert!(receiver_assets_after > receiver_assets_before);
// Receiver's asset balance increased by `amount_to_send - delivery_fees - bought_execution`;
// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
// should be non-zero
assert!(receiver_assets_after < receiver_assets_before + amount_to_send);
}
@@ -14,7 +14,7 @@
// limitations under the License.
mod fellowship_treasury;
mod foreign_assets_transfers;
mod hybrid_transfers;
mod reserve_transfer;
mod send;
mod set_xcm_versions;
@@ -574,7 +574,7 @@ fn reserve_transfer_native_asset_from_relay_to_para() {
let sender = WestendSender::get();
let amount_to_send: Balance = WESTEND_ED * 1000;
// Init values fot Parachain
// Init values for Parachain
let relay_native_asset_location = RelayLocation::get();
let receiver = PenpalAReceiver::get();
@@ -60,15 +60,19 @@ fn send_asset_from_penpal_rococo_through_local_asset_hub_to_westend_asset_hub(
AccountId32Junction { network: None, id: AssetHubWestendReceiver::get().into() }.into();
let assets: Assets = (id.clone(), transfer_amount).into();
let fees_id: AssetId = id.into();
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(assets.len() as u32)),
beneficiary,
}]);
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type(
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type_and_then(
signed_origin,
bx!(destination.into()),
bx!(beneficiary.into()),
bx!(assets.clone().into()),
bx!(TransferType::RemoteReserve(local_asset_hub.clone().into())),
bx!(fees_id.into()),
bx!(TransferType::RemoteReserve(local_asset_hub.into())),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
WeightLimit::Unlimited,
)
}));
@@ -59,15 +59,19 @@ fn send_asset_from_penpal_westend_through_local_asset_hub_to_rococo_asset_hub(
AccountId32Junction { network: None, id: AssetHubRococoReceiver::get().into() }.into();
let assets: Assets = (id.clone(), transfer_amount).into();
let fees_id: AssetId = id.into();
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
assets: Wild(AllCounted(assets.len() as u32)),
beneficiary,
}]);
<PenpalB as PenpalBPallet>::PolkadotXcm::transfer_assets_using_type(
<PenpalB as PenpalBPallet>::PolkadotXcm::transfer_assets_using_type_and_then(
signed_origin,
bx!(destination.into()),
bx!(beneficiary.into()),
bx!(assets.into()),
bx!(TransferType::RemoteReserve(local_asset_hub.clone().into())),
bx!(fees_id.into()),
bx!(TransferType::RemoteReserve(local_asset_hub.into())),
bx!(VersionedXcm::from(custom_xcm_on_dest)),
WeightLimit::Unlimited,
)
}));
+113 -85
View File
@@ -45,7 +45,7 @@ use sp_runtime::{
AccountIdConversion, BadOrigin, BlakeTwo256, BlockNumberProvider, Dispatchable, Hash,
Saturating, Zero,
},
RuntimeDebug,
Either, RuntimeDebug,
};
use sp_std::{boxed::Box, marker::PhantomData, prelude::*, result::Result, vec};
use xcm::{latest::QueryResponseInfo, prelude::*};
@@ -1311,7 +1311,7 @@ pub mod pallet {
Self::do_transfer_assets(
origin,
dest,
beneficiary,
Either::Left(beneficiary),
assets,
assets_transfer_type,
fee_asset_item,
@@ -1421,50 +1421,60 @@ pub mod pallet {
/// - `TransferType::Teleport`: burn local assets and forward XCM to `dest` chain to
/// mint/teleport assets and deposit them to `beneficiary`.
///
/// Fee payment on the source, destination and all intermediary hops, is specified through
/// `fees_id`, but make sure enough of the specified `fees_id` asset is included in the
/// given list of `assets`. `fees_id` should be enough to pay for `weight_limit`. If more
/// weight is needed than `weight_limit`, then the operation will fail and the sent assets
/// may be at risk.
/// On the destination chain, as well as any intermediary hops, `BuyExecution` is used to
/// buy execution using transferred `assets` identified by `remote_fees_id`.
/// Make sure enough of the specified `remote_fees_id` asset is included in the given list
/// of `assets`. `remote_fees_id` should be enough to pay for `weight_limit`. If more weight
/// is needed than `weight_limit`, then the operation will fail and the sent assets may be
/// at risk.
///
/// `fees_id` may use different transfer type than rest of `assets` and can be specified
/// through `fees_transfer_type`.
/// `remote_fees_id` may use different transfer type than rest of `assets` and can be
/// specified through `fees_transfer_type`.
///
/// The caller needs to specify what should happen to the transferred assets once they reach
/// the `dest` chain. This is done through the `custom_xcm_on_dest` parameter, which
/// contains the instructions to execute on `dest` as a final step.
/// This is usually as simple as:
/// `Xcm(vec![DepositAsset { assets: Wild(AllCounted(assets.len())), beneficiary }])`,
/// but could be something more exotic like sending the `assets` even further.
///
/// - `origin`: Must be capable of withdrawing the `assets` and executing XCM.
/// - `dest`: Destination context for the assets. Will typically be `[Parent,
/// Parachain(..)]` to send from parachain to parachain, or `[Parachain(..)]` to send from
/// relay to parachain, or `(parents: 2, (GlobalConsensus(..), ..))` to send from
/// parachain across a bridge to another ecosystem destination.
/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
/// generally be an `AccountId32` value.
/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
/// fee on the `dest` (and possibly reserve) chains.
/// - `assets_transfer_type`: The XCM `TransferType` used to transfer the `assets`.
/// - `fees_id`: One of the included `assets` to be be used to pay fees.
/// - `remote_fees_id`: One of the included `assets` to be be used to pay fees.
/// - `fees_transfer_type`: The XCM `TransferType` used to transfer the `fees` assets.
/// - `custom_xcm_on_dest`: The XCM to be executed on `dest` chain as the last step of the
/// transfer, which also determines what happens to the assets on the destination chain.
/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
#[pallet::call_index(15)]
#[pallet::weight(T::WeightInfo::transfer_assets())]
pub fn transfer_assets_using_type(
pub fn transfer_assets_using_type_and_then(
origin: OriginFor<T>,
dest: Box<VersionedLocation>,
beneficiary: Box<VersionedLocation>,
assets: Box<VersionedAssets>,
assets_transfer_type: Box<TransferType>,
fees_id: Box<VersionedAssetId>,
remote_fees_id: Box<VersionedAssetId>,
fees_transfer_type: Box<TransferType>,
custom_xcm_on_dest: Box<VersionedXcm<()>>,
weight_limit: WeightLimit,
) -> DispatchResult {
let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?;
let dest: Location = (*dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
let beneficiary: Location =
(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
let assets: Assets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
let fees_id: AssetId = (*fees_id).try_into().map_err(|()| Error::<T>::BadVersion)?;
let fees_id: AssetId =
(*remote_fees_id).try_into().map_err(|()| Error::<T>::BadVersion)?;
let remote_xcm: Xcm<()> =
(*custom_xcm_on_dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
log::debug!(
target: "xcm::pallet_xcm::transfer_assets_using_type",
"origin {:?}, dest {:?}, beneficiary {:?}, assets {:?} through {:?}, fees-id {:?} through {:?}",
origin_location, dest, beneficiary, assets, assets_transfer_type, fees_id, fees_transfer_type,
target: "xcm::pallet_xcm::transfer_assets_using_type_and_then",
"origin {origin_location:?}, dest {dest:?}, assets {assets:?} through {assets_transfer_type:?}, \
remote_fees_id {fees_id:?} through {fees_transfer_type:?}, \
custom_xcm_on_dest {remote_xcm:?}, weight-limit {weight_limit:?}",
);
let assets = assets.into_inner();
@@ -1475,7 +1485,7 @@ pub mod pallet {
Self::do_transfer_assets(
origin_location,
dest,
beneficiary,
Either::Right(remote_xcm),
assets,
*assets_transfer_type,
fee_asset_index,
@@ -1650,7 +1660,7 @@ impl<T: Config> Pallet<T> {
let (local_xcm, remote_xcm) = Self::build_xcm_transfer_type(
origin.clone(),
dest.clone(),
beneficiary,
Either::Left(beneficiary),
assets,
assets_transfer_type,
FeesHandling::Batched { fees },
@@ -1692,7 +1702,7 @@ impl<T: Config> Pallet<T> {
let (local_xcm, remote_xcm) = Self::build_xcm_transfer_type(
origin_location.clone(),
dest.clone(),
beneficiary,
Either::Left(beneficiary),
assets,
TransferType::Teleport,
FeesHandling::Batched { fees },
@@ -1704,7 +1714,7 @@ impl<T: Config> Pallet<T> {
fn do_transfer_assets(
origin: Location,
dest: Location,
beneficiary: Location,
beneficiary: Either<Location, Xcm<()>>,
mut assets: Vec<Asset>,
assets_transfer_type: TransferType,
fee_asset_index: usize,
@@ -1770,7 +1780,7 @@ impl<T: Config> Pallet<T> {
fn build_xcm_transfer_type(
origin: Location,
dest: Location,
beneficiary: Location,
beneficiary: Either<Location, Xcm<()>>,
assets: Vec<Asset>,
transfer_type: TransferType,
fees: FeesHandling<T>,
@@ -1782,57 +1792,51 @@ impl<T: Config> Pallet<T> {
fees_handling {:?}, weight_limit: {:?}",
origin, dest, beneficiary, assets, transfer_type, fees, weight_limit,
);
Ok(match transfer_type {
TransferType::LocalReserve => {
let (local, remote) = Self::local_reserve_transfer_programs(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)?;
(local, Some(remote))
},
TransferType::DestinationReserve => {
let (local, remote) = Self::destination_reserve_transfer_programs(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)?;
(local, Some(remote))
},
match transfer_type {
TransferType::LocalReserve => Self::local_reserve_transfer_programs(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)
.map(|(local, remote)| (local, Some(remote))),
TransferType::DestinationReserve => Self::destination_reserve_transfer_programs(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)
.map(|(local, remote)| (local, Some(remote))),
TransferType::RemoteReserve(reserve) => {
let fees = match fees {
FeesHandling::Batched { fees } => fees,
_ => return Err(Error::<T>::InvalidAssetUnsupportedReserve.into()),
};
let local = Self::remote_reserve_transfer_program(
Self::remote_reserve_transfer_program(
origin.clone(),
reserve.try_into().map_err(|()| Error::<T>::BadVersion)?,
dest.clone(),
beneficiary,
dest.clone(),
assets,
fees,
weight_limit,
)?;
(local, None)
)
.map(|local| (local, None))
},
TransferType::Teleport => {
let (local, remote) = Self::teleport_assets_program(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)?;
(local, Some(remote))
},
})
TransferType::Teleport => Self::teleport_assets_program(
origin.clone(),
dest.clone(),
beneficiary,
assets,
fees,
weight_limit,
)
.map(|(local, remote)| (local, Some(remote))),
}
}
fn execute_xcm_transfer(
@@ -1947,7 +1951,7 @@ impl<T: Config> Pallet<T> {
fn local_reserve_transfer_programs(
origin: Location,
dest: Location,
beneficiary: Location,
beneficiary: Either<Location, Xcm<()>>,
assets: Vec<Asset>,
fees: FeesHandling<T>,
weight_limit: WeightLimit,
@@ -1980,10 +1984,16 @@ impl<T: Config> Pallet<T> {
]);
// handle fees
Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
// deposit all remaining assets in holding to `beneficiary` location
xcm_on_dest
.inner_mut()
.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
let custom_remote_xcm = match beneficiary {
Either::Right(custom_xcm) => custom_xcm,
Either::Left(beneficiary) => {
// deposit all remaining assets in holding to `beneficiary` location
Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
},
};
xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
Ok((local_execute_xcm, xcm_on_dest))
}
@@ -2022,7 +2032,7 @@ impl<T: Config> Pallet<T> {
fn destination_reserve_transfer_programs(
origin: Location,
dest: Location,
beneficiary: Location,
beneficiary: Either<Location, Xcm<()>>,
assets: Vec<Asset>,
fees: FeesHandling<T>,
weight_limit: WeightLimit,
@@ -2058,10 +2068,15 @@ impl<T: Config> Pallet<T> {
// handle fees
Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
// deposit all remaining assets in holding to `beneficiary` location
xcm_on_dest
.inner_mut()
.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
let custom_remote_xcm = match beneficiary {
Either::Right(custom_xcm) => custom_xcm,
Either::Left(beneficiary) => {
// deposit all remaining assets in holding to `beneficiary` location
Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
},
};
xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
Ok((local_execute_xcm, xcm_on_dest))
}
@@ -2070,8 +2085,8 @@ impl<T: Config> Pallet<T> {
fn remote_reserve_transfer_program(
origin: Location,
reserve: Location,
beneficiary: Either<Location, Xcm<()>>,
dest: Location,
beneficiary: Location,
assets: Vec<Asset>,
fees: Asset,
weight_limit: WeightLimit,
@@ -2096,10 +2111,17 @@ impl<T: Config> Pallet<T> {
// identifies `dest` as seen by `reserve`
let dest = dest.reanchored(&reserve, &context).map_err(|_| Error::<T>::CannotReanchor)?;
// xcm to be executed at dest
let xcm_on_dest = Xcm(vec![
BuyExecution { fees: dest_fees, weight_limit: weight_limit.clone() },
DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
]);
let mut xcm_on_dest =
Xcm(vec![BuyExecution { fees: dest_fees, weight_limit: weight_limit.clone() }]);
// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
let custom_xcm_on_dest = match beneficiary {
Either::Right(custom_xcm) => custom_xcm,
Either::Left(beneficiary) => {
// deposit all remaining assets in holding to `beneficiary` location
Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
},
};
xcm_on_dest.0.extend(custom_xcm_on_dest.into_iter());
// xcm to be executed on reserve
let xcm_on_reserve = Xcm(vec![
BuyExecution { fees: reserve_fees, weight_limit },
@@ -2171,7 +2193,7 @@ impl<T: Config> Pallet<T> {
fn teleport_assets_program(
origin: Location,
dest: Location,
beneficiary: Location,
beneficiary: Either<Location, Xcm<()>>,
assets: Vec<Asset>,
fees: FeesHandling<T>,
weight_limit: WeightLimit,
@@ -2231,10 +2253,16 @@ impl<T: Config> Pallet<T> {
]);
// handle fees
Self::add_fees_to_xcm(dest, fees, weight_limit, &mut local_execute_xcm, &mut xcm_on_dest)?;
// deposit all remaining assets in holding to `beneficiary` location
xcm_on_dest
.inner_mut()
.push(DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary });
// Use custom XCM on remote chain, or just default to depositing everything to beneficiary.
let custom_remote_xcm = match beneficiary {
Either::Right(custom_xcm) => custom_xcm,
Either::Left(beneficiary) => {
// deposit all remaining assets in holding to `beneficiary` location
Xcm(vec![DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }])
},
};
xcm_on_dest.0.extend(custom_remote_xcm.into_iter());
Ok((local_execute_xcm, xcm_on_dest))
}
+8 -1
View File
@@ -6,7 +6,7 @@ title: "pallet-xcm: add new extrinsic for asset transfers using explicit reserve
doc:
- audience: Runtime User
description: |
pallet-xcm has a new extrinsic `transfer_assets_using_type` for transferring
pallet-xcm has a new extrinsic `transfer_assets_using_type_and_then` for transferring
assets from local chain to destination chain using an explicit XCM transfer
types for transferring the assets and the fees:
- `TransferType::LocalReserve`: transfer assets to sovereign account of destination
@@ -33,6 +33,13 @@ doc:
Same when transferring bridged assets back across the bridge, the local bridging
parachain must be used as the explicit reserve location.
The new method takes a `custom_xcm_on_dest` parameter allowing the caller to specify
what should happen to the transferred assets once they reach
the `dest` chain. The `custom_xcm_on_dest` parameter should contains the instructions
to execute on `dest` as a final step. Usually as simple as:
`Xcm(vec![DepositAsset { assets: Wild(AllCounted(assets.len())), beneficiary }])`,
but could be something more exotic like sending the `assets` even further.
crates:
- name: pallet-xcm
bump: minor