// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see .
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{vec, vec::Vec};
use codec::{Decode, Encode};
use core::{fmt::Debug, marker::PhantomData};
use pezframe_support::{
dispatch::GetDispatchInfo,
ensure,
traits::{Contains, ContainsPair, Defensive, Get, PalletsInfoAccess},
};
use pezsp_core::defer;
use pezsp_io::hashing::blake2_128;
use pezsp_weights::Weight;
use xcm::latest::{prelude::*, AssetTransferFilter};
pub mod traits;
use traits::{
validate_export, AssetExchange, AssetLock, CallDispatcher, ClaimAssets, ConvertOrigin,
DropAssets, Enact, EventEmitter, ExportXcm, FeeManager, FeeReason, HandleHrmpChannelAccepted,
HandleHrmpChannelClosing, HandleHrmpNewChannelOpenRequest, OnResponse, ProcessTransaction,
Properties, ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader,
XcmAssetTransfers,
};
pub use traits::RecordXcm;
mod assets;
pub use assets::AssetsInHolding;
mod config;
pub use config::Config;
#[cfg(test)]
mod tests;
/// A struct to specify how fees are being paid.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct FeesMode {
/// If true, then the fee assets are taken directly from the origin's on-chain account,
/// otherwise the fee assets are taken from the holding register.
///
/// Defaults to false.
pub jit_withdraw: bool,
}
/// The maximum recursion depth allowed when executing nested XCM instructions.
///
/// Exceeding this limit results in `XcmError::ExceedsStackLimit` or
/// `ProcessMessageError::StackLimitReached`.
///
/// Also used in the `DenyRecursively` barrier.
pub const RECURSION_LIMIT: u8 = 10;
environmental::environmental!(recursion_count: u8);
/// The XCM executor.
pub struct XcmExecutor {
holding: AssetsInHolding,
holding_limit: usize,
context: XcmContext,
original_origin: Location,
trader: Config::Trader,
/// The most recent error result and instruction index into the fragment in which it occurred,
/// if any.
error: Option<(u32, XcmError)>,
/// The surplus weight, defined as the amount by which `max_weight` is
/// an over-estimate of the actual weight consumed. We do it this way to avoid needing the
/// execution engine to keep track of all instructions' weights (it only needs to care about
/// the weight of dynamically determined instructions such as `Transact`).
total_surplus: Weight,
total_refunded: Weight,
error_handler: Xcm,
error_handler_weight: Weight,
appendix: Xcm,
appendix_weight: Weight,
transact_status: MaybeErrorCode,
fees_mode: FeesMode,
fees: AssetsInHolding,
/// Asset provided in last `BuyExecution` instruction (if any) in current XCM program. Same
/// asset type will be used for paying any potential delivery fees incurred by the program.
asset_used_in_buy_execution: Option,
/// Stores the current message's weight.
message_weight: Weight,
asset_claimer: Option,
already_paid_fees: bool,
_config: PhantomData,
}
#[cfg(any(test, feature = "runtime-benchmarks"))]
impl XcmExecutor {
pub fn holding(&self) -> &AssetsInHolding {
&self.holding
}
pub fn set_holding(&mut self, v: AssetsInHolding) {
self.holding = v
}
pub fn holding_limit(&self) -> &usize {
&self.holding_limit
}
pub fn set_holding_limit(&mut self, v: usize) {
self.holding_limit = v
}
pub fn origin(&self) -> &Option {
&self.context.origin
}
pub fn set_origin(&mut self, v: Option) {
self.context.origin = v
}
pub fn original_origin(&self) -> &Location {
&self.original_origin
}
pub fn set_original_origin(&mut self, v: Location) {
self.original_origin = v
}
pub fn trader(&self) -> &Config::Trader {
&self.trader
}
pub fn set_trader(&mut self, v: Config::Trader) {
self.trader = v
}
pub fn error(&self) -> &Option<(u32, XcmError)> {
&self.error
}
pub fn set_error(&mut self, v: Option<(u32, XcmError)>) {
self.error = v
}
pub fn total_surplus(&self) -> &Weight {
&self.total_surplus
}
pub fn set_total_surplus(&mut self, v: Weight) {
self.total_surplus = v
}
pub fn total_refunded(&self) -> &Weight {
&self.total_refunded
}
pub fn set_total_refunded(&mut self, v: Weight) {
self.total_refunded = v
}
pub fn error_handler(&self) -> &Xcm {
&self.error_handler
}
pub fn set_error_handler(&mut self, v: Xcm) {
self.error_handler = v
}
pub fn error_handler_weight(&self) -> &Weight {
&self.error_handler_weight
}
pub fn set_error_handler_weight(&mut self, v: Weight) {
self.error_handler_weight = v
}
pub fn appendix(&self) -> &Xcm {
&self.appendix
}
pub fn set_appendix(&mut self, v: Xcm) {
self.appendix = v
}
pub fn appendix_weight(&self) -> &Weight {
&self.appendix_weight
}
pub fn set_appendix_weight(&mut self, v: Weight) {
self.appendix_weight = v
}
pub fn transact_status(&self) -> &MaybeErrorCode {
&self.transact_status
}
pub fn set_transact_status(&mut self, v: MaybeErrorCode) {
self.transact_status = v
}
pub fn fees_mode(&self) -> &FeesMode {
&self.fees_mode
}
pub fn set_fees_mode(&mut self, v: FeesMode) {
self.fees_mode = v
}
pub fn fees(&self) -> &AssetsInHolding {
&self.fees
}
pub fn set_fees(&mut self, value: AssetsInHolding) {
self.fees = value;
}
pub fn topic(&self) -> &Option<[u8; 32]> {
&self.context.topic
}
pub fn set_topic(&mut self, v: Option<[u8; 32]>) {
self.context.topic = v;
}
pub fn asset_claimer(&self) -> Option {
self.asset_claimer.clone()
}
pub fn set_message_weight(&mut self, weight: Weight) {
self.message_weight = weight;
}
pub fn already_paid_fees(&self) -> bool {
self.already_paid_fees
}
}
pub struct WeighedMessage(Weight, Xcm);
impl PreparedMessage for WeighedMessage {
fn weight_of(&self) -> Weight {
self.0
}
}
#[cfg(any(test, feature = "std"))]
impl WeighedMessage {
pub fn new(weight: Weight, message: Xcm) -> Self {
Self(weight, message)
}
}
impl ExecuteXcm for XcmExecutor {
type Prepared = WeighedMessage;
fn prepare(
mut message: Xcm,
weight_limit: Weight,
) -> Result {
match Config::Weigher::weight(&mut message, weight_limit) {
Ok(weight) => Ok(WeighedMessage(weight, message)),
Err(error) => {
tracing::debug!(
target: "xcm::prepare",
?error,
?message,
"Failed to calculate weight for XCM message; execution aborted"
);
Err(error)
},
}
}
fn execute(
origin: impl Into,
WeighedMessage(xcm_weight, mut message): WeighedMessage,
id: &mut XcmHash,
weight_credit: Weight,
) -> Outcome {
let origin = origin.into();
tracing::trace!(
target: "xcm::execute",
?origin,
?message,
?id,
?weight_credit,
"Executing message",
);
let mut properties = Properties { weight_credit, message_id: None };
// We only want to record under certain conditions (mainly only during dry-running),
// so as to not degrade regular performance.
if Config::XcmRecorder::should_record() {
Config::XcmRecorder::record(message.clone().into());
}
if let Err(e) = Config::Barrier::should_execute(
&origin,
message.inner_mut(),
xcm_weight,
&mut properties,
) {
tracing::trace!(
target: "xcm::execute",
?origin,
?message,
?properties,
error = ?e,
"Barrier blocked execution",
);
return Outcome::Incomplete {
used: xcm_weight, // Weight consumed before the error
error: InstructionError { index: 0, error: XcmError::Barrier }, // The error that occurred
};
}
*id = properties.message_id.unwrap_or(*id);
let mut vm = Self::new(origin, *id);
vm.message_weight = xcm_weight;
while !message.0.is_empty() {
let result = vm.process(message);
tracing::trace!(target: "xcm::execute", ?result, "Message executed");
message = if let Err(error) = result {
vm.total_surplus.saturating_accrue(error.weight);
vm.error = Some((error.index, error.xcm_error));
vm.take_error_handler().or_else(|| vm.take_appendix())
} else {
vm.drop_error_handler();
vm.take_appendix()
}
}
vm.post_process(xcm_weight)
}
fn charge_fees(origin: impl Into, fees: Assets) -> XcmResult {
let origin = origin.into();
if !Config::FeeManager::is_waived(Some(&origin), FeeReason::ChargeFees) {
for asset in fees.inner() {
Config::AssetTransactor::withdraw_asset(&asset, &origin, None)?;
}
Config::FeeManager::handle_fee(fees.into(), None, FeeReason::ChargeFees);
}
Ok(())
}
}
impl XcmAssetTransfers for XcmExecutor {
type IsReserve = Config::IsReserve;
type IsTeleporter = Config::IsTeleporter;
type AssetTransactor = Config::AssetTransactor;
}
impl FeeManager for XcmExecutor {
fn is_waived(origin: Option<&Location>, r: FeeReason) -> bool {
Config::FeeManager::is_waived(origin, r)
}
fn handle_fee(fee: Assets, context: Option<&XcmContext>, r: FeeReason) {
Config::FeeManager::handle_fee(fee, context, r)
}
}
#[derive(Debug, PartialEq)]
pub struct ExecutorError {
pub index: u32,
pub xcm_error: XcmError,
pub weight: Weight,
}
#[cfg(feature = "runtime-benchmarks")]
impl From for pezframe_benchmarking::BenchmarkError {
fn from(error: ExecutorError) -> Self {
tracing::error!(
index = ?error.index,
xcm_error = ?error.xcm_error,
weight = ?error.weight,
"XCM ERROR",
);
Self::Stop("xcm executor error: see error logs")
}
}
impl XcmExecutor {
pub fn new(origin: impl Into, message_id: XcmHash) -> Self {
let origin = origin.into();
Self {
holding: AssetsInHolding::new(),
holding_limit: Config::MaxAssetsIntoHolding::get() as usize,
context: XcmContext { origin: Some(origin.clone()), message_id, topic: None },
original_origin: origin,
trader: Config::Trader::new(),
error: None,
total_surplus: Weight::zero(),
total_refunded: Weight::zero(),
error_handler: Xcm(vec![]),
error_handler_weight: Weight::zero(),
appendix: Xcm(vec![]),
appendix_weight: Weight::zero(),
transact_status: Default::default(),
fees_mode: FeesMode { jit_withdraw: false },
fees: AssetsInHolding::new(),
asset_used_in_buy_execution: None,
message_weight: Weight::zero(),
asset_claimer: None,
already_paid_fees: false,
_config: PhantomData,
}
}
/// Execute any final operations after having executed the XCM message.
/// This includes refunding surplus weight, trapping extra holding funds, and returning any
/// errors during execution.
pub fn post_process(mut self, xcm_weight: Weight) -> Outcome {
// We silently drop any error from our attempt to refund the surplus as it's a charitable
// thing so best-effort is all we will do.
let _ = self.refund_surplus();
drop(self.trader);
let mut weight_used = xcm_weight.saturating_sub(self.total_surplus);
if !self.holding.is_empty() {
tracing::trace!(
target: "xcm::post_process",
holding_register = ?self.holding,
context = ?self.context,
original_origin = ?self.original_origin,
"Trapping assets in holding register",
);
let claimer = if let Some(asset_claimer) = self.asset_claimer.as_ref() {
asset_claimer
} else {
self.context.origin.as_ref().unwrap_or(&self.original_origin)
};
let trap_weight = Config::AssetTrap::drop_assets(claimer, self.holding, &self.context);
weight_used.saturating_accrue(trap_weight);
};
match self.error {
None => Outcome::Complete { used: weight_used },
// TODO: #2841 #REALWEIGHT We should deduct the cost of any instructions following
// the error which didn't end up being executed.
Some((index, error)) => {
tracing::trace!(
target: "xcm::post_process",
instruction = ?index,
?error,
original_origin = ?self.original_origin,
"Execution failed",
);
Outcome::Incomplete {
used: weight_used,
error: InstructionError { index: index.try_into().unwrap_or(u8::MAX), error },
}
},
}
}
fn origin_ref(&self) -> Option<&Location> {
self.context.origin.as_ref()
}
fn cloned_origin(&self) -> Option {
self.context.origin.clone()
}
/// Send an XCM, charging fees from Holding as needed.
fn send(
&mut self,
dest: Location,
msg: Xcm<()>,
reason: FeeReason,
) -> Result {
let mut msg = msg;
// Only the last `SetTopic` instruction is considered relevant. If the message does not end
// with it, a `topic_or_message_id()` from the context is appended to it. This behaviour is
// then consistent with `WithUniqueTopic`.
if !matches!(msg.last(), Some(SetTopic(_))) {
let topic_id = self.context.topic_or_message_id();
msg.0.push(SetTopic(topic_id.into()));
}
tracing::trace!(
target: "xcm::send",
?msg,
destination = ?dest,
reason = ?reason,
"Sending msg",
);
let (ticket, fee) = validate_send::(dest.clone(), msg)?;
self.take_fee(fee, reason)?;
match Config::XcmSender::deliver(ticket) {
Ok(message_id) => {
Config::XcmEventEmitter::emit_sent_event(
self.original_origin.clone(),
dest,
None, /* Avoid logging the full XCM message to prevent inconsistencies and
* reduce storage usage. */
message_id,
);
Ok(message_id)
},
Err(error) => {
tracing::debug!(target: "xcm::send", ?error, "XCM failed to deliver with error");
Config::XcmEventEmitter::emit_send_failure_event(
self.original_origin.clone(),
dest,
error.clone(),
self.context.topic_or_message_id(),
);
Err(error.into())
},
}
}
/// Remove the registered error handler and return it. Do not refund its weight.
fn take_error_handler(&mut self) -> Xcm {
let mut r = Xcm::(vec![]);
core::mem::swap(&mut self.error_handler, &mut r);
self.error_handler_weight = Weight::zero();
r
}
/// Drop the registered error handler and refund its weight.
fn drop_error_handler(&mut self) {
self.error_handler = Xcm::(vec![]);
self.total_surplus.saturating_accrue(self.error_handler_weight);
self.error_handler_weight = Weight::zero();
}
/// Remove the registered appendix and return it.
fn take_appendix(&mut self) -> Xcm {
let mut r = Xcm::(vec![]);
core::mem::swap(&mut self.appendix, &mut r);
self.appendix_weight = Weight::zero();
r
}
fn ensure_can_subsume_assets(&self, assets_length: usize) -> Result<(), XcmError> {
// worst-case, holding.len becomes 2 * holding_limit.
// this guarantees that if holding.len() == holding_limit and you have more than
// `holding_limit` items (which has a best case outcome of holding.len() == holding_limit),
// then the operation is guaranteed to succeed.
let worst_case_holding_len = self.holding.len() + assets_length;
tracing::trace!(
target: "xcm::ensure_can_subsume_assets",
?worst_case_holding_len,
holding_limit = ?self.holding_limit,
"Ensuring subsume assets work",
);
ensure!(worst_case_holding_len <= self.holding_limit * 2, XcmError::HoldingWouldOverflow);
Ok(())
}
/// Refund any unused weight.
fn refund_surplus(&mut self) -> Result<(), XcmError> {
let current_surplus = self.total_surplus.saturating_sub(self.total_refunded);
tracing::trace!(
target: "xcm::refund_surplus",
total_surplus = ?self.total_surplus,
total_refunded = ?self.total_refunded,
?current_surplus,
"Refunding surplus",
);
if current_surplus.any_gt(Weight::zero()) {
if let Some(w) = self.trader.refund_weight(current_surplus, &self.context) {
if !self.holding.contains_asset(&(w.id.clone(), 1).into())
&& self.ensure_can_subsume_assets(1).is_err()
{
let _ = self
.trader
.buy_weight(current_surplus, w.into(), &self.context)
.defensive_proof(
"refund_weight returned an asset capable of buying weight; qed",
);
tracing::error!(
target: "xcm::refund_surplus",
"error: HoldingWouldOverflow",
);
return Err(XcmError::HoldingWouldOverflow);
}
self.total_refunded.saturating_accrue(current_surplus);
self.holding.subsume_assets(w.into());
}
}
// If there are any leftover `fees`, merge them with `holding`.
if !self.fees.is_empty() {
let leftover_fees = self.fees.saturating_take(Wild(All));
tracing::trace!(
target: "xcm::refund_surplus",
?leftover_fees,
);
self.holding.subsume_assets(leftover_fees);
}
tracing::trace!(
target: "xcm::refund_surplus",
total_refunded = ?self.total_refunded,
);
Ok(())
}
fn take_fee(&mut self, fees: Assets, reason: FeeReason) -> XcmResult {
if Config::FeeManager::is_waived(self.origin_ref(), reason.clone()) {
return Ok(());
}
tracing::trace!(
target: "xcm::fees",
?fees,
origin_ref = ?self.origin_ref(),
fees_mode = ?self.fees_mode,
?reason,
"Taking fees",
);
// We only ever use the first asset from `fees`.
let asset_needed_for_fees = match fees.get(0) {
Some(fee) => fee,
None => return Ok(()), // No delivery fees need to be paid.
};
// If `BuyExecution` or `PayFees` was called, we use that asset for delivery fees as well.
let asset_to_pay_for_fees =
self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone());
tracing::trace!(target: "xcm::fees", ?asset_to_pay_for_fees);
// We withdraw or take from holding the asset the user wants to use for fee payment.
let withdrawn_fee_asset: AssetsInHolding = if self.fees_mode.jit_withdraw {
let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?;
Config::AssetTransactor::withdraw_asset(
&asset_to_pay_for_fees,
origin,
Some(&self.context),
)?;
tracing::trace!(target: "xcm::fees", ?asset_needed_for_fees);
asset_to_pay_for_fees.clone().into()
} else {
// This condition exists to support `BuyExecution` while the ecosystem
// transitions to `PayFees`.
let assets_to_pay_delivery_fees: AssetsInHolding = if self.fees.is_empty() {
// Means `BuyExecution` was used, we'll find the fees in the `holding` register.
self.holding
.try_take(asset_to_pay_for_fees.clone().into())
.map_err(|e| {
tracing::error!(target: "xcm::fees", ?e, ?asset_to_pay_for_fees,
"Holding doesn't hold enough for fees");
XcmError::NotHoldingFees
})?
.into()
} else {
// Means `PayFees` was used, we'll find the fees in the `fees` register.
self.fees
.try_take(asset_to_pay_for_fees.clone().into())
.map_err(|e| {
tracing::error!(target: "xcm::fees", ?e, ?asset_to_pay_for_fees,
"Fees register doesn't hold enough for fees");
XcmError::NotHoldingFees
})?
.into()
};
tracing::trace!(target: "xcm::fees", ?assets_to_pay_delivery_fees);
let mut iter = assets_to_pay_delivery_fees.fungible_assets_iter();
let asset = iter.next().ok_or(XcmError::NotHoldingFees)?;
asset.into()
};
// We perform the swap, if needed, to pay fees.
let paid = if asset_to_pay_for_fees.id != asset_needed_for_fees.id {
let swapped_asset: Assets = Config::AssetExchanger::exchange_asset(
self.origin_ref(),
withdrawn_fee_asset.clone().into(),
&asset_needed_for_fees.clone().into(),
false,
)
.map_err(|given_assets| {
tracing::error!(
target: "xcm::fees",
?given_assets, "Swap was deemed necessary but couldn't be done for withdrawn_fee_asset: {:?} and asset_needed_for_fees: {:?}", withdrawn_fee_asset.clone(), asset_needed_for_fees,
);
XcmError::FeesNotMet
})?
.into();
swapped_asset
} else {
// If the asset wanted to pay for fees is the one that was needed,
// we don't need to do any swap.
// We just use the assets withdrawn or taken from holding.
withdrawn_fee_asset.into()
};
Config::FeeManager::handle_fee(paid, Some(&self.context), reason);
Ok(())
}
/// Calculates the amount of asset used in `PayFees` or `BuyExecution` that would be
/// charged for swapping to `asset_needed_for_fees`.
///
/// The calculation is done by `Config::AssetExchanger`.
/// If neither `PayFees` or `BuyExecution` were used, or no swap is required,
/// it will just return `asset_needed_for_fees`.
fn calculate_asset_for_delivery_fees(&self, asset_needed_for_fees: Asset) -> Asset {
let Some(asset_wanted_for_fees) =
// we try to swap first asset in the fees register (should only ever be one),
self.fees.fungible.first_key_value().map(|(id, _)| id).or_else(|| {
// or the one used in BuyExecution
self.asset_used_in_buy_execution.as_ref()
})
// if it is different than what we need
.filter(|&id| asset_needed_for_fees.id.ne(id))
else {
// either nothing to swap or we're already holding the right asset
return asset_needed_for_fees
};
Config::AssetExchanger::quote_exchange_price(
&(asset_wanted_for_fees.clone(), Fungible(0)).into(),
&asset_needed_for_fees.clone().into(),
false, // Minimal.
)
.and_then(|necessary_assets| {
// We only use the first asset for fees.
// If this is not enough to swap for the fee asset then it will error later down
// the line.
necessary_assets.into_inner().into_iter().next()
})
.unwrap_or_else(|| {
// If we can't convert, then we return the original asset.
// It will error later in any case.
tracing::trace!(
target: "xcm::calculate_asset_for_delivery_fees",
?asset_wanted_for_fees, "Could not convert fees",
);
asset_needed_for_fees
})
}
/// Calculates what `local_querier` would be from the perspective of `destination`.
fn to_querier(
local_querier: Option,
destination: &Location,
) -> Result