#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
dispatch::{Dispatchable, GetDispatchInfo},
ensure,
};
use sp_runtime::traits::Saturating;
use sp_std::{marker::PhantomData, prelude::*};
use xcm::latest::{
Error as XcmError, ExecuteXcm,
Instruction::{self, *},
MultiAssets, MultiLocation, Outcome, Response, SendXcm, Weight, Xcm,
};
pub mod traits;
use traits::{
ClaimAssets, ConvertOrigin, DropAssets, FilterAssetLocation, InvertLocation, OnResponse,
ShouldExecute, TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader,
};
mod assets;
pub use assets::Assets;
mod config;
pub use config::Config;
pub struct XcmExecutor<Config: config::Config> {
pub holding: Assets,
pub origin: Option<MultiLocation>,
pub original_origin: MultiLocation,
pub trader: Config::Trader,
pub error: Option<(u32, XcmError)>,
pub total_surplus: u64,
pub total_refunded: u64,
pub error_handler: Xcm<Config::RuntimeCall>,
pub error_handler_weight: u64,
pub appendix: Xcm<Config::RuntimeCall>,
pub appendix_weight: u64,
_config: PhantomData<Config>,
}
pub const MAX_RECURSION_LIMIT: u32 = 8;
impl<Config: config::Config> ExecuteXcm<Config::RuntimeCall> for XcmExecutor<Config> {
fn execute_xcm_in_credit(
origin: impl Into<MultiLocation>,
mut message: Xcm<Config::RuntimeCall>,
weight_limit: Weight,
mut weight_credit: Weight,
) -> Outcome {
let origin = origin.into();
log::trace!(
target: "xcm::execute_xcm_in_credit",
"origin: {:?}, message: {:?}, weight_limit: {:?}, weight_credit: {:?}",
origin,
message,
weight_limit,
weight_credit,
);
let xcm_weight = match Config::Weigher::weight(&mut message) {
Ok(x) => x,
Err(()) => {
log::debug!(
target: "xcm::execute_xcm_in_credit",
"Weight not computable! (origin: {:?}, message: {:?}, weight_limit: {:?}, weight_credit: {:?})",
origin,
message,
weight_limit,
weight_credit,
);
return Outcome::Error(XcmError::WeightNotComputable)
},
};
if xcm_weight > weight_limit {
log::debug!(
target: "xcm::execute_xcm_in_credit",
"Weight limit reached! weight > weight_limit: {:?} > {:?}. (origin: {:?}, message: {:?}, weight_limit: {:?}, weight_credit: {:?})",
xcm_weight,
weight_limit,
origin,
message,
weight_limit,
weight_credit,
);
return Outcome::Error(XcmError::WeightLimitReached(xcm_weight))
}
if let Err(e) =
Config::Barrier::should_execute(&origin, &mut message, xcm_weight, &mut weight_credit)
{
log::debug!(
target: "xcm::execute_xcm_in_credit",
"Barrier blocked execution! Error: {:?}. (origin: {:?}, message: {:?}, weight_limit: {:?}, weight_credit: {:?})",
e,
origin,
message,
weight_limit,
weight_credit,
);
return Outcome::Error(XcmError::Barrier)
}
let mut vm = Self::new(origin);
while !message.0.is_empty() {
let result = vm.execute(message);
log::trace!(target: "xcm::execute_xcm_in_credit", "result: {:?}", result);
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_execute(xcm_weight)
}
}
#[derive(Debug)]
pub struct ExecutorError {
pub index: u32,
pub xcm_error: XcmError,
pub weight: u64,
}
#[cfg(feature = "runtime-benchmarks")]
impl From<ExecutorError> for frame_benchmarking::BenchmarkError {
fn from(error: ExecutorError) -> Self {
log::error!(
"XCM ERROR >> Index: {:?}, Error: {:?}, Weight: {:?}",
error.index,
error.xcm_error,
error.weight
);
Self::Stop("xcm executor error: see error logs")
}
}
impl<Config: config::Config> XcmExecutor<Config> {
pub fn new(origin: impl Into<MultiLocation>) -> Self {
let origin = origin.into();
Self {
holding: Assets::new(),
origin: Some(origin.clone()),
original_origin: origin,
trader: Config::Trader::new(),
error: None,
total_surplus: 0,
total_refunded: 0,
error_handler: Xcm(vec![]),
error_handler_weight: 0,
appendix: Xcm(vec![]),
appendix_weight: 0,
_config: PhantomData,
}
}
pub fn execute(&mut self, xcm: Xcm<Config::RuntimeCall>) -> Result<(), ExecutorError> {
log::trace!(
target: "xcm::execute",
"origin: {:?}, total_surplus/refunded: {:?}/{:?}, error_handler_weight: {:?}",
self.origin,
self.total_surplus,
self.total_refunded,
self.error_handler_weight,
);
let mut result = Ok(());
for (i, instr) in xcm.0.into_iter().enumerate() {
match &mut result {
r @ Ok(()) =>
if let Err(e) = self.process_instruction(instr) {
*r = Err(ExecutorError { index: i as u32, xcm_error: e, weight: 0 });
},
Err(ref mut error) =>
if let Ok(x) = Config::Weigher::instr_weight(&instr) {
error.weight.saturating_accrue(x)
},
}
}
result
}
pub fn post_execute(mut self, xcm_weight: Weight) -> Outcome {
self.refund_surplus();
drop(self.trader);
let mut weight_used = xcm_weight.saturating_sub(self.total_surplus);
if !self.holding.is_empty() {
log::trace!(target: "xcm::execute_xcm_in_credit", "Trapping assets in holding register: {:?} (original_origin: {:?})", self.holding, self.original_origin);
let trap_weight = Config::AssetTrap::drop_assets(&self.original_origin, self.holding);
weight_used.saturating_accrue(trap_weight);
};
match self.error {
None => Outcome::Complete(weight_used),
Some((_i, e)) => {
log::debug!(target: "xcm::execute_xcm_in_credit", "Execution errored at {:?}: {:?} (original_origin: {:?})", _i, e, self.original_origin);
Outcome::Incomplete(weight_used, e)
},
}
}
fn take_error_handler(&mut self) -> Xcm<Config::RuntimeCall> {
let mut r = Xcm::<Config::RuntimeCall>(vec![]);
sp_std::mem::swap(&mut self.error_handler, &mut r);
self.error_handler_weight = 0;
r
}
fn drop_error_handler(&mut self) {
self.error_handler = Xcm::<Config::RuntimeCall>(vec![]);
self.total_surplus.saturating_accrue(self.error_handler_weight);
self.error_handler_weight = 0;
}
fn take_appendix(&mut self) -> Xcm<Config::RuntimeCall> {
let mut r = Xcm::<Config::RuntimeCall>(vec![]);
sp_std::mem::swap(&mut self.appendix, &mut r);
self.appendix_weight = 0;
r
}
fn refund_surplus(&mut self) {
let current_surplus = self.total_surplus.saturating_sub(self.total_refunded);
if current_surplus > 0 {
self.total_refunded.saturating_accrue(current_surplus);
if let Some(w) = self.trader.refund_weight(current_surplus) {
self.holding.subsume(w);
}
}
}
fn process_instruction(
&mut self,
instr: Instruction<Config::RuntimeCall>,
) -> Result<(), XcmError> {
match instr {
WithdrawAsset(assets) => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
for asset in assets.drain().into_iter() {
Config::AssetTransactor::withdraw_asset(&asset, origin)?;
self.holding.subsume(asset);
}
Ok(())
},
ReserveAssetDeposited(assets) => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
for asset in assets.drain().into_iter() {
ensure!(
Config::IsReserve::filter_asset_location(&asset, origin),
XcmError::UntrustedReserveLocation
);
self.holding.subsume(asset);
}
Ok(())
},
TransferAsset { assets, beneficiary } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
for asset in assets.inner() {
Config::AssetTransactor::transfer_asset(&asset, origin, &beneficiary)?;
}
Ok(())
},
TransferReserveAsset { mut assets, dest, xcm } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
for asset in assets.inner() {
Config::AssetTransactor::transfer_asset(asset, origin, &dest)?;
}
let ancestry = Config::LocationInverter::ancestry();
assets.reanchor(&dest, &ancestry).map_err(|()| XcmError::MultiLocationFull)?;
let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin];
message.extend(xcm.0.into_iter());
Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into)
},
ReceiveTeleportedAsset(assets) => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
for asset in assets.inner() {
ensure!(
Config::IsTeleporter::filter_asset_location(asset, origin),
XcmError::UntrustedTeleportLocation
);
Config::AssetTransactor::can_check_in(&origin, asset)?;
}
for asset in assets.drain().into_iter() {
Config::AssetTransactor::check_in(origin, &asset);
self.holding.subsume(asset);
}
Ok(())
},
Transact { origin_type, require_weight_at_most, mut call } => {
let origin = self.origin.clone().ok_or(XcmError::BadOrigin)?;
let message_call = call.take_decoded().map_err(|_| XcmError::FailedToDecode)?;
let dispatch_origin = Config::OriginConverter::convert_origin(origin, origin_type)
.map_err(|_| XcmError::BadOrigin)?;
let weight = message_call.get_dispatch_info().weight;
ensure!(weight.ref_time() <= require_weight_at_most, XcmError::MaxWeightInvalid);
let actual_weight = match message_call.dispatch(dispatch_origin) {
Ok(post_info) => post_info.actual_weight,
Err(error_and_info) => {
error_and_info.post_info.actual_weight
},
}
.unwrap_or(weight);
let surplus = weight.saturating_sub(actual_weight);
self.total_surplus.saturating_accrue(surplus.ref_time());
Ok(())
},
QueryResponse { query_id, response, max_weight } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
Config::ResponseHandler::on_response(origin, query_id, response, max_weight);
Ok(())
},
DescendOrigin(who) => self
.origin
.as_mut()
.ok_or(XcmError::BadOrigin)?
.append_with(who)
.map_err(|_| XcmError::MultiLocationFull),
ClearOrigin => {
self.origin = None;
Ok(())
},
ReportError { query_id, dest, max_response_weight: max_weight } => {
let response = Response::ExecutionResult(self.error);
let message = QueryResponse { query_id, response, max_weight };
Config::XcmSender::send_xcm(dest, Xcm(vec![message]))?;
Ok(())
},
DepositAsset { assets, max_assets, beneficiary } => {
let deposited = self.holding.limited_saturating_take(assets, max_assets as usize);
for asset in deposited.into_assets_iter() {
Config::AssetTransactor::deposit_asset(&asset, &beneficiary)?;
}
Ok(())
},
DepositReserveAsset { assets, max_assets, dest, xcm } => {
let deposited = self.holding.limited_saturating_take(assets, max_assets as usize);
for asset in deposited.assets_iter() {
Config::AssetTransactor::deposit_asset(&asset, &dest)?;
}
let assets = Self::reanchored(deposited, &dest, None);
let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin];
message.extend(xcm.0.into_iter());
Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into)
},
InitiateReserveWithdraw { assets, reserve, xcm } => {
let assets = Self::reanchored(
self.holding.saturating_take(assets),
&reserve,
Some(&mut self.holding),
);
let mut message = vec![WithdrawAsset(assets), ClearOrigin];
message.extend(xcm.0.into_iter());
Config::XcmSender::send_xcm(reserve, Xcm(message)).map_err(Into::into)
},
InitiateTeleport { assets, dest, xcm } => {
let assets = self.holding.saturating_take(assets);
for asset in assets.assets_iter() {
Config::AssetTransactor::check_out(&dest, &asset);
}
let assets = Self::reanchored(assets, &dest, None);
let mut message = vec![ReceiveTeleportedAsset(assets), ClearOrigin];
message.extend(xcm.0.into_iter());
Config::XcmSender::send_xcm(dest, Xcm(message)).map_err(Into::into)
},
QueryHolding { query_id, dest, assets, max_response_weight } => {
let assets = Self::reanchored(self.holding.min(&assets), &dest, None);
let max_weight = max_response_weight;
let response = Response::Assets(assets);
let instruction = QueryResponse { query_id, response, max_weight };
Config::XcmSender::send_xcm(dest, Xcm(vec![instruction])).map_err(Into::into)
},
BuyExecution { fees, weight_limit } => {
if let Some(weight) = Option::<u64>::from(weight_limit) {
let max_fee =
self.holding.try_take(fees.into()).map_err(|_| XcmError::NotHoldingFees)?;
let unspent = self.trader.buy_weight(weight, max_fee)?;
self.holding.subsume_assets(unspent);
}
Ok(())
},
RefundSurplus => {
self.refund_surplus();
Ok(())
},
SetErrorHandler(mut handler) => {
let handler_weight = Config::Weigher::weight(&mut handler)
.map_err(|()| XcmError::WeightNotComputable)?;
self.total_surplus.saturating_accrue(self.error_handler_weight);
self.error_handler = handler;
self.error_handler_weight = handler_weight;
Ok(())
},
SetAppendix(mut appendix) => {
let appendix_weight = Config::Weigher::weight(&mut appendix)
.map_err(|()| XcmError::WeightNotComputable)?;
self.total_surplus.saturating_accrue(self.appendix_weight);
self.appendix = appendix;
self.appendix_weight = appendix_weight;
Ok(())
},
ClearError => {
self.error = None;
Ok(())
},
ClaimAsset { assets, ticket } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
let ok = Config::AssetClaims::claim_assets(origin, &ticket, &assets);
ensure!(ok, XcmError::UnknownClaim);
for asset in assets.drain().into_iter() {
self.holding.subsume(asset);
}
Ok(())
},
Trap(code) => Err(XcmError::Trap(code)),
SubscribeVersion { query_id, max_response_weight } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?.clone();
ensure!(self.original_origin == origin, XcmError::BadOrigin);
Config::SubscriptionService::start(&origin, query_id, max_response_weight)
},
UnsubscribeVersion => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
ensure!(&self.original_origin == origin, XcmError::BadOrigin);
Config::SubscriptionService::stop(origin)
},
ExchangeAsset { .. } => Err(XcmError::Unimplemented),
HrmpNewChannelOpenRequest { .. } => Err(XcmError::Unimplemented),
HrmpChannelAccepted { .. } => Err(XcmError::Unimplemented),
HrmpChannelClosing { .. } => Err(XcmError::Unimplemented),
}
}
fn reanchored(
mut assets: Assets,
dest: &MultiLocation,
maybe_failed_bin: Option<&mut Assets>,
) -> MultiAssets {
assets.reanchor(dest, &Config::LocationInverter::ancestry(), maybe_failed_bin);
assets.into_assets_iter().collect::<Vec<_>>().into()
}
}