feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -0,0 +1,75 @@
[package]
name = "snowbridge-outbound-queue-primitives"
description = "Snowbridge Outbound Queue Primitives"
version = "0.2.0"
authors = ["Snowfork <contact@snowfork.com>"]
edition.workspace = true
repository.workspace = true
license = "Apache-2.0"
categories = ["cryptography::cryptocurrencies"]
[lints]
workspace = true
[package.metadata.pezkuwi-sdk]
exclude-from-umbrella = true
[dependencies]
codec = { workspace = true }
hex-literal = { workspace = true, default-features = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
pezkuwi-teyrchain-primitives = { workspace = true }
xcm = { workspace = true }
xcm-builder = { workspace = true }
xcm-executor = { workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-arithmetic = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
alloy-core = { workspace = true, features = ["sol-types"] }
ethabi = { workspace = true }
snowbridge-core = { workspace = true }
snowbridge-verification-primitives = { workspace = true }
[features]
default = ["std"]
std = [
"alloy-core/std",
"codec/std",
"ethabi/std",
"frame-support/std",
"frame-system/std",
"pezkuwi-teyrchain-primitives/std",
"scale-info/std",
"snowbridge-core/std",
"snowbridge-verification-primitives/std",
"sp-arithmetic/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"tracing/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"snowbridge-verification-primitives/runtime-benchmarks",
"sp-io/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
#![cfg_attr(not(feature = "std"), no_std)]
//! # Outbound
//!
//! Common traits and types
pub mod v1;
pub mod v2;
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::PalletError;
use scale_info::TypeInfo;
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
use sp_core::RuntimeDebug;
pub use snowbridge_verification_primitives::*;
/// The operating mode of Channels and Gateway contract on Ethereum.
#[derive(
Copy, Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, RuntimeDebug, TypeInfo,
)]
pub enum OperatingMode {
/// Normal operations. Allow sending and receiving messages.
Normal,
/// Reject outbound messages. This allows receiving governance messages but does now allow
/// enqueuing of new messages from the Ethereum side. This can be used to close off a
/// deprecated channel or pause the bridge for upgrade operations.
RejectingOutboundMessages,
}
/// A trait for getting the local costs associated with sending a message.
pub trait SendMessageFeeProvider {
type Balance: BaseArithmetic + Unsigned + Copy;
/// The local component of the message processing fees in native currency
fn local_fee() -> Self::Balance;
}
/// Reasons why sending to Ethereum could not be initiated
#[derive(
Copy,
Clone,
Encode,
Decode,
DecodeWithMemTracking,
PartialEq,
Eq,
RuntimeDebug,
PalletError,
TypeInfo,
)]
pub enum SendError {
/// Message is too large to be safely executed on Ethereum
MessageTooLarge,
/// The bridge has been halted for maintenance
Halted,
/// Invalid Channel
InvalidChannel,
/// Invalid Origin
InvalidOrigin,
}
@@ -0,0 +1,424 @@
// 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;
use core::slice::Iter;
use codec::{Decode, Encode};
use super::message::{Command, Message, SendMessage};
use frame_support::{ensure, traits::Get};
use snowbridge_core::{AgentId, ChannelId, ParaId, TokenId, TokenIdOf};
use sp_core::{H160, H256};
use sp_runtime::traits::MaybeConvert;
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
use xcm::prelude::*;
use xcm_executor::traits::{ConvertLocation, ExportXcm};
pub struct EthereumBlobExporter<
UniversalLocation,
EthereumNetwork,
OutboundQueue,
AgentHashedDescription,
ConvertAssetId,
>(
PhantomData<(
UniversalLocation,
EthereumNetwork,
OutboundQueue,
AgentHashedDescription,
ConvertAssetId,
)>,
);
impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription, ConvertAssetId>
ExportXcm
for EthereumBlobExporter<
UniversalLocation,
EthereumNetwork,
OutboundQueue,
AgentHashedDescription,
ConvertAssetId,
>
where
UniversalLocation: Get<InteriorLocation>,
EthereumNetwork: Get<NetworkId>,
OutboundQueue: SendMessage<Balance = u128>,
AgentHashedDescription: ConvertLocation<H256>,
ConvertAssetId: MaybeConvert<TokenId, Location>,
{
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> {
let expected_network = EthereumNetwork::get();
let universal_location = UniversalLocation::get();
if network != expected_network {
tracing::trace!(target: "xcm::ethereum_blob_exporter", ?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: "xcm::ethereum_blob_exporter", 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: "xcm::ethereum_blob_exporter", "universal source not provided.");
SendError::MissingArgument
})?
.split_global()
.map_err(|()| {
tracing::error!(target: "xcm::ethereum_blob_exporter", ?universal_source, "could not get global consensus.");
SendError::NotApplicable
})?;
if Ok(local_net) != universal_location.global_consensus() {
tracing::trace!(target: "xcm::ethereum_blob_exporter", 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: "xcm::ethereum_blob_exporter", universal_source=?local_sub, "could not get teyrchain id.");
return Err(SendError::NotApplicable);
},
};
let source_location = Location::new(1, local_sub.clone());
let agent_id = match AgentHashedDescription::convert_location(&source_location) {
Some(id) => id,
None => {
tracing::error!(target: "xcm::ethereum_blob_exporter", ?source_location, "unroutable due to not being able to create agent id.");
return Err(SendError::NotApplicable);
},
};
let message = message.take().ok_or_else(|| {
tracing::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
SendError::MissingArgument
})?;
let mut converter =
XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network, agent_id);
let (command, message_id) = converter.convert().map_err(|err|{
tracing::error!(target: "xcm::ethereum_blob_exporter", error=?err, "unroutable due to pattern matching.");
SendError::Unroutable
})?;
let channel_id: ChannelId = ParaId::from(para_id).into();
let outbound_message = Message { id: Some(message_id.into()), channel_id, command };
// validate the message
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
tracing::error!(target: "xcm::ethereum_blob_exporter", error=?err, "OutboundQueue validation of message failed.");
SendError::Unroutable
})?;
// convert fee to Asset
let fee = Asset::from((Location::parent(), fee.total())).into();
Ok(((ticket.encode(), message_id), fee))
}
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: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
SendError::NotApplicable
})?;
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
tracing::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
SendError::Transport("other transport error")
})?;
tracing::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
Ok(message_id.into())
}
}
/// Errors that can be thrown to the pattern matching step.
#[derive(PartialEq, Debug)]
enum XcmConverterError {
UnexpectedEndOfXcm,
EndOfXcmMessageExpected,
WithdrawAssetExpected,
DepositAssetExpected,
NoReserveAssets,
FilterDoesNotConsumeAllAssets,
TooManyAssets,
ZeroAssetTransfer,
BeneficiaryResolutionFailed,
AssetResolutionFailed,
InvalidFeeAsset,
SetTopicExpected,
ReserveAssetDepositedExpected,
InvalidAsset,
UnexpectedInstruction,
}
/// Macro used for capturing values when the pattern matches.
/// Specifically here for matching against xcm instructions and capture the params in that
/// instruction
macro_rules! match_expression {
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
match $expression {
$( $pattern )|+ $( if $guard )? => Some($value),
_ => None,
}
};
}
struct XcmConverter<'a, ConvertAssetId, Call> {
iter: Peekable<Iter<'a, Instruction<Call>>>,
ethereum_network: NetworkId,
agent_id: AgentId,
_marker: PhantomData<ConvertAssetId>,
}
impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
where
ConvertAssetId: MaybeConvert<TokenId, Location>,
{
fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
Self {
iter: message.inner().iter().peekable(),
ethereum_network,
agent_id,
_marker: Default::default(),
}
}
fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
let result = match self.peek() {
Ok(ReserveAssetDeposited { .. }) => self.make_mint_foreign_token_command(),
// Get withdraw/deposit and make native tokens create message.
Ok(WithdrawAsset { .. }) => self.make_unlock_native_token_command(),
Err(e) => Err(e),
_ => return Err(XcmConverterError::UnexpectedInstruction),
}?;
// All xcm instructions must be consumed before exit.
if self.next().is_ok() {
return Err(XcmConverterError::EndOfXcmMessageExpected);
}
Ok(result)
}
fn make_unlock_native_token_command(
&mut self,
) -> Result<(Command, [u8; 32]), XcmConverterError> {
use XcmConverterError::*;
// Get the reserve assets from WithdrawAsset.
let reserve_assets =
match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
.ok_or(WithdrawAssetExpected)?;
// Check if clear origin exists and skip over it.
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
let _ = self.next();
}
// Get the fee asset item from BuyExecution or continue parsing.
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
if fee_asset.is_some() {
let _ = self.next();
}
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)?;
// Make sure there are reserved assets.
if reserve_assets.len() == 0 {
return Err(NoReserveAssets);
}
// Check the the deposit asset filter matches what was reserved.
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
return Err(FilterDoesNotConsumeAllAssets);
}
// We only support a single asset at a time.
ensure!(reserve_assets.len() == 1, TooManyAssets);
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
// Fees are collected on AH, up front and directly from the user, to cover the
// complete cost of the transfer. Any additional fees provided in the XCM program are
// refunded to the beneficiary. We only validate the fee here if its provided to make sure
// the XCM program is well formed. Another way to think about this from an XCM perspective
// would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
// (no fee) and refund X to the user.
if let Some(fee_asset) = fee_asset {
// The fee asset must be the same as the reserve asset.
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
return Err(InvalidFeeAsset);
}
}
let (token, amount) = match reserve_asset {
Asset { id: AssetId(inner_location), fun: Fungible(amount) } => {
match inner_location.unpack() {
// Get the ERC20 contract address of the token.
(0, [AccountKey20 { network, key }]) if self.network_matches(network) =>
Some((H160(*key), *amount)),
// If there is no ERC20 contract address in the location then signal to the
// gateway that is a native Ether transfer by using
// `0x0000000000000000000000000000000000000000` as the token address.
(0, []) => Some((H160([0; 20]), *amount)),
_ => None,
}
},
_ => None,
}
.ok_or(AssetResolutionFailed)?;
// transfer amount must be greater than 0.
ensure!(amount > 0, ZeroAssetTransfer);
// Check if there is a SetTopic and skip over it if found.
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
Ok((
Command::UnlockNativeToken { agent_id: self.agent_id, token, recipient, amount },
*topic_id,
))
}
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
}
}
/// Convert the xcm for Pezkuwi-native token from AH into the Command
/// To match transfers of Pezkuwi-native tokens, we expect an input of the form:
/// # ReserveAssetDeposited
/// # ClearOrigin
/// # BuyExecution
/// # DepositAsset
/// # SetTopic
fn make_mint_foreign_token_command(
&mut self,
) -> Result<(Command, [u8; 32]), XcmConverterError> {
use XcmConverterError::*;
// Get the reserve assets.
let reserve_assets =
match_expression!(self.next()?, ReserveAssetDeposited(reserve_assets), reserve_assets)
.ok_or(ReserveAssetDepositedExpected)?;
// Check if clear origin exists and skip over it.
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
let _ = self.next();
}
// Get the fee asset item from BuyExecution or continue parsing.
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
if fee_asset.is_some() {
let _ = self.next();
}
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)?;
// Make sure there are reserved assets.
if reserve_assets.len() == 0 {
return Err(NoReserveAssets);
}
// Check the the deposit asset filter matches what was reserved.
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
return Err(FilterDoesNotConsumeAllAssets);
}
// We only support a single asset at a time.
ensure!(reserve_assets.len() == 1, TooManyAssets);
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
// Fees are collected on AH, up front and directly from the user, to cover the
// complete cost of the transfer. Any additional fees provided in the XCM program are
// refunded to the beneficiary. We only validate the fee here if its provided to make sure
// the XCM program is well formed. Another way to think about this from an XCM perspective
// would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
// (no fee) and refund X to the user.
if let Some(fee_asset) = fee_asset {
// The fee asset must be the same as the reserve asset.
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
return Err(InvalidFeeAsset);
}
}
let (asset_id, amount) = match reserve_asset {
Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
Some((inner_location.clone(), *amount)),
_ => None,
}
.ok_or(AssetResolutionFailed)?;
// transfer amount must be greater than 0.
ensure!(amount > 0, ZeroAssetTransfer);
let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?;
// Check if there is a SetTopic and skip over it if found.
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
Ok((Command::MintForeignToken { token_id, recipient, amount }, *topic_id))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,380 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! # Outbound V1 primitives
use crate::{OperatingMode, SendError, SendMessageFeeProvider};
use codec::{Decode, DecodeWithMemTracking, Encode};
use ethabi::Token;
use scale_info::TypeInfo;
use snowbridge_core::{pricing::UD60x18, ChannelId};
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
use sp_core::{RuntimeDebug, H160, H256, U256};
use sp_std::{borrow::ToOwned, vec, vec::Vec};
/// Enqueued outbound messages need to be versioned to prevent data corruption
/// or loss after forkless runtime upgrades
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub enum VersionedQueuedMessage {
V1(QueuedMessage),
}
impl TryFrom<VersionedQueuedMessage> for QueuedMessage {
type Error = ();
fn try_from(x: VersionedQueuedMessage) -> Result<Self, Self::Error> {
use VersionedQueuedMessage::*;
match x {
V1(x) => Ok(x),
}
}
}
impl<T: Into<QueuedMessage>> From<T> for VersionedQueuedMessage {
fn from(x: T) -> Self {
VersionedQueuedMessage::V1(x.into())
}
}
/// A message which can be accepted by implementations of `/[`SendMessage`\]`
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub struct Message {
/// ID for this message. One will be automatically generated if not provided.
///
/// When this message is created from an XCM message, the ID should be extracted
/// from the `SetTopic` instruction.
///
/// The ID plays no role in bridge consensus, and is purely meant for message tracing.
pub id: Option<H256>,
/// The message channel ID
pub channel_id: ChannelId,
/// The stable ID for a receiving gateway contract
pub command: Command,
}
/// A command which is executable by the Gateway contract on Ethereum
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub enum Command {
/// Execute a sub-command within an agent for a consensus system in Pezkuwi
/// DEPRECATED in favour of `UnlockNativeToken`. We still have to keep it around in
/// case buffered and uncommitted messages are using this variant.
AgentExecute {
/// The ID of the agent
agent_id: H256,
/// The sub-command to be executed
command: AgentExecuteCommand,
},
/// Upgrade the Gateway contract
Upgrade {
/// Address of the new implementation contract
impl_address: H160,
/// Codehash of the implementation contract
impl_code_hash: H256,
/// Optionally invoke an initializer in the implementation contract
initializer: Option<Initializer>,
},
/// Set the global operating mode of the Gateway contract
SetOperatingMode {
/// The new operating mode
mode: OperatingMode,
},
/// Set token fees of the Gateway contract
SetTokenTransferFees {
/// The fee(HEZ) for the cost of creating asset on AssetHub
create_asset_xcm: u128,
/// The fee(HEZ) for the cost of sending asset on AssetHub
transfer_asset_xcm: u128,
/// The fee(Ether) for register token to discourage spamming
register_token: U256,
},
/// Set pricing parameters
SetPricingParameters {
// ETH/HEZ exchange rate
exchange_rate: UD60x18,
// Cost of delivering a message from Ethereum to BridgeHub, in TYR/KSM/HEZ
delivery_cost: u128,
// Fee multiplier
multiplier: UD60x18,
},
/// Transfer ERC20 tokens
UnlockNativeToken {
/// ID of the agent
agent_id: H256,
/// Address of the ERC20 token
token: H160,
/// The recipient of the tokens
recipient: H160,
/// The amount of tokens to transfer
amount: u128,
},
/// Register foreign token from Pezkuwi
RegisterForeignToken {
/// ID for the token
token_id: H256,
/// Name of the token
name: Vec<u8>,
/// Short symbol for the token
symbol: Vec<u8>,
/// Number of decimal places
decimals: u8,
},
/// Mint foreign token from Pezkuwi
MintForeignToken {
/// ID for the token
token_id: H256,
/// The recipient of the newly minted tokens
recipient: H160,
/// The amount of tokens to mint
amount: u128,
},
}
impl Command {
/// Compute the enum variant index
pub fn index(&self) -> u8 {
match self {
Command::AgentExecute { .. } => 0,
Command::Upgrade { .. } => 1,
Command::SetOperatingMode { .. } => 5,
Command::SetTokenTransferFees { .. } => 7,
Command::SetPricingParameters { .. } => 8,
Command::UnlockNativeToken { .. } => 9,
Command::RegisterForeignToken { .. } => 10,
Command::MintForeignToken { .. } => 11,
}
}
/// ABI-encode the Command.
pub fn abi_encode(&self) -> Vec<u8> {
match self {
Command::AgentExecute { agent_id, command } => ethabi::encode(&[Token::Tuple(vec![
Token::FixedBytes(agent_id.as_bytes().to_owned()),
Token::Bytes(command.abi_encode()),
])]),
Command::Upgrade { impl_address, impl_code_hash, initializer, .. } =>
ethabi::encode(&[Token::Tuple(vec![
Token::Address(*impl_address),
Token::FixedBytes(impl_code_hash.as_bytes().to_owned()),
initializer.clone().map_or(Token::Bytes(vec![]), |i| Token::Bytes(i.params)),
])]),
Command::SetOperatingMode { mode } =>
ethabi::encode(&[Token::Tuple(vec![Token::Uint(U256::from((*mode) as u64))])]),
Command::SetTokenTransferFees {
create_asset_xcm,
transfer_asset_xcm,
register_token,
} => ethabi::encode(&[Token::Tuple(vec![
Token::Uint(U256::from(*create_asset_xcm)),
Token::Uint(U256::from(*transfer_asset_xcm)),
Token::Uint(*register_token),
])]),
Command::SetPricingParameters { exchange_rate, delivery_cost, multiplier } =>
ethabi::encode(&[Token::Tuple(vec![
Token::Uint(exchange_rate.clone().into_inner()),
Token::Uint(U256::from(*delivery_cost)),
Token::Uint(multiplier.clone().into_inner()),
])]),
Command::UnlockNativeToken { agent_id, token, recipient, amount } =>
ethabi::encode(&[Token::Tuple(vec![
Token::FixedBytes(agent_id.as_bytes().to_owned()),
Token::Address(*token),
Token::Address(*recipient),
Token::Uint(U256::from(*amount)),
])]),
Command::RegisterForeignToken { token_id, name, symbol, decimals } =>
ethabi::encode(&[Token::Tuple(vec![
Token::FixedBytes(token_id.as_bytes().to_owned()),
Token::String(name.to_owned()),
Token::String(symbol.to_owned()),
Token::Uint(U256::from(*decimals)),
])]),
Command::MintForeignToken { token_id, recipient, amount } =>
ethabi::encode(&[Token::Tuple(vec![
Token::FixedBytes(token_id.as_bytes().to_owned()),
Token::Address(*recipient),
Token::Uint(U256::from(*amount)),
])]),
}
}
}
/// Representation of a call to the initializer of an implementation contract.
/// The initializer has the following ABI signature: `initialize(bytes)`.
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Initializer {
/// ABI-encoded params of type `bytes` to pass to the initializer
pub params: Vec<u8>,
/// The initializer is allowed to consume this much gas at most.
pub maximum_required_gas: u64,
}
/// A Sub-command executable within an agent
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub enum AgentExecuteCommand {
/// Transfer ERC20 tokens
TransferToken {
/// Address of the ERC20 token
token: H160,
/// The recipient of the tokens
recipient: H160,
/// The amount of tokens to transfer
amount: u128,
},
}
impl AgentExecuteCommand {
fn index(&self) -> u8 {
match self {
AgentExecuteCommand::TransferToken { .. } => 0,
}
}
/// ABI-encode the sub-command
pub fn abi_encode(&self) -> Vec<u8> {
match self {
AgentExecuteCommand::TransferToken { token, recipient, amount } => ethabi::encode(&[
Token::Uint(self.index().into()),
Token::Bytes(ethabi::encode(&[
Token::Address(*token),
Token::Address(*recipient),
Token::Uint(U256::from(*amount)),
])),
]),
}
}
}
/// Message which is awaiting processing in the MessageQueue pallet
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub struct QueuedMessage {
/// Message ID
pub id: H256,
/// Channel ID
pub channel_id: ChannelId,
/// Command to execute in the Gateway contract
pub command: Command,
}
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(PartialEq))]
/// Fee for delivering message
pub struct Fee<Balance>
where
Balance: BaseArithmetic + Unsigned + Copy,
{
/// Fee to cover cost of processing the message locally
pub local: Balance,
/// Fee to cover cost processing the message remotely
pub remote: Balance,
}
impl<Balance> Fee<Balance>
where
Balance: BaseArithmetic + Unsigned + Copy,
{
pub fn total(&self) -> Balance {
self.local.saturating_add(self.remote)
}
}
impl<Balance> From<(Balance, Balance)> for Fee<Balance>
where
Balance: BaseArithmetic + Unsigned + Copy,
{
fn from((local, remote): (Balance, Balance)) -> Self {
Self { local, remote }
}
}
/// A trait for sending messages to Ethereum
pub trait SendMessage: SendMessageFeeProvider {
type Ticket: Clone + Encode + Decode;
/// Validate an outbound message and return a tuple:
/// 1. Ticket for submitting the message
/// 2. Delivery fee
fn validate(
message: &Message,
) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError>;
/// Submit the message ticket for eventual delivery to Ethereum
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
}
pub trait Ticket: Encode + Decode + Clone {
fn message_id(&self) -> H256;
}
pub trait GasMeter {
/// All the gas used for submitting a message to Ethereum, minus the cost of dispatching
/// the command within the message
const MAXIMUM_BASE_GAS: u64;
/// Total gas consumed at most, including verification & dispatch
fn maximum_gas_used_at_most(command: &Command) -> u64 {
Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
}
/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
/// including validation & verification.
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
}
/// A meter that assigns a constant amount of gas for the execution of a command
///
/// The gas figures are extracted from this report:
/// > forge test --match-path test/Gateway.t.sol --gas-report
///
/// A healthy buffer is added on top of these figures to account for:
/// * The EIP-150 63/64 rule
/// * Future EVM upgrades that may increase gas cost
pub struct ConstantGasMeter;
impl GasMeter for ConstantGasMeter {
// The base transaction cost, which includes:
// 21_000 transaction cost, roughly worst case 64_000 for calldata, and 100_000
// for message verification
const MAXIMUM_BASE_GAS: u64 = 185_000;
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
match command {
Command::SetOperatingMode { .. } => 40_000,
Command::AgentExecute { command, .. } => match command {
// Execute IERC20.transferFrom
//
// Worst-case assumptions are important:
// * No gas refund for clearing storage slot of source account in ERC20 contract
// * Assume dest account in ERC20 contract does not yet have a storage slot
// * ERC20.transferFrom possibly does other business logic besides updating balances
AgentExecuteCommand::TransferToken { .. } => 200_000,
},
Command::Upgrade { initializer, .. } => {
let initializer_max_gas = match *initializer {
Some(Initializer { maximum_required_gas, .. }) => maximum_required_gas,
None => 0,
};
// total maximum gas must also include the gas used for updating the proxy before
// the the initializer is called.
50_000 + initializer_max_gas
},
Command::SetTokenTransferFees { .. } => 60_000,
Command::SetPricingParameters { .. } => 60_000,
Command::UnlockNativeToken { .. } => 200_000,
Command::RegisterForeignToken { .. } => 1_200_000,
Command::MintForeignToken { .. } => 100_000,
}
}
}
impl GasMeter for () {
const MAXIMUM_BASE_GAS: u64 = 1;
fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
1
}
}
pub const ETHER_DECIMALS: u8 = 18;
@@ -0,0 +1,5 @@
pub mod converter;
pub mod message;
pub use converter::*;
pub use message::*;
@@ -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
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::Log;
use alloy_core::{primitives::B256, sol, sol_types::SolEvent};
use codec::Decode;
use frame_support::pallet_prelude::{Encode, TypeInfo};
use sp_core::{RuntimeDebug, H160, H256};
use sp_std::prelude::*;
sol! {
event InboundMessageDispatched(uint64 indexed nonce, bytes32 topic, bool success, bytes32 reward_address);
}
/// Delivery receipt
#[derive(Clone, RuntimeDebug)]
pub struct DeliveryReceipt {
/// The address of the outbound queue on Ethereum that emitted this message as an event log
pub gateway: H160,
/// The nonce of the dispatched message
pub nonce: u64,
/// Message topic
pub topic: H256,
/// Delivery status
pub success: bool,
/// The reward address
pub reward_address: [u8; 32],
}
#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)]
pub enum DeliveryReceiptDecodeError {
DecodeLogFailed,
DecodeAccountFailed,
}
impl TryFrom<&Log> for DeliveryReceipt {
type Error = DeliveryReceiptDecodeError;
fn try_from(log: &Log) -> Result<Self, Self::Error> {
let topics: Vec<B256> = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect();
let event = InboundMessageDispatched::decode_raw_log_validate(topics, &log.data)
.map_err(|_| DeliveryReceiptDecodeError::DecodeLogFailed)?;
Ok(Self {
gateway: log.address,
nonce: event.nonce,
topic: H256::from_slice(event.topic.as_ref()),
success: event.success,
reward_address: event.reward_address.0,
})
}
}
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use core::marker::PhantomData;
use snowbridge_core::operating_mode::ExportPausedQuery;
use sp_std::vec::Vec;
use xcm::{
prelude::{Location, SendError, SendResult, SendXcm, Xcm, XcmHash},
VersionedLocation, VersionedXcm,
};
use xcm_builder::InspectMessageQueues;
pub struct PausableExporter<PausedQuery, InnerExporter>(PhantomData<(PausedQuery, InnerExporter)>);
impl<PausedQuery: ExportPausedQuery, InnerExporter: SendXcm> SendXcm
for PausableExporter<PausedQuery, InnerExporter>
{
type Ticket = InnerExporter::Ticket;
fn validate(
destination: &mut Option<Location>,
message: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
match PausedQuery::is_paused() {
true => Err(SendError::NotApplicable),
false => InnerExporter::validate(destination, message),
}
}
fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
match PausedQuery::is_paused() {
true => Err(SendError::NotApplicable),
false => InnerExporter::deliver(ticket),
}
}
}
impl<Halted: ExportPausedQuery, InnerExporter: SendXcm> InspectMessageQueues
for PausableExporter<Halted, InnerExporter>
{
fn clear_messages() {}
/// This router needs to implement `InspectMessageQueues` but doesn't have to
/// return any messages, since it just reuses the inner router.
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
Vec::new()
}
}
@@ -0,0 +1,309 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! # Outbound V2 primitives
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::{pallet_prelude::ConstU32, BoundedVec};
use scale_info::TypeInfo;
use sp_core::{RuntimeDebug, H160, H256};
use sp_std::vec::Vec;
use crate::{OperatingMode, SendError};
use abi::{
CallContractParams, MintForeignTokenParams, RegisterForeignTokenParams, SetOperatingModeParams,
UnlockNativeTokenParams, UpgradeParams,
};
use alloy_core::{
primitives::{Address, Bytes, FixedBytes, U256},
sol_types::SolValue,
};
pub mod abi {
use alloy_core::sol;
sol! {
struct OutboundMessageWrapper {
// origin
bytes32 origin;
// Message nonce
uint64 nonce;
// Topic
bytes32 topic;
// Commands
CommandWrapper[] commands;
}
struct CommandWrapper {
uint8 kind;
uint64 gas;
bytes payload;
}
// Payload for Upgrade
struct UpgradeParams {
// The address of the implementation contract
address implAddress;
// Codehash of the new implementation contract.
bytes32 implCodeHash;
// Parameters used to upgrade storage of the gateway
bytes initParams;
}
// Payload for SetOperatingMode instruction
struct SetOperatingModeParams {
/// The new operating mode
uint8 mode;
}
// Payload for NativeTokenUnlock instruction
struct UnlockNativeTokenParams {
// Token address
address token;
// Recipient address
address recipient;
// Amount to unlock
uint128 amount;
}
// Payload for RegisterForeignToken
struct RegisterForeignTokenParams {
/// @dev The token ID (hash of stable location id of token)
bytes32 foreignTokenID;
/// @dev The name of the token
bytes name;
/// @dev The symbol of the token
bytes symbol;
/// @dev The decimal of the token
uint8 decimals;
}
// Payload for MintForeignTokenParams instruction
struct MintForeignTokenParams {
// Foreign token ID
bytes32 foreignTokenID;
// Recipient address
address recipient;
// Amount to mint
uint128 amount;
}
// Payload for CallContract
struct CallContractParams {
// target contract
address target;
// Call data
bytes data;
// Ether value
uint256 value;
}
}
}
#[derive(Encode, Decode, TypeInfo, PartialEq, Clone, RuntimeDebug)]
pub struct OutboundCommandWrapper {
pub kind: u8,
pub gas: u64,
pub payload: Vec<u8>,
}
#[derive(Encode, Decode, TypeInfo, PartialEq, Clone, RuntimeDebug)]
pub struct OutboundMessage {
/// Origin
pub origin: H256,
/// Nonce
pub nonce: u64,
/// Topic
pub topic: H256,
/// Commands
pub commands: BoundedVec<OutboundCommandWrapper, ConstU32<MAX_COMMANDS>>,
}
pub const MAX_COMMANDS: u32 = 8;
/// A message which can be accepted by implementations of `/[`SendMessage`\]`
#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Clone, RuntimeDebug)]
pub struct Message {
/// Origin
pub origin: H256,
/// ID
pub id: H256,
/// Fee
pub fee: u128,
/// Commands
pub commands: BoundedVec<Command, ConstU32<MAX_COMMANDS>>,
}
/// A command which is executable by the Gateway contract on Ethereum
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
pub enum Command {
/// Upgrade the Gateway contract
Upgrade {
/// Address of the new implementation contract
impl_address: H160,
/// Codehash of the implementation contract
impl_code_hash: H256,
/// Invoke an initializer in the implementation contract
initializer: Initializer,
},
/// Set the global operating mode of the Gateway contract
SetOperatingMode {
/// The new operating mode
mode: OperatingMode,
},
/// Unlock ERC20 tokens
UnlockNativeToken {
/// Address of the ERC20 token
token: H160,
/// The recipient of the tokens
recipient: H160,
/// The amount of tokens to transfer
amount: u128,
},
/// Register foreign token from Pezkuwi
RegisterForeignToken {
/// ID for the token
token_id: H256,
/// Name of the token
name: Vec<u8>,
/// Short symbol for the token
symbol: Vec<u8>,
/// Number of decimal places
decimals: u8,
},
/// Mint foreign token from Pezkuwi
MintForeignToken {
/// ID for the token
token_id: H256,
/// The recipient of the newly minted tokens
recipient: H160,
/// The amount of tokens to mint
amount: u128,
},
/// Call Contract on Ethereum
CallContract {
/// Target contract address
target: H160,
/// ABI-encoded calldata
calldata: Vec<u8>,
/// Maximum gas to forward to target contract
gas: u64,
/// Include ether held by agent contract
value: u128,
},
}
impl Command {
/// Compute the enum variant index
pub fn index(&self) -> u8 {
match self {
Command::Upgrade { .. } => 0,
Command::SetOperatingMode { .. } => 1,
Command::UnlockNativeToken { .. } => 2,
Command::RegisterForeignToken { .. } => 3,
Command::MintForeignToken { .. } => 4,
Command::CallContract { .. } => 5,
}
}
/// ABI-encode the Command.
pub fn abi_encode(&self) -> Vec<u8> {
match self {
Command::Upgrade { impl_address, impl_code_hash, initializer, .. } => UpgradeParams {
implAddress: Address::from(impl_address.as_fixed_bytes()),
implCodeHash: FixedBytes::from(impl_code_hash.as_fixed_bytes()),
initParams: Bytes::from(initializer.params.clone()),
}
.abi_encode(),
Command::SetOperatingMode { mode } =>
SetOperatingModeParams { mode: (*mode) as u8 }.abi_encode(),
Command::UnlockNativeToken { token, recipient, amount, .. } =>
UnlockNativeTokenParams {
token: Address::from(token.as_fixed_bytes()),
recipient: Address::from(recipient.as_fixed_bytes()),
amount: *amount,
}
.abi_encode(),
Command::RegisterForeignToken { token_id, name, symbol, decimals } =>
RegisterForeignTokenParams {
foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
name: Bytes::from(name.to_vec()),
symbol: Bytes::from(symbol.to_vec()),
decimals: *decimals,
}
.abi_encode(),
Command::MintForeignToken { token_id, recipient, amount } => MintForeignTokenParams {
foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
recipient: Address::from(recipient.as_fixed_bytes()),
amount: *amount,
}
.abi_encode(),
Command::CallContract { target, calldata: data, value, .. } => CallContractParams {
target: Address::from(target.as_fixed_bytes()),
data: Bytes::from(data.to_vec()),
value: U256::try_from(*value).unwrap(),
}
.abi_encode(),
}
}
}
/// Representation of a call to the initializer of an implementation contract.
/// The initializer has the following ABI signature: `initialize(bytes)`.
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Initializer {
/// ABI-encoded params of type `bytes` to pass to the initializer
pub params: Vec<u8>,
/// The initializer is allowed to consume this much gas at most.
pub maximum_required_gas: u64,
}
pub trait SendMessage {
type Ticket: Clone + Encode + Decode;
/// Validate an outbound message and return a tuple:
/// 1. Ticket for submitting the message
/// 2. Delivery fee
fn validate(message: &Message) -> Result<Self::Ticket, SendError>;
/// Submit the message ticket for eventual delivery to Ethereum
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
}
pub trait GasMeter {
/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
/// including validation & verification.
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
}
/// A meter that assigns a constant amount of gas for the execution of a command
///
/// The gas figures are extracted from this report:
/// > forge test --match-path test/Gateway.t.sol --gas-report
///
/// A healthy buffer is added on top of these figures to account for:
/// * The EIP-150 63/64 rule
/// * Future EVM upgrades that may increase gas cost
pub struct ConstantGasMeter;
impl GasMeter for ConstantGasMeter {
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
match command {
Command::SetOperatingMode { .. } => 40_000,
Command::Upgrade { initializer, .. } => {
// total maximum gas must also include the gas used for updating the proxy before
// the the initializer is called.
50_000 + initializer.maximum_required_gas
},
Command::UnlockNativeToken { .. } => 200_000,
Command::RegisterForeignToken { .. } => 1_200_000,
Command::MintForeignToken { .. } => 100_000,
Command::CallContract { gas: gas_limit, .. } => *gas_limit,
}
}
}
impl GasMeter for () {
fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
1
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pub mod converter;
pub mod delivery_receipt;
pub mod exporter;
pub mod message;
pub use converter::*;
pub use delivery_receipt::*;
pub use message::*;
use codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use sp_std::prelude::*;
/// The `XCM::Transact` payload for calling arbitrary smart contracts on Ethereum.
/// On Ethereum, this call will be dispatched by the agent contract acting as a proxy
/// for the XCM origin.
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
pub enum ContractCall {
V1 {
/// Target contract address
target: [u8; 20],
/// ABI-encoded calldata
calldata: Vec<u8>,
/// Include ether held by the agent contract
value: u128,
/// Maximum gas to forward to target contract
gas: u64,
},
}