From 4a9dec9d7e19881dddfbd087f896b49fde908de3 Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Fri, 11 Nov 2022 11:29:20 +0100 Subject: [PATCH] Init of pallet with `transfer_asset_via_bridge` call Init of pallet with `transfer_asset_via_bridge` call Init of pallet with `transfer_asset_via_bridge` call - setup basic tests --- Cargo.lock | 17 + Cargo.toml | 1 + .../pallets/bridge-assets-transfer/Cargo.toml | 40 ++ .../pallets/bridge-assets-transfer/src/lib.rs | 385 ++++++++++++++++++ .../runtimes/assets/statemine/Cargo.toml | 2 + .../runtimes/assets/statemine/src/lib.rs | 11 +- .../assets/statemine/src/xcm_config.rs | 21 +- 7 files changed, 472 insertions(+), 5 deletions(-) create mode 100644 parachains/pallets/bridge-assets-transfer/Cargo.toml create mode 100644 parachains/pallets/bridge-assets-transfer/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ed7176dcda..5cc63dd432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,6 +713,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bridge-assets-transfer" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", + "sp-version", + "xcm", + "xcm-builder", +] + [[package]] name = "bs58" version = "0.4.0" @@ -11517,6 +11533,7 @@ name = "statemine-runtime" version = "2.0.0" dependencies = [ "asset-test-utils", + "bridge-assets-transfer", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", diff --git a/Cargo.toml b/Cargo.toml index 768a5d0f67..c12925ed25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "primitives/utility", "polkadot-parachain", "parachains/common", + "parachains/pallets/bridge-assets-transfer", "parachains/pallets/parachain-info", "parachains/pallets/ping", "parachains/runtimes/testing/rococo-parachain", diff --git a/parachains/pallets/bridge-assets-transfer/Cargo.toml b/parachains/pallets/bridge-assets-transfer/Cargo.toml new file mode 100644 index 0000000000..08b0f58a21 --- /dev/null +++ b/parachains/pallets/bridge-assets-transfer/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "bridge-assets-transfer" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://docs.substrate.io/" +repository = "https://github.com/paritytech/cumulus/" +description = "Pallet message transfers through bridges" +readme = "README.md" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.3.0", default-features = false, features = ["derive"] } +log = { version = "0.4.14", default-features = false } + +# Substrate +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } + +# Polkadot +xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } + +[dev-dependencies] +sp-version = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "master" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "xcm/std", +] diff --git a/parachains/pallets/bridge-assets-transfer/src/lib.rs b/parachains/pallets/bridge-assets-transfer/src/lib.rs new file mode 100644 index 0000000000..c4b3ecb48c --- /dev/null +++ b/parachains/pallets/bridge-assets-transfer/src/lib.rs @@ -0,0 +1,385 @@ +// Copyright (C) 2022 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. + +//! # Bridge Asset Transfer Pallet +//! +//! A utility which could help move assets through bridges, e.g. move assets between different global consensus... + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; +use xcm::prelude::*; + +/// The log target of this pallet. +pub const LOG_TARGET: &str = "runtime::bridge-assets-transfer"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub (super) trait Store)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// XCM sender which sends messages to the BridgeHub + type BridgeXcmSender: SendXcm; + + // TODO: store as persistent and create add_bridge/remove_bridge - then we can have generic impl and dont need to hardcode NetworkId/ParaId in runtime + /// Configuration for supported bridged networks + type SupportedBridges: Get< + sp_std::prelude::Vec<(NetworkId, MultiLocation, Option)>, + >; + + /// Runtime's universal location + type UniversalLocation: Get; + } + + #[pallet::error] + #[cfg_attr(test, derive(PartialEq))] + pub enum Error { + InvalidConfiguration, + UnsupportedDestination, + BridgeCallError(#[codec(skip)] &'static str), + } + + #[pallet::event] + #[pallet::generate_deposit(pub (super) fn deposit_event)] + pub enum Event { + // TODO: add here xcm_hash? + /// Transfer was successfully entered to the system (does not mean already delivered) + TransferInitiated(XcmHash), + } + + #[pallet::call] + impl Pallet { + /// Transfer asset via bridge to different global consensus + /// + /// Parameters: + /// + /// * `assets`: + /// * `destination`: Different consensus location, where the assets will be deposited, e.g. Polkadot's Statemint: `X2(GlobalConsensus(NetworkId::Polkadot), Parachain(1000))` + /// + // TODO: correct weigth + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn transfer_asset_via_bridge( + origin: OriginFor, + assets: VersionedMultiAssets, + destination: VersionedMultiLocation, + ) -> DispatchResult { + let _ = ensure_signed(origin)?; + + // Check remote destination + let remote_destination = Self::ensure_remote_destination(destination)?; + + // TODO: do some checks + // TODO: check assets? + // TODO: check enought fee? + + // Deposit assets into `AccountId` that corresponds to the bridge + // hub. In this way, Statemine acts as a reserve location to the + // bridge, such that it need not trust any consensus system from + // `./Parent/Parent/...`. (It may trust Polkadot, but would + // Polkadot trust Kusama with its DOT?) + + // TODO: xcm - withdraw and fire ReserveAssetDeposited to the other side + + // TODO: send message through bridge + // Construct and send `Xcm(vec![Instruction])` to + // `./Parent/BridgeHubParaId`. + + // TODO: prepare ReserveAssetDeposited msg to bridge to the other side? + let xcm: Xcm<()> = + sp_std::vec![Instruction::ReserveAssetDeposited(Default::default())].into(); + + // TODO: how to compensate if this call fails? + log::info!( + target: LOG_TARGET, + "[T::BridgeXcmSender] send to bridge, remote_destination: {:?}, xcm: {:?}", + remote_destination, + xcm, + ); + // call bridge + let (ticket, fees) = + T::BridgeXcmSender::validate(&mut Some(remote_destination), &mut Some(xcm)) + .map_err(Self::convert_to_error)?; + log::info!( + target: LOG_TARGET, + "[T::BridgeXcmSender::validate] (TODO: process) fees: {:?}", + fees + ); + // TODO: what to do with fees - we have fees here, pay here or ignore? + // TODO: use fn send_msg + let xcm_hash = T::BridgeXcmSender::deliver(ticket).map_err(Self::convert_to_error)?; + + Self::deposit_event(Event::TransferInitiated(xcm_hash)); + Ok(()) + } + } + + impl Pallet { + /// Validates destination and check if we support bridging to this remote global consensus + /// + /// Returns: correct remote location, where we should be able to bridge + pub(crate) fn ensure_remote_destination( + destination: VersionedMultiLocation, + ) -> Result> { + match destination { + VersionedMultiLocation::V3(location) => { + ensure!(location.parent_count() == 2, Error::::UnsupportedDestination); + let local_network = T::UniversalLocation::get() + .global_consensus() + .map_err(|_| Error::::InvalidConfiguration)?; + let remote_network = location + .interior() + .global_consensus() + .map_err(|_| Error::::UnsupportedDestination)?; + ensure!(local_network != remote_network, Error::::UnsupportedDestination); + ensure!( + T::SupportedBridges::get() + .iter() + .find(|sb| sb.0 == remote_network) + .is_some(), + Error::::UnsupportedDestination + ); + Ok(location) + }, + _ => Err(Error::::UnsupportedDestination), + } + } + + fn convert_to_error(error: SendError) -> Error { + log::error!(target: LOG_TARGET, "SendError occurred, error: {:?}", error); + match error { + SendError::NotApplicable => Error::::BridgeCallError("NotApplicable"), + SendError::Transport(error) => Error::::BridgeCallError(error), + SendError::Unroutable => Error::::BridgeCallError("Unroutable"), + SendError::DestinationUnsupported => + Error::::BridgeCallError("DestinationUnsupported"), + SendError::ExceedsMaxMessageSize => + Error::::BridgeCallError("ExceedsMaxMessageSize"), + SendError::MissingArgument => Error::::BridgeCallError("MissingArgument"), + SendError::Fees => Error::::BridgeCallError("Fees"), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as bridge_assets_transfer; + + use frame_support::{parameter_types, sp_io, sp_tracing}; + use sp_runtime::{ + testing::{Header, H256}, + traits::{BlakeTwo256, IdentityLookup}, + }; + use sp_version::RuntimeVersion; + use xcm_builder::{NetworkExportTable, UnpaidRemoteExporter}; + + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum TestRuntime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + BridgeAssetsTransfer: bridge_assets_transfer::{Pallet, Call, Event} = 52, + } + ); + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub Version: RuntimeVersion = RuntimeVersion { + spec_name: sp_version::create_runtime_str!("test"), + impl_name: sp_version::create_runtime_str!("system-test"), + authoring_version: 1, + spec_version: 1, + impl_version: 1, + apis: sp_version::create_apis_vec!([]), + transaction_version: 1, + state_version: 1, + }; + } + + impl frame_system::Config for TestRuntime { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type Version = Version; + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = frame_support::traits::Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + } + + parameter_types! { + // UniversalLocation as statemine + pub const RelayNetwork: NetworkId = NetworkId::Kusama; + pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(1000)); + // Test bridge cfg + pub TestBridgeTable: sp_std::prelude::Vec<(NetworkId, MultiLocation, Option)> = sp_std::vec![ + (NetworkId::Wococo, (Parent, Parachain(1013)).into(), None), + (NetworkId::Polkadot, (Parent, Parachain(1003)).into(), None), + ]; + } + + std::thread_local! { + static ROUTED_MESSAGE: std::cell::RefCell>> = std::cell::RefCell::new(None); + } + + pub struct ThreadLocalXcmRouter; + impl SendXcm for ThreadLocalXcmRouter { + type Ticket = Option>; + + fn validate( + destination: &mut Option, + message: &mut Option>, + ) -> SendResult { + log::info!( + target: super::LOG_TARGET, + "[ThreadLocalXcmRouter]: destination: {:?}, message: {:?}", + destination, + message + ); + Ok((message.take(), MultiAssets::default())) + } + + fn deliver(ticket: Self::Ticket) -> Result { + match ticket { + Some(msg) => { + ROUTED_MESSAGE.with(|rm| *rm.borrow_mut() = Some(msg)); + Ok([0u8; 32]) + }, + None => Err(SendError::MissingArgument), + } + } + } + + /// Bridge router, which wraps and sends xcm to BridgeHub to be delivered to the different GlobalConsensus + pub type TestBridgeXcmSender = UnpaidRemoteExporter< + NetworkExportTable, + ThreadLocalXcmRouter, + UniversalLocation, + >; + + impl Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type BridgeXcmSender = TestBridgeXcmSender; + type SupportedBridges = TestBridgeTable; + type UniversalLocation = UniversalLocation; + } + + pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + frame_system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into() + } + + #[test] + fn test_ensure_remote_destination() { + new_test_ext().execute_with(|| { + // v2 not supported + assert_eq!( + BridgeAssetsTransfer::ensure_remote_destination(VersionedMultiLocation::V2( + xcm::v2::MultiLocation::default() + )), + Err(Error::::UnsupportedDestination) + ); + + // v3 - "parent: 0" wrong + assert_eq!( + BridgeAssetsTransfer::ensure_remote_destination(VersionedMultiLocation::V3( + MultiLocation::new(0, X2(GlobalConsensus(Wococo), Parachain(1000))) + )), + Err(Error::::UnsupportedDestination) + ); + // v3 - "parent: 1" wrong + assert_eq!( + BridgeAssetsTransfer::ensure_remote_destination(VersionedMultiLocation::V3( + MultiLocation::new(1, X2(GlobalConsensus(Wococo), Parachain(1000))) + )), + Err(Error::::UnsupportedDestination) + ); + + // v3 - Rococo is not supported + assert_eq!( + BridgeAssetsTransfer::ensure_remote_destination(VersionedMultiLocation::V3( + MultiLocation::new(2, X2(GlobalConsensus(Rococo), Parachain(1000))) + )), + Err(Error::::UnsupportedDestination) + ); + + // v3 - ok + assert_eq!( + BridgeAssetsTransfer::ensure_remote_destination(VersionedMultiLocation::V3( + MultiLocation::new(2, X2(GlobalConsensus(Wococo), Parachain(1000))) + )), + Ok(MultiLocation::new(2, X2(GlobalConsensus(Wococo), Parachain(1000)))) + ); + }) + } + + #[test] + fn test_transfer_asset_via_bridge_works() { + new_test_ext().execute_with(|| { + assert!(ROUTED_MESSAGE.with(|r| r.borrow().is_none())); + + let assets = VersionedMultiAssets::V3(MultiAssets::default()); + let destination = VersionedMultiLocation::V3(MultiLocation::new( + 2, + X2(GlobalConsensus(Wococo), Parachain(1000)), + )); + + let result = BridgeAssetsTransfer::transfer_asset_via_bridge( + RuntimeOrigin::signed(1), + assets, + destination, + ); + assert_eq!(result, Ok(())); + assert!(ROUTED_MESSAGE.with(|r| r.borrow().is_some())); + }); + } +} diff --git a/parachains/runtimes/assets/statemine/Cargo.toml b/parachains/runtimes/assets/statemine/Cargo.toml index 0880249c31..2e3109b1d5 100644 --- a/parachains/runtimes/assets/statemine/Cargo.toml +++ b/parachains/runtimes/assets/statemine/Cargo.toml @@ -68,6 +68,7 @@ cumulus-primitives-utility = { path = "../../../../primitives/utility", default- pallet-collator-selection = { path = "../../../../pallets/collator-selection", default-features = false } parachain-info = { path = "../../../pallets/parachain-info", default-features = false } parachains-common = { path = "../../../common", default-features = false } +bridge-assets-transfer = { path = "../../../../parachains/pallets/bridge-assets-transfer", default-features = false } pallet-xcm-benchmarks = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false, optional = true } @@ -177,4 +178,5 @@ std = [ "pallet-collator-selection/std", "parachain-info/std", "parachains-common/std", + "bridge-assets-transfer/std", ] diff --git a/parachains/runtimes/assets/statemine/src/lib.rs b/parachains/runtimes/assets/statemine/src/lib.rs index 2a2e1fce7c..e64ed09565 100644 --- a/parachains/runtimes/assets/statemine/src/lib.rs +++ b/parachains/runtimes/assets/statemine/src/lib.rs @@ -63,7 +63,7 @@ use parachains_common::{ opaque, AccountId, AssetId, AuraId, Balance, BlockNumber, Hash, Header, Index, Signature, AVERAGE_ON_INITIALIZE_RATIO, HOURS, MAXIMUM_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; -use xcm_config::{KsmLocation, XcmConfig}; +use xcm_config::{BridgeXcmSender, KsmLocation, XcmConfig}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; @@ -74,6 +74,7 @@ use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; use xcm::latest::BodyId; use xcm_executor::XcmExecutor; +use crate::xcm_config::{BridgeTable, UniversalLocation}; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; impl_opaque_keys! { @@ -569,6 +570,13 @@ impl pallet_uniques::Config for Runtime { type Locker = (); } +impl bridge_assets_transfer::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type BridgeXcmSender = BridgeXcmSender; + type SupportedBridges = BridgeTable; + type UniversalLocation = UniversalLocation; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -611,6 +619,7 @@ construct_runtime!( // The main stage. TrustBackedAssets: pallet_assets::::{Pallet, Call, Storage, Event} = 50, Uniques: pallet_uniques::{Pallet, Call, Storage, Event} = 51, + BridgeAssetsTransfer: bridge_assets_transfer::{Pallet, Call, Event} = 52, } ); diff --git a/parachains/runtimes/assets/statemine/src/xcm_config.rs b/parachains/runtimes/assets/statemine/src/xcm_config.rs index e23808d717..d5c9e41e9e 100644 --- a/parachains/runtimes/assets/statemine/src/xcm_config.rs +++ b/parachains/runtimes/assets/statemine/src/xcm_config.rs @@ -36,9 +36,10 @@ use xcm_builder::{ AccountId32Aliases, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, AsPrefixedGeneralIndex, ConvertedConcreteId, CurrencyAdapter, EnsureXcmOrigin, FungiblesAdapter, IsConcrete, - NativeAsset, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, - SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, - SovereignSignedViaLocation, TakeWeightCredit, UsingComponents, WeightInfoBounds, + NativeAsset, NetworkExportTable, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, UnpaidRemoteExporter, + UsingComponents, WeightInfoBounds, }; use xcm_executor::{traits::JustTry, XcmExecutor}; @@ -46,7 +47,7 @@ parameter_types! { pub const KsmLocation: MultiLocation = MultiLocation::parent(); pub const RelayNetwork: NetworkId = NetworkId::Kusama; pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); - pub UniversalLocation: InteriorMultiLocation = X1(Parachain(ParachainInfo::parachain_id().into())); + pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())); pub const Local: MultiLocation = Here.into_location(); pub TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); @@ -266,3 +267,15 @@ impl cumulus_pallet_xcm::Config for Runtime { type RuntimeEvent = RuntimeEvent; type XcmExecutor = XcmExecutor; } + +parameter_types! { + /// BridgedNetworkConsensus + Multilocation-to-LocalGlobalConsensusBridgeHub + LocalGlobalConsensusBridgeHub + pub BridgeTable: sp_std::prelude::Vec<(NetworkId, MultiLocation, Option)> = sp_std::vec![ + (NetworkId::Wococo, (Parent, Parachain(1013)).into(), None), + (NetworkId::Polkadot, (Parent, Parachain(1003)).into(), None), + ]; +} + +/// Bridge router, which wraps and sends xcm to BridgeHub to be delivered to the different GlobalConsensus +pub type BridgeXcmSender = + UnpaidRemoteExporter, XcmRouter, UniversalLocation>;