diff --git a/Cargo.lock b/Cargo.lock index 70a8e72fe36fa..db8aa749d1088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,6 +886,7 @@ dependencies = [ "primitive-types", "rococo-runtime-constants", "scale-info", + "snowbridge-pallet-xcm-helper", "snowbridge-router-primitives", "sp-api", "sp-block-builder", @@ -19615,6 +19616,35 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "snowbridge-pallet-xcm-helper" +version = "0.2.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "hex-literal", + "log", + "pallet-assets", + "pallet-balances", + "pallet-message-queue", + "parity-scale-codec", + "polkadot-parachain-primitives", + "polkadot-primitives", + "polkadot-runtime-parachains", + "scale-info", + "snowbridge-core", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std 14.0.0", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "snowbridge-router-primitives" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index ba0526b21c369..a9258d62f742b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "bridges/snowbridge/pallets/outbound-queue/runtime-api", "bridges/snowbridge/pallets/system", "bridges/snowbridge/pallets/system/runtime-api", + "bridges/snowbridge/pallets/xcm-helper", "bridges/snowbridge/primitives/beacon", "bridges/snowbridge/primitives/core", "bridges/snowbridge/primitives/ethereum", @@ -1197,6 +1198,7 @@ snowbridge-pallet-inbound-queue = { path = "bridges/snowbridge/pallets/inbound-q snowbridge-pallet-inbound-queue-fixtures = { path = "bridges/snowbridge/pallets/inbound-queue/fixtures", default-features = false } snowbridge-pallet-outbound-queue = { path = "bridges/snowbridge/pallets/outbound-queue", default-features = false } snowbridge-pallet-system = { path = "bridges/snowbridge/pallets/system", default-features = false } +snowbridge-pallet-xcm-helper = { path = "bridges/snowbridge/pallets/xcm-helper", default-features = false } snowbridge-router-primitives = { path = "bridges/snowbridge/primitives/router", default-features = false } snowbridge-runtime-common = { path = "bridges/snowbridge/runtime/runtime-common", default-features = false } snowbridge-runtime-test-common = { path = "bridges/snowbridge/runtime/test-common", default-features = false } diff --git a/bridges/snowbridge/pallets/xcm-helper/Cargo.toml b/bridges/snowbridge/pallets/xcm-helper/Cargo.toml new file mode 100644 index 0000000000000..aa4f1b9df8a2a --- /dev/null +++ b/bridges/snowbridge/pallets/xcm-helper/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = "snowbridge-pallet-xcm-helper" +description = "Snowbridge Xcm Helper Pallet" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.9.0", default-features = false, features = ["derive"] } +frame-benchmarking = { path = "../../../../substrate/frame/benchmarking", default-features = false, optional = true } +frame-support = { path = "../../../../substrate/frame/support", default-features = false } +frame-system = { path = "../../../../substrate/frame/system", default-features = false } +log = { workspace = true } +sp-core = { path = "../../../../substrate/primitives/core", default-features = false } +sp-std = { path = "../../../../substrate/primitives/std", default-features = false } +sp-io = { path = "../../../../substrate/primitives/io", default-features = false } +sp-runtime = { path = "../../../../substrate/primitives/runtime", default-features = false } +xcm = { package = "staging-xcm", path = "../../../../polkadot/xcm", default-features = false } +xcm-executor = { package = "staging-xcm-executor", path = "../../../../polkadot/xcm/xcm-executor", default-features = false } +snowbridge-core = { path = "../../primitives/core", default-features = false } + +[dev-dependencies] +hex = "0.4.1" +hex-literal = { version = "0.4.1" } +pallet-balances = { path = "../../../../substrate/frame/balances" } +sp-keyring = { path = "../../../../substrate/primitives/keyring" } +polkadot-primitives = { path = "../../../../polkadot/primitives" } +pallet-message-queue = { path = "../../../../substrate/frame/message-queue" } +xcm-builder = { package = "staging-xcm-builder", path = "../../../../polkadot/xcm/xcm-builder" } +pallet-assets = { path = "../../../../substrate/frame/assets" } +polkadot-runtime-parachains = { path = "../../../../polkadot/runtime/parachains" } +polkadot-parachain-primitives = { path = "../../../../polkadot/parachain" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "snowbridge-core/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-executor/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "polkadot-parachain-primitives/runtime-benchmarks", + "polkadot-primitives/runtime-benchmarks", + "polkadot-runtime-parachains/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", + "pallet-message-queue/try-runtime", + "polkadot-runtime-parachains/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/bridges/snowbridge/pallets/xcm-helper/src/lib.rs b/bridges/snowbridge/pallets/xcm-helper/src/lib.rs new file mode 100644 index 0000000000000..a28a023784700 --- /dev/null +++ b/bridges/snowbridge/pallets/xcm-helper/src/lib.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + }; + use frame_system::{pallet_prelude::*, unique}; + use sp_core::H160; + use sp_runtime::traits::Dispatchable; + use sp_std::{boxed::Box, vec}; + use xcm::prelude::*; + use xcm_executor::traits::{TransferType, WeightBounds, XcmAssetTransfers}; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The ExecuteXcmOrigin type. + type ExecuteXcmOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = Location, + >; + + /// The XcmRouter type. + type XcmRouter: SendXcm; + + /// The XcmExecutor type. + type XcmExecutor: ExecuteXcm<::RuntimeCall> + XcmAssetTransfers; + + /// The runtime `Origin` type. + type RuntimeOrigin: From + From<::RuntimeOrigin>; + + /// The runtime `Call` type. + type RuntimeCall: Parameter + + GetDispatchInfo + + Dispatchable< + RuntimeOrigin = ::RuntimeOrigin, + PostInfo = PostDispatchInfo, + >; + + /// Means of measuring the weight consumed by an XCM message locally. + type Weigher: WeightBounds<::RuntimeCall>; + + /// Universal location of this runtime. + type UniversalLocation: Get; + + /// Ethereum's location of this runtime. + type Destination: Get; + + /// DeliveryFee for the execution cost on BH + type DeliveryFee: Get; + + /// The location of BH + type Forwarder: Get; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Fees were paid from a location for an operation (often for using `SendXcm`). + FeesPaid { paying: Location, fees: Assets }, + /// Execution of an XCM message was attempted. + Attempted { outcome: Outcome }, + /// A XCM message was sent. + Sent { origin: Location, destination: Location, message: Xcm<()>, message_id: XcmHash }, + } + + #[pallet::origin] + #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum Origin { + /// It comes from somewhere in the XCM space wanting to transact. + Xcm(Location), + } + impl From for Origin { + fn from(location: Location) -> Origin { + Origin::Xcm(location) + } + } + + #[pallet::error] + pub enum Error { + InvalidXcm, + SendFailure, + BadVersion, + Empty, + CannotReanchor, + CannotDetermine, + InvalidAsset, + FeesNotMet, + UnweighableMessage, + LocalExecutionIncomplete, + InvalidNetwork, + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(4_000_000_000, 0))] + pub fn transfer_to_ethereum( + origin: OriginFor, + beneficiary: H160, + asset: Box, + fee: Box, + ) -> DispatchResult { + let origin = T::ExecuteXcmOrigin::ensure_origin(origin)?; + + let beneficiary: Location = Location::new( + 0, + [Junction::AccountKey20 { network: None, key: beneficiary.into() }], + ); + + let dest = T::Destination::get(); + + // If fungible asset, ensure non-zero amount. + let asset: Asset = (*asset).try_into().map_err(|()| Error::::BadVersion)?; + if let Fungible(x) = asset.fun { + ensure!(x > 0, Error::::Empty); + } + + // Find transfer types for fee asset. + let asset_transfer_type = T::XcmExecutor::determine_for(&asset, &dest) + .map_err(|_| Error::::CannotDetermine)?; + + let fee: Asset = (*fee).try_into().map_err(|()| Error::::BadVersion)?; + log::debug!( + target: "xcm::transfer_to_ethereum", + "origin {:?}, dest {:?}, beneficiary {:?}, asset {:?}, fee {:?}, transfer_type {:?}", + origin, dest, beneficiary, asset, fee, asset_transfer_type + ); + + let (local_xcm, remote_xcm) = Self::build_xcm_transfer( + origin.clone(), + dest.clone(), + beneficiary, + &asset, + asset_transfer_type, + &fee, + )?; + + Self::execute_xcm_transfer(origin, T::Forwarder::get(), local_xcm, remote_xcm) + } + } + + impl Pallet { + /// Withdraw given `assets` from the given `location` and pay as XCM fees. + /// + /// Fails if: + /// - the `assets` are not known on this chain; + /// - the `assets` cannot be withdrawn with that location as the Origin. + fn charge_fees(location: Location, assets: Assets) -> DispatchResult { + T::XcmExecutor::charge_fees(location.clone(), assets.clone()) + .map_err(|_| Error::::FeesNotMet)?; + Self::deposit_event(Event::FeesPaid { paying: location, fees: assets }); + Ok(()) + } + + /// Send xcm to bridge hub with designated fee charged + fn send_xcm(origin: Location, dest: Location, remote_xcm: Xcm<()>) -> DispatchResult { + let (ticket, delivery_fee) = + validate_send::(dest.clone(), remote_xcm.clone()) + .map_err(|_| Error::::InvalidXcm)?; + Self::charge_fees(origin.clone(), delivery_fee).map_err(|_| Error::::FeesNotMet)?; + + let message_id = T::XcmRouter::deliver(ticket).map_err(|_| Error::::SendFailure)?; + Self::deposit_event(Event::Sent { + origin, + destination: dest, + message: remote_xcm, + message_id, + }); + Ok(()) + } + + /// Execute the transfer including the local xcm + /// and send the remote xcm to bridge hub + fn execute_xcm_transfer( + origin: Location, + dest: Location, + mut local_xcm: Xcm<::RuntimeCall>, + remote_xcm: Xcm<()>, + ) -> DispatchResult { + log::debug!( + target: "xcm::transfer_to_ethereum", + "origin {:?}, dest {:?}, local_xcm {:?}, remote_xcm {:?}", + origin, dest, local_xcm, remote_xcm, + ); + + let weight = + T::Weigher::weight(&mut local_xcm).map_err(|()| Error::::UnweighableMessage)?; + let mut hash = local_xcm.using_encoded(sp_io::hashing::blake2_256); + let outcome = T::XcmExecutor::prepare_and_execute( + origin.clone(), + local_xcm, + &mut hash, + weight, + weight, + ); + Self::deposit_event(Event::Attempted { outcome: outcome.clone() }); + outcome.ensure_complete().map_err(|_| Error::::LocalExecutionIncomplete)?; + + Self::send_xcm(origin, dest, remote_xcm)?; + + Ok(()) + } + + /// Build the Xcm, a local one and the remote one which will be sent to bridge hub + fn build_xcm_transfer( + origin: Location, + dest: Location, + beneficiary: Location, + asset: &Asset, + transfer_type: TransferType, + fee: &Asset, + ) -> Result<(Xcm<::RuntimeCall>, Xcm<()>), Error> { + let (local, remote) = match transfer_type { + TransferType::LocalReserve => { + let (local, remote) = Self::local_reserve_transfer_programs( + origin.clone(), + dest.clone(), + beneficiary, + asset, + fee, + )?; + Some((local, remote)) + }, + TransferType::DestinationReserve => { + let (local, remote) = Self::destination_reserve_transfer_programs( + origin.clone(), + dest.clone(), + beneficiary, + asset, + fee, + )?; + Some((local, remote)) + }, + _ => None, + } + .ok_or(Error::InvalidAsset)?; + Ok((local, remote)) + } + + /// Construct Xcm for Polkadot native asset + fn local_reserve_transfer_programs( + origin: Location, + dest: Location, + beneficiary: Location, + asset: &Asset, + fee: &Asset, + ) -> Result<(Xcm<::RuntimeCall>, Xcm<()>), Error> { + let assets: Assets = vec![asset.clone()].into(); + let burn_assets: Assets = vec![fee.clone(), T::DeliveryFee::get()].into(); + + // XCM instructions to be executed on local chain + let local_execute_xcm = Xcm(vec![ + // locally move `assets` to `dest`s local sovereign account + TransferAsset { assets: assets.clone(), beneficiary: dest.clone() }, + // withdraw reserve-based assets + WithdrawAsset(burn_assets.clone()), + // burn reserve-based assets + BurnAsset(burn_assets), + ]); + + let network: NetworkId = match T::Destination::get() { + Location { parents: 2, interior: Junctions::X1(junction) } => + match junction.first() { + Some(&GlobalConsensus(network_id)) => Ok(network_id), + _ => Err(Error::::InvalidNetwork), + }, + _ => Err(Error::::InvalidNetwork), + }?; + + let mut inner_xcm = Xcm(vec![ + ReserveAssetDeposited(assets), + ClearOrigin, + BuyExecution { fees: fee.clone(), weight_limit: Unlimited }, + DepositAsset { assets: Wild(AllCounted(1)), beneficiary }, + ]); + let unique_id = unique(&inner_xcm); + inner_xcm.0.push(SetTopic(unique_id)); + + // XCM instructions to be executed on bridge hub + let xcm_on_dest = Xcm(vec![ + DescendOrigin(origin.clone().interior), + ReceiveTeleportedAsset(vec![T::DeliveryFee::get()].into()), + BuyExecution { fees: T::DeliveryFee::get().into(), weight_limit: Unlimited }, + SetAppendix(Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: origin.clone(), + }])), + ExportMessage { network, destination: dest.interior, xcm: inner_xcm }, + ]); + + Ok((local_execute_xcm, xcm_on_dest)) + } + + /// Construct Xcm for Ethereum native asset + fn destination_reserve_transfer_programs( + origin: Location, + dest: Location, + beneficiary: Location, + asset: &Asset, + fee: &Asset, + ) -> Result<(Xcm<::RuntimeCall>, Xcm<()>), Error> { + let assets: Assets = vec![asset.clone(), fee.clone(), T::DeliveryFee::get()].into(); + + // XCM instructions to be executed on local chain + let local_execute_xcm = Xcm(vec![ + // withdraw reserve-based assets + WithdrawAsset(assets.clone()), + // burn reserve-based assets + BurnAsset(assets), + ]); + + let network: NetworkId = match T::Destination::get() { + Location { parents: 2, interior: Junctions::X1(junction) } => + match junction.first() { + Some(&GlobalConsensus(network_id)) => Ok(network_id), + _ => Err(Error::::InvalidNetwork), + }, + _ => Err(Error::::InvalidNetwork), + }?; + + let mut inner_xcm = Xcm(vec![ + WithdrawAsset(vec![asset.clone()].into()), + ClearOrigin, + BuyExecution { fees: fee.clone(), weight_limit: Unlimited }, + DepositAsset { assets: Wild(AllCounted(1)), beneficiary }, + ]); + let unique_id = unique(&inner_xcm); + inner_xcm.0.push(SetTopic(unique_id)); + + // XCM instructions to be executed on bridge hub + let xcm_on_dest = Xcm(vec![ + DescendOrigin(origin.clone().interior), + ReceiveTeleportedAsset(vec![T::DeliveryFee::get()].into()), + BuyExecution { fees: T::DeliveryFee::get().into(), weight_limit: Unlimited }, + SetAppendix(Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: origin.clone(), + }])), + ExportMessage { network, destination: dest.interior, xcm: inner_xcm }, + ]); + + Ok((local_execute_xcm, xcm_on_dest)) + } + } +} diff --git a/bridges/snowbridge/pallets/xcm-helper/src/mock.rs b/bridges/snowbridge/pallets/xcm-helper/src/mock.rs new file mode 100644 index 0000000000000..b8d498f77d38a --- /dev/null +++ b/bridges/snowbridge/pallets/xcm-helper/src/mock.rs @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork + +use codec::Encode; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{ + AsEnsureOriginWithArg, ConstU128, ConstU32, Contains, Equals, Everything, EverythingBut, + Nothing, + }, + weights::Weight, +}; +use frame_system::EnsureRoot; +use polkadot_parachain_primitives::primitives::Id as ParaId; +use polkadot_runtime_parachains::origin; +use sp_core::H256; +use sp_runtime::{traits::IdentityLookup, AccountId32, BuildStorage}; +pub use sp_std::cell::RefCell; +use xcm::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, Case, + ChildParachainAsNative, ChildParachainConvertsVia, ChildSystemParachainAsSuperuser, + DescribeAllTerminal, FixedRateOfFungible, FixedWeightBounds, FrameTransactionalProcessor, + FungibleAdapter, FungiblesAdapter, HashedDescription, IsConcrete, MatchedConvertedConcreteId, + NoChecking, SendXcmFeeToAccount, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, TakeWeightCredit, XcmFeeManagerFromComponents, +}; +use xcm_executor::{ + traits::{Identity, JustTry}, + XcmExecutor, +}; + +use crate::Config; + +pub type AccountId = AccountId32; +pub type Balance = u128; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + AssetsPallet: pallet_assets, + ParasOrigin: origin, + XcmHelper: crate, + } +); + +thread_local! { + pub static SENT_XCM: RefCell)>> = RefCell::new(Vec::new()); + pub static FAIL_SEND_XCM: RefCell = RefCell::new(false); +} +pub(crate) fn sent_xcm() -> Vec<(Location, Xcm<()>)> { + SENT_XCM.with(|q| (*q.borrow()).clone()) +} +pub(crate) fn take_sent_xcm() -> Vec<(Location, Xcm<()>)> { + SENT_XCM.with(|q| { + let mut r = Vec::new(); + std::mem::swap(&mut r, &mut *q.borrow_mut()); + r + }) +} +pub(crate) fn set_send_xcm_artificial_failure(should_fail: bool) { + FAIL_SEND_XCM.with(|q| *q.borrow_mut() = should_fail); +} +/// Sender that never returns error. +pub struct TestSendXcm; +impl SendXcm for TestSendXcm { + type Ticket = (Location, Xcm<()>); + fn validate( + dest: &mut Option, + msg: &mut Option>, + ) -> SendResult<(Location, Xcm<()>)> { + if FAIL_SEND_XCM.with(|q| *q.borrow()) { + return Err(SendError::Transport("Intentional send failure used in tests")); + } + let pair = (dest.take().unwrap(), msg.take().unwrap()); + Ok((pair, Assets::new())) + } + fn deliver(pair: (Location, Xcm<()>)) -> Result { + let hash = fake_message_hash(&pair.1); + SENT_XCM.with(|q| q.borrow_mut().push(pair)); + Ok(hash) + } +} +/// Sender that returns error if `X8` junction and stops routing +pub struct TestSendXcmErrX8; +impl SendXcm for TestSendXcmErrX8 { + type Ticket = (Location, Xcm<()>); + fn validate( + dest: &mut Option, + _: &mut Option>, + ) -> SendResult<(Location, Xcm<()>)> { + if dest.as_ref().unwrap().len() == 8 { + dest.take(); + Err(SendError::Transport("Destination location full")) + } else { + Err(SendError::NotApplicable) + } + } + fn deliver(pair: (Location, Xcm<()>)) -> Result { + let hash = fake_message_hash(&pair.1); + SENT_XCM.with(|q| q.borrow_mut().push(pair)); + Ok(hash) + } +} + +parameter_types! { + pub Para3000: u32 = 3000; + pub Para3000Location: Location = Parachain(Para3000::get()).into(); + pub Para3000PaymentAmount: u128 = 1; + pub Para3000PaymentAssets: Assets = Assets::from(Asset::from((Here, Para3000PaymentAmount::get()))); +} +/// Sender only sends to `Parachain(3000)` destination requiring payment. +pub struct TestPaidForPara3000SendXcm; +impl SendXcm for TestPaidForPara3000SendXcm { + type Ticket = (Location, Xcm<()>); + fn validate( + dest: &mut Option, + msg: &mut Option>, + ) -> SendResult<(Location, Xcm<()>)> { + if let Some(dest) = dest.as_ref() { + if !dest.eq(&Para3000Location::get()) { + return Err(SendError::NotApplicable) + } + } else { + return Err(SendError::NotApplicable) + } + + let pair = (dest.take().unwrap(), msg.take().unwrap()); + Ok((pair, Para3000PaymentAssets::get())) + } + fn deliver(pair: (Location, Xcm<()>)) -> Result { + let hash = fake_message_hash(&pair.1); + SENT_XCM.with(|q| q.borrow_mut().push(pair)); + Ok(hash) + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type MaxLocks = MaxLocks; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; +} + +#[cfg(feature = "runtime-benchmarks")] +/// Simple conversion of `u32` into an `AssetId` for use in benchmarking. +pub struct XcmBenchmarkHelper; +#[cfg(feature = "runtime-benchmarks")] +impl pallet_assets::BenchmarkHelper for XcmBenchmarkHelper { + fn create_asset_id_parameter(id: u32) -> Location { + Location::new(1, [Parachain(id)]) + } +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = Location; + type AssetIdParameter = Location; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = (); + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = XcmBenchmarkHelper; +} + +// This child parachain is a system parachain trusted to teleport native token. +pub const SOME_SYSTEM_PARA: u32 = 1001; + +// This child parachain acts as trusted reserve for its assets in tests. +// USDT allowed to teleport to/from here. +pub const FOREIGN_ASSET_RESERVE_PARA_ID: u32 = 2001; +// Inner junction of reserve asset on `FOREIGN_ASSET_RESERVE_PARA_ID`. +pub const FOREIGN_ASSET_INNER_JUNCTION: Junction = GeneralIndex(1234567); + +// This child parachain acts as trusted reserve for say.. USDC that can be used for fees. +pub const USDC_RESERVE_PARA_ID: u32 = 2002; +// Inner junction of reserve asset on `USDC_RESERVE_PARA_ID`. +pub const USDC_INNER_JUNCTION: Junction = PalletInstance(42); + +// This child parachain is a trusted teleporter for say.. USDT (T from Teleport :)). +// We'll use USDT in tests that teleport fees. +pub const USDT_PARA_ID: u32 = 2003; + +// This child parachain is not configured as trusted reserve or teleport location for any assets. +pub const OTHER_PARA_ID: u32 = 2009; + +// This child parachain is used for filtered/disallowed assets. +pub const FILTERED_PARA_ID: u32 = 2010; + +parameter_types! { + pub const RelayLocation: Location = Here.into_location(); + pub const NativeAsset: Asset = Asset { + fun: Fungible(10), + id: AssetId(Here.into_location()), + }; + pub SystemParachainLocation: Location = Location::new( + 0, + [Parachain(SOME_SYSTEM_PARA)] + ); + pub ForeignReserveLocation: Location = Location::new( + 0, + [Parachain(FOREIGN_ASSET_RESERVE_PARA_ID)] + ); + pub PaidParaForeignReserveLocation: Location = Location::new( + 0, + [Parachain(Para3000::get())] + ); + pub ForeignAsset: Asset = Asset { + fun: Fungible(10), + id: AssetId(Location::new( + 0, + [Parachain(FOREIGN_ASSET_RESERVE_PARA_ID), FOREIGN_ASSET_INNER_JUNCTION], + )), + }; + pub PaidParaForeignAsset: Asset = Asset { + fun: Fungible(10), + id: AssetId(Location::new( + 0, + [Parachain(Para3000::get())], + )), + }; + pub UsdcReserveLocation: Location = Location::new( + 0, + [Parachain(USDC_RESERVE_PARA_ID)] + ); + pub Usdc: Asset = Asset { + fun: Fungible(10), + id: AssetId(Location::new( + 0, + [Parachain(USDC_RESERVE_PARA_ID), USDC_INNER_JUNCTION], + )), + }; + pub UsdtTeleportLocation: Location = Location::new( + 0, + [Parachain(USDT_PARA_ID)] + ); + pub Usdt: Asset = Asset { + fun: Fungible(10), + id: AssetId(Location::new( + 0, + [Parachain(USDT_PARA_ID)], + )), + }; + pub FilteredTeleportLocation: Location = Location::new( + 0, + [Parachain(FILTERED_PARA_ID)] + ); + pub FilteredTeleportAsset: Asset = Asset { + fun: Fungible(10), + id: AssetId(Location::new( + 0, + [Parachain(FILTERED_PARA_ID)], + )), + }; + pub const AnyNetwork: Option = None; + pub UniversalLocation: InteriorLocation = Here; + pub UnitWeightCost: u64 = 1_000; + pub CheckingAccount: AccountId = AccountId::from([1u8; 32]); +} + +pub type SovereignAccountOf = ( + ChildParachainConvertsVia, + AccountId32Aliases, + HashedDescription, +); + +pub type ForeignAssetsConvertedConcreteId = MatchedConvertedConcreteId< + Location, + Balance, + // Excludes relay/parent chain currency + EverythingBut<(Equals,)>, + Identity, + JustTry, +>; + +pub type AssetTransactors = ( + FungibleAdapter, SovereignAccountOf, AccountId, ()>, + FungiblesAdapter< + AssetsPallet, + ForeignAssetsConvertedConcreteId, + SovereignAccountOf, + AccountId, + NoChecking, + CheckingAccount, + >, +); + +type LocalOriginConverter = ( + SovereignSignedViaLocation, + ChildParachainAsNative, + SignedAccountId32AsNative, + ChildSystemParachainAsSuperuser, +); + +parameter_types! { + pub const BaseXcmWeight: Weight = Weight::from_parts(1_000, 1_000); + pub CurrencyPerSecondPerByte: (AssetId, u128, u128) = (AssetId(RelayLocation::get()), 1, 1); + pub TrustedLocal: (AssetFilter, Location) = (All.into(), Here.into()); + pub TrustedSystemPara: (AssetFilter, Location) = (NativeAsset::get().into(), SystemParachainLocation::get()); + pub TrustedUsdt: (AssetFilter, Location) = (Usdt::get().into(), UsdtTeleportLocation::get()); + pub TrustedFilteredTeleport: (AssetFilter, Location) = (FilteredTeleportAsset::get().into(), FilteredTeleportLocation::get()); + pub TeleportUsdtToForeign: (AssetFilter, Location) = (Usdt::get().into(), ForeignReserveLocation::get()); + pub TrustedForeign: (AssetFilter, Location) = (ForeignAsset::get().into(), ForeignReserveLocation::get()); + pub TrustedPaidParaForeign: (AssetFilter, Location) = (PaidParaForeignAsset::get().into(), PaidParaForeignReserveLocation::get()); + + pub TrustedUsdc: (AssetFilter, Location) = (Usdc::get().into(), UsdcReserveLocation::get()); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; + pub TreasuryAccount: AccountId = AccountId::new([167u8; 32]); +} + +pub const XCM_FEES_NOT_WAIVED_USER_ACCOUNT: [u8; 32] = [37u8; 32]; + +pub struct XcmFeesNotWaivedLocations; +impl Contains for XcmFeesNotWaivedLocations { + fn contains(location: &Location) -> bool { + matches!( + location.unpack(), + (0, [Junction::AccountId32 { network: None, id: XCM_FEES_NOT_WAIVED_USER_ACCOUNT }]) + ) + } +} + +pub type Barrier = ( + TakeWeightCredit, + AllowTopLevelPaidExecutionFrom, + AllowSubscriptionsFrom, +); + +pub type XcmRouter = (TestPaidForPara3000SendXcm, TestSendXcmErrX8, TestSendXcm); + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = XcmRouter; + type AssetTransactor = AssetTransactors; + type OriginConverter = LocalOriginConverter; + type IsReserve = (Case, Case, Case); + type IsTeleporter = ( + Case, + Case, + Case, + Case, + Case, + ); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = FixedRateOfFungible; + type ResponseHandler = (); + type AssetTrap = (); + type AssetLocker = (); + type AssetExchanger = (); + type AssetClaims = (); + type SubscriptionService = (); + type PalletInstancesInfo = AllPalletsWithSystem; + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type FeeManager = XcmFeeManagerFromComponents< + EverythingBut, + SendXcmFeeToAccount, + >; + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + type TransactionalProcessor = FrameTransactionalProcessor; + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = (); +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +parameter_types! { + pub EthereumLocation: Location = Location { + parents: 2, + interior: Junctions::from([GlobalConsensus(Ethereum { chain_id: 11155111 })]), + }; + pub BridgeHub: Location = Location { parents:1, interior: Junctions::from([Parachain(1013)])}; + pub DeliveryFee: Asset = Asset::from((Location::parent(),48_000_000)); +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecutor = XcmExecutor; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Destination = EthereumLocation; + type DeliveryFee = DeliveryFee; + type Forwarder = BridgeHub; +} + +impl origin::Config for Test {} + +#[cfg(feature = "runtime-benchmarks")] +pub struct TestDeliveryHelper; +#[cfg(feature = "runtime-benchmarks")] +impl xcm_builder::EnsureDelivery for TestDeliveryHelper { + fn ensure_successful_delivery( + origin_ref: &Location, + _dest: &Location, + _fee_reason: xcm_executor::traits::FeeReason, + ) -> (Option, Option) { + use xcm_executor::traits::ConvertLocation; + let account = SovereignAccountOf::convert_location(origin_ref).expect("Valid location"); + // Give the existential deposit at least + let balance = ExistentialDeposit::get(); + let _ = >::make_free_balance_be( + &account, balance, + ); + (None, None) + } +} + +pub(crate) fn last_event() -> RuntimeEvent { + System::events().pop().expect("RuntimeEvent expected").event +} + +pub(crate) fn last_events(n: usize) -> Vec { + System::events().into_iter().map(|e| e.event).rev().take(n).rev().collect() +} + +pub(crate) fn new_test_ext_with_balances( + balances: Vec<(AccountId, Balance)>, +) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub(crate) fn fake_message_hash(message: &Xcm) -> XcmHash { + message.using_encoded(sp_io::hashing::blake2_256) +} diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs index ddc36ce8cb61b..9d493591860ba 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs @@ -51,7 +51,7 @@ where return Err(SendError::NotApplicable) } - let dest = destination.take().ok_or(SendError::MissingArgument)?; + let dest = destination.clone().take().ok_or(SendError::MissingArgument)?; if dest != Here { log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}."); return Err(SendError::NotApplicable) @@ -281,3 +281,246 @@ impl<'a, Call> XcmConverter<'a, Call> { } } } + +pub mod v2 { + use super::*; + const TARGET: &str = "ethereum_blob_exporter_v2"; + pub struct EthereumBlobExporter< + UniversalLocation, + EthereumNetwork, + OutboundQueue, + AgentHashedDescription, + >(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>); + + impl ExportXcm + for EthereumBlobExporter< + UniversalLocation, + EthereumNetwork, + OutboundQueue, + AgentHashedDescription, + > + where + UniversalLocation: Get, + EthereumNetwork: Get, + OutboundQueue: SendMessage, + AgentHashedDescription: ConvertLocation, + { + type Ticket = (Vec, XcmHash); + + fn validate( + network: NetworkId, + _channel: u32, + universal_source: &mut Option, + destination: &mut Option, + message: &mut Option>, + ) -> SendResult { + let expected_network = EthereumNetwork::get(); + let universal_location = UniversalLocation::get(); + + if network != expected_network { + log::trace!(target: TARGET, "skipped due to unmatched bridge network {network:?}."); + return Err(SendError::NotApplicable) + } + + let destination = destination.clone(); + + let dest = destination.ok_or(SendError::MissingArgument)?; + if dest != Junctions::from([GlobalConsensus(network)]) { + log::trace!(target: TARGET, "skipped due to unmatched remote destination {dest:?}."); + return Err(SendError::NotApplicable) + } + + let (local_net, local_sub) = universal_source + .take() + .ok_or_else(|| { + log::error!(target: TARGET, "universal source not provided."); + SendError::MissingArgument + })? + .split_global() + .map_err(|()| { + log::error!(target: TARGET, "could not get global consensus from universal source '{universal_source:?}'."); + SendError::Unroutable + })?; + + if Ok(local_net) != universal_location.global_consensus() { + log::trace!(target: TARGET, "skipped due to unmatched relay network {local_net:?}."); + return Err(SendError::NotApplicable) + } + + let para_id = match local_sub.as_slice() { + [Parachain(para_id), AccountId32 { .. }] => *para_id, + _ => { + log::error!(target: TARGET, "could not get parachain id from universal source '{local_sub:?}'."); + return Err(SendError::MissingArgument) + }, + }; + + let message = message.take().ok_or_else(|| { + log::error!(target: TARGET, "xcm message not provided."); + SendError::MissingArgument + })?; + + let mut converter = XcmConverter::new(&message, &expected_network); + let (agent_execute_command, message_id) = converter.convert().map_err(|err| { + log::error!(target: TARGET, "unroutable due to pattern matching error '{err:?}'."); + SendError::Unroutable + })?; + + let source_location = Location::new(1, local_sub.clone()); + let agent_id = match AgentHashedDescription::convert_location(&source_location) { + Some(id) => id, + None => { + log::error!(target: TARGET, "unroutable due to not being able to create agent id. '{source_location:?}'"); + return Err(SendError::Unroutable) + }, + }; + + let channel_id: ChannelId = ParaId::from(para_id).into(); + + let outbound_message = Message { + id: Some(message_id.into()), + channel_id, + command: Command::AgentExecute { agent_id, command: agent_execute_command }, + }; + + // validate the message + let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| { + log::error!(target: TARGET, "OutboundQueue validation of message failed. {err:?}"); + SendError::Unroutable + })?; + + // convert fee to Asset + let fee = Asset::from((Location::parent(), fee.local)).into(); + + Ok(((ticket.encode(), message_id), fee)) + } + + fn deliver(blob: (Vec, XcmHash)) -> Result { + let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref()) + .map_err(|_| { + log::trace!(target: TARGET, "undeliverable due to decoding error"); + SendError::NotApplicable + })?; + + let message_id = OutboundQueue::deliver(ticket).map_err(|_| { + log::error!(target: TARGET, "OutboundQueue submit of message failed"); + SendError::Transport("other transport error") + })?; + + log::info!(target: TARGET, "message delivered {message_id:#?}."); + Ok(message_id.into()) + } + } + + struct XcmConverter<'a, Call> { + iter: Peekable>>, + ethereum_network: &'a NetworkId, + } + impl<'a, Call> XcmConverter<'a, Call> { + fn new(message: &'a Xcm, ethereum_network: &'a NetworkId) -> Self { + Self { iter: message.inner().iter().peekable(), ethereum_network } + } + + fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> { + // Get withdraw/deposit and make native tokens create message. + let result = self.native_tokens_unlock_message()?; + + // All xcm instructions must be consumed before exit. + if self.next().is_ok() { + return Err(XcmConverterError::EndOfXcmMessageExpected) + } + + Ok(result) + } + + fn native_tokens_unlock_message( + &mut self, + ) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> { + use XcmConverterError::*; + + // Get the reserve assets from WithdrawAsset. + let reserve_assets = + match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets) + .ok_or(WithdrawAssetExpected)?; + + // Check if clear origin exists and skip over it. + if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() { + let _ = self.next(); + } + + // Get the fee asset item from BuyExecution. + let fee_asset = match_expression!(self.next()?, BuyExecution { fees, .. }, fees) + .ok_or(InvalidFeeAsset)?; + ensure!(fee_asset.clone().id == AssetId::from(Location::parent()), InvalidFeeAsset); + let _fee_amount = match fee_asset.clone().fun { + Fungible(fee_amount) => Ok(fee_amount), + _ => Err(InvalidFeeAsset), + }?; + + let (deposit_assets, beneficiary) = match_expression!( + self.next()?, + DepositAsset { assets, beneficiary }, + (assets, beneficiary) + ) + .ok_or(DepositAssetExpected)?; + + // assert that the beneficiary is AccountKey20. + let recipient = match_expression!( + beneficiary.unpack(), + (0, [AccountKey20 { network, key }]) + if self.network_matches(network), + H160(*key) + ) + .ok_or(BeneficiaryResolutionFailed)?; + + // Make sure there are reserved assets. + if reserve_assets.len() == 0 { + return Err(NoReserveAssets) + } + + // Check the the deposit asset filter matches what was reserved. + if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) { + return Err(FilterDoesNotConsumeAllAssets) + } + + let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?; + + let (token, amount) = match reserve_asset { + Asset { id: AssetId(inner_location), fun: Fungible(amount) } => + match inner_location.unpack() { + (2, [GlobalConsensus(network), AccountKey20 { key, .. }]) + if self.network_matches(&Some(*network)) => + Some((H160(*key), *amount)), + _ => None, + }, + _ => None, + } + .ok_or(AssetResolutionFailed)?; + + // transfer amount must be greater than 0. + ensure!(amount > 0, ZeroAssetTransfer); + + // Check if there is a SetTopic and skip over it if found. + let topic_id = + match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?; + + Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id)) + } + + fn next(&mut self) -> Result<&'a Instruction, XcmConverterError> { + self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm) + } + + fn peek(&mut self) -> Result<&&'a Instruction, XcmConverterError> { + self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm) + } + + fn network_matches(&self, network: &Option) -> bool { + if let Some(network) = network { + network == self.ethereum_network + } else { + true + } + } + } +} diff --git a/bridges/snowbridge/primitives/router/src/outbound/tests.rs b/bridges/snowbridge/primitives/router/src/outbound/tests.rs index 111243bb45a7e..ad788c0b2cc3d 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/tests.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/tests.rs @@ -569,67 +569,6 @@ fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() { assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); } -#[test] -fn xcm_converter_with_different_fee_asset_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location = [AccountKey20 { network: None, key: token_address }].into(); - let fee_asset = - Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(1000) }; - - let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into(); - - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); -} - -#[test] -fn xcm_converter_with_fees_greater_than_reserve_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location: Location = [AccountKey20 { network: None, key: token_address }].into(); - let fee_asset = Asset { id: AssetId(asset_location.clone()), fun: Fungible(1001) }; - - let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into(); - - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); -} - #[test] fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() { let network = BridgedNetwork::get(); diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs index 80d2376c6811d..25857cd6db79c 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/src/lib.rs @@ -50,6 +50,7 @@ decl_test_parachains! { PoolAssets: asset_hub_rococo_runtime::PoolAssets, AssetConversion: asset_hub_rococo_runtime::AssetConversion, Balances: asset_hub_rococo_runtime::Balances, + SnowbridgeXcmHelper: asset_hub_rococo_runtime::SnowbridgeXcmHelper, } }, } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/mod.rs index ceccf98a0240d..44307eff81f13 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/mod.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/mod.rs @@ -19,6 +19,7 @@ mod asset_transfers; mod claim_assets; mod send_xcm; mod snowbridge; +mod snowbridge_transfer_v2; mod teleport; pub(crate) fn asset_hub_westend_location() -> Location { diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs index 4cb8680686e8e..f6648fd63886a 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -34,8 +34,6 @@ use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; const INITIAL_FUND: u128 = 5_000_000_000 * ROCOCO_ED; pub const CHAIN_ID: u64 = 11155111; -const TREASURY_ACCOUNT: [u8; 32] = - hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000"); pub const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); const INSUFFICIENT_XCM_FEE: u128 = 1000; @@ -393,6 +391,7 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { AssetHubRococo::fund_accounts(vec![(AssetHubRococoReceiver::get(), INITIAL_FUND)]); const WETH_AMOUNT: u128 = 1_000_000_000; + const WETH_FEE_AMOUNT: u128 = 1_000_000_000; BridgeHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; @@ -431,16 +430,17 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, ] ); - let assets = vec![Asset { - id: AssetId(Location::new( - 2, - [ - GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), - AccountKey20 { network: None, key: WETH }, - ], - )), - fun: Fungible(WETH_AMOUNT), - }]; + let weth_asset_id = AssetId(Location::new( + 2, + [ + GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), + AccountKey20 { network: None, key: WETH }, + ], + )); + let assets = vec![ + Asset { id: weth_asset_id.clone(), fun: Fungible(WETH_FEE_AMOUNT) }, + Asset { id: weth_asset_id.clone(), fun: Fungible(WETH_AMOUNT) }, + ]; let multi_assets = VersionedAssets::V4(Assets::from(assets)); let destination = VersionedLocation::V4(Location::new( @@ -448,21 +448,25 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })], )); - let beneficiary = VersionedLocation::V4(Location::new( + let beneficiary = Location::new( 0, [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], - )); + ); let free_balance_before = ::Balances::free_balance( AssetHubRococoReceiver::get(), ); // Send the Weth back to Ethereum - ::PolkadotXcm::limited_reserve_transfer_assets( + let custom_xcm_on_dest = + Xcm::<()>(vec![DepositAsset { assets: Wild(AllCounted(1)), beneficiary }]); + ::PolkadotXcm::transfer_assets_using_type_and_then( RuntimeOrigin::signed(AssetHubRococoReceiver::get()), Box::new(destination), - Box::new(beneficiary), Box::new(multi_assets), - 0, + bx!(TransferType::DestinationReserve), + bx!(VersionedAssetId::from(weth_asset_id)), + bx!(TransferType::DestinationReserve), + bx!(VersionedXcm::from(custom_xcm_on_dest)), Unlimited, ) .unwrap(); @@ -489,20 +493,11 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { assert!( events.iter().any(|event| matches!( event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == TREASURY_ACCOUNT.into() && *amount == 16903333 + RuntimeEvent::Balances(pallet_balances::Event::Burned { who, amount }) + if *who == assethub_sovereign.clone().into() && *amount == DefaultBridgeHubEthereumBaseFee::get() )), "Snowbridge sovereign takes local fee." ); - // Check that the remote fee was credited to the AssetHub sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == assethub_sovereign && *amount == 2680000000000, - )), - "AssetHub sovereign takes remote fee." - ); }); } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge_transfer_v2.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge_transfer_v2.rs new file mode 100644 index 0000000000000..fc0c062ed178d --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge_transfer_v2.rs @@ -0,0 +1,143 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::imports::*; +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::TypeInfo; +use hex_literal::hex; +use snowbridge_core::outbound::OperatingMode; +use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; + +const INITIAL_FUND: u128 = 5_000_000_000 * ROCOCO_ED; +const CHAIN_ID: u64 = 11155111; +const TREASURY_ACCOUNT: [u8; 32] = + hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000"); +const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); +const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); +const ETHEREUM_EXECUTION_FEE: u128 = 2_750_872_500_000; + +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub enum ControlCall { + #[codec(index = 3)] + CreateAgent, + #[codec(index = 4)] + CreateChannel { mode: OperatingMode }, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub enum SnowbridgeControl { + #[codec(index = 83)] + Control(ControlCall), +} + +/// Tests the full cycle of token transfers: +/// - registering a token on AssetHub +/// - sending a token to AssetHub +/// - returning the token to Ethereum +#[test] +fn send_weth_asset_from_asset_hub_to_ethereum() { + use asset_hub_rococo_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; + let assethub_location = BridgeHubRococo::sibling_location_of(AssetHubRococo::para_id()); + let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(assethub_location); + + AssetHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + BridgeHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + AssetHubRococo::force_xcm_version( + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]), + XCM_VERSION, + ); + + BridgeHubRococo::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); + AssetHubRococo::fund_accounts(vec![(AssetHubRococoReceiver::get(), INITIAL_FUND)]); + + const WETH_AMOUNT: u128 = 1_000_000_000; + + let weth_asset_location: Location = Location::new( + 2, + [EthereumNetwork::get().into(), AccountKey20 { network: None, key: WETH }], + ); + + AssetHubRococo::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + // Register WETH and Mint some to AssetHubRococoReceiver + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + AssetHubRococoReceiver::get().into(), + false, + 1, + )); + assert_ok!(::ForeignAssets::mint( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + weth_asset_location.clone().try_into().unwrap(), + AssetHubRococoReceiver::get().into(), + WETH_AMOUNT, + )); + + let asset = Asset { + id: AssetId(Location::new( + 2, + [ + GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), + AccountKey20 { network: None, key: WETH }, + ], + )), + fun: Fungible(WETH_AMOUNT), + }; + + let free_balance_before = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + + let fee_asset: Asset = (AssetId::from(Location::parent()), ETHEREUM_EXECUTION_FEE).into(); + // Send the Weth back to Ethereum + ::SnowbridgeXcmHelper::transfer_to_ethereum( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + ETHEREUM_DESTINATION_ADDRESS.into(), + Box::new(VersionedAsset::V4(asset)), + Box::new(VersionedAsset::V4(fee_asset)), + ) + .unwrap(); + let free_balance_after = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Assert at least DefaultBridgeHubEthereumBaseFee charged from the sender + let free_balance_diff = free_balance_before - free_balance_after; + assert!(free_balance_diff > DefaultBridgeHubEthereumBaseFee::get()); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the transfer token back to Ethereum message was queue in the Ethereum + // Outbound Queue + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued +{..}) => {}, ] + ); + let events = BridgeHubRococo::events(); + // Check that the local fee was credited to the Snowbridge sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) + if *who == TREASURY_ACCOUNT.into() && *amount == 16903333 + )), + "Snowbridge sovereign takes local fee." + ); + }); +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 98df41090a407..450c7e5b01bfd 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -93,8 +93,10 @@ bp-asset-hub-rococo = { workspace = true } bp-asset-hub-westend = { workspace = true } bp-bridge-hub-rococo = { workspace = true } bp-bridge-hub-westend = { workspace = true } +snowbridge-pallet-xcm-helper = { workspace = true } snowbridge-router-primitives = { workspace = true } + [dev-dependencies] asset-test-utils = { workspace = true, default-features = true } @@ -134,6 +136,7 @@ runtime-benchmarks = [ "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks", + "snowbridge-pallet-xcm-helper/runtime-benchmarks", "snowbridge-router-primitives/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", @@ -172,6 +175,7 @@ try-runtime = [ "pallet-xcm/try-runtime", "parachain-info/try-runtime", "polkadot-runtime-common/try-runtime", + "snowbridge-pallet-xcm-helper/try-runtime", "sp-runtime/try-runtime", ] std = [ @@ -230,6 +234,7 @@ std = [ "primitive-types/std", "rococo-runtime-constants/std", "scale-info/std", + "snowbridge-pallet-xcm-helper/std", "snowbridge-router-primitives/std", "sp-api/std", "sp-block-builder/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index f09647854cd01..0c4c679a64465 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -542,7 +542,8 @@ impl InstanceFilter for ProxyType { RuntimeCall::Utility { .. } | RuntimeCall::Multisig { .. } | RuntimeCall::NftFractionalization { .. } | - RuntimeCall::Nfts { .. } | RuntimeCall::Uniques { .. } + RuntimeCall::Nfts { .. } | + RuntimeCall::Uniques { .. } ) }, ProxyType::AssetOwner => matches!( @@ -983,6 +984,7 @@ construct_runtime!( // TODO: the pallet instance should be removed once all pools have migrated // to the new account IDs. AssetConversionMigration: pallet_asset_conversion_ops = 200, + SnowbridgeXcmHelper: snowbridge_pallet_xcm_helper = 201, } ); diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index a11dca4f6d7cc..b7cb67e796fe8 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -508,6 +508,33 @@ impl pallet_assets::BenchmarkHelper for XcmBenchmarkHelper { } } +parameter_types! { + pub EthereumLocation: Location = Location { + parents: 2, + interior: Junctions::from([GlobalConsensus(Ethereum { chain_id: 11155111 })]), + }; + pub BridgeHub: Location = Location { parents:1, interior: Junctions::from([Parachain(1013)])}; + pub DeliveryFee: Asset = Asset::from((Location::parent(),60_000_000)); +} + +impl snowbridge_pallet_xcm_helper::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmRouter = WithUniqueTopic; + type XcmExecutor = XcmExecutor; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Weigher = WeightInfoBounds< + crate::weights::xcm::AssetHubRococoXcmWeight, + RuntimeCall, + MaxInstructions, + >; + type UniversalLocation = UniversalLocation; + type Destination = EthereumLocation; + type Forwarder = BridgeHub; + type DeliveryFee = DeliveryFee; +} + /// All configuration related to bridging pub mod bridging { use super::*; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index a5d798835ac89..8140468547072 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -24,7 +24,10 @@ use crate::{ use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; -use snowbridge_router_primitives::{inbound::MessageToXcm, outbound::EthereumBlobExporter}; +use snowbridge_router_primitives::{ + inbound::MessageToXcm, + outbound::{v2::EthereumBlobExporter as EthereumBlobExporterV2, EthereumBlobExporter}, +}; use sp_core::H160; use testnet_parachains_constants::rococo::{ currency::*, @@ -49,6 +52,13 @@ pub type SnowbridgeExporter = EthereumBlobExporter< snowbridge_core::AgentIdOf, >; +pub type SnowbridgeExporterV2 = EthereumBlobExporterV2< + UniversalLocation, + EthereumNetwork, + snowbridge_pallet_outbound_queue::Pallet, + snowbridge_core::AgentIdOf, +>; + // Ethereum Bridge parameter_types! { pub storage EthereumGatewayAddress: H160 = H160(hex_literal::hex!("EDa338E4dC46038493b885327842fD3E301CaB39")); diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs index 2f11b4694e3bb..f59778aad3a45 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs @@ -23,23 +23,24 @@ use bp_messages::LaneId; use bp_relayers::{PayRewardFromAccount, RewardsAccountOwner, RewardsAccountParams}; use bp_runtime::ChainId; use core::marker::PhantomData; +use cumulus_primitives_core::ParaId; use frame_support::{ parameter_types, - traits::{tokens::imbalance::ResolveTo, ConstU32, Contains, Equals, Everything, Nothing}, + traits::{ + tokens::imbalance::ResolveTo, ConstU32, Contains, ContainsPair, Equals, Everything, Nothing, + }, }; use frame_system::EnsureRoot; use pallet_collator_selection::StakingPotAccountId; use pallet_xcm::XcmPassthrough; use parachains_common::{ xcm_config::{ - AllSiblingSystemParachains, ConcreteAssetFromSystem, ParentRelayOrSiblingParachains, - RelayOrOtherSystemParachains, + AllSiblingSystemParachains, ParentRelayOrSiblingParachains, RelayOrOtherSystemParachains, }, TREASURY_PALLET_ID, }; -use polkadot_parachain_primitives::primitives::Sibling; +use polkadot_parachain_primitives::primitives::{IsSystem, Sibling}; use polkadot_runtime_common::xcm_sender::ExponentialPrice; -use snowbridge_runtime_common::XcmExportFeeToSibling; use sp_core::Get; use sp_runtime::traits::AccountIdConversion; use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; @@ -169,9 +170,29 @@ pub type WaivedLocations = ( Equals, ); +pub struct ConcreteAssetFromUserOriginOfSystemChain(PhantomData); +impl> ContainsPair + for ConcreteAssetFromUserOriginOfSystemChain +{ + fn contains(asset: &Asset, origin: &Location) -> bool { + log::trace!(target: "xcm::contains", "ConcreteAssetFromSystem asset: {:?}, origin: {:?}", asset, origin); + let is_system = match origin.unpack() { + // The Relay Chain + (1, []) => true, + // System parachain + (1, [Parachain(id)]) => ParaId::from(*id).is_system(), + // User from system chain + (1, [Parachain(id), AccountId32 { .. }]) => ParaId::from(*id).is_system(), + // Others + _ => false, + }; + asset.id.0 == AssetLocation::get() && is_system + } +} + /// Cases where a remote origin is accepted as trusted Teleporter for a given asset: /// - NativeToken with the parent Relay Chain and sibling parachains. -pub type TrustedTeleporters = ConcreteAssetFromSystem; +pub type TrustedTeleporters = ConcreteAssetFromUserOriginOfSystemChain; pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { @@ -215,14 +236,6 @@ impl xcm_executor::Config for XcmConfig { crate::bridge_to_westend_config::BridgeHubWestendChainId, crate::bridge_to_westend_config::AssetHubRococoToAssetHubWestendMessagesLane, >, - XcmExportFeeToSibling< - bp_rococo::Balance, - AccountId, - TokenLocation, - EthereumNetwork, - Self::AssetTransactor, - crate::EthereumOutboundQueue, - >, SendXcmFeeToAccount, ), >; @@ -230,6 +243,7 @@ impl xcm_executor::Config for XcmConfig { crate::bridge_to_westend_config::ToBridgeHubWestendHaulBlobExporter, crate::bridge_to_bulletin_config::ToRococoBulletinHaulBlobExporter, crate::bridge_to_ethereum_config::SnowbridgeExporter, + crate::bridge_to_ethereum_config::SnowbridgeExporterV2, ); type UniversalAliases = Nothing; type CallDispatcher = RuntimeCall; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs index 5960ab7b55054..733126c034613 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs @@ -25,7 +25,7 @@ use bridge_hub_rococo_runtime::{ SignedExtra, UncheckedExtrinsic, }; use codec::{Decode, Encode}; -use cumulus_primitives_core::XcmError::{FailedToTransactAsset, NotHoldingFees}; +use cumulus_primitives_core::XcmError::{FailedToTransactAsset, TooExpensive}; use frame_support::parameter_types; use parachains_common::{AccountId, AuraId, Balance}; use snowbridge_pallet_ethereum_client::WeightInfo; @@ -90,8 +90,8 @@ pub fn transfer_token_to_ethereum_fee_not_enough() { H160::random(), H160::random(), // fee not enough - 1_000_000_000, - NotHoldingFees, + 4_000_000, + TooExpensive, ) }