diff --git a/Cargo.lock b/Cargo.lock index f7cbd043d52f..e8b5dc6dce42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21173,6 +21173,17 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "snowbridge-inbound-queue-v2-runtime-api" +version = "0.2.0" +dependencies = [ + "snowbridge-core", + "snowbridge-merkle-tree", + "snowbridge-router-primitives", + "sp-api 26.0.0", + "staging-xcm", +] + [[package]] name = "snowbridge-merkle-tree" version = "0.2.0" @@ -21327,6 +21338,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex", "hex-literal", "log", "pallet-balances", @@ -21431,6 +21443,7 @@ name = "snowbridge-router-primitives" version = "0.9.0" dependencies = [ "frame-support", + "frame-system", "hex-literal", "log", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index c85e41de0f32..4c50363fec0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ members = [ "bridges/snowbridge/pallets/inbound-queue", "bridges/snowbridge/pallets/inbound-queue-v2", "bridges/snowbridge/pallets/inbound-queue-v2/fixtures", + "bridges/snowbridge/pallets/inbound-queue-v2/runtime-api", "bridges/snowbridge/pallets/inbound-queue/fixtures", "bridges/snowbridge/pallets/outbound-queue", "bridges/snowbridge/pallets/outbound-queue-v2", diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml index d212b18d2d54..4d589b86e142 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml +++ b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml @@ -45,6 +45,7 @@ frame-benchmarking = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } snowbridge-pallet-ethereum-client = { workspace = true, default-features = true } hex-literal = { workspace = true, default-features = true } +hex = { workspace = true, default-features = true } [features] default = ["std"] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml new file mode 100644 index 000000000000..73a381017426 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "snowbridge-inbound-queue-v2-runtime-api" +description = "Snowbridge Inbound Queue V2 Runtime API" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +sp-api = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +snowbridge-core = { workspace = true } +snowbridge-router-primitives = { workspace = true } +xcm = { workspace = true } + +[features] +default = ["std"] +std = [ + "snowbridge-core/std", + "snowbridge-merkle-tree/std", + "snowbridge-router-primitives/std", + "sp-api/std", + "xcm/std", +] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md new file mode 100644 index 000000000000..89b6b0e157c5 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md @@ -0,0 +1,3 @@ +# Ethereum Inbound Queue V2 Runtime API + +Provides an API to dry-run inbound messages to get the XCM (and its execution cost) that will be executed on AssetHub. diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs new file mode 100644 index 000000000000..03720b7ca3d2 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +#![cfg_attr(not(feature = "std"), no_std)] + +use snowbridge_core::inbound::Proof; +use snowbridge_router_primitives::inbound::v2::Message; +use xcm::latest::Xcm; + +sp_api::decl_runtime_apis! { + pub trait InboundQueueApiV2 + { + /// Dry runs the provided message on AH to provide the XCM payload and execution cost. + fn dry_run(message: Message, proof: Proof) -> (Xcm<()>, u128); + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs new file mode 100644 index 000000000000..a285a7c5af42 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Implements the dry-run API. + +use crate::{Config, Error}; +use snowbridge_core::inbound::Proof; +use snowbridge_router_primitives::inbound::v2::{ConvertMessage, Message}; +use xcm::latest::Xcm; + +pub fn dry_run(message: Message, _proof: Proof) -> Result, Error> +where + T: Config, +{ + let xcm = T::MessageConverter::convert(message).map_err(|e| Error::::ConvertMessage(e))?; + Ok(xcm) +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking/mod.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking/mod.rs index 52461a8a7fbe..4c5df07b27ac 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking/mod.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking/mod.rs @@ -23,21 +23,6 @@ mod benchmarks { create_message.block_roots_root, ); - let sovereign_account = sibling_sovereign_account::(1000u32.into()); - - let minimum_balance = T::Token::minimum_balance(); - - // So that the receiving account exists - assert_ok!(T::Token::mint_into(&caller, minimum_balance)); - // Fund the sovereign account (parachain sovereign account) so it can transfer a reward - // fee to the caller account - assert_ok!(T::Token::mint_into( - &sovereign_account, - 3_000_000_000_000u128 - .try_into() - .unwrap_or_else(|_| panic!("unable to cast sovereign account balance")), - )); - #[block] { assert_ok!(InboundQueue::::submit( diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs index 31a8992442d8..8c9b137c64ba 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs @@ -1,15 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork -use snowbridge_core::{inbound::Log, ChannelId}; +use snowbridge_core::inbound::Log; -use sp_core::{RuntimeDebug, H160, H256}; +use sp_core::{RuntimeDebug, H160}; use sp_std::prelude::*; use alloy_primitives::B256; use alloy_sol_types::{sol, SolEvent}; sol! { - event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload); + event OutboundMessageAccepted(uint64 indexed nonce, uint128 fee, bytes payload); } /// An inbound message that has had its outer envelope decoded. @@ -17,12 +17,10 @@ sol! { pub struct Envelope { /// The address of the outbound queue on Ethereum that emitted this message as an event log pub gateway: H160, - /// The message Channel - pub channel_id: ChannelId, /// A nonce for enforcing replay protection and ordering. pub nonce: u64, - /// An id for tracing the message on its route (has no role in bridge consensus) - pub message_id: H256, + /// Total fee paid in Ether on Ethereum, should cover all the cost + pub fee: u128, /// The inner payload generated from the source application. pub payload: Vec, } @@ -41,9 +39,8 @@ impl TryFrom<&Log> for Envelope { Ok(Self { gateway: log.address, - channel_id: ChannelId::from(event.channel_id.as_ref()), nonce: event.nonce, - message_id: H256::from(event.message_id.as_ref()), + fee: event.fee, payload: event.payload, }) } diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs index c26859dcf5d7..b5b917c31a72 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -22,7 +22,7 @@ //! * [`Call::submit`]: Submit a message for verification and dispatch the final destination //! parachain. #![cfg_attr(not(feature = "std"), no_std)] - +pub mod api; mod envelope; #[cfg(feature = "runtime-benchmarks")] @@ -38,45 +38,28 @@ mod test; use codec::{Decode, DecodeAll, Encode}; use envelope::Envelope; -use frame_support::{ - traits::{ - fungible::{Inspect, Mutate}, - tokens::{Fortitude, Preservation}, - }, - weights::WeightToFee, - PalletError, -}; +use frame_support::PalletError; use frame_system::ensure_signed; use scale_info::TypeInfo; use sp_core::H160; -use sp_runtime::traits::Zero; use sp_std::vec; -use xcm::prelude::{ - send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmContext, XcmHash, -}; -use xcm_executor::traits::TransactAsset; +use xcm::prelude::{send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm}; use snowbridge_core::{ inbound::{Message, VerificationError, Verifier}, - sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters, - StaticLookup, -}; -use snowbridge_router_primitives::inbound::v2::{ - ConvertMessage, ConvertMessageError, VersionedMessage, + BasicOperatingMode, }; -use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; - +use snowbridge_router_primitives::inbound::v2::{ConvertMessage, Message as MessageV2}; pub use weights::WeightInfo; #[cfg(feature = "runtime-benchmarks")] use snowbridge_beacon_primitives::BeaconHeader; -type BalanceOf = - <::Token as Inspect<::AccountId>>::Balance; +use snowbridge_router_primitives::inbound::v2::ConvertMessageError; pub use pallet::*; -pub const LOG_TARGET: &str = "snowbridge-inbound-queue"; +pub const LOG_TARGET: &str = "snowbridge-inbound-queue:v2"; #[frame_support::pallet] pub mod pallet { @@ -84,7 +67,6 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use sp_core::H256; #[pallet::pallet] pub struct Pallet(_); @@ -101,44 +83,17 @@ pub mod pallet { /// The verifier for inbound messages from Ethereum type Verifier: Verifier; - /// Message relayers are rewarded with this asset - type Token: Mutate + Inspect; - /// XCM message sender type XcmSender: SendXcm; - - // Address of the Gateway contract + /// Address of the Gateway contract #[pallet::constant] type GatewayAddress: Get; - - /// Convert inbound message to XCM - type MessageConverter: ConvertMessage< - AccountId = Self::AccountId, - Balance = BalanceOf, - >; - - /// Lookup a channel descriptor - type ChannelLookup: StaticLookup; - - /// Lookup pricing parameters - type PricingParameters: Get>>; - type WeightInfo: WeightInfo; - + /// AssetHub parachain ID + type AssetHubParaId: Get; + type MessageConverter: ConvertMessage; #[cfg(feature = "runtime-benchmarks")] type Helper: BenchmarkHelper; - - /// Convert a weight value into deductible balance type. - type WeightToFee: WeightToFee>; - - /// Convert a length value into deductible balance type - type LengthToFee: WeightToFee>; - - /// The upper limit here only used to estimate delivery cost - type MaxMessageSize: Get; - - /// To withdraw and deposit an asset. - type AssetTransactor: TransactAsset; } #[pallet::hooks] @@ -149,14 +104,10 @@ pub mod pallet { pub enum Event { /// A message was received from Ethereum MessageReceived { - /// The message channel - channel_id: ChannelId, /// The message nonce nonce: u64, /// ID of the XCM message which was forwarded to the final destination parachain message_id: [u8; 32], - /// Fee burned for the teleport - fee_burned: BalanceOf, }, /// Set OperatingMode OperatingModeChanged { mode: BasicOperatingMode }, @@ -215,9 +166,9 @@ pub mod pallet { } } - /// The current nonce for each channel + /// The nonce of the message been processed or not #[pallet::storage] - pub type Nonce = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>; + pub type Nonce = StorageMap<_, Identity, u64, bool, ValueQuery>; /// The current operating mode of the pallet. #[pallet::storage] @@ -230,7 +181,7 @@ pub mod pallet { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::submit())] pub fn submit(origin: OriginFor, message: Message) -> DispatchResult { - let who = ensure_signed(origin)?; + let _who = ensure_signed(origin)?; ensure!(!Self::operating_mode().is_halted(), Error::::Halted); // submit message to verifier for verification @@ -244,63 +195,32 @@ pub mod pallet { // Verify that the message was submitted from the known Gateway contract ensure!(T::GatewayAddress::get() == envelope.gateway, Error::::InvalidGateway); - // Retrieve the registered channel for this message - let channel = - T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::::InvalidChannel)?; - - // Verify message nonce - >::try_mutate(envelope.channel_id, |nonce| -> DispatchResult { - if *nonce == u64::MAX { - return Err(Error::::MaxNonceReached.into()) - } - if envelope.nonce != nonce.saturating_add(1) { - Err(Error::::InvalidNonce.into()) - } else { - *nonce = nonce.saturating_add(1); - Ok(()) - } - })?; - - // Reward relayer from the sovereign account of the destination parachain, only if funds - // are available - let sovereign_account = sibling_sovereign_account::(channel.para_id); - let delivery_cost = Self::calculate_delivery_cost(message.encode().len() as u32); - let amount = T::Token::reducible_balance( - &sovereign_account, - Preservation::Preserve, - Fortitude::Polite, - ) - .min(delivery_cost); - if !amount.is_zero() { - T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?; - } + // Verify the message has not been processed + ensure!(!Nonce::::contains_key(envelope.nonce), Error::::InvalidNonce); - // Decode payload into `VersionedMessage` - let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref()) + // Decode payload into `MessageV2` + let message = MessageV2::decode_all(&mut envelope.payload.as_ref()) .map_err(|_| Error::::InvalidPayload)?; - // Decode message into XCM - let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?; + let xcm = + T::MessageConverter::convert(message).map_err(|e| Error::::ConvertMessage(e))?; - log::info!( - target: LOG_TARGET, - "💫 xcm decoded as {:?} with fee {:?}", - xcm, - fee - ); + // Todo: Deposit fee(in Ether) to RewardLeger which should cover all of: + // T::RewardLeger::deposit(who, envelope.fee.into())?; + // a. The submit extrinsic cost on BH + // b. The delivery cost to AH + // c. The execution cost on AH + // d. The execution cost on destination chain(if any) + // e. The reward - // Burning fees for teleport - Self::burn_fees(channel.para_id, fee)?; + // Attempt to forward XCM to AH + let dest = Location::new(1, [Parachain(T::AssetHubParaId::get())]); + let (message_id, _) = send_xcm::(dest, xcm).map_err(Error::::from)?; - // Attempt to send XCM to a dest parachain - let message_id = Self::send_xcm(xcm, channel.para_id)?; + Self::deposit_event(Event::MessageReceived { nonce: envelope.nonce, message_id }); - Self::deposit_event(Event::MessageReceived { - channel_id: envelope.channel_id, - nonce: envelope.nonce, - message_id, - fee_burned: fee, - }); + // Set nonce flag to true + Nonce::::insert(envelope.nonce, ()) Ok(()) } @@ -318,61 +238,4 @@ pub mod pallet { Ok(()) } } - - impl Pallet { - pub fn do_convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, BalanceOf), Error> { - let (xcm, fee) = T::MessageConverter::convert(message_id, message) - .map_err(|e| Error::::ConvertMessage(e))?; - Ok((xcm, fee)) - } - - pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result> { - let dest = Location::new(1, [Parachain(dest.into())]); - let (xcm_hash, _) = send_xcm::(dest, xcm).map_err(Error::::from)?; - Ok(xcm_hash) - } - - pub fn calculate_delivery_cost(length: u32) -> BalanceOf { - let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit()); - let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0)); - weight_fee - .saturating_add(len_fee) - .saturating_add(T::PricingParameters::get().rewards.local) - } - - /// Burn the amount of the fee embedded into the XCM for teleports - pub fn burn_fees(para_id: ParaId, fee: BalanceOf) -> DispatchResult { - let dummy_context = - XcmContext { origin: None, message_id: Default::default(), topic: None }; - let dest = Location::new(1, [Parachain(para_id.into())]); - let fees = (Location::parent(), fee.saturated_into::()).into(); - T::AssetTransactor::can_check_out(&dest, &fees, &dummy_context).map_err(|error| { - log::error!( - target: LOG_TARGET, - "XCM asset check out failed with error {:?}", error - ); - TokenError::FundsUnavailable - })?; - T::AssetTransactor::check_out(&dest, &fees, &dummy_context); - T::AssetTransactor::withdraw_asset(&fees, &dest, None).map_err(|error| { - log::error!( - target: LOG_TARGET, - "XCM asset withdraw failed with error {:?}", error - ); - TokenError::FundsUnavailable - })?; - Ok(()) - } - } - - /// API for accessing the delivery cost of a message - impl Get> for Pallet { - fn get() -> BalanceOf { - // Cost here based on MaxMessagePayloadSize(the worst case) - Self::calculate_delivery_cost(T::MaxMessageSize::get()) - } - } } diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs index fad62628c0f0..6a9279b75686 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -2,27 +2,24 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; -use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::IdentityFee}; +use crate::{self as inbound_queue}; +use frame_support::{derive_impl, parameter_types, traits::ConstU32}; use hex_literal::hex; use snowbridge_beacon_primitives::{ types::deneb, BeaconHeader, ExecutionProof, Fork, ForkVersions, VersionedExecutionPayloadHeader, }; use snowbridge_core::{ - gwei, inbound::{Log, Proof, VerificationError}, - meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup, TokenId, + TokenId, }; use snowbridge_router_primitives::inbound::v2::MessageToXcm; -use sp_core::{H160, H256}; +use sp_core::H160; use sp_runtime::{ traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, - BuildStorage, FixedU128, MultiSignature, + BuildStorage, MultiSignature, }; use sp_std::{convert::From, default::Default}; use xcm::{latest::SendXcm, prelude::*}; -use xcm_executor::AssetsInHolding; - -use crate::{self as inbound_queue}; type Block = frame_system::mocking::MockBlock; @@ -103,20 +100,6 @@ impl Verifier for MockVerifier { const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"]; -parameter_types! { - pub const EthereumNetwork: xcm::v3::NetworkId = xcm::v3::NetworkId::Ethereum { chain_id: 11155111 }; - pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); - pub const CreateAssetCall: [u8;2] = [53, 0]; - pub const CreateAssetExecutionFee: u128 = 2_000_000_000; - pub const CreateAssetDeposit: u128 = 100_000_000_000; - pub const SendTokenExecutionFee: u128 = 1_000_000_000; - pub const InitialFund: u128 = 1_000_000_000_000; - pub const InboundQueuePalletInstance: u8 = 80; - pub UniversalLocation: InteriorLocation = - [GlobalConsensus(Westend), Parachain(1002)].into(); - pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Westend),Parachain(1000)]); -} - #[cfg(feature = "runtime-benchmarks")] impl BenchmarkHelper for Test { // not implemented since the MockVerifier is used for tests @@ -149,65 +132,6 @@ impl SendXcm for MockXcmSender { } } -parameter_types! { - pub const OwnParaId: ParaId = ParaId::new(1013); - pub Parameters: PricingParameters = PricingParameters { - exchange_rate: FixedU128::from_rational(1, 400), - fee_per_gas: gwei(20), - rewards: Rewards { local: DOT, remote: meth(1) }, - multiplier: FixedU128::from_rational(1, 1), - }; -} - -pub const DOT: u128 = 10_000_000_000; - -pub struct MockChannelLookup; -impl StaticLookup for MockChannelLookup { - type Source = ChannelId; - type Target = Channel; - - fn lookup(channel_id: Self::Source) -> Option { - if channel_id != - hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into() - { - return None - } - Some(Channel { agent_id: H256::zero(), para_id: ASSET_HUB_PARAID.into() }) - } -} - -pub struct SuccessfulTransactor; -impl TransactAsset for SuccessfulTransactor { - fn can_check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult { - Ok(()) - } - - fn can_check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult { - Ok(()) - } - - fn deposit_asset(_what: &Asset, _who: &Location, _context: Option<&XcmContext>) -> XcmResult { - Ok(()) - } - - fn withdraw_asset( - _what: &Asset, - _who: &Location, - _context: Option<&XcmContext>, - ) -> Result { - Ok(AssetsInHolding::default()) - } - - fn internal_transfer_asset( - _what: &Asset, - _from: &Location, - _to: &Location, - _context: &XcmContext, - ) -> Result { - Ok(AssetsInHolding::default()) - } -} - pub struct MockTokenIdConvert; impl MaybeEquivalence for MockTokenIdConvert { fn convert(_id: &TokenId) -> Option { @@ -218,31 +142,24 @@ impl MaybeEquivalence for MockTokenIdConvert { } } +parameter_types! { + pub const EthereumNetwork: xcm::v5::NetworkId = xcm::v5::NetworkId::Ethereum { chain_id: 11155111 }; + pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); + pub const InboundQueuePalletInstance: u8 = 80; + pub AssetHubLocation: InteriorLocation = Parachain(1000).into(); +} + impl inbound_queue::Config for Test { type RuntimeEvent = RuntimeEvent; type Verifier = MockVerifier; - type Token = Balances; type XcmSender = MockXcmSender; type WeightInfo = (); type GatewayAddress = GatewayAddress; - type MessageConverter = MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - MockTokenIdConvert, - UniversalLocation, - AssetHubFromEthereum, - >; - type PricingParameters = Parameters; - type ChannelLookup = MockChannelLookup; + type AssetHubParaId = ConstU32<1000>; + type MessageConverter = + MessageToXcm; #[cfg(feature = "runtime-benchmarks")] type Helper = Test; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; - type MaxMessageSize = ConstU32<1024>; - type AssetTransactor = SuccessfulTransactor; } pub fn last_events(n: usize) -> Vec { @@ -261,16 +178,6 @@ pub fn expect_events(e: Vec) { pub fn setup() { System::set_block_number(1); - Balances::mint_into( - &sibling_sovereign_account::(ASSET_HUB_PARAID.into()), - InitialFund::get(), - ) - .unwrap(); - Balances::mint_into( - &sibling_sovereign_account::(TEMPLATE_PARAID.into()), - InitialFund::get(), - ) - .unwrap(); } pub fn new_tester() -> sp_io::TestExternalities { @@ -286,47 +193,47 @@ pub fn new_tester() -> sp_io::TestExternalities { // cargo test --test register_token -- --nocapture pub fn mock_event_log() -> Log { Log { - // gateway address - address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), - topics: vec![ - hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), - // channel id - hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), - // message id - hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), - ], - // Nonce + Payload - data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(), - } + // gateway address + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + // channel id + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + // message id + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + // Nonce + Payload + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(), + } } pub fn mock_event_log_invalid_channel() -> Log { Log { - address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), - topics: vec![ - hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), - // invalid channel id - hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), - hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), - ], - data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(), - } + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + // invalid channel id + hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(), + } } pub fn mock_event_log_invalid_gateway() -> Log { Log { - // gateway address - address: H160::zero(), - topics: vec![ - hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), - // channel id - hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), - // message id - hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), - ], - // Nonce + Payload - data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(), - } + // gateway address + address: H160::zero(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + // channel id + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + // message id + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + // Nonce + Payload + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(), + } } pub fn mock_execution_proof() -> ExecutionProof { @@ -355,6 +262,3 @@ pub fn mock_execution_proof() -> ExecutionProof { execution_branch: vec![], } } - -pub const ASSET_HUB_PARAID: u32 = 1000u32; -pub const TEMPLATE_PARAID: u32 = 1001u32; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs index 44f6c0ebc658..211959c4bd4d 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs @@ -4,20 +4,27 @@ use super::*; use frame_support::{assert_noop, assert_ok}; use hex_literal::hex; -use snowbridge_core::{inbound::Proof, ChannelId}; +use snowbridge_core::inbound::Proof; use sp_keyring::AccountKeyring as Keyring; use sp_runtime::DispatchError; -use sp_std::convert::From; -use crate::{Error, Event as InboundQueueEvent}; - -use crate::mock::*; +use crate::{mock::*, Error, Event as InboundQueueEvent}; +use codec::DecodeLimit; +use snowbridge_router_primitives::inbound::v2::InboundAsset; +use sp_core::H256; +use xcm::{ + opaque::latest::{ + prelude::{ClearOrigin, ReceiveTeleportedAsset}, + Asset, + }, + prelude::*, + VersionedXcm, MAX_XCM_DECODE_DEPTH, +}; #[test] fn test_submit_happy_path() { new_tester().execute_with(|| { let relayer: AccountId = Keyring::Bob.into(); - let channel_sovereign = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); let origin = RuntimeOrigin::signed(relayer.clone()); @@ -30,34 +37,15 @@ fn test_submit_happy_path() { }, }; - let initial_fund = InitialFund::get(); - assert_eq!(Balances::balance(&relayer), 0); - assert_eq!(Balances::balance(&channel_sovereign), initial_fund); - assert_ok!(InboundQueue::submit(origin.clone(), message.clone())); expect_events(vec![InboundQueueEvent::MessageReceived { - channel_id: hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539") - .into(), nonce: 1, message_id: [ 183, 243, 1, 130, 170, 254, 104, 45, 116, 181, 146, 237, 14, 139, 138, 89, 43, 166, 182, 24, 163, 222, 112, 238, 215, 83, 21, 160, 24, 88, 112, 9, ], - fee_burned: 110000000000, } .into()]); - - let delivery_cost = InboundQueue::calculate_delivery_cost(message.encode().len() as u32); - assert!( - Parameters::get().rewards.local < delivery_cost, - "delivery cost exceeds pure reward" - ); - - assert_eq!(Balances::balance(&relayer), delivery_cost, "relayer was rewarded"); - assert!( - Balances::balance(&channel_sovereign) <= initial_fund - delivery_cost, - "sovereign account paid reward" - ); }); } @@ -67,11 +55,6 @@ fn test_submit_xcm_invalid_channel() { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); - // Deposit funds into sovereign account of parachain 1001 - let sovereign_account = sibling_sovereign_account::(TEMPLATE_PARAID.into()); - println!("account: {}", sovereign_account); - let _ = Balances::mint_into(&sovereign_account, 10000); - // Submit message let message = Message { event_log: mock_event_log_invalid_channel(), @@ -93,10 +76,6 @@ fn test_submit_with_invalid_gateway() { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); - // Deposit funds into sovereign account of Asset Hub (Statemint) - let sovereign_account = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); - let _ = Balances::mint_into(&sovereign_account, 10000); - // Submit message let message = Message { event_log: mock_event_log_invalid_gateway(), @@ -118,10 +97,6 @@ fn test_submit_with_invalid_nonce() { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); - // Deposit funds into sovereign account of Asset Hub (Statemint) - let sovereign_account = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); - let _ = Balances::mint_into(&sovereign_account, 10000); - // Submit message let message = Message { event_log: mock_event_log(), @@ -132,11 +107,6 @@ fn test_submit_with_invalid_nonce() { }; assert_ok!(InboundQueue::submit(origin.clone(), message.clone())); - let nonce: u64 = >::get(ChannelId::from(hex!( - "c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539" - ))); - assert_eq!(nonce, 1); - // Submit the same again assert_noop!( InboundQueue::submit(origin.clone(), message.clone()), @@ -145,29 +115,6 @@ fn test_submit_with_invalid_nonce() { }); } -#[test] -fn test_submit_no_funds_to_reward_relayers_just_ignore() { - new_tester().execute_with(|| { - let relayer: AccountId = Keyring::Bob.into(); - let origin = RuntimeOrigin::signed(relayer); - - // Reset balance of sovereign_account to zero first - let sovereign_account = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); - Balances::set_balance(&sovereign_account, 0); - - // Submit message - let message = Message { - event_log: mock_event_log(), - proof: Proof { - receipt_proof: Default::default(), - execution_proof: mock_execution_proof(), - }, - }; - // Check submit successfully in case no funds available - assert_ok!(InboundQueue::submit(origin.clone(), message.clone())); - }); -} - #[test] fn test_set_operating_mode() { new_tester().execute_with(|| { @@ -204,42 +151,158 @@ fn test_set_operating_mode_root_only() { } #[test] -fn test_submit_no_funds_to_reward_relayers_and_ed_preserved() { +fn test_send_native_erc20_token_payload() { new_tester().execute_with(|| { - let relayer: AccountId = Keyring::Bob.into(); - let origin = RuntimeOrigin::signed(relayer); + // To generate test data: forge test --match-test testSendEther -vvvv + let payload = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf04005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000b2d3595bf00600000000000000000000").to_vec(); + let message = MessageV2::decode(&mut payload.as_ref()); + assert_ok!(message.clone()); + + let inbound_message = message.unwrap(); + + let expected_origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let expected_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); + let expected_value = 500000000000000000u128; + let expected_xcm: Vec = vec![]; + let expected_claimer: Option> = None; + + assert_eq!(expected_origin, inbound_message.origin); + assert_eq!(1, inbound_message.assets.len()); + if let InboundAsset::NativeTokenERC20 { token_id, value } = &inbound_message.assets[0] { + assert_eq!(expected_token_id, *token_id); + assert_eq!(expected_value, *value); + } else { + panic!("Expected NativeTokenERC20 asset"); + } + assert_eq!(expected_xcm, inbound_message.xcm); + assert_eq!(expected_claimer, inbound_message.claimer); + }); +} + +#[test] +fn test_send_foreign_erc20_token_payload() { + new_tester().execute_with(|| { + let payload = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf040197874824853fb4ad04794ccfd1cc8d2a7463839cfcbc6a315a1045c60ab85f400000b2d3595bf00600000000000000000000").to_vec(); + let message = MessageV2::decode(&mut payload.as_ref()); + assert_ok!(message.clone()); + + let inbound_message = message.unwrap(); + + let expected_origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let expected_token_id: H256 = hex!("97874824853fb4ad04794ccfd1cc8d2a7463839cfcbc6a315a1045c60ab85f40").into(); + let expected_value = 500000000000000000u128; + let expected_xcm: Vec = vec![]; + let expected_claimer: Option> = None; + + assert_eq!(expected_origin, inbound_message.origin); + assert_eq!(1, inbound_message.assets.len()); + if let InboundAsset::ForeignTokenERC20 { token_id, value } = &inbound_message.assets[0] { + assert_eq!(expected_token_id, *token_id); + assert_eq!(expected_value, *value); + } else { + panic!("Expected ForeignTokenERC20 asset"); + } + assert_eq!(expected_xcm, inbound_message.xcm); + assert_eq!(expected_claimer, inbound_message.claimer); + }); +} - // Reset balance of sovereign account to (ED+1) first - let sovereign_account = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); - Balances::set_balance(&sovereign_account, ExistentialDeposit::get() + 1); +#[test] +fn test_register_token_inbound_message_with_xcm_and_claimer() { + new_tester().execute_with(|| { + let payload = hex!("5991a2df15a8f6a256d3ec51e99254cd3fb576a904005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000300508020401000002286bee0a015029e3b139f4393adda86303fcdaa35f60bb7092bf").to_vec(); + let message = MessageV2::decode(&mut payload.as_ref()); + assert_ok!(message.clone()); + + let inbound_message = message.unwrap(); + + let expected_origin: H160 = hex!("5991a2df15a8f6a256d3ec51e99254cd3fb576a9").into(); + let expected_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); + let expected_value = 0u128; + let expected_xcm: Vec = hex!("0508020401000002286bee0a").to_vec(); + let expected_claimer: Option> = Some(hex!("29E3b139f4393aDda86303fcdAa35F60Bb7092bF").to_vec()); + + assert_eq!(expected_origin, inbound_message.origin); + assert_eq!(1, inbound_message.assets.len()); + if let InboundAsset::NativeTokenERC20 { token_id, value } = &inbound_message.assets[0] { + assert_eq!(expected_token_id, *token_id); + assert_eq!(expected_value, *value); + } else { + panic!("Expected NativeTokenERC20 asset"); + } + assert_eq!(expected_xcm, inbound_message.xcm); + assert_eq!(expected_claimer, inbound_message.claimer); - // Submit message successfully - let message = Message { - event_log: mock_event_log(), - proof: Proof { - receipt_proof: Default::default(), - execution_proof: mock_execution_proof(), - }, + // decode xcm + let versioned_xcm = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut inbound_message.xcm.as_ref(), + ); + + assert_ok!(versioned_xcm.clone()); + + // Check if decoding was successful + let decoded_instructions = match versioned_xcm.unwrap() { + VersionedXcm::V5(decoded) => decoded, + _ => { + panic!("unexpected xcm version found") + } }; - assert_ok!(InboundQueue::submit(origin.clone(), message.clone())); - // Check balance of sovereign account to ED - let amount = Balances::balance(&sovereign_account); - assert_eq!(amount, ExistentialDeposit::get()); + let mut decoded_instructions = decoded_instructions.into_iter(); + let decoded_first = decoded_instructions.next().take(); + assert!(decoded_first.is_some()); + let decoded_second = decoded_instructions.next().take(); + assert!(decoded_second.is_some()); + assert_eq!(ClearOrigin, decoded_second.unwrap(), "Second instruction (ClearOrigin) does not match."); + }); +} - // Submit another message with nonce set as 2 - let mut event_log = mock_event_log(); - event_log.data[31] = 2; - let message = Message { - event_log, - proof: Proof { - receipt_proof: Default::default(), - execution_proof: mock_execution_proof(), +#[test] +fn encode_xcm() { + new_tester().execute_with(|| { + let total_fee_asset: Asset = (Location::parent(), 1_000_000_000).into(); + + let instructions: Xcm<()> = + vec![ReceiveTeleportedAsset(total_fee_asset.into()), ClearOrigin].into(); + + let versioned_xcm_message = VersionedXcm::V5(instructions.clone()); + + let xcm_bytes = VersionedXcm::encode(&versioned_xcm_message); + let hex_string = hex::encode(xcm_bytes.clone()); + + println!("xcm hex: {}", hex_string); + + let versioned_xcm = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut xcm_bytes.as_ref(), + ); + + assert_ok!(versioned_xcm.clone()); + + // Check if decoding was successful + let decoded_instructions = match versioned_xcm.unwrap() { + VersionedXcm::V5(decoded) => decoded, + _ => { + panic!("unexpected xcm version found") }, }; - assert_ok!(InboundQueue::submit(origin.clone(), message.clone())); - // Check balance of sovereign account as ED does not change - let amount = Balances::balance(&sovereign_account); - assert_eq!(amount, ExistentialDeposit::get()); + + let mut original_instructions = instructions.into_iter(); + let mut decoded_instructions = decoded_instructions.into_iter(); + + let original_first = original_instructions.next().take(); + let decoded_first = decoded_instructions.next().take(); + assert_eq!( + original_first, decoded_first, + "First instruction (ReceiveTeleportedAsset) does not match." + ); + + let original_second = original_instructions.next().take(); + let decoded_second = decoded_instructions.next().take(); + assert_eq!( + original_second, decoded_second, + "Second instruction (ClearOrigin) does not match." + ); }); } diff --git a/bridges/snowbridge/primitives/router/Cargo.toml b/bridges/snowbridge/primitives/router/Cargo.toml index 664f2dbf7930..aa4b3177c00b 100644 --- a/bridges/snowbridge/primitives/router/Cargo.toml +++ b/bridges/snowbridge/primitives/router/Cargo.toml @@ -17,6 +17,7 @@ scale-info = { features = ["derive"], workspace = true } log = { workspace = true } frame-support = { workspace = true } +frame-system = { workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } @@ -37,6 +38,7 @@ default = ["std"] std = [ "codec/std", "frame-support/std", + "frame-system/std", "log/std", "scale-info/std", "snowbridge-core/std", @@ -50,6 +52,7 @@ std = [ ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", diff --git a/bridges/snowbridge/primitives/router/src/inbound/v2.rs b/bridges/snowbridge/primitives/router/src/inbound/v2.rs index 79560f9fe0f0..5994e5a7e724 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/v2.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/v2.rs @@ -2,454 +2,172 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Ethereum to XCM messages -use crate::inbound::{CallIndex, GlobalConsensusEthereumConvertsFor}; -use codec::{Decode, Encode}; +use codec::{Decode, DecodeLimit, Encode}; use core::marker::PhantomData; -use frame_support::{traits::tokens::Balance as BalanceT, PalletError}; +use frame_support::PalletError; use scale_info::TypeInfo; use snowbridge_core::TokenId; use sp_core::{Get, RuntimeDebug, H160, H256}; -use sp_runtime::{traits::MaybeEquivalence, MultiAddress}; +use sp_runtime::traits::MaybeEquivalence; use sp_std::prelude::*; -use xcm::prelude::{Junction::AccountKey20, *}; +use xcm::{ + prelude::{Junction::AccountKey20, *}, + MAX_XCM_DECODE_DEPTH, +}; -const MINIMUM_DEPOSIT: u128 = 1; +const LOG_TARGET: &str = "snowbridge-router-primitives"; /// Messages from Ethereum are versioned. This is because in future, /// we may want to evolve the protocol so that the ethereum side sends XCM messages directly. /// Instead having BridgeHub transcode the messages into XCM. #[derive(Clone, Encode, Decode, RuntimeDebug)] pub enum VersionedMessage { - V1(MessageV1), + V2(Message), } -/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are +/// The ethereum side sends messages which are transcoded into XCM on BH. These messages are /// self-contained, in that they can be transcoded using only information in the message. -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub struct MessageV1 { - /// EIP-155 chain id of the origin Ethereum network - pub chain_id: u64, - /// The command originating from the Gateway contract - pub command: Command, -} - -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum Command { - /// Register a wrapped token on the AssetHub `ForeignAssets` pallet - RegisterToken { - /// The address of the ERC20 token to be bridged over to AssetHub - token: H160, - /// XCM execution fee on AssetHub - fee: u128, - }, - /// Send Ethereum token to AssetHub or another parachain - SendToken { - /// The address of the ERC20 token to be bridged over to AssetHub - token: H160, - /// The destination for the transfer - destination: Destination, - /// Amount to transfer - amount: u128, - /// XCM execution fee on AssetHub - fee: u128, - }, - /// Send Polkadot token back to the original parachain - SendNativeToken { - /// The Id of the token - token_id: TokenId, - /// The destination for the transfer - destination: Destination, - /// Amount to transfer - amount: u128, - /// XCM execution fee on AssetHub - fee: u128, - }, +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Message { + /// The origin address + pub origin: H160, + /// The assets + pub assets: Vec, + // The command originating from the Gateway contract + pub xcm: Vec, + // The claimer in the case that funds get trapped. + pub claimer: Option>, } -/// Destination for bridged tokens -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum Destination { - /// The funds will be deposited into account `id` on AssetHub - AccountId32 { id: [u8; 32] }, - /// The funds will deposited into the sovereign account of destination parachain `para_id` on - /// AssetHub, Account `id` on the destination parachain will receive the funds via a - /// reserve-backed transfer. See - ForeignAccountId32 { - para_id: u32, - id: [u8; 32], - /// XCM execution fee on final destination - fee: u128, +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum Asset { + NativeTokenERC20 { + /// The native token ID + token_id: H160, + /// The monetary value of the asset + value: u128, }, - /// The funds will deposited into the sovereign account of destination parachain `para_id` on - /// AssetHub, Account `id` on the destination parachain will receive the funds via a - /// reserve-backed transfer. See - ForeignAccountId20 { - para_id: u32, - id: [u8; 20], - /// XCM execution fee on final destination - fee: u128, + ForeignTokenERC20 { + /// The foreign token ID + token_id: H256, + /// The monetary value of the asset + value: u128, }, } -pub struct MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, -> where - CreateAssetCall: Get, - CreateAssetDeposit: Get, - Balance: BalanceT, - ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, -{ - _phantom: PhantomData<( - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - )>, -} - /// Reason why a message conversion failed. #[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)] pub enum ConvertMessageError { - /// The message version is not supported for conversion. - UnsupportedVersion, - InvalidDestination, - InvalidToken, - /// The fee asset is not supported for conversion. - UnsupportedFeeAsset, - CannotReanchor, + /// The XCM provided with the message could not be decoded into XCM. + InvalidXCM, + /// The XCM provided with the message could not be decoded into versioned XCM. + InvalidVersionedXCM, + /// Invalid claimer MultiAddress provided in payload. + InvalidClaimer, + /// Invalid foreign ERC20 token ID + InvalidAsset, } -/// convert the inbound message to xcm which will be forwarded to the destination chain pub trait ConvertMessage { - type Balance: BalanceT + From; - type AccountId; - /// Converts a versioned message into an XCM message and an optional topicID - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; + fn convert(message: Message) -> Result, ConvertMessageError>; } -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > ConvertMessage - for MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > +pub struct MessageToXcm where - CreateAssetCall: Get, - CreateAssetDeposit: Get, + EthereumNetwork: Get, InboundQueuePalletInstance: Get, - Balance: BalanceT + From, - AccountId: Into<[u8; 32]>, ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, { - type Balance = Balance; - type AccountId = AccountId; - - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { - use Command::*; - use VersionedMessage::*; - match message { - V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => - Ok(Self::convert_register_token(message_id, chain_id, token, fee)), - V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) => - Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)), - V1(MessageV1 { - chain_id, - command: SendNativeToken { token_id, destination, amount, fee }, - }) => Self::convert_send_native_token( - message_id, - chain_id, - token_id, - destination, - amount, - fee, - ), - } - } + _phantom: PhantomData<(EthereumNetwork, InboundQueuePalletInstance, ConvertAssetId)>, } -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > - MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > +impl ConvertMessage + for MessageToXcm where - CreateAssetCall: Get, - CreateAssetDeposit: Get, + EthereumNetwork: Get, InboundQueuePalletInstance: Get, - Balance: BalanceT + From, - AccountId: Into<[u8; 32]>, ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, { - fn convert_register_token( - message_id: H256, - chain_id: u64, - token: H160, - fee: u128, - ) -> (Xcm<()>, Balance) { - let network = Ethereum { chain_id }; - let xcm_fee: Asset = (Location::parent(), fee).into(); - let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into(); - - let total_amount = fee + CreateAssetDeposit::get(); - let total: Asset = (Location::parent(), total_amount).into(); - - let bridge_location = Location::new(2, GlobalConsensus(network)); - - let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id(&chain_id); - let asset_id = Self::convert_token_address(network, token); - let create_call_index: [u8; 2] = CreateAssetCall::get(); - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); - - let xcm: Xcm<()> = vec![ - // Teleport required fees. - ReceiveTeleportedAsset(total.into()), - // Pay for execution. - BuyExecution { fees: xcm_fee, weight_limit: Unlimited }, - // Fund the snowbridge sovereign with the required deposit for creation. - DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() }, - // This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be - // deposited to snowbridge sovereign, instead of being trapped, regardless of - // `Transact` success or not. - SetAppendix(Xcm(vec![ - RefundSurplus, - DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location }, - ])), - // Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`. - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), - // Change origin to the bridge. - UniversalOrigin(GlobalConsensus(network)), - // Call create_asset on foreign assets pallet. - Transact { - origin_kind: OriginKind::Xcm, - call: ( - create_call_index, - asset_id, - MultiAddress::<[u8; 32], ()>::Id(owner), - MINIMUM_DEPOSIT, - ) - .encode() - .into(), - }, - // Forward message id to Asset Hub - SetTopic(message_id.into()), - // Once the program ends here, appendix program will run, which will deposit any - // leftover fee to snowbridge sovereign. - ] - .into(); + fn convert(message: Message) -> Result, ConvertMessageError> { + let mut message_xcm: Xcm<()> = Xcm::new(); + if message.xcm.len() > 0 { + // Decode xcm + let versioned_xcm = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut message.xcm.as_ref(), + ) + .map_err(|_| ConvertMessageError::InvalidVersionedXCM)?; + message_xcm = versioned_xcm.try_into().map_err(|_| ConvertMessageError::InvalidXCM)?; + } - (xcm, total_amount.into()) - } + log::debug!(target: LOG_TARGET,"xcm decoded as {:?}", message_xcm); - fn convert_send_token( - message_id: H256, - chain_id: u64, - token: H160, - destination: Destination, - amount: u128, - asset_hub_fee: u128, - ) -> (Xcm<()>, Balance) { - let network = Ethereum { chain_id }; - let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - let asset: Asset = (Self::convert_token_address(network, token), amount).into(); + let network = EthereumNetwork::get(); - let (dest_para_id, beneficiary, dest_para_fee) = match destination { - // Final destination is a 32-byte account on AssetHub - Destination::AccountId32 { id } => - (None, Location::new(0, [AccountId32 { network: None, id }]), 0), - // Final destination is a 32-byte account on a sibling of AssetHub - Destination::ForeignAccountId32 { para_id, id, fee } => ( - Some(para_id), - Location::new(0, [AccountId32 { network: None, id }]), - // Total fee needs to cover execution on AssetHub and Sibling - fee, - ), - // Final destination is a 20-byte account on a sibling of AssetHub - Destination::ForeignAccountId20 { para_id, id, fee } => ( - Some(para_id), - Location::new(0, [AccountKey20 { network: None, key: id }]), - // Total fee needs to cover execution on AssetHub and Sibling - fee, - ), - }; + let origin_location = Location::new(2, GlobalConsensus(network)) + .push_interior(AccountKey20 { key: message.origin.into(), network: None }) + .map_err(|_| ConvertMessageError::InvalidXCM)?; - let total_fees = asset_hub_fee.saturating_add(dest_para_fee); - let total_fee_asset: Asset = (Location::parent(), total_fees).into(); - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); + let network = EthereumNetwork::get(); + let fee_asset = Location::new(1, Here); + let fee_value = 1_000_000_000u128; // TODO get from command + let fee: xcm::prelude::Asset = (fee_asset, fee_value).into(); let mut instructions = vec![ - ReceiveTeleportedAsset(total_fee_asset.into()), - BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), + ReceiveTeleportedAsset(fee.clone().into()), + BuyExecution { fees: fee, weight_limit: Unlimited }, + DescendOrigin(PalletInstance(InboundQueuePalletInstance::get()).into()), UniversalOrigin(GlobalConsensus(network)), - ReserveAssetDeposited(asset.clone().into()), - ClearOrigin, ]; - match dest_para_id { - Some(dest_para_id) => { - let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into(); - let bridge_location = Location::new(2, GlobalConsensus(network)); - - instructions.extend(vec![ - // After program finishes deposit any leftover assets to the snowbridge - // sovereign. - SetAppendix(Xcm(vec![DepositAsset { - assets: Wild(AllCounted(2)), - beneficiary: bridge_location, - }])), - // Perform a deposit reserve to send to destination chain. - DepositReserveAsset { - assets: Definite(vec![dest_para_fee_asset.clone(), asset].into()), - dest: Location::new(1, [Parachain(dest_para_id)]), - xcm: vec![ - // Buy execution on target. - BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited }, - // Deposit assets to beneficiary. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - // Forward message id to destination parachain. - SetTopic(message_id.into()), - ] - .into(), - }, - ]); - }, - None => { - instructions.extend(vec![ - // Deposit both asset and fees to beneficiary so the fees will not get - // trapped. Another benefit is when fees left more than ED on AssetHub could be - // used to create the beneficiary account in case it does not exist. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - ]); - }, + for asset in &message.assets { + match asset { + Asset::NativeTokenERC20 { token_id, value } => { + let token_location: Location = Location::new( + 2, + [ + GlobalConsensus(EthereumNetwork::get()), + AccountKey20 { network: None, key: (*token_id).into() }, + ], + ); + instructions.push(ReserveAssetDeposited((token_location, *value).into())); + }, + Asset::ForeignTokenERC20 { token_id, value } => { + let asset_id = ConvertAssetId::convert(&token_id) + .ok_or(ConvertMessageError::InvalidAsset)?; + instructions.push(WithdrawAsset((asset_id, *value).into())); + }, + } } - // Forward message id to Asset Hub. - instructions.push(SetTopic(message_id.into())); - - // The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since - // they are teleported within `instructions`). - (instructions.into(), total_fees.into()) - } - - // Convert ERC20 token address to a location that can be understood by Assets Hub. - fn convert_token_address(network: NetworkId, token: H160) -> Location { - Location::new( - 2, - [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }], - ) - } - - /// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign - /// account of the Gateway contract and either deposits those assets into a recipient account or - /// forwards the assets to another parachain. - fn convert_send_native_token( - message_id: H256, - chain_id: u64, - token_id: TokenId, - destination: Destination, - amount: u128, - asset_hub_fee: u128, - ) -> Result<(Xcm<()>, Balance), ConvertMessageError> { - let network = Ethereum { chain_id }; - let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - - let beneficiary = match destination { - // Final destination is a 32-byte account on AssetHub - Destination::AccountId32 { id } => - Ok(Location::new(0, [AccountId32 { network: None, id }])), - _ => Err(ConvertMessageError::InvalidDestination), - }?; - - let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - - let asset_loc = - ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?; - - let mut reanchored_asset_loc = asset_loc.clone(); - reanchored_asset_loc - .reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get()) - .map_err(|_| ConvertMessageError::CannotReanchor)?; + if let Some(claimer) = message.claimer { + let claimer = Junction::decode(&mut claimer.as_ref()) + .map_err(|_| ConvertMessageError::InvalidClaimer)?; + let claimer_location: Location = Location::new(0, [claimer.into()]); + instructions.push(SetAssetClaimer { location: claimer_location }); + } - let asset: Asset = (reanchored_asset_loc, amount).into(); + // Set the alias origin to the original sender on Ethereum. Important to be before the + // arbitrary XCM that is appended to the message on the next line. + instructions.push(AliasOrigin(origin_location.into())); - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); + // Add the XCM sent in the message to the end of the xcm instruction + instructions.extend(message_xcm.0); - let instructions = vec![ - ReceiveTeleportedAsset(total_fee_asset.clone().into()), - BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), - UniversalOrigin(GlobalConsensus(network)), - WithdrawAsset(asset.clone().into()), - // Deposit both asset and fees to beneficiary so the fees will not get - // trapped. Another benefit is when fees left more than ED on AssetHub could be - // used to create the beneficiary account in case it does not exist. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - SetTopic(message_id.into()), - ]; - - // `total_fees` to burn on this chain when sending `instructions` to run on AH (which also - // teleport fees) - Ok((instructions.into(), asset_hub_fee.into())) + Ok(instructions.into()) } } #[cfg(test)] mod tests { - use crate::inbound::{CallIndex, GlobalConsensusEthereumConvertsFor}; + use crate::inbound::{ + v2::{ConvertMessage, Message, MessageToXcm}, + CallIndex, GlobalConsensusEthereumConvertsFor, + }; + use codec::Decode; use frame_support::{assert_ok, parameter_types}; use hex_literal::hex; + use sp_runtime::traits::ConstU8; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; @@ -517,4 +235,13 @@ mod tests { assert_eq!(reanchored_asset_with_ethereum_context, asset.clone()); } } + + #[test] + fn test_convert_message() { + let payload = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf040197874824853fb4ad04794ccfd1cc8d2a7463839cfcbc6a315a1045c60ab85f400000b2d3595bf00600000000000000000000").to_vec(); + let message = Message::decode(&mut payload.as_ref()); + assert_ok!(message.clone()); + let result = MessageToXcm::>::convert(message.unwrap()); + assert_ok!(result); + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index 0069e1b13b91..29d95239c704 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -26,7 +26,6 @@ use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; use snowbridge_router_primitives::{ - inbound::{v1::MessageToXcm, v2::MessageToXcm as MessageToXcmV2}, outbound::{v1::EthereumBlobExporter, v2::EthereumBlobExporter as EthereumBlobExporterV2}, }; use sp_core::H160; @@ -95,7 +94,7 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type GatewayAddress = EthereumGatewayAddress; #[cfg(feature = "runtime-benchmarks")] type Helper = Runtime; - type MessageConverter = MessageToXcm< + type MessageConverter = snowbridge_router_primitives::inbound::v1::MessageToXcm< CreateAssetCall, CreateAssetDeposit, ConstU8, @@ -116,31 +115,16 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Verifier = snowbridge_pallet_ethereum_client::Pallet; - type Token = Balances; #[cfg(not(feature = "runtime-benchmarks"))] type XcmSender = XcmRouter; #[cfg(feature = "runtime-benchmarks")] type XcmSender = DoNothingRouter; - type ChannelLookup = EthereumSystem; type GatewayAddress = EthereumGatewayAddress; #[cfg(feature = "runtime-benchmarks")] type Helper = Runtime; - type MessageConverter = MessageToXcmV2< - CreateAssetCall, - CreateAssetDeposit, - ConstU8, - AccountId, - Balance, - EthereumSystem, - EthereumUniversalLocation, - AssetHubFromEthereum, - >; - type WeightToFee = WeightToFee; - type LengthToFee = ConstantMultiplier; - type MaxMessageSize = ConstU32<2048>; type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; - type PricingParameters = EthereumSystem; - type AssetTransactor = ::AssetTransactor; + type AssetHubParaId = ConstU32<1000>; + type MessageConverter = snowbridge_router_primitives::inbound::v2::MessageToXcm, EthereumSystem>; } impl snowbridge_pallet_outbound_queue::Config for Runtime {