diff --git a/Cargo.lock b/Cargo.lock index 431ad1dd74ae..c13a3d10168a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2405,6 +2405,7 @@ dependencies = [ "pallet-assets", "pallet-balances", "pallet-bridge-messages", + "pallet-bridge-relayers", "pallet-message-queue", "pallet-xcm", "pallet-xcm-bridge-hub", @@ -11077,6 +11078,8 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex", + "hex-literal", "log", "pallet-balances", "pallet-bridge-grandpa", @@ -11086,11 +11089,16 @@ dependencies = [ "pallet-utility", "parity-scale-codec", "scale-info", + "snowbridge-core", + "snowbridge-router-primitives", "sp-arithmetic 23.0.0", "sp-core 28.0.0", "sp-io 30.0.0", + "sp-keyring", "sp-runtime 31.0.1", "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-executor", ] [[package]] @@ -20893,6 +20901,7 @@ dependencies = [ "frame-system", "hex", "hex-literal", + "log", "parity-scale-codec", "polkadot-parachain-primitives", "scale-info", @@ -21079,12 +21088,15 @@ version = "0.2.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "bp-messages", + "bp-relayers", "frame-benchmarking", "frame-support", "frame-system", "hex-literal", "log", "pallet-balances", + "pallet-bridge-relayers", "parity-scale-codec", "scale-info", "serde", @@ -21131,12 +21143,16 @@ version = "0.2.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "bp-messages", + "bp-relayers", "bridge-hub-common", "ethabi-decode", "frame-benchmarking", "frame-support", "frame-system", "hex-literal", + "pallet-balances", + "pallet-bridge-relayers", "pallet-message-queue", "parity-scale-codec", "scale-info", @@ -21149,6 +21165,8 @@ dependencies = [ "sp-keyring", "sp-runtime 31.0.1", "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-executor", ] [[package]] diff --git a/bridges/modules/relayers/Cargo.toml b/bridges/modules/relayers/Cargo.toml index 0bf889bcca0e..e035ce30bf8d 100644 --- a/bridges/modules/relayers/Cargo.toml +++ b/bridges/modules/relayers/Cargo.toml @@ -32,8 +32,17 @@ pallet-transaction-payment = { workspace = true } sp-arithmetic = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +sp-core = { workspace = true } + +snowbridge-core = { workspace = true } +snowbridge-router-primitives = { workspace = true } + +xcm = { workspace = true } +xcm-executor = { workspace = true } [dev-dependencies] +hex = { workspace = true, default-features = true } +hex-literal = { workspace = true, default-features = true } bp-runtime = { workspace = true } pallet-balances = { workspace = true, default-features = true } sp-io = { workspace = true } @@ -42,7 +51,7 @@ bp-parachains = { workspace = true } bp-polkadot-core = { workspace = true } bp-test-utils = { workspace = true } pallet-utility = { workspace = true } -sp-core = { workspace = true } +sp-keyring = { workspace = true, default-features = true } [features] default = ["std"] @@ -64,12 +73,16 @@ std = [ "pallet-bridge-parachains/std", "pallet-transaction-payment/std", "pallet-utility/std", + "snowbridge-core/std", + "snowbridge-router-primitives/std", "scale-info/std", "sp-arithmetic/std", "sp-core/std", "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-executor/std", + "xcm/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/bridges/modules/relayers/src/lib.rs b/bridges/modules/relayers/src/lib.rs index f06c2e16ac24..521b3ebd966a 100644 --- a/bridges/modules/relayers/src/lib.rs +++ b/bridges/modules/relayers/src/lib.rs @@ -30,11 +30,25 @@ use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero}; use sp_runtime::{traits::CheckedSub, Saturating}; use sp_std::marker::PhantomData; +use frame_support::{ + traits::{ + fungible::{Inspect, Mutate}, + tokens::Preservation, + }, + PalletError, +}; +use frame_system::pallet_prelude::*; pub use pallet::*; pub use payment_adapter::DeliveryConfirmationPaymentsAdapter; +use snowbridge_core::rewards::RewardLedger; +use sp_core::H160; pub use stake_adapter::StakeAndSlashNamed; pub use weights::WeightInfo; pub use weights_ext::WeightInfoExt; +use xcm::prelude::{send_xcm, SendError as XcmpSendError, *}; +use xcm_executor::traits::TransactAsset; + +extern crate alloc; mod mock; mod payment_adapter; @@ -49,12 +63,15 @@ pub mod weights; /// The target that will be used when publishing logs related to this pallet. pub const LOG_TARGET: &str = "runtime::bridge-relayers"; +pub type AccountIdOf = ::AccountId; +pub type BalanceOf = + <>::Token as Inspect<::AccountId>>::Balance; + #[frame_support::pallet] pub mod pallet { use super::*; use bp_messages::LaneIdType; use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; /// `RelayerRewardsKeyProvider` for given configuration. type RelayerRewardsKeyProviderOf = RelayerRewardsKeyProvider< @@ -82,6 +99,22 @@ pub mod pallet { type WeightInfo: WeightInfoExt; /// Lane identifier type. type LaneId: LaneIdType + Send + Sync; + /// AssetHub parachain ID + type AssetHubParaId: Get; + type InboundQueuePalletInstance: Get; + /// Ethereum network ID including the chain ID + type EthereumNetwork: Get; + /// Message relayers are rewarded with this asset + type WethAddress: Get; + /// XCM message sender + type XcmSender: SendXcm; + /// To withdraw and deposit an asset. + type AssetTransactor: TransactAsset; + type AssetHubXCMFee: Get; + type Token: Mutate + Inspect; + /// TreasuryAccount to collect fees + #[pallet::constant] + type TreasuryAccount: Get; } #[pallet::pallet] @@ -228,6 +261,19 @@ pub mod pallet { }, ) } + + /// Claim accumulated relayer rewards. Balance is minted on AH. + /// Fees: + /// BH execution fee - paid in DOT when executing the claim extrinsic + /// XCM delivery fee to AH - paid in DOT to Treasury on BH + /// AH execution fee - paid in Weth, deducated from relayer accumulated rewards + #[pallet::call_index(3)] + #[pallet::weight((T::WeightInfo::claim(), DispatchClass::Operational))] + pub fn claim(origin: OriginFor, deposit_location: Location) -> DispatchResult { + let account_id = ensure_signed(origin)?; + Self::process_claim(account_id, deposit_location)?; + Ok(()) + } } impl, I: 'static> Pallet { @@ -391,6 +437,69 @@ pub mod pallet { Ok(()) } + + /// Claim rewards on AH, based on accumulated rewards balance in storage. + fn process_claim(account_id: AccountIdOf, deposit_location: Location) -> DispatchResult { + let value = RewardsMapping::::get(account_id.clone()); + if value.is_zero() { + return Err(Error::::InsufficientFunds.into()); + } + let reward_balance: u128 = + TryInto::::try_into(value).map_err(|_| Error::::InvalidAmount)?; + + let reward_location = snowbridge_core::location::convert_token_address( + T::EthereumNetwork::get(), + T::WethAddress::get(), + ); + let reward_asset: Asset = (reward_location.clone(), reward_balance).into(); + let fee_asset: Asset = (reward_location, T::AssetHubXCMFee::get()).into(); + + let xcm: Xcm<()> = alloc::vec![ + DescendOrigin(PalletInstance(T::InboundQueuePalletInstance::get()).into()), + UniversalOrigin(GlobalConsensus(T::EthereumNetwork::get())), + ReserveAssetDeposited(reward_asset.clone().into()), + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, + DepositAsset { + assets: AllCounted(1).into(), + beneficiary: deposit_location.clone() + }, + SetAppendix(Xcm(alloc::vec![ + RefundSurplus, + DepositAsset { + assets: AllCounted(1).into(), + beneficiary: deposit_location.clone() + }, + ])), + ] + .into(); + + // Remove the reward since it has been claimed. + RewardsMapping::::remove(account_id.clone()); + + let dest = Location::new(1, [Parachain(T::AssetHubParaId::get().into())]); + let (_xcm_hash, xcm_delivery_fee) = + send_xcm::(dest, xcm).map_err(Error::::from)?; + + match xcm_delivery_fee.get(0) { + Some(fee_asset) => + if let Fungible(amount) = fee_asset.fun { + let xcm_delivery_fee_balance: BalanceOf = + TryInto::>::try_into(amount) + .map_err(|_| Error::::InvalidAmount)?; + let treasury_account = T::TreasuryAccount::get(); + T::Token::transfer( + &account_id, + &treasury_account, + xcm_delivery_fee_balance, + Preservation::Preserve, + )?; + }, + None => (), + }; + + Self::deposit_event(Event::RewardClaimed { account_id, deposit_location, value }); + Ok(()) + } } #[pallet::event] @@ -433,6 +542,21 @@ pub mod pallet { /// Registration that was removed. registration: Registration, T::Reward>, }, + /// A relayer reward was deposited + RewardDeposited { + /// The relayer account to which the reward was deposited. + account_id: AccountIdOf, + /// The reward value. + value: BalanceOf, + }, + RewardClaimed { + /// The relayer account that claimed the reward. + account_id: AccountIdOf, + /// The location that received the reward on AH. + deposit_location: Location, + /// The claimed reward value. + value: BalanceOf, + }, } #[pallet::error] @@ -454,6 +578,38 @@ pub mod pallet { NotRegistered, /// Failed to `deregister` relayer, because lease is still active. RegistrationIsStillActive, + /// XCMP send failure + Send(SendError), + /// The relayer rewards balance is lower than the claimed amount. + InsufficientFunds, + InvalidAmount, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)] + pub enum SendError { + NotApplicable, + NotRoutable, + Transport, + DestinationUnsupported, + ExceedsMaxMessageSize, + MissingArgument, + Fees, + } + + impl, I: 'static> From for Error { + fn from(e: XcmpSendError) -> Self { + match e { + XcmpSendError::NotApplicable => Error::::Send(SendError::NotApplicable), + XcmpSendError::Unroutable => Error::::Send(SendError::NotRoutable), + XcmpSendError::Transport(_) => Error::::Send(SendError::Transport), + XcmpSendError::DestinationUnsupported => + Error::::Send(SendError::DestinationUnsupported), + XcmpSendError::ExceedsMaxMessageSize => + Error::::Send(SendError::ExceedsMaxMessageSize), + XcmpSendError::MissingArgument => Error::::Send(SendError::MissingArgument), + XcmpSendError::Fees => Error::::Send(SendError::Fees), + } + } } /// Map of the relayer => accumulated reward. @@ -484,6 +640,21 @@ pub mod pallet { Registration, T::Reward>, OptionQuery, >; + + #[pallet::storage] + pub type RewardsMapping, I: 'static = ()> = + StorageMap<_, Identity, AccountIdOf, BalanceOf, ValueQuery>; + + impl, I: 'static> RewardLedger, BalanceOf> for Pallet { + fn deposit(account_id: AccountIdOf, value: BalanceOf) -> DispatchResult { + RewardsMapping::::mutate(account_id.clone(), |current_value| { + *current_value = current_value.saturating_add(value); + }); + Self::deposit_event(Event::RewardDeposited { account_id, value }); + + Ok(()) + } + } } #[cfg(test)] @@ -491,11 +662,12 @@ mod tests { use super::*; use bp_messages::LaneIdType; use mock::{RuntimeEvent as TestEvent, *}; + use sp_core::H256; use crate::Event::{RewardPaid, RewardRegistered}; use bp_relayers::RewardsAccountOwner; use frame_support::{ - assert_noop, assert_ok, + assert_err, assert_noop, assert_ok, traits::fungible::{Inspect, Mutate}, }; use frame_system::{EventRecord, Pallet as System, Phase}; @@ -943,4 +1115,58 @@ mod tests { assert!(Pallet::::is_registration_active(®ISTER_RELAYER)); }); } + + const WETH: u64 = 1_000_000_000_000_000_000; + + #[test] + fn test_deposit() { + run_test(|| { + // Check a new deposit works + let relayer: u64 = 1; + let result = Pallet::::deposit(relayer, 2 * WETH); + assert_ok!(result); + assert_eq!(>::get(relayer), 2 * WETH); + + // Check accumulation works + let result2 = Pallet::::deposit(relayer, 3 * WETH); + assert_ok!(result2); + assert_eq!(>::get(relayer), 5 * WETH); + + // Check another relayer deposit works. + let another_relayer: u64 = 2; + let result3 = Pallet::::deposit(another_relayer, 1 * WETH); + assert_ok!(result3); + assert_eq!(>::get(another_relayer), 1 * WETH); + }); + } + + #[test] + fn test_claim() { + run_test(|| { + let relayer: u64 = 1; + let interior: InteriorLocation = [ + Parachain(1000), + Junction::AccountId32 { network: None, id: H256::random().into() }, + ] + .into(); + let claim_location = Location::new(1, interior); + + let result = Pallet::::claim( + RuntimeOrigin::signed(relayer), + claim_location.clone(), + ); + // No rewards yet + assert_err!(result, Error::::InsufficientFunds); + + // Deposit rewards + let result2 = Pallet::::deposit(relayer, 3 * WETH); + assert_ok!(result2); + + // Claim rewards + let result3 = + Pallet::::claim(RuntimeOrigin::signed(relayer), claim_location); + assert_ok!(result3); + assert_eq!(>::get(relayer), 0); + }); + } } diff --git a/bridges/modules/relayers/src/mock.rs b/bridges/modules/relayers/src/mock.rs index d186e968e648..541ab2f2ffdd 100644 --- a/bridges/modules/relayers/src/mock.rs +++ b/bridges/modules/relayers/src/mock.rs @@ -18,6 +18,7 @@ use crate as pallet_bridge_relayers; +use crate::XcmpSendError; use bp_header_chain::ChainWithGrandpa; use bp_messages::{ target_chain::{DispatchMessage, MessageDispatch}, @@ -34,12 +35,15 @@ use frame_support::{ traits::fungible::Mutate, weights::{ConstantMultiplier, IdentityFee, RuntimeDbWeight, Weight}, }; +use hex_literal::hex; use pallet_transaction_payment::Multiplier; -use sp_core::{ConstU64, ConstU8, H256}; +use sp_core::{ConstU64, ConstU8, H160, H256}; use sp_runtime::{ - traits::{BlakeTwo256, ConstU32}, + traits::{BlakeTwo256, ConstU128, ConstU32}, BuildStorage, FixedPointNumber, Perquintill, StateVersion, }; +use xcm::{latest::SendXcm, prelude::*}; +use xcm_executor::{traits::TransactAsset, AssetsInHolding}; /// Account identifier at `ThisChain`. pub type ThisChainAccountId = u64; @@ -277,6 +281,11 @@ impl pallet_bridge_messages::Config for TestRuntime { type BridgedHeaderChain = BridgeGrandpa; } +parameter_types! { + pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 }; + pub WethAddress: H160 = hex!("774667629726ec1FaBEbCEc0D9139bD1C8f72a23").into(); +} + impl pallet_bridge_relayers::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type Reward = ThisChainBalance; @@ -284,6 +293,16 @@ impl pallet_bridge_relayers::Config for TestRuntime { type StakeAndSlash = TestStakeAndSlash; type WeightInfo = (); type LaneId = TestLaneIdType; + + type Token = Balances; + type AssetHubParaId = ConstU32<1000>; + type EthereumNetwork = EthereumNetwork; + type WethAddress = WethAddress; + type XcmSender = MockXcmSender; + + type AssetTransactor = SuccessfulTransactor; + type AssetHubXCMFee = ConstU128<1_000_000_000_000u128>; + type InboundQueuePalletInstance = ConstU8<80>; } #[cfg(feature = "runtime-benchmarks")] @@ -371,6 +390,64 @@ impl MessageDispatch for DummyMessageDispatch { } } +// Mock XCM sender that always succeeds +pub struct MockXcmSender; + +impl SendXcm for MockXcmSender { + type Ticket = Xcm<()>; + + fn validate( + dest: &mut Option, + xcm: &mut Option>, + ) -> SendResult { + if let Some(location) = dest { + match location.unpack() { + (_, [Parachain(1001)]) => return Err(XcmpSendError::NotApplicable), + _ => Ok((xcm.clone().unwrap(), Assets::default())), + } + } else { + Ok((xcm.clone().unwrap(), Assets::default())) + } + } + + fn deliver(xcm: Self::Ticket) -> core::result::Result { + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + Ok(hash) + } +} + +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()) + } +} + /// Reward account params that we are using in tests. pub fn test_reward_account_param() -> RewardsAccountParams { RewardsAccountParams::new( diff --git a/bridges/modules/relayers/src/weights.rs b/bridges/modules/relayers/src/weights.rs index c2c065b0c0a2..642d26752e6a 100644 --- a/bridges/modules/relayers/src/weights.rs +++ b/bridges/modules/relayers/src/weights.rs @@ -55,6 +55,7 @@ pub trait WeightInfo { fn deregister() -> Weight; fn slash_and_deregister() -> Weight; fn register_relayer_reward() -> Weight; + fn claim() -> Weight; } /// Weights for `pallet_bridge_relayers` that are generated using one of the Bridge testnets. @@ -157,6 +158,16 @@ impl WeightInfo for BridgeWeight { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + + fn claim() -> Weight { + // Proof Size summary in bytes: + // Measured: `80` + // Estimated: `3517` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(44_000_000, 3517) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } } // For backwards compatibility and tests @@ -256,4 +267,14 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + + fn claim() -> Weight { + // Proof Size summary in bytes: + // Measured: `80` + // Estimated: `3517` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(44_000_000, 3517) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } } diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml index d212b18d2d54..d80c477c6fbd 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml +++ b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml @@ -44,6 +44,9 @@ snowbridge-pallet-inbound-queue-fixtures-v2 = { optional = true, workspace = tru frame-benchmarking = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } snowbridge-pallet-ethereum-client = { workspace = true, default-features = true } +pallet-bridge-relayers = { workspace = true, default-features = true } +bp-relayers = { workspace = true, default-features = true } +bp-messages = { workspace = true, default-features = true } hex-literal = { workspace = true, default-features = true } [features] 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..98575ac6113b 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -38,53 +38,43 @@ 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::{ + prelude::{send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm}, + VersionedXcm, MAX_XCM_DECODE_DEPTH, }; -use xcm_executor::traits::TransactAsset; +use frame_support::traits::fungible::{Inspect, Mutate}; 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, + rewards::RewardLedger, + BasicOperatingMode, }; -use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; +use snowbridge_router_primitives::inbound::v2::Message as MessageV2; pub use weights::WeightInfo; #[cfg(feature = "runtime-benchmarks")] use snowbridge_beacon_primitives::BeaconHeader; +pub use pallet::*; +pub type AccountIdOf = ::AccountId; type BalanceOf = <::Token as Inspect<::AccountId>>::Balance; -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 { use super::*; + use codec::DecodeLimit; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use sp_core::H256; #[pallet::pallet] pub struct Pallet(_); @@ -100,45 +90,23 @@ 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; #[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; + /// To keep track of relayer rewards. + type RewardLedger: RewardLedger>; } #[pallet::hooks] @@ -149,14 +117,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 }, @@ -184,8 +148,7 @@ pub mod pallet { Verification(VerificationError), /// XCMP send failure Send(SendError), - /// Message conversion error - ConvertMessage(ConvertMessageError), + InvalidFee, } #[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)] @@ -215,9 +178,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] @@ -244,63 +207,47 @@ 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(()) - } - })?; + // Verify the message has not been processed + ensure!(!>::contains_key(envelope.nonce), Error::::InvalidNonce); - // 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)?; - } - - // 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())?; + // Decode xcm + let versioned_xcm = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut message.xcm.as_ref(), + ) + .map_err(|_| Error::::InvalidPayload)?; + let xcm: Xcm<()> = versioned_xcm.try_into().map_err(|_| >::InvalidPayload)?; log::info!( target: LOG_TARGET, - "💫 xcm decoded as {:?} with fee {:?}", + "💫 xcm decoded as {:?}", xcm, - fee ); - // Burning fees for teleport - Self::burn_fees(channel.para_id, fee)?; + // Set nonce flag to true + >::try_mutate(envelope.nonce, |done| -> DispatchResult { + *done = true; + Ok(()) + })?; - // Attempt to send XCM to a dest parachain - let message_id = Self::send_xcm(xcm, channel.para_id)?; + let fee: BalanceOf = envelope.fee.try_into().map_err(|_| >::InvalidFee)?; + T::RewardLedger::deposit(who, fee)?; + // Fee should cover all of: + // 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 - Self::deposit_event(Event::MessageReceived { - channel_id: envelope.channel_id, - nonce: envelope.nonce, - message_id, - fee_burned: fee, - }); + // Attempt to forward XCM to AH + let dest = Location::new(1, [Parachain(1000)]); + let (message_id, _) = send_xcm::(dest, xcm).map_err(Error::::from)?; + + Self::deposit_event(Event::MessageReceived { nonce: envelope.nonce, message_id }); Ok(()) } @@ -318,61 +265,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..84445511d6c8 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -2,29 +2,30 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; -use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::IdentityFee}; +use bp_messages::{HashedLaneId, LaneIdType}; +use bp_relayers::{PayRewardFromAccount, PaymentProcedure, RewardsAccountParams}; +use frame_support::{derive_impl, parameter_types, traits::ConstU32, PalletId}; 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::{ConstU128, ConstU8, H160}; use sp_runtime::{ - traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, - BuildStorage, FixedU128, MultiSignature, + traits::{AccountIdConversion, IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, + BuildStorage, MultiSignature, }; use sp_std::{convert::From, default::Default}; use xcm::{latest::SendXcm, prelude::*}; -use xcm_executor::AssetsInHolding; +use xcm_executor::{traits::TransactAsset, AssetsInHolding}; use crate::{self as inbound_queue}; -type Block = frame_system::mocking::MockBlock; +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +pub type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test @@ -33,6 +34,7 @@ frame_support::construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, EthereumBeaconClient: snowbridge_pallet_ethereum_client::{Pallet, Call, Storage, Event}, InboundQueue: inbound_queue::{Pallet, Call, Storage, Event}, + BridgeRelayers: pallet_bridge_relayers::{Pallet, Call, Storage, Event}, } ); @@ -149,65 +151,8 @@ 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 { @@ -221,28 +166,59 @@ impl MaybeEquivalence for MockTokenIdConvert { 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; #[cfg(feature = "runtime-benchmarks")] type Helper = Test; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; - type MaxMessageSize = ConstU32<1024>; + type Token = Balances; + type RewardLedger = BridgeRelayers; +} + +parameter_types! { + pub WethAddress: H160 = hex!("774667629726ec1FaBEbCEc0D9139bD1C8f72a23").into(); + pub TreasuryAccount: AccountId = PalletId(*b"py/trsry").into_account_truncating(); +} + +pub type TestLaneIdType = HashedLaneId; + +pub struct TestPaymentProcedure; + +impl TestPaymentProcedure { + pub fn rewards_account(params: RewardsAccountParams) -> AccountId { + PayRewardFromAccount::<(), AccountId, TestLaneIdType>::rewards_account(params) + } +} + +impl PaymentProcedure for TestPaymentProcedure { + type Error = (); + type LaneId = TestLaneIdType; + + fn pay_reward( + relayer: &AccountId, + _lane_id: RewardsAccountParams, + _reward: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl pallet_bridge_relayers::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Reward = Balance; + type PaymentProcedure = TestPaymentProcedure; + type StakeAndSlash = (); + type WeightInfo = (); + type LaneId = TestLaneIdType; + type Token = Balances; + type AssetHubParaId = ConstU32<1000>; + type EthereumNetwork = EthereumNetwork; + type InboundQueuePalletInstance = ConstU8<80>; + type WethAddress = WethAddress; + type XcmSender = MockXcmSender; type AssetTransactor = SuccessfulTransactor; + type AssetHubXCMFee = ConstU128<15u128>; + type TreasuryAccount = TreasuryAccount; } pub fn last_events(n: usize) -> Vec { @@ -261,16 +237,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 +252,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 { @@ -356,5 +322,37 @@ pub fn mock_execution_proof() -> ExecutionProof { } } +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 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 41c38460aabf..a62a1b9d1a4c 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs @@ -3,11 +3,9 @@ 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}; @@ -17,7 +15,6 @@ use crate::mock::*; 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 +27,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: [ 255, 125, 48, 71, 174, 185, 100, 26, 159, 43, 108, 6, 116, 218, 55, 155, 223, 143, 141, 22, 124, 110, 241, 18, 122, 217, 130, 29, 139, 76, 97, 201, ], - 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 +45,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 +66,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 +87,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 +97,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()), @@ -151,10 +111,6 @@ fn test_submit_no_funds_to_reward_relayers_just_ignore() { 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(), @@ -209,10 +165,6 @@ fn test_submit_no_funds_to_reward_relayers_and_ed_preserved() { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); - // 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); - // Submit message successfully let message = Message { event_log: mock_event_log(), @@ -223,10 +175,6 @@ fn test_submit_no_funds_to_reward_relayers_and_ed_preserved() { }; 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()); - // Submit another message with nonce set as 2 let mut event_log = mock_event_log(); event_log.data[31] = 2; @@ -238,8 +186,5 @@ fn test_submit_no_funds_to_reward_relayers_and_ed_preserved() { }, }; 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()); }); } diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index 5814886fe355..6a2fec5587a8 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -52,11 +52,12 @@ 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, + send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmHash, }; use xcm_executor::traits::TransactAsset; use snowbridge_core::{ + fees::burn_fees, inbound::{Message, VerificationError, Verifier}, sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters, StaticLookup, @@ -64,7 +65,7 @@ use snowbridge_core::{ use snowbridge_router_primitives::inbound::v1::{ ConvertMessage, ConvertMessageError, VersionedMessage, }; -use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; +use sp_runtime::Saturating; pub use weights::WeightInfo; @@ -290,7 +291,8 @@ pub mod pallet { ); // Burning fees for teleport - Self::burn_fees(channel.para_id, fee)?; + let parachain_location = Location::new(1, [Parachain(channel.para_id.into())]); + burn_fees::>(parachain_location, fee)?; // Attempt to send XCM to a dest parachain let message_id = Self::send_xcm(xcm, channel.para_id)?; @@ -342,30 +344,6 @@ pub mod pallet { .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 diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml b/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml index 8a6afa290189..c7ac69e5497a 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml +++ b/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml @@ -40,6 +40,12 @@ snowbridge-merkle-tree = { workspace = true } [dev-dependencies] pallet-message-queue = { workspace = true } sp-keyring = { workspace = true, default-features = true } +pallet-bridge-relayers = { workspace = true, default-features = true } +bp-relayers = { workspace = true, default-features = true } +bp-messages = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +xcm = { workspace = true } +xcm-executor = { workspace = true } [features] default = ["std"] diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs index 41427c0f9d28..ad703fc7d825 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs @@ -109,13 +109,18 @@ use codec::Decode; use envelope::Envelope; use frame_support::{ storage::StorageStreamIter, - traits::{tokens::Balance, EnqueueMessage, Get, ProcessMessageError}, + traits::{ + fungible::{Inspect, Mutate}, + tokens::Balance, + EnqueueMessage, Get, ProcessMessageError, + }, weights::{Weight, WeightToFee}, }; use snowbridge_core::{ inbound::Message as DeliveryMessage, outbound::v2::{CommandWrapper, Fee, GasMeter, InboundMessage, Message}, - BasicOperatingMode, RewardLedger, + rewards::RewardLedger, + BasicOperatingMode, }; use snowbridge_merkle_tree::merkle_root; use sp_core::H256; @@ -133,6 +138,8 @@ use alloy_sol_types::SolValue; use sp_runtime::traits::TrailingZeroInput; +type BalanceOf = + <::Token as Inspect<::AccountId>>::Balance; #[frame_support::pallet] pub mod pallet { use super::*; @@ -149,6 +156,8 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type Hashing: Hash; + /// Message relayers are rewarded with this asset + type Token: Mutate + Inspect; type MessageQueue: EnqueueMessage; @@ -177,9 +186,8 @@ pub mod pallet { /// Address of the Gateway contract #[pallet::constant] type GatewayAddress: Get; - - /// Reward leger - type RewardLedger: RewardLedger<::AccountId, Self::Balance>; + /// To keep track of relayer rewards. + type RewardLedger: RewardLedger>; } #[pallet::event] @@ -227,6 +235,9 @@ pub mod pallet { InvalidGateway, /// No pending nonce PendingNonceNotExist, + /// Invalid Relayer Reward Account + InvalidRewardAccount, + InvalidFee, } /// Messages to be committed in the current block. This storage value is killed in @@ -321,7 +332,8 @@ pub mod pallet { let account = T::AccountId::decode(&mut &envelope.reward_address[..]).unwrap_or( T::AccountId::decode(&mut TrailingZeroInput::zeroes()).expect("zero address"), ); - T::RewardLedger::deposit(account, order.fee.into())?; + let fee: BalanceOf = order.fee.try_into().map_err(|_| >::InvalidFee)?; + T::RewardLedger::deposit(account, fee)?; >::remove(nonce); diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs index 7a5c5f9b401c..3d4cff6e1024 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs @@ -9,6 +9,10 @@ use frame_support::{ BoundedVec, }; +use bp_messages::HashedLaneId; +use bp_relayers::{PayRewardFromAccount, PaymentProcedure, RewardsAccountParams}; +use codec::Encode; +use frame_support::PalletId; use hex_literal::hex; use snowbridge_core::{ gwei, @@ -18,12 +22,17 @@ use snowbridge_core::{ pricing::{PricingParameters, Rewards}, ParaId, }; -use sp_core::{ConstU32, H160, H256}; +use sp_core::{ConstU128, ConstU32, ConstU8, H160, H256}; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup, Keccak256}, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, Keccak256}, AccountId32, BuildStorage, FixedU128, }; use sp_std::marker::PhantomData; +use xcm::{ + latest::SendXcm, + prelude::{SendError as XcmpSendError, *}, +}; +use xcm_executor::{traits::TransactAsset, AssetsInHolding}; type Block = frame_system::mocking::MockBlock; type AccountId = AccountId32; @@ -32,8 +41,10 @@ frame_support::construct_runtime!( pub enum Test { System: frame_system::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event}, OutboundQueue: crate::{Pallet, Storage, Event}, + BridgeRelayers: pallet_bridge_relayers::{Pallet, Call, Storage, Event}, } ); @@ -47,6 +58,7 @@ impl frame_system::Config for Test { type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; + type AccountData = pallet_balances::AccountData; type RuntimeEvent = RuntimeEvent; type PalletInfo = PalletInfo; type Nonce = u64; @@ -72,6 +84,17 @@ impl pallet_message_queue::Config for Test { type QueuePausedQuery = (); } +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = Balance; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; +} + // Mock verifier pub struct MockVerifier; @@ -94,6 +117,8 @@ parameter_types! { pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); } +type Balance = u128; + pub const DOT: u128 = 10_000_000_000; impl crate::Config for Test { type RuntimeEvent = RuntimeEvent; @@ -107,7 +132,84 @@ impl crate::Config for Test { type Balance = u128; type WeightToFee = IdentityFee; type WeightInfo = (); - type RewardLedger = (); + type Token = Balances; + type RewardLedger = BridgeRelayers; +} + +parameter_types! { + pub WethAddress: H160 = hex!("774667629726ec1FaBEbCEc0D9139bD1C8f72a23").into(); + pub TreasuryAccount: AccountId = PalletId(*b"py/trsry").into_account_truncating(); +} + +pub type TestLaneIdType = HashedLaneId; + +pub struct TestPaymentProcedure; + +impl TestPaymentProcedure { + pub fn rewards_account(params: RewardsAccountParams) -> AccountId { + PayRewardFromAccount::<(), AccountId, TestLaneIdType>::rewards_account(params) + } +} + +impl PaymentProcedure for TestPaymentProcedure { + type Error = (); + type LaneId = TestLaneIdType; + + fn pay_reward( + _relayer: &AccountId, + _lane_id: RewardsAccountParams, + _reward: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +parameter_types! { + pub const EthereumNetwork: xcm::v3::NetworkId = xcm::v3::NetworkId::Ethereum { chain_id: 11155111 }; +} + +impl pallet_bridge_relayers::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Reward = Balance; + type PaymentProcedure = TestPaymentProcedure; + type StakeAndSlash = (); + type WeightInfo = (); + type LaneId = TestLaneIdType; + type Token = Balances; + type AssetHubParaId = ConstU32<1000>; + type EthereumNetwork = EthereumNetwork; + type InboundQueuePalletInstance = ConstU8<80>; + type WethAddress = WethAddress; + type XcmSender = MockXcmSender; + + type AssetTransactor = SuccessfulTransactor; + type AssetHubXCMFee = ConstU128<15u128>; + type TreasuryAccount = TreasuryAccount; +} + +// Mock XCM sender that always succeeds +pub struct MockXcmSender; + +impl SendXcm for MockXcmSender { + type Ticket = Xcm<()>; + + fn validate( + dest: &mut Option, + xcm: &mut Option>, + ) -> SendResult { + if let Some(location) = dest { + match location.unpack() { + _ => Ok((xcm.clone().unwrap(), Assets::default())), + } + } else { + Ok((xcm.clone().unwrap(), Assets::default())) + } + } + + fn deliver(xcm: Self::Ticket) -> core::result::Result { + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + Ok(hash) + } } fn setup() { @@ -193,3 +295,35 @@ pub fn mock_message(sibling_para_id: u32) -> Message { .unwrap(), } } + +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()) + } +} diff --git a/bridges/snowbridge/primitives/core/Cargo.toml b/bridges/snowbridge/primitives/core/Cargo.toml index 0e696f0d2256..a7dbc4b47306 100644 --- a/bridges/snowbridge/primitives/core/Cargo.toml +++ b/bridges/snowbridge/primitives/core/Cargo.toml @@ -16,6 +16,7 @@ serde = { optional = true, features = ["alloc", "derive"], workspace = true } codec = { workspace = true } scale-info = { features = ["derive"], workspace = true } hex-literal = { workspace = true, default-features = true } +log = { workspace = true } polkadot-parachain-primitives = { workspace = true } xcm = { workspace = true } @@ -34,10 +35,10 @@ snowbridge-beacon-primitives = { workspace = true } ethabi = { workspace = true } alloy-primitives = { features = ["rlp"], workspace = true } alloy-sol-types = { workspace = true } +xcm-executor = { workspace = true, default-features = false } [dev-dependencies] hex = { workspace = true, default-features = true } -xcm-executor = { workspace = true, default-features = true } [features] default = ["std"] @@ -48,6 +49,7 @@ std = [ "ethabi/std", "frame-support/std", "frame-system/std", + "log/std", "polkadot-parachain-primitives/std", "scale-info/std", "serde/std", @@ -59,6 +61,7 @@ std = [ "sp-std/std", "xcm-builder/std", "xcm/std", + "xcm-executor/std", ] serde = ["dep:serde", "scale-info/serde"] runtime-benchmarks = [ diff --git a/bridges/snowbridge/primitives/core/src/fees.rs b/bridges/snowbridge/primitives/core/src/fees.rs new file mode 100644 index 000000000000..44c800a2d820 --- /dev/null +++ b/bridges/snowbridge/primitives/core/src/fees.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use log; +use sp_runtime::{DispatchResult, SaturatedConversion, Saturating, TokenError}; +use xcm::opaque::lts::{Location, XcmContext}; +use xcm_executor::traits::TransactAsset; +const LOG_TARGET: &str = "xcm_fees"; + +/// Burns the fees embedded in the XCM for teleports. +pub fn burn_fees(dest: Location, fee: Balance) -> DispatchResult +where + AssetTransactor: TransactAsset, + Balance: Saturating + TryInto + Copy, +{ + let dummy_context = XcmContext { origin: None, message_id: Default::default(), topic: None }; + let fees = (Location::parent(), fee.saturated_into::()).into(); + + // Check if the asset can be checked out + 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 + })?; + + // Check out the asset + AssetTransactor::check_out(&dest, &fees, &dummy_context); + + // Withdraw the asset and handle potential errors + AssetTransactor::withdraw_asset(&fees, &dest, None).map_err(|error| { + log::error!( + target: LOG_TARGET, + "XCM asset withdraw failed with error {:?}", + error + ); + TokenError::FundsUnavailable + })?; + + Ok(()) +} diff --git a/bridges/snowbridge/primitives/core/src/lib.rs b/bridges/snowbridge/primitives/core/src/lib.rs index 88ac8124a15b..de4dd1a67c6d 100644 --- a/bridges/snowbridge/primitives/core/src/lib.rs +++ b/bridges/snowbridge/primitives/core/src/lib.rs @@ -8,12 +8,13 @@ #[cfg(test)] mod tests; +pub mod fees; pub mod inbound; pub mod location; pub mod operating_mode; pub mod outbound; pub mod pricing; -pub mod reward; +pub mod rewards; pub mod ringbuffer; pub use location::{AgentId, AgentIdOf, TokenId, TokenIdOf}; @@ -38,7 +39,7 @@ pub use operating_mode::BasicOperatingMode; pub use pricing::{PricingParameters, Rewards}; -pub use reward::RewardLedger; +pub use rewards::RewardLedger; pub fn sibling_sovereign_account(para_id: ParaId) -> T::AccountId where diff --git a/bridges/snowbridge/primitives/core/src/location.rs b/bridges/snowbridge/primitives/core/src/location.rs index aad1c9ece05c..dc59e5a58fc6 100644 --- a/bridges/snowbridge/primitives/core/src/location.rs +++ b/bridges/snowbridge/primitives/core/src/location.rs @@ -7,7 +7,9 @@ pub use polkadot_parachain_primitives::primitives::{ Id as ParaId, IsSystem, Sibling as SiblingParaId, }; +use sp_core::H160; pub use sp_core::U256; +use xcm::opaque::lts::NetworkId; use codec::Encode; use sp_core::H256; @@ -203,3 +205,9 @@ mod tests { } } } + +// Convert ERC20 token address to a location that can be understood by Assets Hub. +pub fn convert_token_address(network: NetworkId, token: H160) -> Location { + Location::new(2, [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }]) +} + diff --git a/bridges/snowbridge/primitives/core/src/rewards.rs b/bridges/snowbridge/primitives/core/src/rewards.rs new file mode 100644 index 000000000000..80e0d9b492d8 --- /dev/null +++ b/bridges/snowbridge/primitives/core/src/rewards.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork + +use frame_support::pallet_prelude::DispatchResult; + +pub trait RewardLedger { + // Deposit reward which can later be claimed by `account` + fn deposit(account: AccountId, value: Balance) -> DispatchResult; +} + +impl RewardLedger for () { + fn deposit(_: AccountId, _: Balance) -> DispatchResult { + Ok(()) + } +} diff --git a/bridges/snowbridge/primitives/router/src/inbound/v2.rs b/bridges/snowbridge/primitives/router/src/inbound/v2.rs index 05c054080620..91aef5c111fb 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/v2.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/v2.rs @@ -2,447 +2,50 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Ethereum to XCM messages -use crate::inbound::{CallIndex, GlobalConsensusEthereumConvertsFor}; use codec::{Decode, Encode}; use core::marker::PhantomData; -use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError}; -use scale_info::TypeInfo; -use snowbridge_core::TokenId; -use sp_core::{Get, RuntimeDebug, H160, H256}; -use sp_runtime::{traits::MaybeEquivalence, MultiAddress}; +use sp_core::{RuntimeDebug, H160}; +use sp_io::hashing::blake2_256; use sp_std::prelude::*; -use xcm::prelude::{Junction::AccountKey20, *}; +use xcm::prelude::*; +use xcm_executor::traits::ConvertLocation; -const MINIMUM_DEPOSIT: u128 = 1; /// 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 +/// For V2, the ethereum side sends messages which are transcoded into XCM. 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, +pub struct Message { + /// The origin address + pub origin: H160, /// The command originating from the Gateway contract - pub command: Command, + pub xcm: Vec, } -#[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, - }, -} - -/// 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, - }, - /// 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, - }, -} - -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, -} - -/// 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>; -} - -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > ConvertMessage - for MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > +pub struct GlobalConsensusEthereumConvertsFor(PhantomData); +impl ConvertLocation for GlobalConsensusEthereumConvertsFor where - CreateAssetCall: Get, - CreateAssetDeposit: Get, - InboundQueuePalletInstance: Get, - Balance: BalanceT + From, - AccountId: Into<[u8; 32]>, - ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, + AccountId: From<[u8; 32]> + Clone, { - 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, - ), + fn convert_location(location: &Location) -> Option { + match location.unpack() { + (_, [GlobalConsensus(Ethereum { chain_id })]) => + Some(Self::from_chain_id(chain_id).into()), + _ => None, } } } -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > - MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > -where - CreateAssetCall: Get, - CreateAssetDeposit: 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, - require_weight_at_most: Weight::from_parts(400_000_000, 8_000), - 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(); - - (xcm, total_amount.into()) - } - - 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 (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 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 mut instructions = vec![ - ReceiveTeleportedAsset(total_fee_asset.into()), - BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, - DescendOrigin(PalletInstance(inbound_queue_pallet_index).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 }, - ]); - }, - } - - // 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)?; - - let asset: Asset = (reanchored_asset_loc, amount).into(); - - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); - - 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())) +impl GlobalConsensusEthereumConvertsFor { + pub fn from_chain_id(chain_id: &u64) -> [u8; 32] { + (b"ethereum-chain", chain_id).using_encoded(blake2_256) } } diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs index e7a28ebf4a46..075fd84492e9 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs @@ -47,6 +47,8 @@ decl_test_parachains! { PolkadotXcm: bridge_hub_westend_runtime::PolkadotXcm, Balances: bridge_hub_westend_runtime::Balances, EthereumSystem: bridge_hub_westend_runtime::EthereumSystem, + EthereumInboundQueue: bridge_hub_westend_runtime::EthereumInboundQueue, + BridgeRelayers: bridge_hub_westend_runtime::BridgeRelayers, } }, } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml index d263fc9ac46d..27e238082e3f 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml @@ -51,3 +51,4 @@ snowbridge-pallet-outbound-queue = { workspace = true } snowbridge-pallet-inbound-queue = { workspace = true } snowbridge-pallet-inbound-queue-fixtures = { workspace = true } snowbridge-pallet-outbound-queue-v2 = { workspace = true } +pallet-bridge-relayers = { workspace = true } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs index 71c6494db1ea..3bcb52335e0a 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs @@ -254,7 +254,6 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { }); BridgeHubWestend::execute_with(|| { - use bridge_hub_westend_runtime::xcm_config::TreasuryAccount; type RuntimeEvent = ::RuntimeEvent; // Check that the transfer token back to Ethereum message was queue in the Ethereum // Outbound Queue diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs index a3c315f5555f..97b692b85844 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs @@ -13,25 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::imports::*; -use asset_hub_westend_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; use bridge_hub_westend_runtime::EthereumInboundQueue; -use codec::{Decode, Encode}; -use emulated_integration_tests_common::RESERVABLE_ASSET_ID; -use frame_support::pallet_prelude::TypeInfo; use hex_literal::hex; -use rococo_westend_system_emulated_network::asset_hub_westend_emulated_chain::genesis::AssetHubWestendAssetOwner; -use snowbridge_core::{outbound::OperatingMode, AssetMetadata, TokenIdOf}; -use snowbridge_router_primitives::inbound::{ - v1::{Command, Destination, MessageV1, VersionedMessage}, - GlobalConsensusEthereumConvertsFor, +use snowbridge_core::rewards::RewardLedger; +use snowbridge_router_primitives::inbound::v1::{ + Command, Destination, MessageV1, VersionedMessage, }; -use sp_core::H256; use testnet_parachains_constants::westend::snowbridge::EthereumNetwork; -use xcm_executor::traits::ConvertLocation; const INITIAL_FUND: u128 = 5_000_000_000_000; pub const CHAIN_ID: u64 = 11155111; pub const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); +pub const ETH: u128 = 1_000_000_000_000_000_000; const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); const XCM_FEE: u128 = 100_000_000_000; const TOKEN_AMOUNT: u128 = 100_000_000_000; @@ -146,7 +139,6 @@ fn send_weth_from_asset_hub_to_ethereum_by_executing_raw_xcm() { }); BridgeHubWestend::execute_with(|| { - use bridge_hub_westend_runtime::xcm_config::TreasuryAccount; type RuntimeEvent = ::RuntimeEvent; // Check that the transfer token back to Ethereum message was queue in the Ethereum // Outbound Queue @@ -166,3 +158,78 @@ fn send_weth_from_asset_hub_to_ethereum_by_executing_raw_xcm() { ); }); } + +#[test] +fn claim_rewards_works() { + let weth: [u8; 20] = hex!("fff9976782d46cc05630d1f6ebab18b2324d6b14"); + let assethub_location = BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()); + let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(assethub_location); + let weth_asset_location: Location = + (Parent, Parent, EthereumNetwork::get(), AccountKey20 { network: None, key: weth }).into(); + + let relayer = BridgeHubWestendSender::get(); + + BridgeHubWestend::fund_accounts(vec![ + (assethub_sovereign.clone(), INITIAL_FUND), + (relayer.clone(), INITIAL_FUND), + ]); + + AssetHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + assethub_sovereign.clone().into(), + true, + 1, + )); + + assert!(::ForeignAssets::asset_exists( + weth_asset_location.clone().try_into().unwrap(), + )); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let reward_address = AssetHubWestendReceiver::get(); + type BridgeRelayers = ::BridgeRelayers; + assert_ok!(BridgeRelayers::deposit(relayer.clone().into(), 2 * ETH)); + + // Check that the message was sent + assert_expected_events!( + BridgeHubWestend, + vec![ + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardDeposited { .. }) => {}, + ] + ); + + let relayer_location = Location::new( + 1, + [Parachain(1000), Junction::AccountId32 { id: reward_address.into(), network: None }], + ); + let result = + BridgeRelayers::claim(RuntimeOrigin::signed(relayer.clone()), relayer_location.clone()); + assert_ok!(result); + + let events = BridgeHubWestend::events(); + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardClaimed { account_id, deposit_location, value, }) + if *account_id == relayer && *deposit_location == relayer_location && *value > 1 *ETH, + )), + "RewardClaimed event with correct fields." + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + }) +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs index 5dca45d326b8..34d78d082754 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs @@ -22,8 +22,14 @@ //! GRANDPA tracking pallet only needs to be aware of one chain. use super::{weights, AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent}; +use crate::{xcm_config, TreasuryAccount, XcmRouter}; use bp_parachains::SingleParaStoredHeaderDataBuilder; use frame_support::{parameter_types, traits::ConstU32}; +use sp_core::H160; +use sp_runtime::traits::{ConstU128, ConstU8}; +use testnet_parachains_constants::rococo::snowbridge::{ + EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, +}; parameter_types! { pub const RelayChainHeadersToKeep: u32 = 1024; @@ -83,6 +89,18 @@ impl pallet_bridge_relayers::Config fo >; type WeightInfo = weights::pallet_bridge_relayers::WeightInfo; type LaneId = bp_messages::LegacyLaneId; + type AssetHubParaId = ConstU32; + type EthereumNetwork = EthereumNetwork; + type WethAddress = WethAddress; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = DoNothingRouter; + type Token = Balances; + type AssetTransactor = ::AssetTransactor; + type InboundQueuePalletInstance = ConstU8; + type AssetHubXCMFee = ConstU128<15>; + type TreasuryAccount = TreasuryAccount; } /// Allows collect and claim rewards for relayers @@ -105,8 +123,26 @@ impl pallet_bridge_relayers::Config for >; type WeightInfo = weights::pallet_bridge_relayers::WeightInfo; type LaneId = bp_messages::HashedLaneId; + type AssetHubParaId = ConstU32; + type EthereumNetwork = EthereumNetwork; + type WethAddress = WethAddress; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = DoNothingRouter; + type Token = Balances; + type AssetTransactor = ::AssetTransactor; + type InboundQueuePalletInstance = ConstU8; + type AssetHubXCMFee = ConstU128<15>; + type TreasuryAccount = TreasuryAccount; } +parameter_types! { + pub WethAddress: H160 = H160(hex_literal::hex!("fff9976782d46cc05630d1f6ebab18b2324d6b14")); +} + +pub const ASSET_HUB_ID: u32 = rococo_runtime_constants::system_parachain::ASSET_HUB_ID; + /// Add GRANDPA bridge pallet to track Rococo Bulletin chain. pub type BridgeGrandpaRococoBulletinInstance = pallet_bridge_grandpa::Instance4; impl pallet_bridge_grandpa::Config for Runtime { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_bridge_relayers.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_bridge_relayers.rs index b7318361c7d9..f951797537c7 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_bridge_relayers.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_bridge_relayers.rs @@ -120,4 +120,13 @@ impl pallet_bridge_relayers::WeightInfo for WeightInfo< .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + fn claim() -> Weight { + // Proof Size summary in bytes: + // Measured: `80` + // Estimated: `3517` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(44_000_000, 3517) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml index 7cf210f51cac..a7ccc01f417e 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml @@ -118,7 +118,6 @@ snowbridge-pallet-outbound-queue-v2 = { workspace = true } snowbridge-outbound-queue-runtime-api-v2 = { workspace = true } snowbridge-merkle-tree = { workspace = true } - [dev-dependencies] bridge-hub-test-utils = { workspace = true, default-features = true } bridge-runtime-common = { features = ["integrity-test"], workspace = true, default-features = true } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_common_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_common_config.rs index 0872d0498f85..7a250156d258 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_common_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_common_config.rs @@ -22,8 +22,14 @@ //! GRANDPA tracking pallet only needs to be aware of one chain. use super::{weights, AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent}; +use crate::{xcm_config, xcm_config::TreasuryAccount, XcmRouter}; use bp_messages::LegacyLaneId; use frame_support::parameter_types; +use sp_core::H160; +use sp_runtime::traits::{ConstU128, ConstU32, ConstU8}; +use testnet_parachains_constants::westend::snowbridge::{ + EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, +}; parameter_types! { pub storage RequiredStakeForStakeAndSlash: Balance = 1_000_000; @@ -33,6 +39,12 @@ parameter_types! { pub storage DeliveryRewardInBalance: u64 = 1_000_000; } +parameter_types! { + pub WethAddress: H160 = H160(hex_literal::hex!("fff9976782d46cc05630d1f6ebab18b2324d6b14")); +} + +pub const ASSET_HUB_ID: u32 = westend_runtime_constants::system_parachain::ASSET_HUB_ID; + /// Allows collect and claim rewards for relayers pub type RelayersForLegacyLaneIdsMessagesInstance = (); impl pallet_bridge_relayers::Config for Runtime { @@ -53,4 +65,19 @@ impl pallet_bridge_relayers::Config fo >; type WeightInfo = weights::pallet_bridge_relayers::WeightInfo; type LaneId = LegacyLaneId; + + type AssetHubParaId = ConstU32; + type EthereumNetwork = EthereumNetwork; + type WethAddress = WethAddress; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = DoNothingRouter; + type Token = Balances; + type AssetTransactor = ::AssetTransactor; + type InboundQueuePalletInstance = ConstU8; + /// Execution cost on AH in Weth. Cost is approximately 0.000000000000000008, added a slightly + /// buffer. + type AssetHubXCMFee = ConstU128<15>; + type TreasuryAccount = TreasuryAccount; } 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 eb046b7edaa1..cf9384c62a84 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,17 +19,18 @@ use crate::XcmRouter; use crate::{ xcm_config, xcm_config::{TreasuryAccount, UniversalLocation}, - Balances, EthereumInboundQueue, EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, - RuntimeEvent, TransactionByteFee, + Balances, BridgeRelayers, EthereumInboundQueue, EthereumOutboundQueue, EthereumSystem, + MessageQueue, Runtime, RuntimeEvent, TransactionByteFee, }; 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}, + inbound::v1::MessageToXcm, outbound::{v1::EthereumBlobExporter, v2::EthereumBlobExporter as EthereumBlobExporterV2}, }; use sp_core::H160; +use sp_runtime::traits::ConstU8; use testnet_parachains_constants::westend::{ currency::*, fee::WeightToFee, @@ -42,7 +43,7 @@ use benchmark_helpers::DoNothingRouter; use frame_support::{parameter_types, weights::ConstantMultiplier}; use pallet_xcm::EnsureXcm; use sp_runtime::{ - traits::{ConstU32, ConstU8, Keccak256}, + traits::{ConstU32, Keccak256}, FixedU128, }; use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, Parachain}; @@ -116,31 +117,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 RewardLedger = BridgeRelayers; + type Token = Balances; type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; - type PricingParameters = EthereumSystem; - type AssetTransactor = ::AssetTransactor; } impl snowbridge_pallet_outbound_queue::Config for Runtime { @@ -169,8 +155,9 @@ impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { type WeightToFee = WeightToFee; type Verifier = snowbridge_pallet_ethereum_client::Pallet; type GatewayAddress = EthereumGatewayAddress; + type RewardLedger = BridgeRelayers; + type Token = Balances; type WeightInfo = crate::weights::snowbridge_pallet_outbound_queue_v2::WeightInfo; - type RewardLedger = (); } #[cfg(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test))] diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_bridge_relayers.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_bridge_relayers.rs index 74be73df1403..98ebb57f2273 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_bridge_relayers.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/pallet_bridge_relayers.rs @@ -120,4 +120,13 @@ impl pallet_bridge_relayers::WeightInfo for WeightInfo< .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + fn claim() -> Weight { + // Proof Size summary in bytes: + // Measured: `80` + // Estimated: `3517` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(44_000_000, 3517) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/snowbridge.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/snowbridge.rs index c5f3871c0790..605a557b8c58 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/snowbridge.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/snowbridge.rs @@ -123,6 +123,8 @@ fn max_message_queue_service_weight_is_more_than_beacon_extrinsic_weights() { #[test] fn ethereum_client_consensus_extrinsics_work() { + println!("RuntimeBlockWeights: {:?}", bridge_hub_westend_runtime::RuntimeBlockWeights::get()); + snowbridge_runtime_test_common::ethereum_extrinsic( collator_session_keys(), BRIDGE_HUB_WESTEND_PARACHAIN_ID,