Skip to content

Commit 2ea6aba

Browse files
committed
Sending XCM messages to other chains requires paying a "transport fee".
This can be paid either: - from `origin` local account if `jit_withdraw = true`, - taken from Holding register otherwise. This currently works for following hops/scenarios: 1. On destination no transport fee needed (only sending costs, not receiving), 2. Local/originating chain: just set JIT=true and fee will be paid from signed account, 3. Intermediary hops - only if intermediary is acting as reserve between two untrusted chains (aka only for `DepositReserveAsset` instruction) - this was fixed in paritytech#3142 But now we're seeing more complex asset transfers that are mixing reserve transfers with teleports depending on the involved chains. E.g. transferring DOT between Relay and parachain, but through AH (using AH instead of the Relay chain as parachain's DOT reserve). In the `Parachain --1--> AssetHub --2--> Relay` scenario, DOT has to be reserve-withdrawn in leg `1`, then teleported in leg `2`. On the intermediary hop (AssetHub), `InitiateTeleport` fails to send onward message because of missing transport fees. We also can't rely on `jit_withdraw` because the original origin is lost on the way, and even if it weren't we can't rely on the user having funded accounts on each hop along the way. - Charge the transport fee in the executor from the transferred assets (if available), - Only charge from transferred assets if JIT_WITHDRAW was not set, - Only charge from transferred assets if Holding doesn't already contain enough (other) assets to pay for the transport fee. Added regression tests in emulated transfers. Fixes paritytech#4832 Signed-off-by: Adrian Catangiu <[email protected]>
1 parent 3d8da81 commit 2ea6aba

4 files changed

Lines changed: 193 additions & 12 deletions

File tree

cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ mod imports {
106106
pub type ParaToParaThroughRelayTest = Test<PenpalA, PenpalB, Westend>;
107107
pub type ParaToParaThroughAHTest = Test<PenpalA, PenpalB, AssetHubWestend>;
108108
pub type RelayToParaThroughAHTest = Test<Westend, PenpalA, AssetHubWestend>;
109+
pub type PenpalToRelayThroughAHTest = Test<PenpalA, Westend, AssetHubWestend>;
109110
}
110111

111112
#[cfg(test)]

cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -658,13 +658,13 @@ fn bidirectional_teleport_foreign_asset_between_para_and_asset_hub_using_explici
658658
}
659659

660660
// ===============================================================
661-
// ===== Transfer - Native Asset - Relay->AssetHub->Parachain ====
661+
// ====== Transfer - Native Asset - Relay->AssetHub->Penpal ======
662662
// ===============================================================
663-
/// Transfers of native asset Relay to Parachain (using AssetHub reserve). Parachains want to avoid
663+
/// Transfers of native asset Relay to Penpal (using AssetHub reserve). Parachains want to avoid
664664
/// managing SAs on all system chains, thus want all their DOT-in-reserve to be held in their
665665
/// Sovereign Account on Asset Hub.
666666
#[test]
667-
fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
667+
fn transfer_native_asset_from_relay_to_penpal_through_asset_hub() {
668668
// Init values for Relay
669669
let destination = Westend::child_location_of(PenpalA::para_id());
670670
let sender = WestendSender::get();
@@ -820,6 +820,137 @@ fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
820820
assert!(receiver_assets_after < receiver_assets_before + amount_to_send);
821821
}
822822

823+
// ===============================================================
824+
// ===== Transfer - Native Asset - Penpal->AssetHub->Relay =======
825+
// ===============================================================
826+
/// Transfers of native asset Penpal to Relay (using AssetHub reserve). Parachains want to avoid
827+
/// managing SAs on all system chains, thus want all their DOT-in-reserve to be held in their
828+
/// Sovereign Account on Asset Hub.
829+
#[test]
830+
fn transfer_native_asset_from_penpal_to_relay_through_asset_hub() {
831+
// Init values for Penpal
832+
let destination = RelayLocation::get();
833+
let sender = PenpalASender::get();
834+
let amount_to_send: Balance = WESTEND_ED * 100;
835+
836+
// Init values for Penpal
837+
let relay_native_asset_location = RelayLocation::get();
838+
let receiver = WestendReceiver::get();
839+
840+
// Init Test
841+
let test_args = TestContext {
842+
sender: sender.clone(),
843+
receiver: receiver.clone(),
844+
args: TestArgs::new_para(
845+
destination.clone(),
846+
receiver.clone(),
847+
amount_to_send,
848+
(Parent, amount_to_send).into(),
849+
None,
850+
0,
851+
),
852+
};
853+
let mut test = PenpalToRelayThroughAHTest::new(test_args);
854+
855+
let sov_penpal_on_ah = AssetHubWestend::sovereign_account_id_of(
856+
AssetHubWestend::sibling_location_of(PenpalA::para_id()),
857+
);
858+
// fund Penpal's sender account
859+
PenpalA::mint_foreign_asset(
860+
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
861+
relay_native_asset_location.clone(),
862+
sender.clone(),
863+
amount_to_send * 2,
864+
);
865+
// fund Penpal's SA on AssetHub with the assets held in reserve
866+
AssetHubWestend::fund_accounts(vec![(sov_penpal_on_ah.clone().into(), amount_to_send * 2)]);
867+
868+
// prefund Relay checking account so we accept teleport "back" from AssetHub
869+
let check_account =
870+
Westend::execute_with(|| <Westend as WestendPallet>::XcmPallet::check_account());
871+
Westend::fund_accounts(vec![(check_account, amount_to_send)]);
872+
873+
// Query initial balances
874+
let sender_balance_before = PenpalA::execute_with(|| {
875+
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
876+
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &sender)
877+
});
878+
let sov_penpal_on_ah_before = AssetHubWestend::execute_with(|| {
879+
<AssetHubWestend as AssetHubWestendPallet>::Balances::free_balance(sov_penpal_on_ah.clone())
880+
});
881+
let receiver_balance_before = Westend::execute_with(|| {
882+
<Westend as WestendPallet>::Balances::free_balance(receiver.clone())
883+
});
884+
885+
fn transfer_assets_dispatchable(t: PenpalToRelayThroughAHTest) -> DispatchResult {
886+
let fee_idx = t.args.fee_asset_item as usize;
887+
let fee: Asset = t.args.assets.inner().get(fee_idx).cloned().unwrap();
888+
let asset_hub_location = PenpalA::sibling_location_of(AssetHubWestend::para_id());
889+
let context = PenpalUniversalLocation::get();
890+
891+
// reanchor fees to the view of destination (Westend Relay)
892+
let mut remote_fees = fee.clone().reanchored(&t.args.dest, &context).unwrap();
893+
if let Fungible(ref mut amount) = remote_fees.fun {
894+
// we already spent some fees along the way, just use half of what we started with
895+
*amount = *amount / 2;
896+
}
897+
let xcm_on_final_dest = Xcm::<()>(vec![
898+
BuyExecution { fees: remote_fees, weight_limit: t.args.weight_limit.clone() },
899+
DepositAsset {
900+
assets: Wild(AllCounted(t.args.assets.len() as u32)),
901+
beneficiary: t.args.beneficiary,
902+
},
903+
]);
904+
905+
// reanchor final dest (Westend Relay) to the view of hop (Asset Hub)
906+
let mut dest = t.args.dest.clone();
907+
dest.reanchor(&asset_hub_location, &context).unwrap();
908+
// on Asset Hub
909+
let xcm_on_hop = Xcm::<()>(vec![InitiateTeleport {
910+
assets: Wild(AllCounted(t.args.assets.len() as u32)),
911+
dest,
912+
xcm: xcm_on_final_dest,
913+
}]);
914+
915+
// First leg is a reserve-withdraw, from there a teleport to final dest
916+
<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets_using_type_and_then(
917+
t.signed_origin,
918+
bx!(asset_hub_location.into()),
919+
bx!(t.args.assets.into()),
920+
bx!(TransferType::DestinationReserve),
921+
bx!(fee.id.into()),
922+
bx!(TransferType::DestinationReserve),
923+
bx!(VersionedXcm::from(xcm_on_hop)),
924+
t.args.weight_limit,
925+
)
926+
}
927+
test.set_dispatchable::<PenpalA>(transfer_assets_dispatchable);
928+
test.assert();
929+
930+
// Query final balances
931+
let sender_balance_after = PenpalA::execute_with(|| {
932+
type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
933+
<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &sender)
934+
});
935+
let sov_penpal_on_ah_after = AssetHubWestend::execute_with(|| {
936+
<AssetHubWestend as AssetHubWestendPallet>::Balances::free_balance(sov_penpal_on_ah.clone())
937+
});
938+
let receiver_balance_after = Westend::execute_with(|| {
939+
<Westend as WestendPallet>::Balances::free_balance(receiver.clone())
940+
});
941+
942+
// Sender's asset balance is reduced by amount sent plus delivery fees
943+
assert!(sender_balance_after < sender_balance_before - amount_to_send);
944+
// SA on AH balance is decreased by `amount_to_send`
945+
assert_eq!(sov_penpal_on_ah_after, sov_penpal_on_ah_before - amount_to_send);
946+
// Receiver's balance is increased
947+
assert!(receiver_balance_after > receiver_balance_before);
948+
// Receiver's balance increased by `amount_to_send - delivery_fees - bought_execution`;
949+
// `delivery_fees` might be paid from transfer or JIT, also `bought_execution` is unknown but
950+
// should be non-zero
951+
assert!(receiver_balance_after < receiver_balance_before + amount_to_send);
952+
}
953+
823954
// ==============================================================================================
824955
// ==== Bidirectional Transfer - Native + Teleportable Foreign Assets - Parachain<->AssetHub ====
825956
// ==============================================================================================

polkadot/xcm/xcm-executor/src/lib.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,12 +1086,13 @@ impl<Config: config::Config> XcmExecutor<Config> {
10861086
DepositReserveAsset { assets, dest, xcm } => {
10871087
let old_holding = self.holding.clone();
10881088
let result = Config::TransactionalProcessor::process(|| {
1089-
let maybe_delivery_fee_from_holding = if self.fees.is_empty() {
1090-
self.get_delivery_fee_from_holding(&assets, &dest, &xcm)?
1089+
// When not using `PayFees`, nor `JIT_WITHDRAW`, transport fees are paid from
1090+
// transferred assets.
1091+
let maybe_transport_fee_from_holding = if self.fees.is_empty() && !self.fees_mode.jit_withdraw {
1092+
self.get_delivery_fee_from_holding(&assets, &dest, FeeReason::DepositReserveAsset, &xcm)?
10911093
} else {
10921094
None
10931095
};
1094-
10951096
let mut message = Vec::with_capacity(xcm.len() + 2);
10961097
// now take assets to deposit (after having taken delivery fees)
10971098
let deposited = self.holding.saturating_take(assets);
@@ -1106,7 +1107,7 @@ impl<Config: config::Config> XcmExecutor<Config> {
11061107
message.push(ClearOrigin);
11071108
// append custom instructions
11081109
message.extend(xcm.0.into_iter());
1109-
if let Some(delivery_fee) = maybe_delivery_fee_from_holding {
1110+
if let Some(delivery_fee) = maybe_transport_fee_from_holding {
11101111
// Put back delivery_fee in holding register to be charged by XcmSender.
11111112
self.holding.subsume_assets(delivery_fee);
11121113
}
@@ -1121,6 +1122,13 @@ impl<Config: config::Config> XcmExecutor<Config> {
11211122
InitiateReserveWithdraw { assets, reserve, xcm } => {
11221123
let old_holding = self.holding.clone();
11231124
let result = Config::TransactionalProcessor::process(|| {
1125+
// When not using `PayFees`, nor `JIT_WITHDRAW`, transport fees are paid from
1126+
// transferred assets.
1127+
let maybe_transport_fee_from_holding = if self.fees.is_empty() && !self.fees_mode.jit_withdraw {
1128+
self.get_delivery_fee_from_holding(&assets, &reserve, FeeReason::InitiateReserveWithdraw, &xcm)?
1129+
} else {
1130+
None
1131+
};
11241132
let assets = self.holding.saturating_take(assets);
11251133
let mut message = Vec::with_capacity(xcm.len() + 2);
11261134
Self::do_reserve_withdraw_assets(
@@ -1133,6 +1141,10 @@ impl<Config: config::Config> XcmExecutor<Config> {
11331141
message.push(ClearOrigin);
11341142
// append custom instructions
11351143
message.extend(xcm.0.into_iter());
1144+
if let Some(delivery_fee) = maybe_transport_fee_from_holding {
1145+
// Put back delivery_fee in holding register to be charged by XcmSender.
1146+
self.holding.subsume_assets(delivery_fee);
1147+
}
11361148
self.send(reserve, Xcm(message), FeeReason::InitiateReserveWithdraw)?;
11371149
Ok(())
11381150
});
@@ -1144,13 +1156,24 @@ impl<Config: config::Config> XcmExecutor<Config> {
11441156
InitiateTeleport { assets, dest, xcm } => {
11451157
let old_holding = self.holding.clone();
11461158
let result = Config::TransactionalProcessor::process(|| {
1159+
// When not using `PayFees`, nor `JIT_WITHDRAW`, transport fees are paid from
1160+
// transferred assets.
1161+
let maybe_transport_fee_from_holding = if self.fees.is_empty() && !self.fees_mode.jit_withdraw {
1162+
self.get_delivery_fee_from_holding(&assets, &dest, FeeReason::InitiateTeleport, &xcm)?
1163+
} else {
1164+
None
1165+
};
11471166
let assets = self.holding.saturating_take(assets);
11481167
let mut message = Vec::with_capacity(xcm.len() + 2);
11491168
Self::do_teleport_assets(assets, &dest, &mut message, &self.context)?;
11501169
// clear origin for subsequent custom instructions
11511170
message.push(ClearOrigin);
11521171
// append custom instructions
11531172
message.extend(xcm.0.into_iter());
1173+
if let Some(delivery_fee) = maybe_transport_fee_from_holding {
1174+
// Put back delivery_fee in holding register to be charged by XcmSender.
1175+
self.holding.subsume_assets(delivery_fee);
1176+
}
11541177
self.send(dest.clone(), Xcm(message), FeeReason::InitiateTeleport)?;
11551178
Ok(())
11561179
});
@@ -1707,36 +1730,51 @@ impl<Config: config::Config> XcmExecutor<Config> {
17071730
Ok(())
17081731
}
17091732

1710-
/// Gets the necessary delivery fee to send a reserve transfer message to `destination` from
1711-
/// holding.
1733+
/// Gets the necessary delivery fee from holding to send an onward transfer message to
1734+
/// `destination`.
17121735
///
17131736
/// Will be removed once the transition from `BuyExecution` to `PayFees` is complete.
17141737
fn get_delivery_fee_from_holding(
17151738
&mut self,
17161739
assets: &AssetFilter,
17171740
destination: &Location,
1741+
reason: FeeReason,
17181742
xcm: &Xcm<()>,
17191743
) -> Result<Option<AssetsInHolding>, XcmError> {
17201744
// we need to do this take/put cycle to solve wildcards and get exact assets to
17211745
// be weighed
17221746
let to_weigh = self.holding.saturating_take(assets.clone());
17231747
self.holding.subsume_assets(to_weigh.clone());
17241748
let to_weigh_reanchored = Self::reanchored(to_weigh, &destination, None);
1725-
let mut message_to_weigh = vec![ReserveAssetDeposited(to_weigh_reanchored), ClearOrigin];
1749+
let remote_instruction = match reason {
1750+
FeeReason::DepositReserveAsset => ReserveAssetDeposited(to_weigh_reanchored),
1751+
FeeReason::InitiateReserveWithdraw => WithdrawAsset(to_weigh_reanchored),
1752+
FeeReason::InitiateTeleport => ReceiveTeleportedAsset(to_weigh_reanchored),
1753+
_ => {
1754+
tracing::debug!(
1755+
target: "xcm::get_delivery_fee_from_holding",
1756+
"Unexpected transport fee reason",
1757+
);
1758+
return Err(XcmError::NotHoldingFees);
1759+
},
1760+
};
1761+
let mut message_to_weigh = Vec::with_capacity(xcm.len() + 2);
1762+
message_to_weigh.push(remote_instruction);
1763+
message_to_weigh.push(ClearOrigin);
17261764
message_to_weigh.extend(xcm.0.clone().into_iter());
17271765
let (_, fee) =
17281766
validate_send::<Config::XcmSender>(destination.clone(), Xcm(message_to_weigh))?;
17291767
let maybe_delivery_fee = fee.get(0).map(|asset_needed_for_fees| {
17301768
tracing::trace!(
1731-
target: "xcm::fees::DepositReserveAsset",
1769+
target: "xcm::fees::get_delivery_fee_from_holding",
17321770
"Asset provided to pay for fees {:?}, asset required for delivery fees: {:?}",
17331771
self.asset_used_in_buy_execution, asset_needed_for_fees,
17341772
);
17351773
let asset_to_pay_for_fees =
17361774
self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone());
17371775
// set aside fee to be charged by XcmSender
17381776
let delivery_fee = self.holding.saturating_take(asset_to_pay_for_fees.into());
1739-
tracing::trace!(target: "xcm::fees::DepositReserveAsset", ?delivery_fee);
1777+
tracing::trace!(target: "xcm::fees::get_delivery_fee_from_holding", ?delivery_fee);
17401778
delivery_fee
17411779
});
17421780
Ok(maybe_delivery_fee)

prdoc/pr_4834.prdoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
title: "xcm-executor: take transport fee from transferred assets if necessary"
2+
3+
doc:
4+
- audience: Runtime Dev
5+
description: |
6+
In asset transfers, as a last resort, XCM transport fees are taken from
7+
transferred assets rather than failing the transfer.
8+
9+
crates:
10+
- name: staging-xcm-executor
11+
bump: patch

0 commit comments

Comments
 (0)