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//!
30//! Please refer to implementation below for more info.
31//!
32
33use crate::AccountId;
34
35use frame_support::{
36    traits::{tokens::fungibles, ContainsPair, Get},
37    weights::constants::WEIGHT_REF_TIME_PER_SECOND,
38};
39use sp_runtime::traits::{Bounded, Convert, MaybeEquivalence, Zero};
40use sp_std::marker::PhantomData;
41
42// Polkadot imports
43use xcm::latest::{prelude::*, Weight};
44use xcm_builder::TakeRevenue;
45use xcm_executor::traits::{MatchesFungibles, WeightTrader};
46
47// ORML imports
48use orml_traits::location::Reserve;
49
50use pallet_xc_asset_config::{ExecutionPaymentRate, XcAssetLocation};
51
52#[cfg(test)]
53mod tests;
54
55pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16);
56pub const MAX_ASSETS: u32 = 64;
57pub const ASSET_HUB_PARA_ID: u32 = 1000;
58
59/// Used to convert between cross-chain asset multilocation and local asset Id.
60///
61/// This implementation relies on `XcAssetConfig` pallet to handle mapping.
62/// In case asset location hasn't been mapped, it means the asset isn't supported (yet).
63pub struct AssetLocationIdConverter<AssetId, AssetMapper>(PhantomData<(AssetId, AssetMapper)>);
64impl<AssetId, AssetMapper> MaybeEquivalence<Location, AssetId>
65    for AssetLocationIdConverter<AssetId, AssetMapper>
66where
67    AssetId: Clone + Eq + Bounded,
68    AssetMapper: XcAssetLocation<AssetId>,
69{
70    fn convert(location: &Location) -> Option<AssetId> {
71        AssetMapper::get_asset_id(location.clone())
72    }
73
74    fn convert_back(id: &AssetId) -> Option<Location> {
75        AssetMapper::get_xc_asset_location(id.clone())
76    }
77}
78
79/// Used as weight trader for foreign assets.
80///
81/// In case foreign asset is supported as payment asset, XCM execution time
82/// on-chain can be paid by the foreign asset, using the configured rate.
83pub struct FixedRateOfForeignAsset<T: ExecutionPaymentRate, R: TakeRevenue> {
84    /// Total used weight
85    weight: Weight,
86    /// Total consumed assets
87    consumed: u128,
88    /// Asset Id (as Location) and units per second for payment
89    asset_location_and_units_per_second: Option<(Location, u128)>,
90    _pd: PhantomData<(T, R)>,
91}
92
93impl<T: ExecutionPaymentRate, R: TakeRevenue> WeightTrader for FixedRateOfForeignAsset<T, R> {
94    fn new() -> Self {
95        Self {
96            weight: Weight::zero(),
97            consumed: 0,
98            asset_location_and_units_per_second: None,
99            _pd: PhantomData,
100        }
101    }
102
103    fn buy_weight(
104        &mut self,
105        weight: Weight,
106        payment: xcm_executor::AssetsInHolding,
107        _: &XcmContext,
108    ) -> Result<xcm_executor::AssetsInHolding, XcmError> {
109        log::trace!(
110            target: "xcm::weight",
111            "FixedRateOfForeignAsset::buy_weight weight: {:?}, payment: {:?}",
112            weight, payment,
113        );
114
115        // Atm in pallet, we only support one asset so this should work
116        let payment_asset = payment
117            .fungible_assets_iter()
118            .next()
119            .ok_or(XcmError::TooExpensive)?;
120
121        match payment_asset {
122            Asset {
123                id: AssetId(asset_location),
124                fun: Fungibility::Fungible(_),
125            } => {
126                if let Some(units_per_second) = T::get_units_per_second(asset_location.clone()) {
127                    let amount = units_per_second.saturating_mul(weight.ref_time() as u128) // TODO: change this to u64?
128                        / (WEIGHT_REF_TIME_PER_SECOND as u128);
129                    if amount == 0 {
130                        return Ok(payment);
131                    }
132
133                    // This trader tracks a single fee asset.
134                    if let Some((tracked_asset_location, _)) =
135                        &self.asset_location_and_units_per_second
136                    {
137                        if *tracked_asset_location != asset_location {
138                            return Err(XcmError::NotWithdrawable);
139                        }
140                    }
141
142                    let unused = payment
143                        .checked_sub((asset_location.clone(), amount).into())
144                        .map_err(|_| XcmError::TooExpensive)?;
145
146                    self.weight = self.weight.saturating_add(weight);
147                    self.consumed = self.consumed.saturating_add(amount);
148                    self.asset_location_and_units_per_second =
149                        Some((asset_location, units_per_second));
150
151                    Ok(unused)
152                } else {
153                    Err(XcmError::TooExpensive)
154                }
155            }
156            _ => Err(XcmError::TooExpensive),
157        }
158    }
159
160    fn refund_weight(&mut self, weight: Weight, _: &XcmContext) -> Option<Asset> {
161        log::trace!(target: "xcm::weight", "FixedRateOfForeignAsset::refund_weight weight: {:?}", weight);
162
163        if let Some((asset_location, units_per_second)) =
164            self.asset_location_and_units_per_second.clone()
165        {
166            let weight = weight.min(self.weight);
167            // Never hand back more of the asset than was actually taken for it.
168            let amount = units_per_second
169                .saturating_mul(weight.ref_time() as u128)
170                .saturating_div(WEIGHT_REF_TIME_PER_SECOND as u128)
171                .min(self.consumed);
172
173            self.weight = self.weight.saturating_sub(weight);
174            self.consumed = self.consumed.saturating_sub(amount);
175
176            if amount > 0 {
177                Some((asset_location, amount).into())
178            } else {
179                None
180            }
181        } else {
182            None
183        }
184    }
185}
186
187impl<T: ExecutionPaymentRate, R: TakeRevenue> Drop for FixedRateOfForeignAsset<T, R> {
188    fn drop(&mut self) {
189        if let Some((asset_location, _)) = self.asset_location_and_units_per_second.clone() {
190            if self.consumed > 0 {
191                R::take_revenue((asset_location, self.consumed).into());
192            }
193        }
194    }
195}
196
197/// Used to determine whether the cross-chain asset is coming from a trusted reserve or not
198///
199/// Basically, we trust any cross-chain asset from any location to act as a reserve since
200/// in order to support the xc-asset, we need to first register it in the `XcAssetConfig` pallet.
201///
202pub struct ReserveAssetFilter;
203impl ContainsPair<Asset, Location> for ReserveAssetFilter {
204    fn contains(asset: &Asset, origin: &Location) -> bool {
205        let AssetId(location) = &asset.id;
206        match (location.parents, location.first_interior()) {
207            // sibling parachain reserve
208            (1, Some(Parachain(id))) => origin == &Location::new(1, [Parachain(*id)]),
209            // relay token (DOT/KSM) - only Asset Hub is valid reserve now
210            (1, None) => origin == &Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]),
211            _ => false,
212        }
213    }
214}
215
216/// Used to deposit XCM fees into a destination account.
217///
218/// Only handles fungible assets for now.
219/// If for any reason taking of the fee fails, it will be burned and and error trace will be printed.
220///
221pub struct XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>(
222    sp_std::marker::PhantomData<(AccountId, Matcher, Assets, FeeDestination)>,
223);
224impl<
225        AccountId: Eq,
226        Assets: fungibles::Mutate<AccountId>,
227        Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
228        FeeDestination: Get<AccountId>,
229    > TakeRevenue for XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>
230{
231    fn take_revenue(revenue: Asset) {
232        match Matcher::matches_fungibles(&revenue) {
233            Ok((asset_id, amount)) => {
234                if amount > Zero::zero() {
235                    if let Err(error) =
236                        Assets::mint_into(asset_id.clone(), &FeeDestination::get(), amount)
237                    {
238                        log::error!(
239                            target: "xcm::weight",
240                            "XcmFeeHandler::take_revenue failed when minting asset: {:?}", error,
241                        );
242                    } else {
243                        log::trace!(
244                            target: "xcm::weight",
245                            "XcmFeeHandler::take_revenue took {:?} of asset Id {:?}",
246                            amount, asset_id,
247                        );
248                    }
249                }
250            }
251            Err(_) => {
252                log::error!(
253                    target: "xcm::weight",
254                    "XcmFeeHandler:take_revenue failed to match fungible asset, it has been burned."
255                );
256            }
257        }
258    }
259}
260
261/// Convert `AccountId` to `Location`.
262pub struct AccountIdToMultiLocation;
263impl Convert<AccountId, Location> for AccountIdToMultiLocation {
264    fn convert(account: AccountId) -> Location {
265        AccountId32 {
266            network: None,
267            id: account.into(),
268        }
269        .into()
270    }
271}
272
273/// `Asset` reserve location provider.
274/// Converts self absolute location to relative location.
275pub struct AbsoluteAndRelativeReserveProvider<AbsoluteLocation>(PhantomData<AbsoluteLocation>);
276impl<AbsoluteLocation: Get<Location>> Reserve
277    for AbsoluteAndRelativeReserveProvider<AbsoluteLocation>
278{
279    fn reserve(asset: &Asset) -> Option<Location> {
280        // Local/native assets (parents==0, no Parachain junction) → Here.
281        // Parent relay chain, sibling or child parachains → their chain prefix.
282        let reserve_location = {
283            let AssetId(location) = &asset.id;
284            match (location.parents, location.first_interior()) {
285                (1, Some(Parachain(id))) => Some(Location::new(1, [Parachain(*id)])),
286                (1, _) => Some(Location::parent()),
287                (0, Some(Parachain(id))) => Some(Location::new(0, [Parachain(*id)])),
288                (0, _) => Some(Location::here()), // local asset, no chain prefix
289                _ => None,
290            }
291        }?;
292
293        if reserve_location == AbsoluteLocation::get() {
294            return Some(Location::here());
295        }
296
297        let is_relay_token = reserve_location.contains_parents_only(1);
298        if is_relay_token {
299            return Some(Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]));
300        }
301
302        Some(reserve_location)
303    }
304}