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::{tokens::fungibles, 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::traits::{MatchesFungibles, TransferType, WeightTrader, XcmAssetTransfers};
47
48use pallet_xc_asset_config::{ExecutionPaymentRate, XcAssetLocation};
49
50#[cfg(test)]
51mod tests;
52
53pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16);
54pub const MAX_ASSETS: u32 = 64;
55pub const ASSET_HUB_PARA_ID: u32 = 1000;
56
57/// Used to convert between cross-chain asset multilocation and local asset Id.
58///
59/// This implementation relies on `XcAssetConfig` pallet to handle mapping.
60/// In case asset location hasn't been mapped, it means the asset isn't supported (yet).
61pub struct AssetLocationIdConverter<AssetId, AssetMapper>(PhantomData<(AssetId, AssetMapper)>);
62impl<AssetId, AssetMapper> MaybeEquivalence<Location, AssetId>
63    for AssetLocationIdConverter<AssetId, AssetMapper>
64where
65    AssetId: Clone + Eq + Bounded,
66    AssetMapper: XcAssetLocation<AssetId>,
67{
68    fn convert(location: &Location) -> Option<AssetId> {
69        AssetMapper::get_asset_id(location.clone())
70    }
71
72    fn convert_back(id: &AssetId) -> Option<Location> {
73        AssetMapper::get_xc_asset_location(id.clone())
74    }
75}
76
77/// Used as weight trader for foreign assets.
78///
79/// In case foreign asset is supported as payment asset, XCM execution time
80/// on-chain can be paid by the foreign asset, using the configured rate.
81pub struct FixedRateOfForeignAsset<T: ExecutionPaymentRate, R: TakeRevenue> {
82    /// Total used weight
83    weight: Weight,
84    /// Total consumed assets
85    consumed: u128,
86    /// Asset Id (as Location) and units per second for payment
87    asset_location_and_units_per_second: Option<(Location, u128)>,
88    _pd: PhantomData<(T, R)>,
89}
90
91impl<T: ExecutionPaymentRate, R: TakeRevenue> WeightTrader for FixedRateOfForeignAsset<T, R> {
92    fn new() -> Self {
93        Self {
94            weight: Weight::zero(),
95            consumed: 0,
96            asset_location_and_units_per_second: None,
97            _pd: PhantomData,
98        }
99    }
100
101    fn buy_weight(
102        &mut self,
103        weight: Weight,
104        payment: xcm_executor::AssetsInHolding,
105        _: &XcmContext,
106    ) -> Result<xcm_executor::AssetsInHolding, XcmError> {
107        log::trace!(
108            target: "xcm::weight",
109            "FixedRateOfForeignAsset::buy_weight weight: {:?}, payment: {:?}",
110            weight, payment,
111        );
112
113        // Atm in pallet, we only support one asset so this should work
114        let payment_asset = payment
115            .fungible_assets_iter()
116            .next()
117            .ok_or(XcmError::TooExpensive)?;
118
119        match payment_asset {
120            Asset {
121                id: AssetId(asset_location),
122                fun: Fungibility::Fungible(_),
123            } => {
124                if let Some(units_per_second) = T::get_units_per_second(asset_location.clone()) {
125                    let amount = units_per_second.saturating_mul(weight.ref_time() as u128) // TODO: change this to u64?
126                        / (WEIGHT_REF_TIME_PER_SECOND as u128);
127                    if amount == 0 {
128                        return Ok(payment);
129                    }
130
131                    // This trader tracks a single fee asset.
132                    if let Some((tracked_asset_location, _)) =
133                        &self.asset_location_and_units_per_second
134                    {
135                        if *tracked_asset_location != asset_location {
136                            return Err(XcmError::NotWithdrawable);
137                        }
138                    }
139
140                    let unused = payment
141                        .checked_sub((asset_location.clone(), amount).into())
142                        .map_err(|_| XcmError::TooExpensive)?;
143
144                    self.weight = self.weight.saturating_add(weight);
145                    self.consumed = self.consumed.saturating_add(amount);
146                    self.asset_location_and_units_per_second =
147                        Some((asset_location, units_per_second));
148
149                    Ok(unused)
150                } else {
151                    Err(XcmError::TooExpensive)
152                }
153            }
154            _ => Err(XcmError::TooExpensive),
155        }
156    }
157
158    fn refund_weight(&mut self, weight: Weight, _: &XcmContext) -> Option<Asset> {
159        log::trace!(target: "xcm::weight", "FixedRateOfForeignAsset::refund_weight weight: {:?}", weight);
160
161        if let Some((asset_location, units_per_second)) =
162            self.asset_location_and_units_per_second.clone()
163        {
164            let weight = weight.min(self.weight);
165            // Never hand back more of the asset than was actually taken for it.
166            let amount = units_per_second
167                .saturating_mul(weight.ref_time() as u128)
168                .saturating_div(WEIGHT_REF_TIME_PER_SECOND as u128)
169                .min(self.consumed);
170
171            self.weight = self.weight.saturating_sub(weight);
172            self.consumed = self.consumed.saturating_sub(amount);
173
174            if amount > 0 {
175                Some((asset_location, amount).into())
176            } else {
177                None
178            }
179        } else {
180            None
181        }
182    }
183}
184
185impl<T: ExecutionPaymentRate, R: TakeRevenue> Drop for FixedRateOfForeignAsset<T, R> {
186    fn drop(&mut self) {
187        if let Some((asset_location, _)) = self.asset_location_and_units_per_second.clone() {
188            if self.consumed > 0 {
189                R::take_revenue((asset_location, self.consumed).into());
190            }
191        }
192    }
193}
194
195/// Used to determine whether the cross-chain asset is coming from a trusted reserve or not
196///
197/// Basically, we trust any cross-chain asset from any location to act as a reserve since
198/// in order to support the xc-asset, we need to first register it in the `XcAssetConfig` pallet.
199///
200pub struct ReserveAssetFilter;
201impl ContainsPair<Asset, Location> for ReserveAssetFilter {
202    fn contains(asset: &Asset, origin: &Location) -> bool {
203        let AssetId(location) = &asset.id;
204        match (location.parents, location.first_interior()) {
205            // sibling parachain reserve
206            (1, Some(Parachain(id))) => origin == &Location::new(1, [Parachain(*id)]),
207            // relay token (DOT/KSM) - only Asset Hub is valid reserve now
208            (1, None) => origin == &Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]),
209            _ => false,
210        }
211    }
212}
213
214/// Used to deposit XCM fees into a destination account.
215///
216/// Only handles fungible assets for now.
217/// If for any reason taking of the fee fails, it will be burned and and error trace will be printed.
218///
219pub struct XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>(
220    sp_std::marker::PhantomData<(AccountId, Matcher, Assets, FeeDestination)>,
221);
222impl<
223        AccountId: Eq,
224        Assets: fungibles::Mutate<AccountId>,
225        Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
226        FeeDestination: Get<AccountId>,
227    > TakeRevenue for XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>
228{
229    fn take_revenue(revenue: Asset) {
230        match Matcher::matches_fungibles(&revenue) {
231            Ok((asset_id, amount)) => {
232                if amount > Zero::zero() {
233                    if let Err(error) =
234                        Assets::mint_into(asset_id.clone(), &FeeDestination::get(), amount)
235                    {
236                        log::error!(
237                            target: "xcm::weight",
238                            "XcmFeeHandler::take_revenue failed when minting asset: {:?}", error,
239                        );
240                    } else {
241                        log::trace!(
242                            target: "xcm::weight",
243                            "XcmFeeHandler::take_revenue took {:?} of asset Id {:?}",
244                            amount, asset_id,
245                        );
246                    }
247                }
248            }
249            Err(_) => {
250                log::error!(
251                    target: "xcm::weight",
252                    "XcmFeeHandler:take_revenue failed to match fungible asset, it has been burned."
253                );
254            }
255        }
256    }
257}
258
259/// Splits a combined `Location` into its chain part and its beneficiary part.
260///
261/// Junctions are popped off the tail until a chain identifier (`Parachain`/`GlobalConsensus`)
262/// is reached; whatever was popped becomes the beneficiary, relative to the chain part.
263///
264/// A location with no chain identifier is only valid when it has exactly one parent, in which
265/// case the chain part is the relay chain. Returns `None` for anything else.
266pub fn split_location_into_chain_part_and_beneficiary(
267    mut location: Location,
268) -> Option<(Location, Location)> {
269    let mut beneficiary_junctions = Junctions::Here;
270
271    while let Some(junction) = location.last() {
272        if matches!(
273            junction,
274            Junction::Parachain(_) | Junction::GlobalConsensus(_)
275        ) {
276            return Some((location, beneficiary_junctions.into_location()));
277        }
278
279        let (prefix, maybe_last) = location.split_last_interior();
280        location = prefix;
281        if let Some(junction) = maybe_last {
282            beneficiary_junctions.push_front(junction).ok()?;
283        }
284    }
285
286    // No chain identifier found: only the relay chain qualifies.
287    if location.parent_count() == 1 {
288        Some((Location::parent(), beneficiary_junctions.into_location()))
289    } else {
290        None
291    }
292}
293
294/// Resolves which reserve model `pallet_xcm` should use to move `asset` to `dest`.
295///
296/// Defers to the XCM executor's own determination, which is driven by the runtime's
297/// [`ReserveAssetFilter`] and teleport filter - the same logic `pallet_xcm::transfer_assets` runs
298/// internally. One case the executor cannot resolve on its own: the relay-native token (`DOT`,
299/// `KSM`, ...) is identified by the bare parent location, but its trusted reserve is Asset Hub
300/// rather than the relay chain. For any destination other than Asset Hub itself the executor gives
301/// up, so we name Asset Hub as a remote reserve explicitly.
302///
303/// Returns `None` when no reserve can be determined - the caller should reject the transfer.
304pub fn resolve_transfer_type<XcmExecutor: XcmAssetTransfers>(
305    asset: &Asset,
306    dest: &Location,
307) -> Option<TransferType> {
308    if let Ok(transfer_type) = XcmExecutor::determine_for(asset, dest) {
309        return Some(transfer_type);
310    }
311
312    let AssetId(location) = &asset.id;
313    if location == &Location::parent() {
314        return Some(TransferType::RemoteReserve(
315            Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]).into(),
316        ));
317    }
318
319    None
320}