astar_primitives/xcm/
mod.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! # XCM Primitives
20//!
21//! ## Overview
22//!
23//! Collection of common XCM primitives used by runtimes.
24//!
25//! - `AssetLocationIdConverter` - conversion between local asset Id and cross-chain asset multilocation
26//! - `FixedRateOfForeignAsset` - weight trader for execution payment in foreign asset
27//! - `ReserveAssetFilter` - used to check whether asset/origin are a valid reserve location
28//! - `XcmFungibleFeeHandler` - used to handle XCM fee execution fees
29//! - `split_location_into_chain_part_and_beneficiary` - splits a combined `Location` into
30//!   the destination chain part and the beneficiary part, as required by `pallet_xcm`
31//! - `resolve_transfer_type` - picks the reserve model `pallet_xcm` should use for a transfer
32//!
33//! Please refer to implementation below for more info.
34//!
35
36use frame_support::{
37    traits::{ContainsPair, Get},
38    weights::constants::WEIGHT_REF_TIME_PER_SECOND,
39};
40use sp_runtime::traits::{Bounded, MaybeEquivalence, Zero};
41use sp_std::marker::PhantomData;
42
43// Polkadot imports
44use xcm::latest::{prelude::*, Weight};
45use xcm_builder::TakeRevenue;
46use xcm_executor::{
47    traits::{TransactAsset, TransferType, WeightTrader, XcmAssetTransfers},
48    AssetsInHolding,
49};
50
51use pallet_xc_asset_config::{ExecutionPaymentRate, XcAssetLocation};
52
53#[cfg(test)]
54mod tests;
55
56pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16);
57pub const MAX_ASSETS: u32 = 64;
58pub const ASSET_HUB_PARA_ID: u32 = 1000;
59
60/// Used to convert between cross-chain asset multilocation and local asset Id.
61///
62/// This implementation relies on `XcAssetConfig` pallet to handle mapping.
63/// In case asset location hasn't been mapped, it means the asset isn't supported (yet).
64pub struct AssetLocationIdConverter<AssetId, AssetMapper>(PhantomData<(AssetId, AssetMapper)>);
65impl<AssetId, AssetMapper> MaybeEquivalence<Location, AssetId>
66    for AssetLocationIdConverter<AssetId, AssetMapper>
67where
68    AssetId: Clone + Eq + Bounded,
69    AssetMapper: XcAssetLocation<AssetId>,
70{
71    fn convert(location: &Location) -> Option<AssetId> {
72        AssetMapper::get_asset_id(location.clone())
73    }
74
75    fn convert_back(id: &AssetId) -> Option<Location> {
76        AssetMapper::get_xc_asset_location(id.clone())
77    }
78}
79
80/// Used as weight trader for foreign assets.
81///
82/// In case foreign asset is supported as payment asset, XCM execution time
83/// on-chain can be paid by the foreign asset, using the configured rate.
84pub struct FixedRateOfForeignAsset<T: ExecutionPaymentRate, R: TakeRevenue> {
85    /// Total used weight
86    weight: Weight,
87    /// Assets taken from the payment so far, handed to `R` on drop
88    taken: AssetsInHolding,
89    /// Asset Id (as Location) and units per second for payment
90    asset_location_and_units_per_second: Option<(Location, u128)>,
91    _pd: PhantomData<(T, R)>,
92}
93
94impl<T: ExecutionPaymentRate, R: TakeRevenue> WeightTrader for FixedRateOfForeignAsset<T, R> {
95    fn new() -> Self {
96        Self {
97            weight: Weight::zero(),
98            taken: AssetsInHolding::new(),
99            asset_location_and_units_per_second: None,
100            _pd: PhantomData,
101        }
102    }
103
104    fn buy_weight(
105        &mut self,
106        weight: Weight,
107        mut payment: AssetsInHolding,
108        _: &XcmContext,
109    ) -> Result<AssetsInHolding, (AssetsInHolding, XcmError)> {
110        log::trace!(
111            target: "xcm::weight",
112            "FixedRateOfForeignAsset::buy_weight weight: {:?}, payment: {:?}",
113            weight, payment,
114        );
115
116        // Atm in pallet, we only support one asset so this should work
117        let Some(Asset {
118            id: AssetId(asset_location),
119            fun: Fungibility::Fungible(_),
120        }) = payment.fungible_assets_iter().next()
121        else {
122            return Err((payment, XcmError::TooExpensive));
123        };
124
125        let Some(units_per_second) = T::get_units_per_second(asset_location.clone()) else {
126            return Err((payment, XcmError::TooExpensive));
127        };
128
129        let amount = execution_fee(weight, units_per_second);
130        if amount == 0 {
131            return Ok(payment);
132        }
133
134        // This trader tracks a single fee asset.
135        if let Some((tracked_asset_location, _)) = &self.asset_location_and_units_per_second {
136            if *tracked_asset_location != asset_location {
137                return Err((payment, XcmError::NotWithdrawable));
138            }
139        }
140
141        let to_charge: Asset = (asset_location.clone(), amount).into();
142        let Ok(taken) = payment.try_take(to_charge.into()) else {
143            return Err((payment, XcmError::TooExpensive));
144        };
145
146        self.weight = self.weight.saturating_add(weight);
147        self.taken.subsume_assets(taken);
148        self.asset_location_and_units_per_second = Some((asset_location, units_per_second));
149
150        Ok(payment)
151    }
152
153    fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option<AssetsInHolding> {
154        log::trace!(target: "xcm::weight", "FixedRateOfForeignAsset::refund_weight weight: {:?}", weight);
155
156        let (asset_location, _) = self.asset_location_and_units_per_second.clone()?;
157        let weight = weight.min(self.weight);
158        let quote = self
159            .quote_weight(weight, AssetId(asset_location), context)
160            .ok()?;
161
162        self.weight = self.weight.saturating_sub(weight);
163        // Never hand back more of the asset than was actually taken for it.
164        let refunded = self.taken.saturating_take(quote.into());
165
166        if refunded.is_empty() {
167            None
168        } else {
169            Some(refunded)
170        }
171    }
172
173    fn quote_weight(
174        &mut self,
175        weight: Weight,
176        given: AssetId,
177        _: &XcmContext,
178    ) -> Result<Asset, XcmError> {
179        log::trace!(
180            target: "xcm::weight",
181            "FixedRateOfForeignAsset::quote_weight weight: {:?}, given: {:?}",
182            weight, given,
183        );
184
185        if weight.is_zero() {
186            return Err(XcmError::NoDeal);
187        }
188        let units_per_second =
189            T::get_units_per_second(given.0.clone()).ok_or(XcmError::NotHoldingFees)?;
190
191        Ok((given, execution_fee(weight, units_per_second)).into())
192    }
193}
194
195impl<T: ExecutionPaymentRate, R: TakeRevenue> Drop for FixedRateOfForeignAsset<T, R> {
196    fn drop(&mut self) {
197        if !self.taken.is_empty() {
198            R::take_revenue(core::mem::replace(&mut self.taken, AssetsInHolding::new()));
199        }
200    }
201}
202
203/// Fee for `weight` at the given rate, in the smallest unit of the fee asset.
204fn execution_fee(weight: Weight, units_per_second: u128) -> u128 {
205    units_per_second.saturating_mul(weight.ref_time() as u128)
206        / (WEIGHT_REF_TIME_PER_SECOND as u128)
207}
208
209/// Used to determine whether the cross-chain asset is coming from a trusted reserve or not
210///
211/// Basically, we trust any cross-chain asset from any location to act as a reserve since
212/// in order to support the xc-asset, we need to first register it in the `XcAssetConfig` pallet.
213///
214pub struct ReserveAssetFilter;
215impl ContainsPair<Asset, Location> for ReserveAssetFilter {
216    fn contains(asset: &Asset, origin: &Location) -> bool {
217        let AssetId(location) = &asset.id;
218        match (location.parents, location.first_interior()) {
219            // sibling parachain reserve
220            (1, Some(Parachain(id))) => origin == &Location::new(1, [Parachain(*id)]),
221            // relay token (DOT/KSM) - only Asset Hub is valid reserve now
222            (1, None) => origin == &Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]),
223            _ => false,
224        }
225    }
226}
227
228/// Used to deposit XCM fees into a destination location.
229///
230/// Only handles fungible assets for now.
231/// `AssetTransactor` routes each credit to the pallet that issued it.
232/// If for any reason taking of the fee fails, it will be burned and and error trace will be printed.
233///
234pub struct XcmFungibleFeeHandler<AssetTransactor, FeeDestination>(
235    sp_std::marker::PhantomData<(AssetTransactor, FeeDestination)>,
236);
237impl<AssetTransactor: TransactAsset, FeeDestination: Get<Location>> TakeRevenue
238    for XcmFungibleFeeHandler<AssetTransactor, FeeDestination>
239{
240    fn take_revenue(revenue: AssetsInHolding) {
241        let destination = FeeDestination::get();
242        for (id, credit) in revenue.fungible.into_iter() {
243            let fee = AssetsInHolding::new_from_fungible_credit(id, credit);
244            log::trace!(target: "xcm::weight", "XcmFeeHandler::take_revenue depositing {:?}", fee);
245            // An undeposited credit is dropped here, which burns it.
246            if let Err((unspent, error)) = AssetTransactor::deposit_asset(fee, &destination, None) {
247                log::error!(
248                    target: "xcm::weight",
249                    "XcmFeeHandler::take_revenue failed to deposit {:?}: {:?}, it has been burned.",
250                    unspent, error,
251                );
252            }
253        }
254    }
255}
256
257/// Splits a combined `Location` into its chain part and its beneficiary part.
258///
259/// Junctions are popped off the tail until a chain identifier (`Parachain`/`GlobalConsensus`)
260/// is reached; whatever was popped becomes the beneficiary, relative to the chain part.
261///
262/// A location with no chain identifier is only valid when it has exactly one parent, in which
263/// case the chain part is the relay chain. Returns `None` for anything else.
264pub fn split_location_into_chain_part_and_beneficiary(
265    mut location: Location,
266) -> Option<(Location, Location)> {
267    let mut beneficiary_junctions = Junctions::Here;
268
269    while let Some(junction) = location.last() {
270        if matches!(
271            junction,
272            Junction::Parachain(_) | Junction::GlobalConsensus(_)
273        ) {
274            return Some((location, beneficiary_junctions.into_location()));
275        }
276
277        let (prefix, maybe_last) = location.split_last_interior();
278        location = prefix;
279        if let Some(junction) = maybe_last {
280            beneficiary_junctions.push_front(junction).ok()?;
281        }
282    }
283
284    // No chain identifier found: only the relay chain qualifies.
285    if location.parent_count() == 1 {
286        Some((Location::parent(), beneficiary_junctions.into_location()))
287    } else {
288        None
289    }
290}
291
292/// Resolves which reserve model `pallet_xcm` should use to move `asset` to `dest`.
293///
294/// Defers to the XCM executor's own determination, which is driven by the runtime's
295/// [`ReserveAssetFilter`] and teleport filter - the same logic `pallet_xcm::transfer_assets` runs
296/// internally. One case the executor cannot resolve on its own: the relay-native token (`DOT`,
297/// `KSM`, ...) is identified by the bare parent location, but its trusted reserve is Asset Hub
298/// rather than the relay chain. For any destination other than Asset Hub itself the executor gives
299/// up, so we name Asset Hub as a remote reserve explicitly.
300///
301/// Returns `None` when no reserve can be determined - the caller should reject the transfer.
302pub fn resolve_transfer_type<XcmExecutor: XcmAssetTransfers>(
303    asset: &Asset,
304    dest: &Location,
305) -> Option<TransferType> {
306    if let Ok(transfer_type) = XcmExecutor::determine_for(asset, dest) {
307        return Some(transfer_type);
308    }
309
310    let AssetId(location) = &asset.id;
311    if location == &Location::parent() {
312        return Some(TransferType::RemoteReserve(
313            Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]).into(),
314        ));
315    }
316
317    None
318}