diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/send_message_impl.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/send_message_impl.rs index 2071c8c516fb..c37cf0dfa530 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/send_message_impl.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/send_message_impl.rs @@ -9,8 +9,8 @@ use frame_support::{ traits::{EnqueueMessage, Get}, }; use snowbridge_core::outbound::{ - v2::{primary_governance_origin, Message, SendError, SendMessage}, - SendMessageFeeProvider, + v2::{primary_governance_origin, Message, SendMessage}, + SendError, SendMessageFeeProvider, }; use sp_core::H256; use sp_runtime::BoundedVec; diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/test.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/test.rs index f982a3b83baf..aadb9e520e1a 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/test.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/test.rs @@ -11,7 +11,10 @@ use frame_support::{ use codec::Encode; use snowbridge_core::{ - outbound::v2::{primary_governance_origin, Command, SendError, SendMessage}, + outbound::{ + v2::{primary_governance_origin, Command, SendMessage}, + SendError, + }, ChannelId, ParaId, }; use sp_core::H256; diff --git a/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs b/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs index 3bb7b8895384..39b41b1c792a 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs @@ -12,8 +12,8 @@ use frame_support::{ use frame_system::unique; use snowbridge_core::{ outbound::{ - v1::{Fee, Message, QueuedMessage, SendError, SendMessage, VersionedQueuedMessage}, - SendMessageFeeProvider, + v1::{Fee, Message, QueuedMessage, SendMessage, VersionedQueuedMessage}, + SendError, SendMessageFeeProvider, }, ChannelId, PRIMARY_GOVERNANCE_CHANNEL, }; diff --git a/bridges/snowbridge/pallets/outbound-queue/src/test.rs b/bridges/snowbridge/pallets/outbound-queue/src/test.rs index 580f32a8b68f..36227817f368 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/test.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/test.rs @@ -10,7 +10,10 @@ use frame_support::{ use codec::Encode; use snowbridge_core::{ - outbound::v1::{Command, SendError, SendMessage}, + outbound::{ + v1::{Command, SendMessage}, + SendError, + }, ParaId, PricingParameters, Rewards, }; use sp_arithmetic::FixedU128; diff --git a/bridges/snowbridge/pallets/system/src/lib.rs b/bridges/snowbridge/pallets/system/src/lib.rs index 0979fb61cc5f..52cc28b7de75 100644 --- a/bridges/snowbridge/pallets/system/src/lib.rs +++ b/bridges/snowbridge/pallets/system/src/lib.rs @@ -69,8 +69,9 @@ use frame_system::pallet_prelude::*; use snowbridge_core::{ meth, outbound::{ - v1::{Command, Initializer, Message, SendError, SendMessage}, - OperatingMode, + v1::{Command, Initializer, Message, SendMessage}, + v2::{Command as CommandV2, Message as MessageV2, SendMessage as SendMessageV2}, + OperatingMode, SendError, }, sibling_sovereign_account, AgentId, AssetMetadata, Channel, ChannelId, ParaId, PricingParameters as PricingParametersRecord, TokenId, TokenIdOf, PRIMARY_GOVERNANCE_CHANNEL, @@ -140,7 +141,7 @@ where #[frame_support::pallet] pub mod pallet { use frame_support::dispatch::PostDispatchInfo; - use snowbridge_core::StaticLookup; + use snowbridge_core::{outbound::v2::second_governance_origin, StaticLookup}; use sp_core::U256; use super::*; @@ -155,6 +156,8 @@ pub mod pallet { /// Send messages to Ethereum type OutboundQueue: SendMessage>; + type OutboundQueueV2: SendMessageV2>; + /// Origin check for XCM locations that can create agents type SiblingOrigin: EnsureOrigin; @@ -638,6 +641,34 @@ pub mod pallet { pays_fee: Pays::No, }) } + + /// Registers a Polkadot-native token as a wrapped ERC20 token on Ethereum. + /// Privileged. Can only be called by root. + /// + /// Fee required: No + /// + /// - `origin`: Must be root + /// - `location`: Location of the asset (relative to this chain) + /// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::register_token())] + pub fn register_token_v2( + origin: OriginFor, + location: Box, + metadata: AssetMetadata, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + let location: Location = + (*location).try_into().map_err(|_| Error::::UnsupportedLocationVersion)?; + + Self::do_register_token_v2(&location, metadata, PaysFee::::No)?; + + Ok(PostDispatchInfo { + actual_weight: Some(T::WeightInfo::register_token()), + pays_fee: Pays::No, + }) + } } impl Pallet { @@ -763,6 +794,72 @@ pub mod pallet { Ok(()) } + + pub(crate) fn do_register_token_v2( + location: &Location, + metadata: AssetMetadata, + pays_fee: PaysFee, + ) -> Result<(), DispatchError> { + let ethereum_location = T::EthereumLocation::get(); + // reanchor to Ethereum context + let location = location + .clone() + .reanchored(ðereum_location, &T::UniversalLocation::get()) + .map_err(|_| Error::::LocationConversionFailed)?; + + let token_id = TokenIdOf::convert_location(&location) + .ok_or(Error::::LocationConversionFailed)?; + + if !ForeignToNativeId::::contains_key(token_id) { + NativeToForeignId::::insert(location.clone(), token_id); + ForeignToNativeId::::insert(token_id, location.clone()); + } + + let command = CommandV2::RegisterForeignToken { + token_id, + name: metadata.name.into_inner(), + symbol: metadata.symbol.into_inner(), + decimals: metadata.decimals, + }; + Self::send_v2(second_governance_origin(), command, pays_fee)?; + + Self::deposit_event(Event::::RegisterToken { + location: location.clone().into(), + foreign_token_id: token_id, + }); + + Ok(()) + } + + /// Send `command` to the Gateway on the Channel identified by `channel_id` + fn send_v2(origin: H256, command: CommandV2, pays_fee: PaysFee) -> DispatchResult { + let message = MessageV2 { + origin, + id: Default::default(), + fee: Default::default(), + commands: BoundedVec::try_from(vec![command]).unwrap(), + }; + + let (ticket, fee) = + T::OutboundQueueV2::validate(&message).map_err(|err| Error::::Send(err))?; + + let payment = match pays_fee { + PaysFee::Yes(account) | PaysFee::Partial(account) => Some((account, fee.total())), + PaysFee::No => None, + }; + + if let Some((payer, fee)) = payment { + T::Token::transfer( + &payer, + &T::TreasuryAccount::get(), + fee, + Preservation::Preserve, + )?; + } + + T::OutboundQueueV2::deliver(ticket).map_err(|err| Error::::Send(err))?; + Ok(()) + } } impl StaticLookup for Pallet { diff --git a/bridges/snowbridge/pallets/system/src/mock.rs b/bridges/snowbridge/pallets/system/src/mock.rs index 5b83c0d856b6..b02aa62e52f3 100644 --- a/bridges/snowbridge/pallets/system/src/mock.rs +++ b/bridges/snowbridge/pallets/system/src/mock.rs @@ -11,8 +11,15 @@ use sp_core::H256; use xcm_executor::traits::ConvertLocation; use snowbridge_core::{ - gwei, meth, outbound::v1::ConstantGasMeter, sibling_sovereign_account, AgentId, - AllowSiblingsOnly, ParaId, PricingParameters, Rewards, + gwei, meth, + outbound::{ + v1::ConstantGasMeter, + v2::{ + DefaultOutboundQueue, Fee as FeeV2, Message as MessageV2, SendMessage as SendMessageV2, + }, + SendError, SendMessageFeeProvider, + }, + sibling_sovereign_account, AgentId, AllowSiblingsOnly, ParaId, PricingParameters, Rewards, }; use sp_runtime::{ traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, Keccak256}, @@ -213,6 +220,7 @@ impl crate::Config for Test { type EthereumLocation = EthereumDestination; #[cfg(feature = "runtime-benchmarks")] type Helper = (); + type OutboundQueueV2 = DefaultOutboundQueue; } // Build genesis storage according to the mock runtime. diff --git a/bridges/snowbridge/primitives/core/src/outbound/mod.rs b/bridges/snowbridge/primitives/core/src/outbound/mod.rs index 30ffda06e5c4..6197211acbc0 100644 --- a/bridges/snowbridge/primitives/core/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/core/src/outbound/mod.rs @@ -4,6 +4,7 @@ //! //! Common traits and types use codec::{Decode, Encode}; +use frame_support::PalletError; use scale_info::TypeInfo; use sp_arithmetic::traits::{BaseArithmetic, Unsigned}; use sp_core::RuntimeDebug; @@ -30,6 +31,17 @@ pub trait SendMessageFeeProvider { fn local_fee() -> Self::Balance; } +/// Reasons why sending to Ethereum could not be initiated +#[derive(Copy, Clone, Encode, Decode, 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, +} + #[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)] pub enum DryRunError { ConvertFailed, diff --git a/bridges/snowbridge/primitives/core/src/outbound/v1.rs b/bridges/snowbridge/primitives/core/src/outbound/v1.rs index 3215131eae33..037fc21db017 100644 --- a/bridges/snowbridge/primitives/core/src/outbound/v1.rs +++ b/bridges/snowbridge/primitives/core/src/outbound/v1.rs @@ -3,13 +3,12 @@ //! # Outbound V1 primitives use crate::{ - outbound::{OperatingMode, SendMessageFeeProvider}, + outbound::{OperatingMode, SendError, SendMessageFeeProvider}, pricing::UD60x18, ChannelId, }; use codec::{Decode, Encode}; use ethabi::Token; -use frame_support::PalletError; use scale_info::TypeInfo; use sp_arithmetic::traits::{BaseArithmetic, Unsigned}; use sp_core::{RuntimeDebug, H160, H256, U256}; @@ -365,17 +364,6 @@ pub trait Ticket: Encode + Decode + Clone { fn message_id(&self) -> H256; } -/// Reasons why sending to Ethereum could not be initiated -#[derive(Copy, Clone, Encode, Decode, 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, -} - pub trait GasMeter { /// All the gas used for submitting a message to Ethereum, minus the cost of dispatching /// the command within the message diff --git a/bridges/snowbridge/primitives/core/src/outbound/v2.rs b/bridges/snowbridge/primitives/core/src/outbound/v2.rs index 308eb8d2aef9..57cb5b90fe2c 100644 --- a/bridges/snowbridge/primitives/core/src/outbound/v2.rs +++ b/bridges/snowbridge/primitives/core/src/outbound/v2.rs @@ -2,10 +2,10 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! # Outbound V2 primitives -use crate::outbound::{OperatingMode, SendMessageFeeProvider}; +use crate::outbound::{OperatingMode, SendError, SendMessageFeeProvider}; use alloy_sol_types::sol; use codec::{Decode, Encode}; -use frame_support::{pallet_prelude::ConstU32, BoundedVec, PalletError}; +use frame_support::{pallet_prelude::ConstU32, BoundedVec}; use hex_literal::hex; use scale_info::TypeInfo; use sp_arithmetic::traits::{BaseArithmetic, Unsigned}; @@ -266,15 +266,25 @@ pub trait SendMessage: SendMessageFeeProvider { fn deliver(ticket: Self::Ticket) -> Result; } -/// Reasons why sending to Ethereum could not be initiated -#[derive(Copy, Clone, Encode, Decode, 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, +pub struct DefaultOutboundQueue; +impl SendMessage for DefaultOutboundQueue { + type Ticket = (); + + fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> { + Ok(((), Fee { local: Default::default() })) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(H256::zero()) + } +} + +impl SendMessageFeeProvider for DefaultOutboundQueue { + type Balance = u128; + + fn local_fee() -> Self::Balance { + Default::default() + } } pub trait GasMeter { diff --git a/bridges/snowbridge/primitives/router/src/outbound/v1.rs b/bridges/snowbridge/primitives/router/src/outbound/v1.rs index 184475c2b4fa..4c92ff244530 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/v1.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/v1.rs @@ -410,10 +410,7 @@ mod tests { use frame_support::parameter_types; use hex_literal::hex; use snowbridge_core::{ - outbound::{ - v1::{Fee, SendError}, - SendMessageFeeProvider, - }, + outbound::{v1::Fee, SendError, SendMessageFeeProvider}, AgentIdOf, }; use sp_std::default::Default; diff --git a/bridges/snowbridge/primitives/router/src/outbound/v2.rs b/bridges/snowbridge/primitives/router/src/outbound/v2.rs index cd25fd0aed4b..2525625005d7 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/v2.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/v2.rs @@ -482,10 +482,7 @@ mod tests { use frame_support::parameter_types; use hex_literal::hex; use snowbridge_core::{ - outbound::{ - v2::{Fee, SendError}, - SendMessageFeeProvider, - }, + outbound::{v2::Fee, SendError, SendMessageFeeProvider}, AgentIdOf, }; use sp_std::default::Default; diff --git a/bridges/snowbridge/runtime/runtime-common/src/tests.rs b/bridges/snowbridge/runtime/runtime-common/src/tests.rs index bc73ad7a44e5..dea5ad5411c2 100644 --- a/bridges/snowbridge/runtime/runtime-common/src/tests.rs +++ b/bridges/snowbridge/runtime/runtime-common/src/tests.rs @@ -1,8 +1,8 @@ use crate::XcmExportFeeToSibling; use frame_support::{parameter_types, sp_runtime::testing::H256}; use snowbridge_core::outbound::{ - v1::{Fee, Message, SendError, SendMessage}, - SendMessageFeeProvider, + v1::{Fee, Message, SendMessage}, + SendError, SendMessageFeeProvider, }; use xcm::prelude::{ Asset, Assets, Here, Kusama, Location, NetworkId, Parachain, XcmContext, XcmError, XcmHash, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index 5a00216c59d2..a405bd5b002b 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -37,6 +37,7 @@ use crate::xcm_config::RelayNetwork; use benchmark_helpers::DoNothingRouter; use frame_support::{parameter_types, weights::ConstantMultiplier}; use pallet_xcm::EnsureXcm; +use snowbridge_core::outbound::v2::DefaultOutboundQueue; use sp_runtime::{ traits::{ConstU32, ConstU8, Keccak256}, FixedU128, @@ -191,6 +192,7 @@ impl snowbridge_pallet_system::Config for Runtime { type InboundDeliveryCost = EthereumInboundQueue; type UniversalLocation = UniversalLocation; type EthereumLocation = EthereumLocation; + type OutboundQueueV2 = DefaultOutboundQueue; } #[cfg(feature = "runtime-benchmarks")] 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..5c9a38135f9f 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 @@ -19,8 +19,8 @@ use crate::XcmRouter; use crate::{ xcm_config, xcm_config::{TreasuryAccount, UniversalLocation}, - Balances, EthereumInboundQueue, EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, - RuntimeEvent, TransactionByteFee, + Balances, EthereumInboundQueue, EthereumOutboundQueue, EthereumOutboundQueueV2, EthereumSystem, + MessageQueue, Runtime, RuntimeEvent, TransactionByteFee, }; use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; @@ -248,6 +248,7 @@ impl snowbridge_pallet_system::Config for Runtime { type InboundDeliveryCost = EthereumInboundQueue; type UniversalLocation = UniversalLocation; type EthereumLocation = EthereumLocation; + type OutboundQueueV2 = EthereumOutboundQueueV2; } #[cfg(feature = "runtime-benchmarks")]