feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts XCM messages into InboundMessage that can be processed by the Gateway contract
|
||||
|
||||
use codec::DecodeAll;
|
||||
use core::slice::Iter;
|
||||
use frame_support::{ensure, BoundedVec};
|
||||
use snowbridge_core::{AgentIdOf, TokenId, TokenIdOf};
|
||||
|
||||
use crate::v2::{
|
||||
message::{Command, Message},
|
||||
ContractCall,
|
||||
};
|
||||
|
||||
use crate::v2::convert::XcmConverterError::{AssetResolutionFailed, FilterDoesNotConsumeAllAssets};
|
||||
use sp_core::H160;
|
||||
use sp_runtime::traits::MaybeConvert;
|
||||
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
|
||||
use xcm::prelude::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
use XcmConverterError::*;
|
||||
|
||||
/// Errors that can be thrown to the pattern matching step.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum XcmConverterError {
|
||||
UnexpectedEndOfXcm,
|
||||
EndOfXcmMessageExpected,
|
||||
WithdrawAssetExpected,
|
||||
DepositAssetExpected,
|
||||
NoReserveAssets,
|
||||
FilterDoesNotConsumeAllAssets,
|
||||
TooManyAssets,
|
||||
ZeroAssetTransfer,
|
||||
BeneficiaryResolutionFailed,
|
||||
AssetResolutionFailed,
|
||||
InvalidFeeAsset,
|
||||
SetTopicExpected,
|
||||
ReserveAssetDepositedExpected,
|
||||
InvalidAsset,
|
||||
UnexpectedInstruction,
|
||||
TooManyCommands,
|
||||
AliasOriginExpected,
|
||||
InvalidOrigin,
|
||||
TransactDecodeFailed,
|
||||
TransactParamsDecodeFailed,
|
||||
FeeAssetResolutionFailed,
|
||||
CallContractValueInsufficient,
|
||||
NoCommands,
|
||||
}
|
||||
|
||||
macro_rules! match_expression {
|
||||
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => Some($value),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct XcmConverter<'a, ConvertAssetId, Call> {
|
||||
iter: Peekable<Iter<'a, Instruction<Call>>>,
|
||||
ethereum_network: NetworkId,
|
||||
_marker: PhantomData<ConvertAssetId>,
|
||||
}
|
||||
impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
|
||||
where
|
||||
ConvertAssetId: MaybeConvert<TokenId, Location>,
|
||||
{
|
||||
pub fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId) -> Self {
|
||||
Self {
|
||||
iter: message.inner().iter().peekable(),
|
||||
ethereum_network,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn network_matches(&self, network: &Option<NetworkId>) -> bool {
|
||||
if let Some(network) = network {
|
||||
*network == self.ethereum_network
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the fee asset item from PayFees(V5)
|
||||
fn extract_remote_fee(&mut self) -> Result<u128, XcmConverterError> {
|
||||
use XcmConverterError::*;
|
||||
let reserved_fee_assets = match_expression!(self.next()?, WithdrawAsset(fee), fee)
|
||||
.ok_or(WithdrawAssetExpected)?;
|
||||
ensure!(reserved_fee_assets.len() == 1, AssetResolutionFailed);
|
||||
let reserved_fee_asset =
|
||||
reserved_fee_assets.inner().first().cloned().ok_or(AssetResolutionFailed)?;
|
||||
let (reserved_fee_asset_id, reserved_fee_amount) = match reserved_fee_asset {
|
||||
Asset { id: asset_id, fun: Fungible(amount) } => Ok((asset_id, amount)),
|
||||
_ => Err(AssetResolutionFailed),
|
||||
}?;
|
||||
let fee_asset =
|
||||
match_expression!(self.next()?, PayFees { asset: fee }, fee).ok_or(InvalidFeeAsset)?;
|
||||
let (fee_asset_id, fee_amount) = match fee_asset {
|
||||
Asset { id: asset_id, fun: Fungible(amount) } => Ok((asset_id, *amount)),
|
||||
_ => Err(AssetResolutionFailed),
|
||||
}?;
|
||||
// Check the fee asset is Ether (XCM is evaluated in Ethereum context).
|
||||
ensure!(fee_asset_id.0 == Here.into(), InvalidFeeAsset);
|
||||
ensure!(reserved_fee_asset_id.0 == Here.into(), InvalidFeeAsset);
|
||||
ensure!(reserved_fee_amount >= fee_amount, InvalidFeeAsset);
|
||||
Ok(fee_amount)
|
||||
}
|
||||
|
||||
/// Extract ethereum native assets
|
||||
fn extract_ethereum_native_assets(
|
||||
&mut self,
|
||||
enas: &Assets,
|
||||
deposit_assets: &AssetFilter,
|
||||
recipient: H160,
|
||||
) -> Result<Vec<Command>, XcmConverterError> {
|
||||
let mut commands: Vec<Command> = Vec::new();
|
||||
for ena in enas.clone().into_inner().into_iter() {
|
||||
// Check the the deposit asset filter matches what was reserved.
|
||||
if !deposit_assets.matches(&ena) {
|
||||
return Err(FilterDoesNotConsumeAllAssets);
|
||||
}
|
||||
|
||||
// only fungible asset is allowed
|
||||
let (token, amount) = match ena {
|
||||
Asset { id: AssetId(inner_location), fun: Fungible(amount) } => {
|
||||
match inner_location.unpack() {
|
||||
(0, [AccountKey20 { network, key }]) if self.network_matches(network) =>
|
||||
Ok((H160(*key), amount)),
|
||||
// To allow ether
|
||||
(0, []) => Ok((H160([0; 20]), amount)),
|
||||
_ => Err(AssetResolutionFailed),
|
||||
}
|
||||
},
|
||||
_ => Err(AssetResolutionFailed),
|
||||
}?;
|
||||
|
||||
// transfer amount must be greater than 0.
|
||||
ensure!(amount > 0, ZeroAssetTransfer);
|
||||
|
||||
commands.push(Command::UnlockNativeToken { token, recipient, amount });
|
||||
}
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
/// Extract pezkuwi native assets
|
||||
fn extract_pezkuwi_native_assets(
|
||||
&mut self,
|
||||
pnas: &Assets,
|
||||
deposit_assets: &AssetFilter,
|
||||
recipient: H160,
|
||||
) -> Result<Vec<Command>, XcmConverterError> {
|
||||
let mut commands: Vec<Command> = Vec::new();
|
||||
ensure!(pnas.len() > 0, NoReserveAssets);
|
||||
for pna in pnas.clone().into_inner().into_iter() {
|
||||
if !deposit_assets.matches(&pna) {
|
||||
return Err(FilterDoesNotConsumeAllAssets);
|
||||
}
|
||||
|
||||
// Only fungible is allowed
|
||||
let Asset { id: AssetId(asset_id), fun: Fungible(amount) } = pna else {
|
||||
return Err(AssetResolutionFailed);
|
||||
};
|
||||
|
||||
// transfer amount must be greater than 0.
|
||||
ensure!(amount > 0, ZeroAssetTransfer);
|
||||
|
||||
// Ensure PNA already registered
|
||||
let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
|
||||
ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?;
|
||||
|
||||
commands.push(Command::MintForeignToken { token_id, recipient, amount });
|
||||
}
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
/// Convert the XCM into an outbound message which can be dispatched to
|
||||
/// the Gateway contract on Ethereum
|
||||
///
|
||||
/// Assets being transferred can either be Pezkuwi-native assets (PNA)
|
||||
/// or Ethereum-native assets (ENA).
|
||||
///
|
||||
/// The XCM is evaluated in Ethereum context.
|
||||
///
|
||||
/// Expected Input Syntax:
|
||||
/// ```ignore
|
||||
/// WithdrawAsset(ETH)
|
||||
/// PayFees(ETH)
|
||||
/// ReserveAssetDeposited(PNA) | WithdrawAsset(ENA)
|
||||
/// AliasOrigin(Origin)
|
||||
/// DepositAsset(Asset)
|
||||
/// Transact() [OPTIONAL]
|
||||
/// SetTopic(Topic)
|
||||
/// ```
|
||||
/// Notes:
|
||||
/// a. Fee asset will be checked and currently only Ether is allowed
|
||||
/// b. For a specific transfer, either `ReserveAssetDeposited` or `WithdrawAsset` should be
|
||||
/// present
|
||||
/// c. `ReserveAssetDeposited` and `WithdrawAsset` can also be present in any order within the
|
||||
/// same message
|
||||
/// d. Currently, teleport asset is not allowed, transfer types other than
|
||||
/// above will cause the conversion to fail
|
||||
/// e. Currently, `AliasOrigin` is always required, can distinguish the V2 process from V1.
|
||||
/// it's required also for dispatching transact from that specific origin.
|
||||
/// f. SetTopic is required for tracing the message all the way along.
|
||||
pub fn convert(&mut self) -> Result<Message, XcmConverterError> {
|
||||
// Get fee amount
|
||||
let fee_amount = self.extract_remote_fee()?;
|
||||
|
||||
// Get ENA reserve asset from WithdrawAsset.
|
||||
let mut enas =
|
||||
match_expression!(self.peek(), Ok(WithdrawAsset(reserve_assets)), reserve_assets);
|
||||
if enas.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
// Get PNA reserve asset from ReserveAssetDeposited
|
||||
let pnas = match_expression!(
|
||||
self.peek(),
|
||||
Ok(ReserveAssetDeposited(reserve_assets)),
|
||||
reserve_assets
|
||||
);
|
||||
if pnas.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
// Try to get ENA again if it is after PNA
|
||||
if enas.is_none() {
|
||||
enas =
|
||||
match_expression!(self.peek(), Ok(WithdrawAsset(reserve_assets)), reserve_assets);
|
||||
if enas.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
}
|
||||
// Check AliasOrigin.
|
||||
let origin_location = match_expression!(self.next()?, AliasOrigin(origin), origin)
|
||||
.ok_or(AliasOriginExpected)?;
|
||||
let origin = AgentIdOf::convert_location(origin_location).ok_or(InvalidOrigin)?;
|
||||
|
||||
let (deposit_assets, beneficiary) = match_expression!(
|
||||
self.next()?,
|
||||
DepositAsset { assets, beneficiary },
|
||||
(assets, beneficiary)
|
||||
)
|
||||
.ok_or(DepositAssetExpected)?;
|
||||
|
||||
// assert that the beneficiary is AccountKey20.
|
||||
let recipient = match_expression!(
|
||||
beneficiary.unpack(),
|
||||
(0, [AccountKey20 { network, key }])
|
||||
if self.network_matches(network),
|
||||
H160(*key)
|
||||
)
|
||||
.ok_or(BeneficiaryResolutionFailed)?;
|
||||
|
||||
let mut commands: Vec<Command> = Vec::new();
|
||||
|
||||
// ENA transfer commands
|
||||
if let Some(enas) = enas {
|
||||
commands.append(&mut self.extract_ethereum_native_assets(
|
||||
enas,
|
||||
deposit_assets,
|
||||
recipient,
|
||||
)?);
|
||||
}
|
||||
|
||||
// PNA transfer commands
|
||||
if let Some(pnas) = pnas {
|
||||
commands.append(&mut self.extract_pezkuwi_native_assets(
|
||||
pnas,
|
||||
deposit_assets,
|
||||
recipient,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Transact commands
|
||||
let transact_call = match_expression!(self.peek(), Ok(Transact { call, .. }), call);
|
||||
if let Some(transact_call) = transact_call {
|
||||
let _ = self.next();
|
||||
let transact =
|
||||
ContractCall::decode_all(&mut transact_call.clone().into_encoded().as_slice())
|
||||
.map_err(|_| TransactDecodeFailed)?;
|
||||
match transact {
|
||||
ContractCall::V1 { target, calldata, gas, value } => commands
|
||||
.push(Command::CallContract { target: target.into(), calldata, gas, value }),
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(commands.len() > 0, NoCommands);
|
||||
|
||||
// ensure SetTopic exists
|
||||
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
|
||||
|
||||
let message = Message {
|
||||
id: (*topic_id).into(),
|
||||
origin,
|
||||
fee: fee_amount,
|
||||
commands: BoundedVec::try_from(commands).map_err(|_| TooManyCommands)?,
|
||||
};
|
||||
|
||||
// All xcm instructions must be consumed before exit.
|
||||
if self.next().is_ok() {
|
||||
return Err(EndOfXcmMessageExpected);
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts XCM messages into simpler commands that can be processed by the Gateway contract
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod convert;
|
||||
pub use convert::XcmConverter;
|
||||
|
||||
use super::message::SendMessage;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{Contains, Get, ProcessMessageError},
|
||||
};
|
||||
use snowbridge_core::{ParaId, TokenId};
|
||||
use sp_runtime::traits::MaybeConvert;
|
||||
use sp_std::{marker::PhantomData, ops::ControlFlow, prelude::*};
|
||||
use xcm::prelude::*;
|
||||
use xcm_builder::{CreateMatcher, ExporterFor, MatchXcm};
|
||||
use xcm_executor::traits::ExportXcm;
|
||||
|
||||
pub const TARGET: &'static str = "xcm::ethereum_blob_exporter::v2";
|
||||
|
||||
/// Used to process ExportMessages where the destination is Ethereum. It takes an ExportMessage
|
||||
/// and converts it into a simpler message that the Ethereum gateway contract can understand.
|
||||
pub struct EthereumBlobExporter<
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
ConvertAssetId,
|
||||
AssetHubParaId,
|
||||
>(
|
||||
PhantomData<(
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
ConvertAssetId,
|
||||
AssetHubParaId,
|
||||
)>,
|
||||
);
|
||||
|
||||
impl<UniversalLocation, EthereumNetwork, OutboundQueue, ConvertAssetId, AssetHubParaId> ExportXcm
|
||||
for EthereumBlobExporter<
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
ConvertAssetId,
|
||||
AssetHubParaId,
|
||||
>
|
||||
where
|
||||
UniversalLocation: Get<InteriorLocation>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
OutboundQueue: SendMessage,
|
||||
ConvertAssetId: MaybeConvert<TokenId, Location>,
|
||||
AssetHubParaId: Get<ParaId>,
|
||||
{
|
||||
type Ticket = (Vec<u8>, XcmHash);
|
||||
|
||||
fn validate(
|
||||
network: NetworkId,
|
||||
_channel: u32,
|
||||
universal_source: &mut Option<InteriorLocation>,
|
||||
destination: &mut Option<InteriorLocation>,
|
||||
message: &mut Option<Xcm<()>>,
|
||||
) -> SendResult<Self::Ticket> {
|
||||
tracing::debug!(target: TARGET, ?message, "message route through bridge.");
|
||||
|
||||
let expected_network = EthereumNetwork::get();
|
||||
let universal_location = UniversalLocation::get();
|
||||
|
||||
if network != expected_network {
|
||||
tracing::trace!(target: TARGET, ?network, "skipped due to unmatched bridge network.");
|
||||
return Err(SendError::NotApplicable);
|
||||
}
|
||||
|
||||
// Cloning destination to avoid modifying the value so subsequent exporters can use it.
|
||||
let dest = destination.clone().ok_or(SendError::MissingArgument)?;
|
||||
if dest != Here {
|
||||
tracing::trace!(target: TARGET, destination=?dest, "skipped due to unmatched remote destination.");
|
||||
return Err(SendError::NotApplicable);
|
||||
}
|
||||
|
||||
// Cloning universal_source to avoid modifying the value so subsequent exporters can use it.
|
||||
let (local_net, local_sub) = universal_source
|
||||
.clone()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!(target: TARGET, "universal source not provided.");
|
||||
SendError::MissingArgument
|
||||
})?
|
||||
.split_global()
|
||||
.map_err(|()| {
|
||||
tracing::error!(target: TARGET, ?universal_source, "could not get global consensus.");
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
|
||||
if Ok(local_net) != universal_location.global_consensus() {
|
||||
tracing::trace!(target: TARGET, relay_network=?local_net, "skipped due to unmatched relay network.");
|
||||
return Err(SendError::NotApplicable);
|
||||
}
|
||||
|
||||
let para_id = match local_sub.as_slice() {
|
||||
[Teyrchain(para_id)] => *para_id,
|
||||
_ => {
|
||||
tracing::error!(target: TARGET, universal_source=?local_sub, "could not get teyrchain id.");
|
||||
return Err(SendError::NotApplicable);
|
||||
},
|
||||
};
|
||||
|
||||
if ParaId::from(para_id) != AssetHubParaId::get() {
|
||||
tracing::error!(target: TARGET, ?para_id, "is not from asset hub.");
|
||||
return Err(SendError::NotApplicable);
|
||||
}
|
||||
|
||||
let message = message.clone().ok_or_else(|| {
|
||||
tracing::error!(target: TARGET, "xcm message not provided.");
|
||||
SendError::MissingArgument
|
||||
})?;
|
||||
|
||||
// Inspect `AliasOrigin` as V2 message. This exporter should only process Snowbridge V2
|
||||
// messages. We use the presence of an `AliasOrigin` instruction to distinguish between
|
||||
// Snowbridge V2 and Snowbridge V1 messages, since XCM V5 came after Snowbridge V1 and
|
||||
// so it's not supported in Snowbridge V1. Snowbridge V1 messages are processed by the
|
||||
// snowbridge-outbound-queue-primitives v1 exporter.
|
||||
let mut instructions = message.clone().0;
|
||||
let result = instructions.matcher().match_next_inst_while(
|
||||
|_| true,
|
||||
|inst| {
|
||||
return match inst {
|
||||
AliasOrigin(..) => Err(ProcessMessageError::Yield),
|
||||
_ => Ok(ControlFlow::Continue(())),
|
||||
};
|
||||
},
|
||||
);
|
||||
ensure!(result.is_err(), SendError::NotApplicable);
|
||||
|
||||
let mut converter = XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network);
|
||||
let message = converter.convert().map_err(|err| {
|
||||
tracing::error!(target: TARGET, error=?err, "unroutable due to pattern matching.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
// validate the message
|
||||
let ticket = OutboundQueue::validate(&message).map_err(|err| {
|
||||
tracing::error!(target: TARGET, error=?err, "OutboundQueue validation of message failed.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
Ok(((ticket.encode(), XcmHash::from(message.id)), Assets::default()))
|
||||
}
|
||||
|
||||
fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
|
||||
let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
|
||||
.map_err(|_| {
|
||||
tracing::trace!(target: TARGET, "undeliverable due to decoding error");
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
|
||||
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
|
||||
tracing::error!(target: TARGET, "OutboundQueue submit of message failed");
|
||||
SendError::Transport("other transport error")
|
||||
})?;
|
||||
|
||||
tracing::info!(target: TARGET, "message delivered {message_id:#?}.");
|
||||
Ok(message_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter for the implementation of `ExporterFor`, which attempts to find the
|
||||
/// `(bridge_location, payment)` for the requested `network` and `remote_location` and `xcm`
|
||||
/// in the provided `T` table containing various exporters.
|
||||
pub struct XcmFilterExporter<T, M>(core::marker::PhantomData<(T, M)>);
|
||||
impl<T: ExporterFor, M: Contains<Xcm<()>>> ExporterFor for XcmFilterExporter<T, M> {
|
||||
fn exporter_for(
|
||||
network: &NetworkId,
|
||||
remote_location: &InteriorLocation,
|
||||
xcm: &Xcm<()>,
|
||||
) -> Option<(Location, Option<Asset>)> {
|
||||
// check the XCM
|
||||
if !M::contains(xcm) {
|
||||
return None;
|
||||
}
|
||||
// check `network` and `remote_location`
|
||||
T::exporter_for(network, remote_location, xcm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Xcm for SnowbridgeV2 which requires XCMV5
|
||||
pub struct XcmForSnowbridgeV2;
|
||||
impl Contains<Xcm<()>> for XcmForSnowbridgeV2 {
|
||||
fn contains(xcm: &Xcm<()>) -> bool {
|
||||
let mut instructions = xcm.clone().0;
|
||||
let result = instructions.matcher().match_next_inst_while(
|
||||
|_| true,
|
||||
|inst| {
|
||||
return match inst {
|
||||
AliasOrigin(..) => Err(ProcessMessageError::Yield),
|
||||
_ => Ok(ControlFlow::Continue(())),
|
||||
};
|
||||
},
|
||||
);
|
||||
result.is_err()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user