feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user