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    ensure,
37    traits::{tokens::fungibles, Contains, ContainsPair, Get, ProcessMessageError},
38    weights::constants::WEIGHT_REF_TIME_PER_SECOND,
39};
40use sp_runtime::traits::{Bounded, Convert, MaybeEquivalence, Zero};
41use sp_std::marker::PhantomData;
42
43// Polkadot imports
44use xcm::latest::{prelude::*, Weight};
45use xcm_builder::{CreateMatcher, MatchXcm, TakeRevenue};
46use xcm_executor::traits::{MatchesFungibles, Properties, ShouldExecute, WeightTrader};
47
48// ORML imports
49use orml_traits::location::{RelativeReserveProvider, Reserve};
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 foreigin 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    /// Total consumed assets
88    consumed: u128,
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            consumed: 0,
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        payment: xcm_executor::AssetsInHolding,
108        _: &XcmContext,
109    ) -> Result<xcm_executor::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 payment_asset = payment
118            .fungible_assets_iter()
119            .next()
120            .ok_or(XcmError::TooExpensive)?;
121
122        match payment_asset {
123            Asset {
124                id: AssetId(asset_location),
125                fun: Fungibility::Fungible(_),
126            } => {
127                if let Some(units_per_second) = T::get_units_per_second(asset_location.clone()) {
128                    let amount = units_per_second.saturating_mul(weight.ref_time() as u128) // TODO: change this to u64?
129                        / (WEIGHT_REF_TIME_PER_SECOND as u128);
130                    if amount == 0 {
131                        return Ok(payment);
132                    }
133
134                    let unused = payment
135                        .checked_sub((asset_location.clone(), amount).into())
136                        .map_err(|_| XcmError::TooExpensive)?;
137
138                    self.weight = self.weight.saturating_add(weight);
139
140                    // If there are multiple calls to `BuyExecution` but with different assets, we need to be able to handle that.
141                    // Current primitive implementation will just keep total track of consumed asset for the FIRST consumed asset.
142                    // Others will just be ignored when refund is concerned.
143                    if let Some((old_asset_location, _)) =
144                        self.asset_location_and_units_per_second.clone()
145                    {
146                        if old_asset_location == asset_location {
147                            self.consumed = self.consumed.saturating_add(amount);
148                        }
149                    } else {
150                        self.consumed = self.consumed.saturating_add(amount);
151                        self.asset_location_and_units_per_second =
152                            Some((asset_location, units_per_second));
153                    }
154
155                    Ok(unused)
156                } else {
157                    Err(XcmError::TooExpensive)
158                }
159            }
160            _ => Err(XcmError::TooExpensive),
161        }
162    }
163
164    fn refund_weight(&mut self, weight: Weight, _: &XcmContext) -> Option<Asset> {
165        log::trace!(target: "xcm::weight", "FixedRateOfForeignAsset::refund_weight weight: {:?}", weight);
166
167        if let Some((asset_location, units_per_second)) =
168            self.asset_location_and_units_per_second.clone()
169        {
170            let weight = weight.min(self.weight);
171            let amount = units_per_second.saturating_mul(weight.ref_time() as u128)
172                / (WEIGHT_REF_TIME_PER_SECOND as u128);
173
174            self.weight = self.weight.saturating_sub(weight);
175            self.consumed = self.consumed.saturating_sub(amount);
176
177            if amount > 0 {
178                Some((asset_location, amount).into())
179            } else {
180                None
181            }
182        } else {
183            None
184        }
185    }
186}
187
188impl<T: ExecutionPaymentRate, R: TakeRevenue> Drop for FixedRateOfForeignAsset<T, R> {
189    fn drop(&mut self) {
190        if let Some((asset_location, _)) = self.asset_location_and_units_per_second.clone() {
191            if self.consumed > 0 {
192                R::take_revenue((asset_location, self.consumed).into());
193            }
194        }
195    }
196}
197
198/// Used to determine whether the cross-chain asset is coming from a trusted reserve or not
199///
200/// Basically, we trust any cross-chain asset from any location to act as a reserve since
201/// in order to support the xc-asset, we need to first register it in the `XcAssetConfig` pallet.
202///
203pub struct ReserveAssetFilter;
204impl ContainsPair<Asset, Location> for ReserveAssetFilter {
205    fn contains(asset: &Asset, origin: &Location) -> bool {
206        let AssetId(location) = &asset.id;
207        match (location.parents, location.first_interior()) {
208            // sibling parachain reserve
209            (1, Some(Parachain(id))) => origin == &Location::new(1, [Parachain(*id)]),
210            // relay token (DOT/KSM) - only Asset Hub is valid reserve now
211            (1, None) => origin == &Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]),
212            _ => false,
213        }
214    }
215}
216
217/// Used to deposit XCM fees into a destination account.
218///
219/// Only handles fungible assets for now.
220/// If for any reason taking of the fee fails, it will be burned and and error trace will be printed.
221///
222pub struct XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>(
223    sp_std::marker::PhantomData<(AccountId, Matcher, Assets, FeeDestination)>,
224);
225impl<
226        AccountId: Eq,
227        Assets: fungibles::Mutate<AccountId>,
228        Matcher: MatchesFungibles<Assets::AssetId, Assets::Balance>,
229        FeeDestination: Get<AccountId>,
230    > TakeRevenue for XcmFungibleFeeHandler<AccountId, Matcher, Assets, FeeDestination>
231{
232    fn take_revenue(revenue: Asset) {
233        match Matcher::matches_fungibles(&revenue) {
234            Ok((asset_id, amount)) => {
235                if amount > Zero::zero() {
236                    if let Err(error) =
237                        Assets::mint_into(asset_id.clone(), &FeeDestination::get(), amount)
238                    {
239                        log::error!(
240                            target: "xcm::weight",
241                            "XcmFeeHandler::take_revenue failed when minting asset: {:?}", error,
242                        );
243                    } else {
244                        log::trace!(
245                            target: "xcm::weight",
246                            "XcmFeeHandler::take_revenue took {:?} of asset Id {:?}",
247                            amount, asset_id,
248                        );
249                    }
250                }
251            }
252            Err(_) => {
253                log::error!(
254                    target: "xcm::weight",
255                    "XcmFeeHandler:take_revenue failed to match fungible asset, it has been burned."
256                );
257            }
258        }
259    }
260}
261
262/// Convert `AccountId` to `Location`.
263pub struct AccountIdToMultiLocation;
264impl Convert<AccountId, Location> for AccountIdToMultiLocation {
265    fn convert(account: AccountId) -> Location {
266        AccountId32 {
267            network: None,
268            id: account.into(),
269        }
270        .into()
271    }
272}
273
274/// `Asset` reserve location provider. It's based on `RelativeReserveProvider` and in
275/// addition will convert self absolute location to relative location.
276pub struct AbsoluteAndRelativeReserveProvider<AbsoluteLocation>(PhantomData<AbsoluteLocation>);
277impl<AbsoluteLocation: Get<Location>> Reserve
278    for AbsoluteAndRelativeReserveProvider<AbsoluteLocation>
279{
280    fn reserve(asset: &Asset) -> Option<Location> {
281        let reserve_location = RelativeReserveProvider::reserve(asset)?;
282
283        if reserve_location == AbsoluteLocation::get() {
284            return Some(Location::here());
285        }
286
287        let is_relay_token = reserve_location.contains_parents_only(1);
288        if is_relay_token {
289            return Some(Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]));
290        }
291
292        Some(reserve_location)
293    }
294}
295
296// Copying the barrier here due to this issue - https://github.com/paritytech/polkadot-sdk/issues/1638
297// The fix was introduced in v1.3.0 via this PR - https://github.com/paritytech/polkadot-sdk/pull/1733
298// Below is the exact same copy from the fix PR.
299
300const MAX_ASSETS_FOR_BUY_EXECUTION: usize = 2;
301
302/// Allows execution from `origin` if it is contained in `T` (i.e. `T::Contains(origin)`) taking
303/// payments into account.
304///
305/// Only allows for `TeleportAsset`, `WithdrawAsset`, `ClaimAsset` and `ReserveAssetDeposit` XCMs
306/// because they are the only ones that place assets in the Holding Register to pay for execution.
307pub struct AllowTopLevelPaidExecutionFrom<T>(PhantomData<T>);
308impl<T: Contains<Location>> ShouldExecute for AllowTopLevelPaidExecutionFrom<T> {
309    fn should_execute<RuntimeCall>(
310        origin: &Location,
311        instructions: &mut [Instruction<RuntimeCall>],
312        max_weight: Weight,
313        _properties: &mut Properties,
314    ) -> Result<(), ProcessMessageError> {
315        log::trace!(
316            target: "xcm::barriers",
317            "AllowTopLevelPaidExecutionFrom origin: {:?}, instructions: {:?}, max_weight: {:?}, properties: {:?}",
318            origin, instructions, max_weight, _properties,
319        );
320
321        ensure!(T::contains(origin), ProcessMessageError::Unsupported);
322        // We will read up to 5 instructions. This allows up to 3 `ClearOrigin` instructions. We
323        // allow for more than one since anything beyond the first is a no-op and it's conceivable
324        // that composition of operations might result in more than one being appended.
325        let end = instructions.len().min(5);
326        instructions[..end]
327            .matcher()
328            .match_next_inst(|inst| match inst {
329                ReceiveTeleportedAsset(..) | ReserveAssetDeposited(..) => Ok(()),
330                WithdrawAsset(ref assets) if assets.len() <= MAX_ASSETS_FOR_BUY_EXECUTION => Ok(()),
331                ClaimAsset { ref assets, .. } if assets.len() <= MAX_ASSETS_FOR_BUY_EXECUTION => {
332                    Ok(())
333                }
334                _ => Err(ProcessMessageError::BadFormat),
335            })?
336            .skip_inst_while(|inst| matches!(inst, ClearOrigin))?
337            .match_next_inst(|inst| match inst {
338                BuyExecution {
339                    weight_limit: Limited(ref mut weight),
340                    ..
341                } if weight.all_gte(max_weight) => {
342                    *weight = max_weight;
343                    Ok(())
344                }
345                BuyExecution {
346                    ref mut weight_limit,
347                    ..
348                } if weight_limit == &Unlimited => {
349                    *weight_limit = Limited(max_weight);
350                    Ok(())
351                }
352                _ => Err(ProcessMessageError::Overweight(max_weight)),
353            })?;
354        Ok(())
355    }
356}