diff --git a/Cargo.lock b/Cargo.lock index 4724837ad77..a6fa60d14c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,26 +334,42 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" name = "asset-test-utils" version = "1.0.0" dependencies = [ + "assets-common", + "cumulus-pallet-parachain-system", + "cumulus-pallet-xcmp-queue", + "cumulus-primitives-core", + "cumulus-primitives-parachain-inherent", + "cumulus-test-relay-sproof-builder", "frame-support", "frame-system", "hex-literal", + "pallet-assets", "pallet-balances", "pallet-collator-selection", "pallet-session", + "pallet-xcm", + "parachain-info", "parachains-common", + "parity-scale-codec", + "polkadot-parachain", "sp-consensus-aura", "sp-core", "sp-io", "sp-runtime", "sp-std", "substrate-wasm-builder", + "xcm", + "xcm-executor", ] [[package]] name = "assets-common" version = "0.1.0" dependencies = [ + "cumulus-primitives-core", "frame-support", + "log", + "pallet-xcm", "parachains-common", "parity-scale-codec", "sp-api", diff --git a/parachains/common/src/impls.rs b/parachains/common/src/impls.rs index 40387617bcd..cc89a248e2e 100644 --- a/parachains/common/src/impls.rs +++ b/parachains/common/src/impls.rs @@ -95,6 +95,18 @@ where } } +/// Allow checking in assets that exists. +pub struct AssetExists(PhantomData<(AccountId, Assets)>); +impl Contains<>::AssetId> + for AssetExists +where + Assets: fungibles::Inspect, +{ + fn contains(id: &>::AssetId) -> bool { + Assets::asset_exists(*id) + } +} + /// Asset filter that allows all assets from a certain location. pub struct AssetsFrom(PhantomData); impl> ContainsPair for AssetsFrom { diff --git a/parachains/runtimes/assets/common/Cargo.toml b/parachains/runtimes/assets/common/Cargo.toml index c551a97757b..7a795057cd5 100644 --- a/parachains/runtimes/assets/common/Cargo.toml +++ b/parachains/runtimes/assets/common/Cargo.toml @@ -7,6 +7,7 @@ description = "Assets common utilities" [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } # Substrate frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } @@ -14,12 +15,14 @@ sp-api = { git = "https://github.com/paritytech/substrate", default-features = f sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } # Polkadot +pallet-xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } xcm-builder = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } xcm-executor = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } # Cumulus parachains-common = { path = "../../../common", default-features = false } +cumulus-primitives-core = { path = "../../../../primitives/core", default-features = false } [build-dependencies] substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", branch = "master" } @@ -28,11 +31,20 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", bran default = [ "std" ] std = [ "codec/std", + "log/std", "frame-support/std", "parachains-common/std", + "cumulus-primitives-core/std", "sp-api/std", "sp-std/std", + "pallet-xcm/std", "xcm/std", "xcm-builder/std", "xcm-executor/std", ] + +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "pallet-xcm/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", +] diff --git a/parachains/runtimes/assets/common/src/foreign_creators.rs b/parachains/runtimes/assets/common/src/foreign_creators.rs new file mode 100644 index 00000000000..3d7567409f6 --- /dev/null +++ b/parachains/runtimes/assets/common/src/foreign_creators.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2023 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 frame_support::traits::{ + ContainsPair, EnsureOrigin, EnsureOriginWithArg, Everything, OriginTrait, +}; +use pallet_xcm::{EnsureXcm, Origin as XcmOrigin}; +use xcm::latest::MultiLocation; +use xcm_executor::traits::Convert; + +// `EnsureOriginWithArg` impl for `CreateOrigin` that allows only XCM origins that are locations +// containing the class location. +pub struct ForeignCreators( + sp_std::marker::PhantomData<(IsForeign, AccountOf, AccountId)>, +); +impl< + IsForeign: ContainsPair, + AccountOf: Convert, + AccountId: Clone, + RuntimeOrigin: From + OriginTrait + Clone, + > EnsureOriginWithArg + for ForeignCreators +where + RuntimeOrigin::PalletsOrigin: + From + TryInto, +{ + type Success = AccountId; + + fn try_origin( + origin: RuntimeOrigin, + asset_location: &MultiLocation, + ) -> sp_std::result::Result { + let origin_location = EnsureXcm::::try_origin(origin.clone())?; + if !IsForeign::contains(&asset_location, &origin_location) { + return Err(origin) + } + AccountOf::convert(origin_location).map_err(|_| origin) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin(a: &MultiLocation) -> Result { + Ok(pallet_xcm::Origin::Xcm(a.clone()).into()) + } +} diff --git a/parachains/runtimes/assets/common/src/fungible_conversion.rs b/parachains/runtimes/assets/common/src/fungible_conversion.rs index 2b8413cfe6e..8ffb44b086b 100644 --- a/parachains/runtimes/assets/common/src/fungible_conversion.rs +++ b/parachains/runtimes/assets/common/src/fungible_conversion.rs @@ -1,27 +1,25 @@ -// This file is part of Substrate. - -// Copyright (C) 2018-2022 Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// Copyright (C) 2023 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. //! Runtime API definition for assets. use crate::runtime_api::FungiblesAccessError; +use frame_support::traits::Contains; use sp_std::{borrow::Borrow, vec::Vec}; use xcm::latest::{MultiAsset, MultiLocation}; -use xcm_builder::ConvertedConcreteId; +use xcm_builder::{ConvertedConcreteId, MatchedConvertedConcreteId}; use xcm_executor::traits::{Convert, MatchesFungibles}; /// Converting any [`(AssetId, Balance)`] to [`MultiAsset`] @@ -60,6 +58,29 @@ impl< } } +impl< + AssetId: Clone, + Balance: Clone, + MatchAssetId: Contains, + ConvertAssetId: Convert, + ConvertBalance: Convert, + > MultiAssetConverter + for MatchedConvertedConcreteId +{ + fn convert_ref( + value: impl Borrow<(AssetId, Balance)>, + ) -> Result { + let (asset_id, balance) = value.borrow(); + match ConvertAssetId::reverse_ref(asset_id) { + Ok(asset_id_as_multilocation) => match ConvertBalance::reverse_ref(balance) { + Ok(amount) => Ok((asset_id_as_multilocation, amount).into()), + Err(_) => Err(FungiblesAccessError::AmountToBalanceConversionFailed), + }, + Err(_) => Err(FungiblesAccessError::AssetIdConversionFailed), + } + } +} + /// Helper function to convert collections with [`(AssetId, Balance)`] to [`MultiAsset`] pub fn convert<'a, AssetId, Balance, ConvertAssetId, ConvertBalance, Converter>( items: impl Iterator, @@ -90,11 +111,12 @@ pub fn convert_balance< #[cfg(test)] mod tests { use super::*; + use frame_support::traits::Everything; use xcm::latest::prelude::*; use xcm_executor::traits::{Identity, JustTry}; - type Converter = ConvertedConcreteId; + type Converter = MatchedConvertedConcreteId; #[test] fn converted_concrete_id_fungible_multi_asset_conversion_roundtrip_works() { diff --git a/parachains/runtimes/assets/common/src/lib.rs b/parachains/runtimes/assets/common/src/lib.rs index 28d8ca59106..8a321ad97aa 100644 --- a/parachains/runtimes/assets/common/src/lib.rs +++ b/parachains/runtimes/assets/common/src/lib.rs @@ -15,39 +15,79 @@ #![cfg_attr(not(feature = "std"), no_std)] +pub mod foreign_creators; pub mod fungible_conversion; +pub mod matching; pub mod runtime_api; +use crate::matching::{Equals, LocalMultiLocationPattern, ParentLocation, StartsWith}; +use frame_support::traits::EverythingBut; use parachains_common::AssetIdForTrustBackedAssets; -use xcm_builder::{AsPrefixedGeneralIndex, ConvertedConcreteId}; -use xcm_executor::traits::JustTry; +use xcm::prelude::MultiLocation; +use xcm_builder::{AsPrefixedGeneralIndex, MatchedConvertedConcreteId}; +use xcm_executor::traits::{Identity, JustTry}; /// `MultiLocation` vs `AssetIdForTrustBackedAssets` converter for `TrustBackedAssets` pub type AssetIdForTrustBackedAssetsConvert = AsPrefixedGeneralIndex; -/// [`ConvertedConcreteId`] converter dedicated for `TrustBackedAssets` +/// [`MatchedConvertedConcreteId`] converter dedicated for `TrustBackedAssets` pub type TrustBackedAssetsConvertedConcreteId = - ConvertedConcreteId< + MatchedConvertedConcreteId< AssetIdForTrustBackedAssets, Balance, + StartsWith, AssetIdForTrustBackedAssetsConvert, JustTry, >; +/// AssetId used for identifying assets by MultiLocation. +pub type MultiLocationForAssetId = MultiLocation; + +/// [`MatchedConvertedConcreteId`] converter dedicated for storing `AssetId` as `MultiLocation`. +pub type MultiLocationConvertedConcreteId = + MatchedConvertedConcreteId< + MultiLocationForAssetId, + Balance, + MultiLocationFilter, + Identity, + JustTry, + >; + +/// [`MatchedConvertedConcreteId`] converter dedicated for storing `ForeignAssets` with `AssetId` as `MultiLocation`. +/// +/// Excludes by default: +/// - parent as relay chain +/// - all local MultiLocations +/// +/// `AdditionalMultiLocationExclusionFilter` can customize additional excluded MultiLocations +pub type ForeignAssetsConvertedConcreteId = + MultiLocationConvertedConcreteId< + EverythingBut<( + // Excludes relay/parent chain currency + Equals, + // Here we rely on fact that something like this works: + // assert!(MultiLocation::new(1, X1(Parachain(100))).starts_with(&MultiLocation::parent())); + // assert!(X1(Parachain(100)).starts_with(&Here)); + StartsWith, + // Here we can exclude more stuff or leave it as `()` + AdditionalMultiLocationExclusionFilter, + )>, + Balance, + >; + #[cfg(test)] mod tests { - use super::*; + use crate::matching::StartsWithExplicitGlobalConsensus; use xcm::latest::prelude::*; - use xcm_executor::traits::Convert; - - frame_support::parameter_types! { - pub TrustBackedAssetsPalletLocation: MultiLocation = MultiLocation::new(5, X1(PalletInstance(13))); - } + use xcm_executor::traits::{Convert, Error as MatchError, MatchesFungibles}; #[test] fn asset_id_for_trust_backed_assets_convert_works() { + frame_support::parameter_types! { + pub TrustBackedAssetsPalletLocation: MultiLocation = MultiLocation::new(5, X1(PalletInstance(13))); + } let local_asset_id = 123456789 as AssetIdForTrustBackedAssets; let expected_reverse_ref = MultiLocation::new(5, X2(PalletInstance(13), GeneralIndex(local_asset_id.into()))); @@ -67,4 +107,201 @@ mod tests { local_asset_id ); } + + #[test] + fn trust_backed_assets_match_fungibles_works() { + frame_support::parameter_types! { + pub TrustBackedAssetsPalletLocation: MultiLocation = MultiLocation::new(0, X1(PalletInstance(13))); + } + // setup convert + type TrustBackedAssetsConvert = + TrustBackedAssetsConvertedConcreteId; + + let test_data = vec![ + // missing GeneralIndex + (ma_1000(0, X1(PalletInstance(13))), Err(MatchError::AssetIdConversionFailed)), + ( + ma_1000(0, X2(PalletInstance(13), GeneralKey { data: [0; 32], length: 32 })), + Err(MatchError::AssetIdConversionFailed), + ), + ( + ma_1000(0, X2(PalletInstance(13), Parachain(1000))), + Err(MatchError::AssetIdConversionFailed), + ), + // OK + (ma_1000(0, X2(PalletInstance(13), GeneralIndex(1234))), Ok((1234, 1000))), + ( + ma_1000(0, X3(PalletInstance(13), GeneralIndex(1234), GeneralIndex(2222))), + Ok((1234, 1000)), + ), + ( + ma_1000( + 0, + X4( + PalletInstance(13), + GeneralIndex(1234), + GeneralIndex(2222), + GeneralKey { data: [0; 32], length: 32 }, + ), + ), + Ok((1234, 1000)), + ), + // wrong pallet instance + ( + ma_1000(0, X2(PalletInstance(77), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(0, X3(PalletInstance(77), GeneralIndex(1234), GeneralIndex(2222))), + Err(MatchError::AssetNotHandled), + ), + // wrong parent + ( + ma_1000(1, X2(PalletInstance(13), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(1, X3(PalletInstance(13), GeneralIndex(1234), GeneralIndex(2222))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(1, X2(PalletInstance(77), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(1, X3(PalletInstance(77), GeneralIndex(1234), GeneralIndex(2222))), + Err(MatchError::AssetNotHandled), + ), + // wrong parent + ( + ma_1000(2, X2(PalletInstance(13), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(2, X3(PalletInstance(13), GeneralIndex(1234), GeneralIndex(2222))), + Err(MatchError::AssetNotHandled), + ), + // missing GeneralIndex + (ma_1000(0, X1(PalletInstance(77))), Err(MatchError::AssetNotHandled)), + (ma_1000(1, X1(PalletInstance(13))), Err(MatchError::AssetNotHandled)), + (ma_1000(2, X1(PalletInstance(13))), Err(MatchError::AssetNotHandled)), + ]; + + for (multi_asset, expected_result) in test_data { + assert_eq!( + >::matches_fungibles(&multi_asset), + expected_result, "multi_asset: {:?}", multi_asset); + } + } + + #[test] + fn multi_location_converted_concrete_id_converter_works() { + frame_support::parameter_types! { + pub Parachain100Pattern: MultiLocation = MultiLocation::new(1, X1(Parachain(100))); + pub UniversalLocationNetworkId: NetworkId = NetworkId::ByGenesis([9; 32]); + } + + // setup convert + type Convert = ForeignAssetsConvertedConcreteId< + ( + StartsWith, + StartsWithExplicitGlobalConsensus, + ), + u128, + >; + + let test_data = vec![ + // excluded as local + (ma_1000(0, Here), Err(MatchError::AssetNotHandled)), + (ma_1000(0, X1(Parachain(100))), Err(MatchError::AssetNotHandled)), + ( + ma_1000(0, X2(PalletInstance(13), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + // excluded as parent + (ma_1000(1, Here), Err(MatchError::AssetNotHandled)), + // excluded as additional filter - Parachain100Pattern + (ma_1000(1, X1(Parachain(100))), Err(MatchError::AssetNotHandled)), + (ma_1000(1, X2(Parachain(100), GeneralIndex(1234))), Err(MatchError::AssetNotHandled)), + ( + ma_1000(1, X3(Parachain(100), PalletInstance(13), GeneralIndex(1234))), + Err(MatchError::AssetNotHandled), + ), + // excluded as additional filter - StartsWithExplicitGlobalConsensus + ( + ma_1000(1, X1(GlobalConsensus(NetworkId::ByGenesis([9; 32])))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000(2, X1(GlobalConsensus(NetworkId::ByGenesis([9; 32])))), + Err(MatchError::AssetNotHandled), + ), + ( + ma_1000( + 2, + X3( + GlobalConsensus(NetworkId::ByGenesis([9; 32])), + Parachain(200), + GeneralIndex(1234), + ), + ), + Err(MatchError::AssetNotHandled), + ), + // ok + (ma_1000(1, X1(Parachain(200))), Ok((MultiLocation::new(1, X1(Parachain(200))), 1000))), + (ma_1000(2, X1(Parachain(200))), Ok((MultiLocation::new(2, X1(Parachain(200))), 1000))), + ( + ma_1000(1, X2(Parachain(200), GeneralIndex(1234))), + Ok((MultiLocation::new(1, X2(Parachain(200), GeneralIndex(1234))), 1000)), + ), + ( + ma_1000(2, X2(Parachain(200), GeneralIndex(1234))), + Ok((MultiLocation::new(2, X2(Parachain(200), GeneralIndex(1234))), 1000)), + ), + ( + ma_1000(2, X1(GlobalConsensus(NetworkId::ByGenesis([7; 32])))), + Ok(( + MultiLocation::new(2, X1(GlobalConsensus(NetworkId::ByGenesis([7; 32])))), + 1000, + )), + ), + ( + ma_1000( + 2, + X3( + GlobalConsensus(NetworkId::ByGenesis([7; 32])), + Parachain(200), + GeneralIndex(1234), + ), + ), + Ok(( + MultiLocation::new( + 2, + X3( + GlobalConsensus(NetworkId::ByGenesis([7; 32])), + Parachain(200), + GeneralIndex(1234), + ), + ), + 1000, + )), + ), + ]; + + for (multi_asset, expected_result) in test_data { + assert_eq!( + >::matches_fungibles( + &multi_asset + ), + expected_result, + "multi_asset: {:?}", + multi_asset + ); + } + } + + // Create MultiAsset + fn ma_1000(parents: u8, interior: Junctions) -> MultiAsset { + (MultiLocation::new(parents, interior), 1000).into() + } } diff --git a/parachains/runtimes/assets/common/src/matching.rs b/parachains/runtimes/assets/common/src/matching.rs new file mode 100644 index 00000000000..a5e030412b9 --- /dev/null +++ b/parachains/runtimes/assets/common/src/matching.rs @@ -0,0 +1,91 @@ +// Copyright (C) 2023 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 cumulus_primitives_core::ParaId; +use frame_support::{ + pallet_prelude::Get, + traits::{Contains, ContainsPair}, +}; +use xcm::{ + latest::prelude::{MultiAsset, MultiLocation}, + prelude::*, +}; + +pub struct StartsWith(sp_std::marker::PhantomData); +impl> Contains for StartsWith { + fn contains(t: &MultiLocation) -> bool { + t.starts_with(&Location::get()) + } +} + +pub struct Equals(sp_std::marker::PhantomData); +impl> Contains for Equals { + fn contains(t: &MultiLocation) -> bool { + t == &Location::get() + } +} + +pub struct StartsWithExplicitGlobalConsensus(sp_std::marker::PhantomData); +impl> Contains + for StartsWithExplicitGlobalConsensus +{ + fn contains(t: &MultiLocation) -> bool { + match t.interior.global_consensus() { + Ok(requested_network) if requested_network.eq(&Network::get()) => true, + _ => false, + } + } +} + +frame_support::parameter_types! { + pub LocalMultiLocationPattern: MultiLocation = MultiLocation::new(0, Here); + pub ParentLocation: MultiLocation = MultiLocation::parent(); +} + +/// Accepts an asset if it is from the origin. +pub struct IsForeignConcreteAsset(sp_std::marker::PhantomData); +impl> ContainsPair + for IsForeignConcreteAsset +{ + fn contains(asset: &MultiAsset, origin: &MultiLocation) -> bool { + log::trace!(target: "xcm::contains", "IsForeignConcreteAsset asset: {:?}, origin: {:?}", asset, origin); + matches!(asset.id, Concrete(ref id) if IsForeign::contains(id, &origin)) + } +} + +/// Checks if `a` is from sibling location `b`. Checks that `MultiLocation-a` starts with +/// `MultiLocation-b`, and that the `ParaId` of `b` is not equal to `a`. +pub struct FromSiblingParachain(sp_std::marker::PhantomData); +impl> ContainsPair + for FromSiblingParachain +{ + fn contains(&a: &MultiLocation, b: &MultiLocation) -> bool { + // `a` needs to be from `b` at least + if !a.starts_with(&b) { + return false + } + + // here we check if sibling + match a { + MultiLocation { parents: 1, interior } => match interior.first() { + Some(Parachain(sibling_para_id)) + if sibling_para_id.ne(&u32::from(SelfParaId::get())) => + true, + _ => false, + }, + _ => false, + } + } +} diff --git a/parachains/runtimes/assets/statemine/Cargo.toml b/parachains/runtimes/assets/statemine/Cargo.toml index e2943d0d74d..31f34516a5c 100644 --- a/parachains/runtimes/assets/statemine/Cargo.toml +++ b/parachains/runtimes/assets/statemine/Cargo.toml @@ -112,6 +112,7 @@ runtime-benchmarks = [ "cumulus-pallet-xcmp-queue/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", "pallet-state-trie-migration/runtime-benchmarks", + "assets-common/runtime-benchmarks", ] try-runtime = [ "cumulus-pallet-aura-ext/try-runtime", diff --git a/parachains/runtimes/assets/statemine/src/lib.rs b/parachains/runtimes/assets/statemine/src/lib.rs index c4fbad66349..4f03b049efa 100644 --- a/parachains/runtimes/assets/statemine/src/lib.rs +++ b/parachains/runtimes/assets/statemine/src/lib.rs @@ -66,19 +66,23 @@ use parachains_common::{ NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; use xcm_config::{ - FellowshipLocation, GovernanceLocation, KsmLocation, TrustBackedAssetsConvertedConcreteId, - XcmConfig, + FellowshipLocation, ForeignAssetsConvertedConcreteId, GovernanceLocation, KsmLocation, + TrustBackedAssetsConvertedConcreteId, XcmConfig, }; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; // Polkadot imports +use assets_common::{ + foreign_creators::ForeignCreators, matching::FromSiblingParachain, MultiLocationForAssetId, +}; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; use xcm::latest::BodyId; use xcm_executor::XcmExecutor; +use crate::xcm_config::ForeignCreatorsSovereignAccountOf; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; impl_opaque_keys! { @@ -264,6 +268,48 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = (); } +parameter_types! { + // we just reuse the same deposits + pub const ForeignAssetsAssetDeposit: Balance = AssetDeposit::get(); + pub const ForeignAssetsAssetAccountDeposit: Balance = AssetAccountDeposit::get(); + pub const ForeignAssetsApprovalDeposit: Balance = ApprovalDeposit::get(); + pub const ForeignAssetsAssetsStringLimit: u32 = AssetsStringLimit::get(); + pub const ForeignAssetsMetadataDepositBase: Balance = MetadataDepositBase::get(); + pub const ForeignAssetsMetadataDepositPerByte: Balance = MetadataDepositPerByte::get(); +} + +/// Assets managed by some foreign location. Note: we do not declare a `ForeignAssetsCall` type, as +/// this type is used in proxy definitions. We assume that a foreign location would not want to set +/// an individual, local account as a proxy for the issuance of their assets. This issuance should +/// be managed by the foreign location's governance. +pub type ForeignAssetsInstance = pallet_assets::Instance2; +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = MultiLocationForAssetId; + type AssetIdParameter = MultiLocationForAssetId; + type Currency = Balances; + type CreateOrigin = ForeignCreators< + (FromSiblingParachain>,), + ForeignCreatorsSovereignAccountOf, + AccountId, + >; + type ForceOrigin = AssetsForceOrigin; + type AssetDeposit = ForeignAssetsAssetDeposit; + type MetadataDepositBase = ForeignAssetsMetadataDepositBase; + type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte; + type ApprovalDeposit = ForeignAssetsApprovalDeposit; + type StringLimit = ForeignAssetsAssetsStringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = weights::pallet_assets::WeightInfo; + type CallbackHandle = (); + type AssetAccountDeposit = ForeignAssetsAssetAccountDeposit; + type RemoveItemsLimit = frame_support::traits::ConstU32<1000>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; +} + parameter_types! { // One storage item; key size is 32; value is size 4+4+16+32 bytes = 56 bytes. pub const DepositBase: Balance = deposit(1, 88); @@ -336,6 +382,7 @@ impl Default for ProxyType { Self::Any } } + impl InstanceFilter for ProxyType { fn filter(&self, c: &RuntimeCall) -> bool { match self { @@ -689,6 +736,7 @@ construct_runtime!( Assets: pallet_assets::::{Pallet, Call, Storage, Event} = 50, Uniques: pallet_uniques::{Pallet, Call, Storage, Event} = 51, Nfts: pallet_nfts::{Pallet, Call, Storage, Event} = 52, + ForeignAssets: pallet_assets::::{Pallet, Call, Storage, Event} = 53, #[cfg(feature = "state-trie-version-1")] StateTrieMigration: pallet_state_trie_migration = 70, @@ -741,6 +789,7 @@ mod benches { define_benchmarks!( [frame_system, SystemBench::] [pallet_assets, Assets] + [pallet_assets, ForeignAssets] [pallet_balances, Balances] [pallet_multisig, Multisig] [pallet_nfts, Nfts] @@ -916,11 +965,17 @@ impl_runtime_apis! { }, // collect pallet_assets (TrustBackedAssets) convert::<_, _, _, _, TrustBackedAssetsConvertedConcreteId>( - Assets::account_balances(account) + Assets::account_balances(account.clone()) + .iter() + .filter(|(_, balance)| balance > &0) + )?, + // collect pallet_assets (ForeignAssets) + convert::<_, _, _, _, ForeignAssetsConvertedConcreteId>( + ForeignAssets::account_balances(account) .iter() .filter(|(_, balance)| balance > &0) )?, - // collect ... e.g. pallet_assets ForeignAssets + // collect ... e.g. other tokens ].concat()) } } diff --git a/parachains/runtimes/assets/statemine/src/xcm_config.rs b/parachains/runtimes/assets/statemine/src/xcm_config.rs index acabe9c878b..5d4c45fd4f3 100644 --- a/parachains/runtimes/assets/statemine/src/xcm_config.rs +++ b/parachains/runtimes/assets/statemine/src/xcm_config.rs @@ -14,10 +14,13 @@ // limitations under the License. use super::{ - AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, ParachainInfo, - ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, ForeignAssets, + ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, TrustBackedAssetsInstance, WeightToFee, XcmpQueue, }; +use assets_common::matching::{ + FromSiblingParachain, IsForeignConcreteAsset, StartsWith, StartsWithExplicitGlobalConsensus, +}; use frame_support::{ match_types, parameter_types, traits::{ConstU32, Contains, Everything, Nothing, PalletInfoAccess}, @@ -36,8 +39,8 @@ use xcm::latest::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, CurrencyAdapter, EnsureXcmOrigin, - FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, NoChecking, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, UsingComponents, WeightInfoBounds, WithComputedOrigin, }; @@ -49,7 +52,7 @@ parameter_types! { pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(ParachainInfo::parachain_id().into())); - pub const Local: MultiLocation = Here.into_location(); + pub UniversalLocationNetworkId: NetworkId = UniversalLocation::get().global_consensus().unwrap(); pub TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); @@ -83,7 +86,7 @@ pub type CurrencyTransactor = CurrencyAdapter< (), >; -/// `AssetId/Balancer` converter for `TrustBackedAssets` +/// `AssetId/Balance` converter for `TrustBackedAssets` pub type TrustBackedAssetsConvertedConcreteId = assets_common::TrustBackedAssetsConvertedConcreteId; @@ -103,8 +106,38 @@ pub type FungiblesTransactor = FungiblesAdapter< // The account to use for tracking teleports. CheckingAccount, >; + +/// `AssetId/Balance` converter for `TrustBackedAssets` +pub type ForeignAssetsConvertedConcreteId = assets_common::ForeignAssetsConvertedConcreteId< + ( + // Ignore `TrustBackedAssets` explicitly + StartsWith, + // Ignore asset which starts explicitly with our `GlobalConsensus(NetworkId)`, means: + // - foreign assets from our consensus should be: `MultiLocation {parent: 1, X*(Parachain(xyz))} + // - foreign assets outside our consensus with the same `GlobalConsensus(NetworkId)` wont be accepted here + StartsWithExplicitGlobalConsensus, + ), + Balance, +>; + +/// Means for transacting foreign assets from different global consensus. +pub type ForeignFungiblesTransactor = FungiblesAdapter< + // Use this fungibles implementation: + ForeignAssets, + // Use this currency when it is a fungible asset matching the given location or name: + ForeignAssetsConvertedConcreteId, + // Convert an XCM MultiLocation into a local account id: + LocationToAccountId, + // Our chain's account ID type (we can't get away without mentioning it explicitly): + AccountId, + // We dont need to check teleports here. + NoChecking, + // The account to use for tracking teleports. + CheckingAccount, +>; + /// Means for transacting assets on this chain. -pub type AssetTransactors = (CurrencyTransactor, FungiblesTransactor); +pub type AssetTransactors = (CurrencyTransactor, FungiblesTransactor, ForeignFungiblesTransactor); /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, /// ready for dispatching a transaction with Xcm's `Transact`. There is an `OriginKind` which can @@ -213,6 +246,35 @@ impl Contains for SafeCallFilter { pallet_assets::Call::touch { .. } | pallet_assets::Call::refund { .. }, ) | + RuntimeCall::ForeignAssets( + pallet_assets::Call::create { .. } | + pallet_assets::Call::force_create { .. } | + pallet_assets::Call::start_destroy { .. } | + pallet_assets::Call::destroy_accounts { .. } | + pallet_assets::Call::destroy_approvals { .. } | + pallet_assets::Call::finish_destroy { .. } | + pallet_assets::Call::mint { .. } | + pallet_assets::Call::burn { .. } | + pallet_assets::Call::transfer { .. } | + pallet_assets::Call::transfer_keep_alive { .. } | + pallet_assets::Call::force_transfer { .. } | + pallet_assets::Call::freeze { .. } | + pallet_assets::Call::thaw { .. } | + pallet_assets::Call::freeze_asset { .. } | + pallet_assets::Call::thaw_asset { .. } | + pallet_assets::Call::transfer_ownership { .. } | + pallet_assets::Call::set_team { .. } | + pallet_assets::Call::set_metadata { .. } | + pallet_assets::Call::clear_metadata { .. } | + pallet_assets::Call::force_clear_metadata { .. } | + pallet_assets::Call::force_asset_status { .. } | + pallet_assets::Call::approve_transfer { .. } | + pallet_assets::Call::cancel_approval { .. } | + pallet_assets::Call::force_cancel_approval { .. } | + pallet_assets::Call::transfer_approved { .. } | + pallet_assets::Call::touch { .. } | + pallet_assets::Call::refund { .. }, + ) | RuntimeCall::Nfts( pallet_nfts::Call::create { .. } | pallet_nfts::Call::force_create { .. } | @@ -322,7 +384,13 @@ impl xcm_executor::Config for XcmConfig { // Statemine acting _as_ a reserve location for KSM and assets created under `pallet-assets`. // For KSM, users must use teleport where allowed (e.g. with the Relay Chain). type IsReserve = (); - type IsTeleporter = NativeAsset; // <- should be enough to allow teleportation of KSM + // We allow: + // - teleportation of KSM + // - teleportation of sibling parachain's assets (as ForeignCreators) + type IsTeleporter = ( + NativeAsset, + IsForeignConcreteAsset>>, + ); type UniversalLocation = UniversalLocation; type Barrier = Barrier; type Weigher = WeightInfoBounds< @@ -415,3 +483,20 @@ impl cumulus_pallet_xcm::Config for Runtime { type RuntimeEvent = RuntimeEvent; type XcmExecutor = XcmExecutor; } + +pub type ForeignCreatorsSovereignAccountOf = ( + SiblingParachainConvertsVia, + AccountId32Aliases, + ParentIsPreset, +); + +/// Simple conversion of `u32` into an `AssetId` for use in benchmarking. +pub struct XcmBenchmarkHelper; +#[cfg(feature = "runtime-benchmarks")] +use pallet_assets::BenchmarkHelper; +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for XcmBenchmarkHelper { + fn create_asset_id_parameter(id: u32) -> MultiLocation { + MultiLocation { parents: 1, interior: X1(Parachain(id)) } + } +} diff --git a/parachains/runtimes/assets/statemine/tests/tests.rs b/parachains/runtimes/assets/statemine/tests/tests.rs index b696e4ef1e3..972abe9d427 100644 --- a/parachains/runtimes/assets/statemine/tests/tests.rs +++ b/parachains/runtimes/assets/statemine/tests/tests.rs @@ -1,25 +1,27 @@ use asset_test_utils::{ExtBuilder, RuntimeHelper}; -use codec::Encode; +use codec::{Decode, Encode}; use cumulus_primitives_utility::ChargeWeightInFungibles; use frame_support::{ - assert_noop, assert_ok, sp_io, + assert_noop, assert_ok, + traits::fungibles::InspectEnumerable, weights::{Weight, WeightToFee as WeightToFeeT}, }; -use parachains_common::{AccountId, AuraId, Balance}; +use parachains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId, Balance}; use statemine_runtime::xcm_config::{ AssetFeeAsExistentialDepositMultiplierFeeCharger, KsmLocation, TrustBackedAssetsPalletLocation, }; pub use statemine_runtime::{ - constants::fee::WeightToFee, xcm_config::XcmConfig, Assets, Balances, ExistentialDeposit, - ReservedDmpWeight, Runtime, SessionKeys, System, + constants::fee::WeightToFee, + xcm_config::{CheckingAccount, ForeignCreatorsSovereignAccountOf, XcmConfig}, + AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, + MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, Runtime, RuntimeCall, + RuntimeEvent, SessionKeys, System, TrustBackedAssetsInstance, }; use xcm::latest::prelude::*; -use xcm_executor::{ - traits::{Convert, WeightTrader}, - XcmExecutor, -}; +use xcm_executor::traits::{Convert, Identity, JustTry, WeightTrader}; -pub const ALICE: [u8; 32] = [1u8; 32]; +const ALICE: [u8; 32] = [1u8; 32]; +const SOME_ASSET_ADMIN: [u8; 32] = [5u8; 32]; type AssetIdForTrustBackedAssetsConvert = assets_common::AssetIdForTrustBackedAssetsConvert; @@ -371,9 +373,15 @@ fn test_assets_balances_api_works() { .build() .execute_with(|| { let local_asset_id = 1; + let foreign_asset_id_multilocation = + MultiLocation { parents: 1, interior: X2(Parachain(1234), GeneralIndex(12345)) }; // check before assert_eq!(Assets::balance(local_asset_id, AccountId::from(ALICE)), 0); + assert_eq!( + ForeignAssets::balance(foreign_asset_id_multilocation, AccountId::from(ALICE)), + 0 + ); assert_eq!(Balances::free_balance(AccountId::from(ALICE)), 0); assert!(Runtime::query_account_balances(AccountId::from(ALICE)).unwrap().is_empty()); @@ -400,15 +408,37 @@ fn test_assets_balances_api_works() { minimum_asset_balance )); + // create foreign asset + let foreign_asset_minimum_asset_balance = 3333333_u128; + assert_ok!(ForeignAssets::force_create( + RuntimeHelper::::root_origin(), + foreign_asset_id_multilocation.clone().into(), + AccountId::from(SOME_ASSET_ADMIN).into(), + false, + foreign_asset_minimum_asset_balance + )); + + // We first mint enough asset for the account to exist for assets + assert_ok!(ForeignAssets::mint( + RuntimeHelper::::origin_of(AccountId::from(SOME_ASSET_ADMIN)), + foreign_asset_id_multilocation.clone().into(), + AccountId::from(ALICE).into(), + 6 * foreign_asset_minimum_asset_balance + )); + // check after assert_eq!( Assets::balance(local_asset_id, AccountId::from(ALICE)), minimum_asset_balance ); + assert_eq!( + ForeignAssets::balance(foreign_asset_id_multilocation, AccountId::from(ALICE)), + 6 * minimum_asset_balance + ); assert_eq!(Balances::free_balance(AccountId::from(ALICE)), some_currency); let result = Runtime::query_account_balances(AccountId::from(ALICE)).unwrap(); - assert_eq!(result.len(), 2); + assert_eq!(result.len(), 3); // check currency assert!(result.iter().any(|asset| asset.eq( @@ -423,53 +453,162 @@ fn test_assets_balances_api_works() { minimum_asset_balance ) .into()))); + // check foreign asset + assert!(result.iter().any(|asset| asset.eq(&( + Identity::reverse_ref(foreign_asset_id_multilocation).unwrap(), + 6 * foreign_asset_minimum_asset_balance + ) + .into()))); }); } -#[test] -fn receive_teleported_asset_works() { - ExtBuilder::::default() - .with_collators(vec![AccountId::from(ALICE)]) - .with_session_keys(vec![( - AccountId::from(ALICE), - AccountId::from(ALICE), - SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) }, - )]) - .build() - .execute_with(|| { - let xcm = Xcm(vec![ - ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }])), - ClearOrigin, - BuyExecution { - fees: MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }, - weight_limit: Limited(Weight::from_parts(303531000, 65536)), - }, - DepositAsset { - assets: Wild(AllCounted(1)), - beneficiary: MultiLocation { - parents: 0, - interior: X1(AccountId32 { - network: None, - id: [ - 18, 153, 85, 112, 1, 245, 88, 21, 211, 252, 181, 60, 116, 70, 58, - 203, 12, 246, 209, 77, 70, 57, 179, 64, 152, 44, 96, 135, 127, 56, - 70, 9, - ], - }), - }, - }, - ]); - let hash = xcm.using_encoded(sp_io::hashing::blake2_256); - - let weight_limit = ReservedDmpWeight::get(); - - let outcome = XcmExecutor::::execute_xcm(Parent, xcm, hash, weight_limit); - assert_eq!(outcome.ensure_complete(), Ok(())); - }) -} +asset_test_utils::include_teleports_for_native_asset_works!( + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + ParachainSystem, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }) +); + +asset_test_utils::include_teleports_for_foreign_assets_works!( + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + ParachainSystem, + ForeignCreatorsSovereignAccountOf, + ForeignAssetsInstance, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_local_consensus_currency_works!( + Runtime, + XcmConfig, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_pallet_assets_instance_works!( + asset_transactor_transfer_with_trust_backed_assets_works, + Runtime, + XcmConfig, + TrustBackedAssetsInstance, + AssetIdForTrustBackedAssets, + AssetIdForTrustBackedAssetsConvert, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + 12345, + Box::new(|| { + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_pallet_assets_instance_works!( + asset_transactor_transfer_with_foreign_assets_works, + Runtime, + XcmConfig, + ForeignAssetsInstance, + MultiLocation, + JustTry, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + MultiLocation { parents: 1, interior: X2(Parachain(1313), GeneralIndex(12345)) }, + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works!( + Runtime, + XcmConfig, + WeightToFee, + ForeignCreatorsSovereignAccountOf, + ForeignAssetsInstance, + MultiLocation, + JustTry, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + AssetDeposit::get(), + MetadataDepositBase::get(), + MetadataDepositPerByte::get(), + Box::new(|pallet_asset_call| RuntimeCall::ForeignAssets(pallet_asset_call).encode()), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::ForeignAssets(pallet_asset_event)) => Some(pallet_asset_event), + _ => None, + } + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert_eq!(ForeignAssets::asset_ids().collect::>().len(), 1); + }) +); diff --git a/parachains/runtimes/assets/statemint/Cargo.toml b/parachains/runtimes/assets/statemint/Cargo.toml index 3b1ee08643e..a8de1be1994 100644 --- a/parachains/runtimes/assets/statemint/Cargo.toml +++ b/parachains/runtimes/assets/statemint/Cargo.toml @@ -101,6 +101,7 @@ runtime-benchmarks = [ "pallet-collator-selection/runtime-benchmarks", "cumulus-pallet-xcmp-queue/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", + "assets-common/runtime-benchmarks", ] try-runtime = [ "cumulus-pallet-aura-ext/try-runtime", diff --git a/parachains/runtimes/assets/statemint/src/xcm_config.rs b/parachains/runtimes/assets/statemint/src/xcm_config.rs index 3456eda5559..90b0ee85fef 100644 --- a/parachains/runtimes/assets/statemint/src/xcm_config.rs +++ b/parachains/runtimes/assets/statemint/src/xcm_config.rs @@ -49,7 +49,6 @@ parameter_types! { pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(ParachainInfo::parachain_id().into())); - pub const Local: MultiLocation = MultiLocation::here(); pub TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); @@ -83,7 +82,7 @@ pub type CurrencyTransactor = CurrencyAdapter< (), >; -/// `AssetId/Balancer` converter for `TrustBackedAssets`` +/// `AssetId/Balance` converter for `TrustBackedAssets`` pub type TrustBackedAssetsConvertedConcreteId = assets_common::TrustBackedAssetsConvertedConcreteId; diff --git a/parachains/runtimes/assets/statemint/tests/tests.rs b/parachains/runtimes/assets/statemint/tests/tests.rs index 81aa458b476..501c16960aa 100644 --- a/parachains/runtimes/assets/statemint/tests/tests.rs +++ b/parachains/runtimes/assets/statemint/tests/tests.rs @@ -1,25 +1,27 @@ use asset_test_utils::{ExtBuilder, RuntimeHelper}; -use codec::Encode; +use codec::Decode; use cumulus_primitives_utility::ChargeWeightInFungibles; use frame_support::{ - assert_noop, assert_ok, sp_io, + assert_noop, assert_ok, + traits::fungibles::InspectEnumerable, weights::{Weight, WeightToFee as WeightToFeeT}, }; -use parachains_common::{AccountId, Balance, StatemintAuraId as AuraId}; +use parachains_common::{ + AccountId, AssetIdForTrustBackedAssets, Balance, StatemintAuraId as AuraId, +}; use statemint_runtime::xcm_config::{ - AssetFeeAsExistentialDepositMultiplierFeeCharger, DotLocation, TrustBackedAssetsPalletLocation, + AssetFeeAsExistentialDepositMultiplierFeeCharger, CheckingAccount, DotLocation, + TrustBackedAssetsPalletLocation, }; pub use statemint_runtime::{ - constants::fee::WeightToFee, xcm_config::XcmConfig, Assets, Balances, ExistentialDeposit, - ReservedDmpWeight, Runtime, SessionKeys, System, + constants::fee::WeightToFee, xcm_config::XcmConfig, AssetDeposit, Assets, Balances, + ExistentialDeposit, ParachainSystem, Runtime, RuntimeEvent, SessionKeys, System, + TrustBackedAssetsInstance, }; use xcm::latest::prelude::*; -use xcm_executor::{ - traits::{Convert, WeightTrader}, - XcmExecutor, -}; +use xcm_executor::traits::{Convert, WeightTrader}; -pub const ALICE: [u8; 32] = [1u8; 32]; +const ALICE: [u8; 32] = [1u8; 32]; type AssetIdForTrustBackedAssetsConvert = assets_common::AssetIdForTrustBackedAssetsConvert; @@ -438,50 +440,63 @@ fn test_assets_balances_api_works() { }); } -#[test] -fn receive_teleported_asset_works() { - ExtBuilder::::default() - .with_collators(vec![AccountId::from(ALICE)]) - .with_session_keys(vec![( - AccountId::from(ALICE), - AccountId::from(ALICE), - SessionKeys { aura: AuraId::from(sp_core::ed25519::Public::from_raw(ALICE)) }, - )]) - .build() - .execute_with(|| { - let xcm = Xcm(vec![ - ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }])), - ClearOrigin, - BuyExecution { - fees: MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }, - weight_limit: Limited(Weight::from_parts(303531000, 65536)), - }, - DepositAsset { - assets: Wild(AllCounted(1)), - beneficiary: MultiLocation { - parents: 0, - interior: X1(AccountId32 { - network: None, - id: [ - 18, 153, 85, 112, 1, 245, 88, 21, 211, 252, 181, 60, 116, 70, 58, - 203, 12, 246, 209, 77, 70, 57, 179, 64, 152, 44, 96, 135, 127, 56, - 70, 9, - ], - }), - }, - }, - ]); - let hash = xcm.using_encoded(sp_io::hashing::blake2_256); - - let weight_limit = ReservedDmpWeight::get(); - - let outcome = XcmExecutor::::execute_xcm(Parent, xcm, hash, weight_limit); - assert_eq!(outcome.ensure_complete(), Ok(())); - }) -} +asset_test_utils::include_teleports_for_native_asset_works!( + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + ParachainSystem, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::ed25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_local_consensus_currency_works!( + Runtime, + XcmConfig, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::ed25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_pallet_assets_instance_works!( + asset_transactor_transfer_with_pallet_assets_instance_works, + Runtime, + XcmConfig, + TrustBackedAssetsInstance, + AssetIdForTrustBackedAssets, + AssetIdForTrustBackedAssetsConvert, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::ed25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + 12345, + Box::new(|| {}), + Box::new(|| {}) +); diff --git a/parachains/runtimes/assets/test-utils/Cargo.toml b/parachains/runtimes/assets/test-utils/Cargo.toml index 52ce1d2d9a8..2a9e70ce2dd 100644 --- a/parachains/runtimes/assets/test-utils/Cargo.toml +++ b/parachains/runtimes/assets/test-utils/Cargo.toml @@ -6,11 +6,12 @@ edition = "2021" description = "Statemint parachain runtime" [dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } # Substrate - 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" } +pallet-assets = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } pallet-balances = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } pallet-session = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } sp-consensus-aura = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } @@ -20,8 +21,21 @@ sp-std = { git = "https://github.com/paritytech/substrate", default-features = f sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } # Cumulus +cumulus-pallet-parachain-system = { path = "../../../../pallets/parachain-system", default-features = false } +cumulus-pallet-xcmp-queue = { path = "../../../../pallets/xcmp-queue", default-features = false } pallet-collator-selection = { path = "../../../../pallets/collator-selection", default-features = false } parachains-common = { path = "../../../common", default-features = false } +assets-common = { path = "../common", default-features = false } +cumulus-primitives-core = { path = "../../../../primitives/core", default-features = false } +cumulus-primitives-parachain-inherent = { path = "../../../../primitives/parachain-inherent", default-features = false } +cumulus-test-relay-sproof-builder = { path = "../../../../test/relay-sproof-builder", default-features = false } +parachain-info = { path = "../../../../parachains/pallets/parachain-info", default-features = false } + +# Polkadot +xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } +xcm-executor = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } +pallet-xcm = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } +polkadot-parachain = { git = "https://github.com/paritytech/polkadot", default-features = false, branch = "master" } [dev-dependencies] hex-literal = "0.3.4" @@ -32,14 +46,27 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", bran [features] default = [ "std" ] std = [ + "cumulus-pallet-parachain-system/std", + "cumulus-primitives-core/std", + "cumulus-test-relay-sproof-builder/std", + "cumulus-primitives-parachain-inherent/std", "frame-support/std", "frame-system/std", + "pallet-assets/std", "pallet-balances/std", + "cumulus-pallet-parachain-system/std", "pallet-collator-selection/std", "pallet-session/std", + "assets-common/std", "parachains-common/std", + "parachain-info/std", + "polkadot-parachain/std", "sp-consensus-aura/std", "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm/std", + "xcm-executor/std", + "pallet-xcm/std", + "cumulus-pallet-xcmp-queue/std", ] diff --git a/parachains/runtimes/assets/test-utils/src/lib.rs b/parachains/runtimes/assets/test-utils/src/lib.rs index fb4750bae9e..06d6282e008 100644 --- a/parachains/runtimes/assets/test-utils/src/lib.rs +++ b/parachains/runtimes/assets/test-utils/src/lib.rs @@ -1,11 +1,27 @@ -use frame_support::traits::GenesisBuild; use sp_std::marker::PhantomData; -use frame_support::traits::OriginTrait; +use cumulus_primitives_core::{AbridgedHrmpChannel, ParaId, PersistedValidationData}; +use cumulus_primitives_parachain_inherent::ParachainInherentData; +use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; +use frame_support::{ + dispatch::{DispatchResult, RawOrigin, UnfilteredDispatchable}, + inherent::{InherentData, ProvideInherent}, + traits::{GenesisBuild, OriginTrait}, + weights::Weight, +}; use parachains_common::AccountId; +use polkadot_parachain::primitives::{HrmpChannelId, RelayChainBlockNumber}; use sp_consensus_aura::AURA_ENGINE_ID; use sp_core::Encode; use sp_runtime::{Digest, DigestItem}; +use xcm::{ + latest::{MultiAsset, MultiLocation, XcmContext, XcmHash}, + prelude::{Concrete, Fungible, Outcome, XcmError, XcmVersion}, +}; +use xcm_executor::{traits::TransactAsset, Assets}; + +pub mod test_cases; +pub use test_cases::CollatorSessionKeys; pub type BalanceOf = ::Balance; pub type AccountIdOf = ::AccountId; @@ -14,7 +30,11 @@ pub type SessionKeysOf = ::Keys; // Basic builder based on balances, collators and pallet_sessopm pub struct ExtBuilder< - Runtime: frame_system::Config + pallet_balances::Config + pallet_session::Config, + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config, > { // endowed accounts with balances balances: Vec<(AccountIdOf, BalanceOf)>, @@ -22,19 +42,40 @@ pub struct ExtBuilder< collators: Vec>, // keys added to pallet session keys: Vec<(AccountIdOf, ValidatorIdOf, SessionKeysOf)>, + // safe xcm version for pallet_xcm + safe_xcm_version: Option, + // para id + para_id: Option, _runtime: PhantomData, } -impl Default - for ExtBuilder +impl< + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config, + > Default for ExtBuilder { fn default() -> ExtBuilder { - ExtBuilder { balances: vec![], collators: vec![], keys: vec![], _runtime: PhantomData } + ExtBuilder { + balances: vec![], + collators: vec![], + keys: vec![], + safe_xcm_version: None, + para_id: None, + _runtime: PhantomData, + } } } -impl - ExtBuilder +impl< + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config, + > ExtBuilder { pub fn with_balances( mut self, @@ -56,6 +97,21 @@ impl Self { + frame_support::sp_tracing::try_init_simple(); + self + } + + pub fn with_safe_xcm_version(mut self, safe_xcm_version: XcmVersion) -> Self { + self.safe_xcm_version = Some(safe_xcm_version); + self + } + + pub fn with_para_id(mut self, para_id: ParaId) -> Self { + self.para_id = Some(para_id); + self + } + pub fn build(self) -> sp_io::TestExternalities where Runtime: @@ -64,6 +120,20 @@ impl().unwrap(); + >::assimilate_storage( + &pallet_xcm::GenesisConfig { safe_xcm_version: self.safe_xcm_version }, + &mut t, + ) + .unwrap(); + + if let Some(para_id) = self.para_id { + >::assimilate_storage( + ¶chain_info::GenesisConfig { parachain_id: para_id }, + &mut t, + ) + .unwrap(); + } + pallet_balances::GenesisConfig:: { balances: self.balances.into() } .assimilate_storage(&mut t) .unwrap(); @@ -132,3 +202,184 @@ where ::RuntimeOrigin::signed(account_id.into()) } } + +impl RuntimeHelper { + pub fn do_transfer( + from: MultiLocation, + to: MultiLocation, + (asset, amount): (MultiLocation, u128), + ) -> Result { + ::transfer_asset( + &MultiAsset { id: Concrete(asset), fun: Fungible(amount) }, + &from, + &to, + // We aren't able to track the XCM that initiated the fee deposit, so we create a + // fake message hash here + &XcmContext::with_message_hash([0; 32]), + ) + } +} + +impl RuntimeHelper { + pub fn do_teleport_assets( + origin: ::RuntimeOrigin, + dest: MultiLocation, + beneficiary: MultiLocation, + (asset, amount): (MultiLocation, u128), + open_hrmp_channel: Option<(u32, u32)>, + ) -> DispatchResult + where + HrmpChannelOpener: frame_support::inherent::ProvideInherent< + Call = cumulus_pallet_parachain_system::Call, + >, + { + // open hrmp (if needed) + if let Some((source_para_id, target_para_id)) = open_hrmp_channel { + mock_open_hrmp_channel::( + source_para_id.into(), + target_para_id.into(), + ); + } + + // do teleport + >::teleport_assets( + origin, + Box::new(dest.into()), + Box::new(beneficiary.into()), + Box::new((Concrete(asset), amount).into()), + 0, + ) + } +} + +pub enum XcmReceivedFrom { + Parent, + Sibling, +} + +impl RuntimeHelper { + pub fn xcm_max_weight(from: XcmReceivedFrom) -> Weight { + use frame_support::traits::Get; + match from { + XcmReceivedFrom::Parent => ParachainSystem::ReservedDmpWeight::get(), + XcmReceivedFrom::Sibling => ParachainSystem::ReservedXcmpWeight::get(), + } + } +} + +impl RuntimeHelper { + pub fn assert_pallet_xcm_event_outcome( + unwrap_pallet_xcm_event: &Box) -> Option>>, + assert_outcome: fn(Outcome), + ) { + let outcome = >::events() + .into_iter() + .filter_map(|e| unwrap_pallet_xcm_event(e.event.encode())) + .find_map(|e| match e { + pallet_xcm::Event::Attempted(outcome) => Some(outcome), + _ => None, + }); + match outcome { + Some(outcome) => assert_outcome(outcome), + None => assert!(false, "No `pallet_xcm::Event::Attempted(outcome)` event found!"), + } + } +} + +impl RuntimeHelper { + pub fn xcmp_queue_message_sent( + unwrap_xcmp_queue_event: Box< + dyn Fn(Vec) -> Option>, + >, + ) -> Option { + >::events() + .into_iter() + .filter_map(|e| unwrap_xcmp_queue_event(e.event.encode())) + .find_map(|e| match e { + cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { message_hash } => message_hash, + _ => None, + }) + } +} + +pub fn assert_metadata( + asset_id: impl Into + Copy, + expected_name: &str, + expected_symbol: &str, + expected_decimals: u8, +) where + Fungibles: frame_support::traits::tokens::fungibles::metadata::Inspect + + frame_support::traits::tokens::fungibles::Inspect, +{ + assert_eq!(Fungibles::name(asset_id.into()), Vec::from(expected_name),); + assert_eq!(Fungibles::symbol(asset_id.into()), Vec::from(expected_symbol),); + assert_eq!(Fungibles::decimals(asset_id.into()), expected_decimals); +} + +pub fn assert_total( + asset_id: impl Into + Copy, + expected_total_issuance: impl Into, + expected_active_issuance: impl Into, +) where + Fungibles: frame_support::traits::tokens::fungibles::metadata::Inspect + + frame_support::traits::tokens::fungibles::Inspect, +{ + assert_eq!(Fungibles::total_issuance(asset_id.into()), expected_total_issuance.into()); + assert_eq!(Fungibles::active_issuance(asset_id.into()), expected_active_issuance.into()); +} + +/// Helper function which emulates opening HRMP channel which is needed for `XcmpQueue` to pass +pub fn mock_open_hrmp_channel< + C: cumulus_pallet_parachain_system::Config, + T: ProvideInherent>, +>( + sender: ParaId, + recipient: ParaId, +) { + let n = 1_u32; + let mut sproof_builder = RelayStateSproofBuilder::default(); + sproof_builder.para_id = sender; + sproof_builder.hrmp_channels.insert( + HrmpChannelId { sender, recipient }, + AbridgedHrmpChannel { + max_capacity: 10, + max_total_size: 10_000_000_u32, + max_message_size: 10_000_000_u32, + msg_count: 0, + total_size: 0_u32, + mqc_head: None, + }, + ); + sproof_builder.hrmp_egress_channel_index = Some(vec![recipient]); + + let (relay_parent_storage_root, relay_chain_state) = sproof_builder.into_state_root_and_proof(); + let vfp = PersistedValidationData { + relay_parent_number: n as RelayChainBlockNumber, + relay_parent_storage_root, + ..Default::default() + }; + // It is insufficient to push the validation function params + // to storage; they must also be included in the inherent data. + let inherent_data = { + let mut inherent_data = InherentData::default(); + let system_inherent_data = ParachainInherentData { + validation_data: vfp.clone(), + relay_chain_state, + downward_messages: Default::default(), + horizontal_messages: Default::default(), + }; + inherent_data + .put_data( + cumulus_primitives_parachain_inherent::INHERENT_IDENTIFIER, + &system_inherent_data, + ) + .expect("failed to put VFP inherent"); + inherent_data + }; + + // execute the block + T::create_inherent(&inherent_data) + .expect("got an inherent") + .dispatch_bypass_filter(RawOrigin::None.into()) + .expect("dispatch succeeded"); +} diff --git a/parachains/runtimes/assets/test-utils/src/test_cases.rs b/parachains/runtimes/assets/test-utils/src/test_cases.rs new file mode 100644 index 00000000000..3614f03e7d9 --- /dev/null +++ b/parachains/runtimes/assets/test-utils/src/test_cases.rs @@ -0,0 +1,1323 @@ +// Copyright (C) 2023 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. + +//! Module contains predefined test-case scenarios for `Runtime` with various assets. + +use crate::{ + assert_metadata, assert_total, AccountIdOf, BalanceOf, ExtBuilder, RuntimeHelper, + SessionKeysOf, ValidatorIdOf, XcmReceivedFrom, +}; +use codec::Encode; +use frame_support::{ + assert_noop, assert_ok, + traits::{fungibles::InspectEnumerable, Get, OriginTrait}, + weights::Weight, +}; +use parachains_common::Balance; +use sp_runtime::{ + traits::{StaticLookup, Zero}, + DispatchError, Saturating, +}; +use xcm::latest::prelude::*; +use xcm_executor::{traits::Convert, XcmExecutor}; + +pub struct CollatorSessionKeys< + Runtime: frame_system::Config + pallet_balances::Config + pallet_session::Config, +> { + collator: AccountIdOf, + validator: ValidatorIdOf, + key: SessionKeysOf, +} + +impl + CollatorSessionKeys +{ + pub fn new( + collator: AccountIdOf, + validator: ValidatorIdOf, + key: SessionKeysOf, + ) -> Self { + Self { collator, validator, key } + } + pub fn collators(&self) -> Vec> { + vec![self.collator.clone()] + } + + pub fn session_keys( + &self, + ) -> Vec<(AccountIdOf, ValidatorIdOf, SessionKeysOf)> { + vec![(self.collator.clone(), self.validator.clone(), self.key.clone())] + } +} + +/// Test-case makes sure that `Runtime` can receive native asset from relay chain +/// and can teleport it back and to the other parachains +pub fn teleports_for_native_asset_works< + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + HrmpChannelOpener, +>( + collator_session_keys: CollatorSessionKeys, + existential_deposit: BalanceOf, + target_account: AccountIdOf, + unwrap_pallet_xcm_event: Box) -> Option>>, + unwrap_xcmp_queue_event: Box< + dyn Fn(Vec) -> Option>, + >, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + cumulus_pallet_xcmp_queue::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From + Into, + WeightToFee: frame_support::weights::WeightToFee, + ::Balance: From + Into, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + XcmConfig: xcm_executor::Config, + CheckingAccount: Get>, + HrmpChannelOpener: frame_support::inherent::ProvideInherent< + Call = cumulus_pallet_parachain_system::Call, + >, +{ + let runtime_para_id = 1000; + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_safe_xcm_version(XCM_VERSION) + .with_para_id(runtime_para_id.into()) + .build() + .execute_with(|| { + // check Balances before + assert_eq!(>::free_balance(&target_account), 0.into()); + assert_eq!( + >::free_balance(&CheckingAccount::get()), + 0.into() + ); + + let native_asset_id = MultiLocation::parent(); + let buy_execution_fee_amount_eta = + WeightToFee::weight_to_fee(&Weight::from_parts(90_000_000_000, 0)); + let native_asset_amount_unit = existential_deposit; + let native_asset_amount_received = + native_asset_amount_unit * 10.into() + buy_execution_fee_amount_eta.into(); + + // 1. process received teleported assets from relaychain + let xcm = Xcm(vec![ + ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { + id: Concrete(native_asset_id), + fun: Fungible(native_asset_amount_received.into()), + }])), + ClearOrigin, + BuyExecution { + fees: MultiAsset { + id: Concrete(native_asset_id), + fun: Fungible(buy_execution_fee_amount_eta.into()), + }, + weight_limit: Limited(Weight::from_parts(303531000, 65536)), + }, + DepositAsset { + assets: Wild(AllCounted(1)), + beneficiary: MultiLocation { + parents: 0, + interior: X1(AccountId32 { + network: None, + id: target_account.clone().into(), + }), + }, + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]); + + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + + let outcome = XcmExecutor::::execute_xcm( + Parent, + xcm, + hash, + RuntimeHelper::::xcm_max_weight(XcmReceivedFrom::Parent), + ); + assert_eq!(outcome.ensure_complete(), Ok(())); + + // check Balances after + assert_ne!(>::free_balance(&target_account), 0.into()); + assert_eq!( + >::free_balance(&CheckingAccount::get()), + 0.into() + ); + + // 2. try to teleport asset back to the relaychain + { + let dest = MultiLocation::parent(); + let dest_beneficiary = MultiLocation::parent() + .appended_with(AccountId32 { + network: None, + id: sp_runtime::AccountId32::new([3; 32]).into(), + }) + .unwrap(); + + let target_account_balance_before_teleport = + >::free_balance(&target_account); + let native_asset_to_teleport_away = native_asset_amount_unit * 3.into(); + assert!( + native_asset_to_teleport_away < + target_account_balance_before_teleport - existential_deposit + ); + + assert_ok!(RuntimeHelper::::do_teleport_assets::( + RuntimeHelper::::origin_of(target_account.clone()), + dest, + dest_beneficiary, + (native_asset_id, native_asset_to_teleport_away.clone().into()), + None, + )); + // check balances + assert_eq!( + >::free_balance(&target_account), + target_account_balance_before_teleport - native_asset_to_teleport_away + ); + assert_eq!( + >::free_balance(&CheckingAccount::get()), + 0.into() + ); + + // check events + RuntimeHelper::::assert_pallet_xcm_event_outcome( + &unwrap_pallet_xcm_event, + |outcome| { + assert_ok!(outcome.ensure_complete()); + }, + ); + } + + // 3. try to teleport asset away to other parachain (1234) + { + let other_para_id = 1234; + let dest = MultiLocation::new(1, X1(Parachain(other_para_id))); + let dest_beneficiary = MultiLocation::new(1, X1(Parachain(other_para_id))) + .appended_with(AccountId32 { + network: None, + id: sp_runtime::AccountId32::new([3; 32]).into(), + }) + .unwrap(); + + let target_account_balance_before_teleport = + >::free_balance(&target_account); + let native_asset_to_teleport_away = native_asset_amount_unit * 3.into(); + assert!( + native_asset_to_teleport_away < + target_account_balance_before_teleport - existential_deposit + ); + + assert_ok!(RuntimeHelper::::do_teleport_assets::( + RuntimeHelper::::origin_of(target_account.clone()), + dest, + dest_beneficiary, + (native_asset_id, native_asset_to_teleport_away.clone().into()), + Some((runtime_para_id, other_para_id)), + )); + + // check balances + assert_eq!( + >::free_balance(&target_account), + target_account_balance_before_teleport - native_asset_to_teleport_away + ); + assert_eq!( + >::free_balance(&CheckingAccount::get()), + 0.into() + ); + + // check events + RuntimeHelper::::assert_pallet_xcm_event_outcome( + &unwrap_pallet_xcm_event, + |outcome| { + assert_ok!(outcome.ensure_complete()); + }, + ); + assert!(RuntimeHelper::::xcmp_queue_message_sent(unwrap_xcmp_queue_event) + .is_some()); + } + }) +} + +#[macro_export] +macro_rules! include_teleports_for_native_asset_works( + ( + $runtime:path, + $xcm_config:path, + $checking_account:path, + $weight_to_fee:path, + $hrmp_channel_opener:path, + $collator_session_key:expr, + $existential_deposit:expr, + $unwrap_pallet_xcm_event:expr, + $unwrap_xcmp_queue_event:expr + ) => { + #[test] + fn teleports_for_native_asset_works() { + const BOB: [u8; 32] = [2u8; 32]; + let target_account = parachains_common::AccountId::from(BOB); + + asset_test_utils::test_cases::teleports_for_native_asset_works::< + $runtime, + $xcm_config, + $checking_account, + $weight_to_fee, + $hrmp_channel_opener + >( + $collator_session_key, + $existential_deposit, + target_account, + $unwrap_pallet_xcm_event, + $unwrap_xcmp_queue_event + ) + } + } +); + +/// Test-case makes sure that `Runtime` can receive teleported assets from sibling parachain relay chain +pub fn teleports_for_foreign_assets_works< + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + HrmpChannelOpener, + SovereignAccountOf, + ForeignAssetsPalletInstance, +>( + collator_session_keys: CollatorSessionKeys, + target_account: AccountIdOf, + existential_deposit: BalanceOf, + asset_owner: AccountIdOf, + unwrap_pallet_xcm_event: Box) -> Option>>, + unwrap_xcmp_queue_event: Box< + dyn Fn(Vec) -> Option>, + >, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + cumulus_pallet_xcmp_queue::Config + + pallet_assets::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + XcmConfig: xcm_executor::Config, + CheckingAccount: Get>, + HrmpChannelOpener: frame_support::inherent::ProvideInherent< + Call = cumulus_pallet_parachain_system::Call, + >, + WeightToFee: frame_support::weights::WeightToFee, + ::Balance: From + Into, + SovereignAccountOf: Convert>, + >::AssetId: + From + Into, + >::AssetIdParameter: + From + Into, + >::Balance: + From + Into, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + ForeignAssetsPalletInstance: 'static, +{ + // foreign parachain with the same consenus currency as asset + let foreign_para_id = 2222; + let foreign_asset_id_multilocation = MultiLocation { + parents: 1, + interior: X2(Parachain(foreign_para_id), GeneralIndex(1234567)), + }; + + // foreign creator, which can be sibling parachain to match ForeignCreators + let foreign_creator = MultiLocation { parents: 1, interior: X1(Parachain(foreign_para_id)) }; + let foreign_creator_as_account_id = SovereignAccountOf::convert(foreign_creator).expect(""); + + // we want to buy execution with local relay chain currency + let buy_execution_fee_amount = + WeightToFee::weight_to_fee(&Weight::from_parts(90_000_000_000, 0)); + let buy_execution_fee = MultiAsset { + id: Concrete(MultiLocation::parent()), + fun: Fungible(buy_execution_fee_amount.into()), + }; + + let teleported_foreign_asset_amount = 10000000000000; + let runtime_para_id = 1000; + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_balances(vec![ + ( + foreign_creator_as_account_id.clone(), + existential_deposit + (buy_execution_fee_amount * 2).into(), + ), + (target_account.clone(), existential_deposit), + (CheckingAccount::get(), existential_deposit), + ]) + .with_safe_xcm_version(XCM_VERSION) + .with_para_id(runtime_para_id.into()) + .with_tracing() + .build() + .execute_with(|| { + // checks target_account before + assert_eq!( + >::free_balance(&target_account), + existential_deposit + ); + assert_eq!( + >::free_balance(&CheckingAccount::get()), + existential_deposit + ); + // check `CheckingAccount` before + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &target_account + ), + 0.into() + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &CheckingAccount::get() + ), + 0.into() + ); + // check totals before + assert_total::< + pallet_assets::Pallet, + AccountIdOf, + >(foreign_asset_id_multilocation, 0, 0); + + // create foreign asset (0 total issuance) + let asset_minimum_asset_balance = 3333333_u128; + assert_ok!( + >::force_create( + RuntimeHelper::::root_origin(), + foreign_asset_id_multilocation.into(), + asset_owner.into(), + false, + asset_minimum_asset_balance.into() + ) + ); + assert_total::< + pallet_assets::Pallet, + AccountIdOf, + >(foreign_asset_id_multilocation, 0, 0); + assert!(teleported_foreign_asset_amount > asset_minimum_asset_balance); + + // 1. process received teleported assets from relaychain + let xcm = Xcm(vec![ + // BuyExecution with relaychain native token + WithdrawAsset(buy_execution_fee.clone().into()), + BuyExecution { + fees: MultiAsset { + id: Concrete(MultiLocation::parent()), + fun: Fungible(buy_execution_fee_amount.into()), + }, + weight_limit: Limited(Weight::from_parts(403531000, 65536)), + }, + // Process teleported asset + ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { + id: Concrete(foreign_asset_id_multilocation), + fun: Fungible(teleported_foreign_asset_amount), + }])), + DepositAsset { + assets: Wild(AllOf { + id: Concrete(foreign_asset_id_multilocation), + fun: WildFungibility::Fungible, + }), + beneficiary: MultiLocation { + parents: 0, + interior: X1(AccountId32 { + network: None, + id: target_account.clone().into(), + }), + }, + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]); + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + + let outcome = XcmExecutor::::execute_xcm( + foreign_creator, + xcm, + hash, + RuntimeHelper::::xcm_max_weight(XcmReceivedFrom::Sibling), + ); + assert_eq!(outcome.ensure_complete(), Ok(())); + + // checks target_account after + assert_eq!( + >::free_balance(&target_account), + existential_deposit + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &target_account + ), + teleported_foreign_asset_amount.into() + ); + // checks `CheckingAccount` after + assert_eq!( + >::free_balance(&CheckingAccount::get()), + existential_deposit + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &CheckingAccount::get() + ), + 0.into() + ); + // check total after (twice: target_account + CheckingAccount) + assert_total::< + pallet_assets::Pallet, + AccountIdOf, + >( + foreign_asset_id_multilocation, + teleported_foreign_asset_amount, + teleported_foreign_asset_amount, + ); + + // 2. try to teleport asset back to source parachain (foreign_para_id) + { + let dest = MultiLocation::new(1, X1(Parachain(foreign_para_id))); + let dest_beneficiary = MultiLocation::new(1, X1(Parachain(foreign_para_id))) + .appended_with(AccountId32 { + network: None, + id: sp_runtime::AccountId32::new([3; 32]).into(), + }) + .unwrap(); + + let target_account_balance_before_teleport = + >::balance( + foreign_asset_id_multilocation.into(), + &target_account, + ); + let asset_to_teleport_away = asset_minimum_asset_balance * 3; + assert!( + asset_to_teleport_away < + (target_account_balance_before_teleport - + asset_minimum_asset_balance.into()) + .into() + ); + + assert_ok!(RuntimeHelper::::do_teleport_assets::( + RuntimeHelper::::origin_of(target_account.clone()), + dest, + dest_beneficiary, + (foreign_asset_id_multilocation, asset_to_teleport_away.clone().into()), + Some((runtime_para_id, foreign_para_id)), + )); + + // check balances + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &target_account + ), + (target_account_balance_before_teleport - asset_to_teleport_away.into()) + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &CheckingAccount::get() + ), + 0.into() + ); + // check total after (twice: target_account + CheckingAccount) + assert_total::< + pallet_assets::Pallet, + AccountIdOf, + >( + foreign_asset_id_multilocation, + teleported_foreign_asset_amount - asset_to_teleport_away, + teleported_foreign_asset_amount - asset_to_teleport_away, + ); + + // check events + RuntimeHelper::::assert_pallet_xcm_event_outcome( + &unwrap_pallet_xcm_event, + |outcome| { + assert_ok!(outcome.ensure_complete()); + }, + ); + assert!(RuntimeHelper::::xcmp_queue_message_sent(unwrap_xcmp_queue_event) + .is_some()); + } + }) +} + +#[macro_export] +macro_rules! include_teleports_for_foreign_assets_works( + ( + $runtime:path, + $xcm_config:path, + $checking_account:path, + $weight_to_fee:path, + $hrmp_channel_opener:path, + $sovereign_account_of:path, + $assets_pallet_instance:path, + $collator_session_key:expr, + $existential_deposit:expr, + $unwrap_pallet_xcm_event:expr, + $unwrap_xcmp_queue_event:expr + ) => { + #[test] + fn teleports_for_foreign_assets_works() { + const BOB: [u8; 32] = [2u8; 32]; + let target_account = parachains_common::AccountId::from(BOB); + const SOME_ASSET_OWNER: [u8; 32] = [5u8; 32]; + let asset_owner = parachains_common::AccountId::from(SOME_ASSET_OWNER); + + asset_test_utils::test_cases::teleports_for_foreign_assets_works::< + $runtime, + $xcm_config, + $checking_account, + $weight_to_fee, + $hrmp_channel_opener, + $sovereign_account_of, + $assets_pallet_instance + >( + $collator_session_key, + target_account, + $existential_deposit, + asset_owner, + $unwrap_pallet_xcm_event, + $unwrap_xcmp_queue_event + ) + } + } +); + +/// Test-case makes sure that `Runtime`'s `xcm::AssetTransactor` can handle native relay chain currency +pub fn asset_transactor_transfer_with_local_consensus_currency_works( + collator_session_keys: CollatorSessionKeys, + source_account: AccountIdOf, + target_account: AccountIdOf, + existential_deposit: BalanceOf, + additional_checks_before: Box, + additional_checks_after: Box, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + XcmConfig: xcm_executor::Config, + ::Balance: From + Into, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, +{ + let unit = existential_deposit; + + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_balances(vec![(source_account.clone(), (BalanceOf::::from(10_u128) * unit))]) + .with_tracing() + .build() + .execute_with(|| { + // check Balances before + assert_eq!( + >::free_balance(&source_account), + (BalanceOf::::from(10_u128) * unit) + ); + assert_eq!( + >::free_balance(&target_account), + (BalanceOf::::zero() * unit) + ); + + // additional check before + additional_checks_before(); + + // transfer_asset (deposit/withdraw) ALICE -> BOB + let _ = RuntimeHelper::::do_transfer( + MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: None, id: source_account.clone().into() }), + }, + MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: None, id: target_account.clone().into() }), + }, + // local_consensus_currency_asset, e.g.: relaychain token (KSM, DOT, ...) + ( + MultiLocation { parents: 1, interior: Here }, + (BalanceOf::::from(1_u128) * unit).into(), + ), + ) + .expect("no error"); + + // check Balances after + assert_eq!( + >::free_balance(source_account), + (BalanceOf::::from(9_u128) * unit) + ); + assert_eq!( + >::free_balance(target_account), + (BalanceOf::::from(1_u128) * unit) + ); + + additional_checks_after(); + }) +} + +#[macro_export] +macro_rules! include_asset_transactor_transfer_with_local_consensus_currency_works( + ( + $runtime:path, + $xcm_config:path, + $collator_session_key:expr, + $existential_deposit:expr, + $additional_checks_before:expr, + $additional_checks_after:expr + ) => { + #[test] + fn asset_transactor_transfer_with_local_consensus_currency_works() { + const ALICE: [u8; 32] = [1u8; 32]; + let source_account = parachains_common::AccountId::from(ALICE); + const BOB: [u8; 32] = [2u8; 32]; + let target_account = parachains_common::AccountId::from(BOB); + + asset_test_utils::test_cases::asset_transactor_transfer_with_local_consensus_currency_works::< + $runtime, + $xcm_config + >( + $collator_session_key, + source_account, + target_account, + $existential_deposit, + $additional_checks_before, + $additional_checks_after + ) + } + } +); + +///Test-case makes sure that `Runtime`'s `xcm::AssetTransactor` can handle native relay chain currency +pub fn asset_transactor_transfer_with_pallet_assets_instance_works< + Runtime, + XcmConfig, + AssetsPalletInstance, + AssetId, + AssetIdConverter, +>( + collator_session_keys: CollatorSessionKeys, + existential_deposit: BalanceOf, + asset_id: AssetId, + asset_owner: AccountIdOf, + alice_account: AccountIdOf, + bob_account: AccountIdOf, + charlie_account: AccountIdOf, + additional_checks_before: Box, + additional_checks_after: Box, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + pallet_assets::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + XcmConfig: xcm_executor::Config, + >::AssetId: + From + Into, + >::AssetIdParameter: + From + Into, + >::Balance: From + Into, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + AssetsPalletInstance: 'static, + AssetId: Clone + Copy, + AssetIdConverter: Convert, +{ + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_balances(vec![ + (asset_owner.clone(), existential_deposit), + (alice_account.clone(), existential_deposit), + (bob_account.clone(), existential_deposit), + ]) + .with_tracing() + .build() + .execute_with(|| { + // create some asset class + let asset_minimum_asset_balance = 3333333_u128; + let asset_id_as_multilocation = AssetIdConverter::reverse_ref(&asset_id).unwrap(); + assert_ok!(>::force_create( + RuntimeHelper::::root_origin(), + asset_id.into(), + asset_owner.clone().into(), + false, + asset_minimum_asset_balance.into() + )); + + // We first mint enough asset for the account to exist for assets + assert_ok!(>::mint( + RuntimeHelper::::origin_of(asset_owner.clone()), + asset_id.into(), + alice_account.clone().into(), + (6 * asset_minimum_asset_balance).into() + )); + + // check Assets before + assert_eq!( + >::balance( + asset_id.into(), + &alice_account + ), + (6 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &bob_account + ), + 0.into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &charlie_account + ), + 0.into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &asset_owner + ), + 0.into() + ); + assert_eq!( + >::free_balance(&alice_account), + existential_deposit + ); + assert_eq!( + >::free_balance(&bob_account), + existential_deposit + ); + assert_eq!( + >::free_balance(&charlie_account), + 0.into() + ); + assert_eq!( + >::free_balance(&asset_owner), + existential_deposit + ); + additional_checks_before(); + + // transfer_asset (deposit/withdraw) ALICE -> CHARLIE (not ok - Charlie does not have ExistentialDeposit) + assert_noop!( + RuntimeHelper::::do_transfer( + MultiLocation { + parents: 0, + interior: X1(AccountId32 { + network: None, + id: alice_account.clone().into() + }), + }, + MultiLocation { + parents: 0, + interior: X1(AccountId32 { + network: None, + id: charlie_account.clone().into() + }), + }, + (asset_id_as_multilocation, 1 * asset_minimum_asset_balance), + ), + XcmError::FailedToTransactAsset(Into::<&str>::into( + sp_runtime::TokenError::CannotCreate + )) + ); + + // transfer_asset (deposit/withdraw) ALICE -> BOB (ok - has ExistentialDeposit) + assert!(matches!( + RuntimeHelper::::do_transfer( + MultiLocation { + parents: 0, + interior: X1(AccountId32 { + network: None, + id: alice_account.clone().into() + }), + }, + MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: None, id: bob_account.clone().into() }), + }, + (asset_id_as_multilocation, 1 * asset_minimum_asset_balance), + ), + Ok(_) + )); + + // check Assets after + assert_eq!( + >::balance( + asset_id.into(), + &alice_account + ), + (5 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &bob_account + ), + (1 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &charlie_account + ), + 0.into() + ); + assert_eq!( + >::balance( + asset_id.into(), + &asset_owner + ), + 0.into() + ); + assert_eq!( + >::free_balance(&alice_account), + existential_deposit + ); + assert_eq!( + >::free_balance(&bob_account), + existential_deposit + ); + assert_eq!( + >::free_balance(&charlie_account), + 0.into() + ); + assert_eq!( + >::free_balance(&asset_owner), + existential_deposit + ); + + additional_checks_after(); + }) +} + +#[macro_export] +macro_rules! include_asset_transactor_transfer_with_pallet_assets_instance_works( + ( + $test_name:tt, + $runtime:path, + $xcm_config:path, + $assets_pallet_instance:path, + $asset_id:path, + $asset_id_converter:path, + $collator_session_key:expr, + $existential_deposit:expr, + $tested_asset_id:expr, + $additional_checks_before:expr, + $additional_checks_after:expr + ) => { + #[test] + fn $test_name() { + const SOME_ASSET_OWNER: [u8; 32] = [5u8; 32]; + let asset_owner = parachains_common::AccountId::from(SOME_ASSET_OWNER); + const ALICE: [u8; 32] = [1u8; 32]; + let alice_account = parachains_common::AccountId::from(ALICE); + const BOB: [u8; 32] = [2u8; 32]; + let bob_account = parachains_common::AccountId::from(BOB); + const CHARLIE: [u8; 32] = [3u8; 32]; + let charlie_account = parachains_common::AccountId::from(CHARLIE); + + asset_test_utils::test_cases::asset_transactor_transfer_with_pallet_assets_instance_works::< + $runtime, + $xcm_config, + $assets_pallet_instance, + $asset_id, + $asset_id_converter + >( + $collator_session_key, + $existential_deposit, + $tested_asset_id, + asset_owner, + alice_account, + bob_account, + charlie_account, + $additional_checks_before, + $additional_checks_after + ) + } + } +); + +pub fn create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works< + Runtime, + XcmConfig, + WeightToFee, + SovereignAccountOf, + ForeignAssetsPalletInstance, + AssetId, + AssetIdConverter, +>( + collator_session_keys: CollatorSessionKeys, + existential_deposit: BalanceOf, + asset_deposit: BalanceOf, + metadata_deposit_base: BalanceOf, + metadata_deposit_per_byte: BalanceOf, + alice_account: AccountIdOf, + bob_account: AccountIdOf, + runtime_call_encode: Box< + dyn Fn(pallet_assets::Call) -> Vec, + >, + unwrap_pallet_assets_event: Box< + dyn Fn(Vec) -> Option>, + >, + additional_checks_before: Box, + additional_checks_after: Box, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + pallet_assets::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + XcmConfig: xcm_executor::Config, + WeightToFee: frame_support::weights::WeightToFee, + ::Balance: From + Into, + SovereignAccountOf: Convert>, + >::AssetId: + From + Into, + >::AssetIdParameter: + From + Into, + >::Balance: + From + Into, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + ForeignAssetsPalletInstance: 'static, + AssetId: Clone + Copy, + AssetIdConverter: Convert, +{ + // foreign parachain with the same consenus currency as asset + let foreign_asset_id_multilocation = + MultiLocation { parents: 1, interior: X2(Parachain(2222), GeneralIndex(1234567)) }; + let asset_id = AssetIdConverter::convert(foreign_asset_id_multilocation).unwrap(); + + // foreign creator, which can be sibling parachain to match ForeignCreators + let foreign_creator = MultiLocation { parents: 1, interior: X1(Parachain(2222)) }; + let foreign_creator_as_account_id = SovereignAccountOf::convert(foreign_creator).expect(""); + + // we want to buy execution with local relay chain currency + let buy_execution_fee_amount = + WeightToFee::weight_to_fee(&Weight::from_parts(90_000_000_000, 0)); + let buy_execution_fee = MultiAsset { + id: Concrete(MultiLocation::parent()), + fun: Fungible(buy_execution_fee_amount), + }; + + const ASSET_NAME: &str = "My super coin"; + const ASSET_SYMBOL: &str = "MY_S_COIN"; + let metadata_deposit_per_byte_eta = metadata_deposit_per_byte + .saturating_mul(((ASSET_NAME.len() + ASSET_SYMBOL.len()) as u128).into()); + + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_balances(vec![( + foreign_creator_as_account_id.clone(), + existential_deposit + + asset_deposit + metadata_deposit_base + + metadata_deposit_per_byte_eta + + buy_execution_fee_amount.into() + + buy_execution_fee_amount.into(), + )]) + .with_tracing() + .build() + .execute_with(|| { + assert!(>::asset_ids() + .collect::>() + .is_empty()); + assert_eq!( + >::free_balance(&foreign_creator_as_account_id), + existential_deposit + + asset_deposit + metadata_deposit_base + + metadata_deposit_per_byte_eta + + buy_execution_fee_amount.into() + + buy_execution_fee_amount.into() + ); + additional_checks_before(); + + // execute XCM with Transacts to create/manage foreign assets by foreign governance + // prepapre data for xcm::Transact(create) + let foreign_asset_create = runtime_call_encode(pallet_assets::Call::< + Runtime, + ForeignAssetsPalletInstance, + >::create { + id: asset_id.into(), + // admin as sovereign_account + admin: foreign_creator_as_account_id.clone().into(), + min_balance: 1.into(), + }); + // prepapre data for xcm::Transact(set_metadata) + let foreign_asset_set_metadata = runtime_call_encode(pallet_assets::Call::< + Runtime, + ForeignAssetsPalletInstance, + >::set_metadata { + id: asset_id.into(), + name: Vec::from(ASSET_NAME), + symbol: Vec::from(ASSET_SYMBOL), + decimals: 12, + }); + // prepapre data for xcm::Transact(set_team - change just freezer to Bob) + let foreign_asset_set_team = runtime_call_encode(pallet_assets::Call::< + Runtime, + ForeignAssetsPalletInstance, + >::set_team { + id: asset_id.into(), + issuer: foreign_creator_as_account_id.clone().into(), + admin: foreign_creator_as_account_id.clone().into(), + freezer: bob_account.clone().into(), + }); + + // lets simulate this was triggered by relay chain from local consensus sibling parachain + let xcm = Xcm(vec![ + WithdrawAsset(buy_execution_fee.clone().into()), + BuyExecution { fees: buy_execution_fee.clone().into(), weight_limit: Unlimited }, + Transact { + origin_kind: OriginKind::Xcm, + require_weight_at_most: Weight::from_parts(40_000_000_000, 6000), + call: foreign_asset_create.into(), + }, + Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(20_000_000_000, 6000), + call: foreign_asset_set_metadata.into(), + }, + Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(20_000_000_000, 6000), + call: foreign_asset_set_team.into(), + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]); + + // messages with different consensus should go through the local bridge-hub + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + + // execute xcm as XcmpQueue would do + let outcome = XcmExecutor::::execute_xcm( + foreign_creator, + xcm, + hash, + RuntimeHelper::::xcm_max_weight(XcmReceivedFrom::Sibling), + ); + assert_eq!(outcome.ensure_complete(), Ok(())); + + // check events + let mut events = >::events() + .into_iter() + .filter_map(|e| unwrap_pallet_assets_event(e.event.encode())); + assert!(events.any(|e| matches!(e, pallet_assets::Event::Created { .. }))); + assert!(events.any(|e| matches!(e, pallet_assets::Event::MetadataSet { .. }))); + assert!(events.any(|e| matches!(e, pallet_assets::Event::TeamChanged { .. }))); + + // check assets after + assert!(!>::asset_ids() + .collect::>() + .is_empty()); + + // check update metadata + use frame_support::traits::tokens::fungibles::roles::Inspect as InspectRoles; + assert_eq!( + >::owner( + asset_id.into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::admin( + asset_id.into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::issuer( + asset_id.into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::freezer( + asset_id.into() + ), + Some(bob_account.clone()) + ); + assert!( + >::free_balance(&foreign_creator_as_account_id) >= + existential_deposit + buy_execution_fee_amount.into(), + "Free balance: {:?} should be ge {:?}", + >::free_balance(&foreign_creator_as_account_id), + existential_deposit + buy_execution_fee_amount.into() + ); + assert_metadata::< + pallet_assets::Pallet, + AccountIdOf, + >(asset_id, ASSET_NAME, ASSET_SYMBOL, 12); + + // check if changed freezer, can freeze + assert_noop!( + >::freeze( + RuntimeHelper::::origin_of(bob_account), + asset_id.into(), + alice_account.clone().into() + ), + pallet_assets::Error::::NoAccount + ); + assert_noop!( + >::freeze( + RuntimeHelper::::origin_of(foreign_creator_as_account_id.clone()), + asset_id.into(), + alice_account.into() + ), + pallet_assets::Error::::NoPermission + ); + + // lets try create asset for different parachain(3333) (foreign_creator(2222) can create just his assets) + let foreign_asset_id_multilocation = + MultiLocation { parents: 1, interior: X2(Parachain(3333), GeneralIndex(1234567)) }; + let asset_id = AssetIdConverter::convert(foreign_asset_id_multilocation).unwrap(); + + // prepare data for xcm::Transact(create) + let foreign_asset_create = runtime_call_encode(pallet_assets::Call::< + Runtime, + ForeignAssetsPalletInstance, + >::create { + id: asset_id.into(), + // admin as sovereign_account + admin: foreign_creator_as_account_id.clone().into(), + min_balance: 1.into(), + }); + let xcm = Xcm(vec![ + WithdrawAsset(buy_execution_fee.clone().into()), + BuyExecution { fees: buy_execution_fee.clone().into(), weight_limit: Unlimited }, + Transact { + origin_kind: OriginKind::Xcm, + require_weight_at_most: Weight::from_parts(20_000_000_000, 6000), + call: foreign_asset_create.into(), + }, + ExpectTransactStatus(MaybeErrorCode::from(DispatchError::BadOrigin.encode())), + ]); + + // messages with different consensus should go through the local bridge-hub + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + + // execute xcm as XcmpQueue would do + let outcome = XcmExecutor::::execute_xcm( + foreign_creator, + xcm, + hash, + RuntimeHelper::::xcm_max_weight(XcmReceivedFrom::Sibling), + ); + assert_eq!(outcome.ensure_complete(), Ok(())); + + additional_checks_after(); + }) +} + +#[macro_export] +macro_rules! include_create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works( + ( + $runtime:path, + $xcm_config:path, + $weight_to_fee:path, + $sovereign_account_of:path, + $assets_pallet_instance:path, + $asset_id:path, + $asset_id_converter:path, + $collator_session_key:expr, + $existential_deposit:expr, + $asset_deposit:expr, + $metadata_deposit_base:expr, + $metadata_deposit_per_byte:expr, + $runtime_call_encode:expr, + $unwrap_pallet_assets_event:expr, + $additional_checks_before:expr, + $additional_checks_after:expr + ) => { + #[test] + fn create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works() { + const ALICE: [u8; 32] = [1u8; 32]; + let alice_account = parachains_common::AccountId::from(ALICE); + const BOB: [u8; 32] = [2u8; 32]; + let bob_account = parachains_common::AccountId::from(BOB); + + asset_test_utils::test_cases::create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works::< + $runtime, + $xcm_config, + $weight_to_fee, + $sovereign_account_of, + $assets_pallet_instance, + $asset_id, + $asset_id_converter + >( + $collator_session_key, + $existential_deposit, + $asset_deposit, + $metadata_deposit_base, + $metadata_deposit_per_byte, + alice_account, + bob_account, + $runtime_call_encode, + $unwrap_pallet_assets_event, + $additional_checks_before, + $additional_checks_after + ) + } + } +); diff --git a/parachains/runtimes/assets/westmint/Cargo.toml b/parachains/runtimes/assets/westmint/Cargo.toml index 31e98071b18..c08d432ec33 100644 --- a/parachains/runtimes/assets/westmint/Cargo.toml +++ b/parachains/runtimes/assets/westmint/Cargo.toml @@ -103,6 +103,7 @@ runtime-benchmarks = [ "pallet-collator-selection/runtime-benchmarks", "cumulus-pallet-xcmp-queue/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", + "assets-common/runtime-benchmarks", ] try-runtime = [ "cumulus-pallet-aura-ext/try-runtime", diff --git a/parachains/runtimes/assets/westmint/src/lib.rs b/parachains/runtimes/assets/westmint/src/lib.rs index 5f36862ae98..8fca37c35bf 100644 --- a/parachains/runtimes/assets/westmint/src/lib.rs +++ b/parachains/runtimes/assets/westmint/src/lib.rs @@ -69,17 +69,20 @@ use parachains_common::{ NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; use xcm_config::{ - TrustBackedAssetsConvertedConcreteId, WestendLocation, XcmConfig, - XcmOriginToTransactDispatchOrigin, + ForeignAssetsConvertedConcreteId, TrustBackedAssetsConvertedConcreteId, WestendLocation, + XcmConfig, XcmOriginToTransactDispatchOrigin, }; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; -// Polkadot imports +use assets_common::{ + foreign_creators::ForeignCreators, matching::FromSiblingParachain, MultiLocationForAssetId, +}; use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; use xcm_executor::XcmExecutor; +use crate::xcm_config::ForeignCreatorsSovereignAccountOf; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; impl_opaque_keys! { @@ -250,6 +253,48 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = (); } +parameter_types! { + // we just reuse the same deposits + pub const ForeignAssetsAssetDeposit: Balance = AssetDeposit::get(); + pub const ForeignAssetsAssetAccountDeposit: Balance = AssetAccountDeposit::get(); + pub const ForeignAssetsApprovalDeposit: Balance = ApprovalDeposit::get(); + pub const ForeignAssetsAssetsStringLimit: u32 = AssetsStringLimit::get(); + pub const ForeignAssetsMetadataDepositBase: Balance = MetadataDepositBase::get(); + pub const ForeignAssetsMetadataDepositPerByte: Balance = MetadataDepositPerByte::get(); +} + +/// Assets managed by some foreign location. Note: we do not declare a `ForeignAssetsCall` type, as +/// this type is used in proxy definitions. We assume that a foreign location would not want to set +/// an individual, local account as a proxy for the issuance of their assets. This issuance should +/// be managed by the foreign location's governance. +pub type ForeignAssetsInstance = pallet_assets::Instance2; +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = MultiLocationForAssetId; + type AssetIdParameter = MultiLocationForAssetId; + type Currency = Balances; + type CreateOrigin = ForeignCreators< + (FromSiblingParachain>,), + ForeignCreatorsSovereignAccountOf, + AccountId, + >; + type ForceOrigin = AssetsForceOrigin; + type AssetDeposit = ForeignAssetsAssetDeposit; + type MetadataDepositBase = ForeignAssetsMetadataDepositBase; + type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte; + type ApprovalDeposit = ForeignAssetsApprovalDeposit; + type StringLimit = ForeignAssetsAssetsStringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = weights::pallet_assets::WeightInfo; + type CallbackHandle = (); + type AssetAccountDeposit = ForeignAssetsAssetAccountDeposit; + type RemoveItemsLimit = frame_support::traits::ConstU32<1000>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; +} + parameter_types! { // One storage item; key size is 32; value is size 4+4+16+32 bytes = 56 bytes. pub const DepositBase: Balance = deposit(1, 88); @@ -322,6 +367,7 @@ impl Default for ProxyType { Self::Any } } + impl InstanceFilter for ProxyType { fn filter(&self, c: &RuntimeCall) -> bool { match self { @@ -661,6 +707,7 @@ construct_runtime!( Assets: pallet_assets::::{Pallet, Call, Storage, Event} = 50, Uniques: pallet_uniques::{Pallet, Call, Storage, Event} = 51, Nfts: pallet_nfts::{Pallet, Call, Storage, Event} = 52, + ForeignAssets: pallet_assets::::{Pallet, Call, Storage, Event} = 53, } ); @@ -710,6 +757,7 @@ mod benches { define_benchmarks!( [frame_system, SystemBench::] [pallet_assets, Assets] + [pallet_assets, ForeignAssets] [pallet_balances, Balances] [pallet_multisig, Multisig] [pallet_nfts, Nfts] @@ -929,11 +977,17 @@ impl_runtime_apis! { }, // collect pallet_assets (TrustBackedAssets) convert::<_, _, _, _, TrustBackedAssetsConvertedConcreteId>( - Assets::account_balances(account) + Assets::account_balances(account.clone()) + .iter() + .filter(|(_, balance)| balance > &0) + )?, + // collect pallet_assets (ForeignAssets) + convert::<_, _, _, _, ForeignAssetsConvertedConcreteId>( + ForeignAssets::account_balances(account) .iter() .filter(|(_, balance)| balance > &0) )?, - // collect ... e.g. pallet_assets ForeignAssets + // collect ... e.g. other tokens ].concat()) } } diff --git a/parachains/runtimes/assets/westmint/src/xcm_config.rs b/parachains/runtimes/assets/westmint/src/xcm_config.rs index a3db0208685..1f57b34fcae 100644 --- a/parachains/runtimes/assets/westmint/src/xcm_config.rs +++ b/parachains/runtimes/assets/westmint/src/xcm_config.rs @@ -18,6 +18,10 @@ use super::{ ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, TrustBackedAssetsInstance, WeightToFee, XcmpQueue, }; +use crate::ForeignAssets; +use assets_common::matching::{ + FromSiblingParachain, IsForeignConcreteAsset, StartsWith, StartsWithExplicitGlobalConsensus, +}; use frame_support::{ match_types, parameter_types, traits::{ConstU32, Contains, Everything, Nothing, PalletInfoAccess}, @@ -36,8 +40,8 @@ use xcm::latest::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, CurrencyAdapter, EnsureXcmOrigin, - FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, NoChecking, ParentAsSuperuser, + ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, UsingComponents, WeightInfoBounds, WithComputedOrigin, }; @@ -49,7 +53,7 @@ parameter_types! { pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(ParachainInfo::parachain_id().into())); - pub const Local: MultiLocation = Here.into_location(); + pub UniversalLocationNetworkId: NetworkId = UniversalLocation::get().global_consensus().unwrap(); pub TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); @@ -81,7 +85,7 @@ pub type CurrencyTransactor = CurrencyAdapter< (), >; -/// `AssetId/Balancer` converter for `TrustBackedAssets` +/// `AssetId/Balance` converter for `TrustBackedAssets` pub type TrustBackedAssetsConvertedConcreteId = assets_common::TrustBackedAssetsConvertedConcreteId; @@ -101,8 +105,38 @@ pub type FungiblesTransactor = FungiblesAdapter< // The account to use for tracking teleports. CheckingAccount, >; + +/// `AssetId/Balance` converter for `TrustBackedAssets` +pub type ForeignAssetsConvertedConcreteId = assets_common::ForeignAssetsConvertedConcreteId< + ( + // Ignore `TrustBackedAssets` explicitly + StartsWith, + // Ignore asset which starts explicitly with our `GlobalConsensus(NetworkId)`, means: + // - foreign assets from our consensus should be: `MultiLocation {parent: 1, X*(Parachain(xyz))} + // - foreign assets outside our consensus with the same `GlobalConsensus(NetworkId)` wont be accepted here + StartsWithExplicitGlobalConsensus, + ), + Balance, +>; + +/// Means for transacting foreign assets from different global consensus. +pub type ForeignFungiblesTransactor = FungiblesAdapter< + // Use this fungibles implementation: + ForeignAssets, + // Use this currency when it is a fungible asset matching the given location or name: + ForeignAssetsConvertedConcreteId, + // Convert an XCM MultiLocation into a local account id: + LocationToAccountId, + // Our chain's account ID type (we can't get away without mentioning it explicitly): + AccountId, + // We dont need to check teleports here. + NoChecking, + // The account to use for tracking teleports. + CheckingAccount, +>; + /// Means for transacting assets on this chain. -pub type AssetTransactors = (CurrencyTransactor, FungiblesTransactor); +pub type AssetTransactors = (CurrencyTransactor, FungiblesTransactor, ForeignFungiblesTransactor); /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, /// ready for dispatching a transaction with Xcm's `Transact`. There is an `OriginKind` which can @@ -177,7 +211,11 @@ impl Contains for SafeCallFilter { RuntimeCall::Session(pallet_session::Call::purge_keys { .. }) | RuntimeCall::XcmpQueue(..) | RuntimeCall::DmpQueue(..) | - RuntimeCall::Utility(pallet_utility::Call::as_derivative { .. }) | + RuntimeCall::Utility( + pallet_utility::Call::as_derivative { .. } | + pallet_utility::Call::batch { .. } | + pallet_utility::Call::batch_all { .. }, + ) | RuntimeCall::Assets( pallet_assets::Call::create { .. } | pallet_assets::Call::force_create { .. } | @@ -206,6 +244,35 @@ impl Contains for SafeCallFilter { pallet_assets::Call::touch { .. } | pallet_assets::Call::refund { .. }, ) | + RuntimeCall::ForeignAssets( + pallet_assets::Call::create { .. } | + pallet_assets::Call::force_create { .. } | + pallet_assets::Call::start_destroy { .. } | + pallet_assets::Call::destroy_accounts { .. } | + pallet_assets::Call::destroy_approvals { .. } | + pallet_assets::Call::finish_destroy { .. } | + pallet_assets::Call::mint { .. } | + pallet_assets::Call::burn { .. } | + pallet_assets::Call::transfer { .. } | + pallet_assets::Call::transfer_keep_alive { .. } | + pallet_assets::Call::force_transfer { .. } | + pallet_assets::Call::freeze { .. } | + pallet_assets::Call::thaw { .. } | + pallet_assets::Call::freeze_asset { .. } | + pallet_assets::Call::thaw_asset { .. } | + pallet_assets::Call::transfer_ownership { .. } | + pallet_assets::Call::set_team { .. } | + pallet_assets::Call::set_metadata { .. } | + pallet_assets::Call::clear_metadata { .. } | + pallet_assets::Call::force_clear_metadata { .. } | + pallet_assets::Call::force_asset_status { .. } | + pallet_assets::Call::approve_transfer { .. } | + pallet_assets::Call::cancel_approval { .. } | + pallet_assets::Call::force_cancel_approval { .. } | + pallet_assets::Call::transfer_approved { .. } | + pallet_assets::Call::touch { .. } | + pallet_assets::Call::refund { .. }, + ) | RuntimeCall::Nfts( pallet_nfts::Call::create { .. } | pallet_nfts::Call::force_create { .. } | @@ -315,7 +382,13 @@ impl xcm_executor::Config for XcmConfig { // Westmint acting _as_ a reserve location for WND and assets created under `pallet-assets`. // For WND, users must use teleport where allowed (e.g. with the Relay Chain). type IsReserve = (); - type IsTeleporter = NativeAsset; // <- should be enough to allow teleportation of WND + // We allow: + // - teleportation of WND + // - teleportation of sibling parachain's assets (as ForeignCreators) + type IsTeleporter = ( + NativeAsset, + IsForeignConcreteAsset>>, + ); type UniversalLocation = UniversalLocation; type Barrier = Barrier; type Weigher = WeightInfoBounds< @@ -403,3 +476,20 @@ impl cumulus_pallet_xcm::Config for Runtime { type RuntimeEvent = RuntimeEvent; type XcmExecutor = XcmExecutor; } + +pub type ForeignCreatorsSovereignAccountOf = ( + SiblingParachainConvertsVia, + AccountId32Aliases, + ParentIsPreset, +); + +/// Simple conversion of `u32` into an `AssetId` for use in benchmarking. +pub struct XcmBenchmarkHelper; +#[cfg(feature = "runtime-benchmarks")] +use pallet_assets::BenchmarkHelper; +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for XcmBenchmarkHelper { + fn create_asset_id_parameter(id: u32) -> MultiLocation { + MultiLocation { parents: 1, interior: X1(Parachain(id)) } + } +} diff --git a/parachains/runtimes/assets/westmint/tests/tests.rs b/parachains/runtimes/assets/westmint/tests/tests.rs index 5d7a6187869..e04c163b01f 100644 --- a/parachains/runtimes/assets/westmint/tests/tests.rs +++ b/parachains/runtimes/assets/westmint/tests/tests.rs @@ -1,27 +1,34 @@ -use asset_test_utils::{ExtBuilder, RuntimeHelper}; -use codec::{DecodeLimit, Encode}; +use asset_test_utils::{ExtBuilder, RuntimeHelper, XcmReceivedFrom}; +use codec::{Decode, DecodeLimit, Encode}; use cumulus_primitives_utility::ChargeWeightInFungibles; use frame_support::{ assert_noop, assert_ok, sp_io, + traits::fungibles::InspectEnumerable, weights::{Weight, WeightToFee as WeightToFeeT}, }; -use parachains_common::{AccountId, AuraId, Balance}; +use parachains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId, Balance}; +use std::convert::Into; pub use westmint_runtime::{ constants::fee::WeightToFee, - xcm_config::{TrustBackedAssetsPalletLocation, XcmConfig}, - Assets, Balances, ExistentialDeposit, ReservedDmpWeight, Runtime, SessionKeys, System, + xcm_config::{CheckingAccount, TrustBackedAssetsPalletLocation, XcmConfig}, + AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, + ParachainSystem, Runtime, SessionKeys, System, TrustBackedAssetsInstance, }; use westmint_runtime::{ - xcm_config::{AssetFeeAsExistentialDepositMultiplierFeeCharger, WestendLocation}, - RuntimeCall, + xcm_config::{ + AssetFeeAsExistentialDepositMultiplierFeeCharger, ForeignCreatorsSovereignAccountOf, + WestendLocation, + }, + MetadataDepositBase, MetadataDepositPerByte, RuntimeCall, RuntimeEvent, }; use xcm::{latest::prelude::*, VersionedXcm, MAX_XCM_DECODE_DEPTH}; use xcm_executor::{ - traits::{Convert, WeightTrader}, + traits::{Convert, Identity, JustTry, WeightTrader}, XcmExecutor, }; -pub const ALICE: [u8; 32] = [1u8; 32]; +const ALICE: [u8; 32] = [1u8; 32]; +const SOME_ASSET_ADMIN: [u8; 32] = [5u8; 32]; type AssetIdForTrustBackedAssetsConvert = assets_common::AssetIdForTrustBackedAssetsConvert; @@ -371,9 +378,15 @@ fn test_assets_balances_api_works() { .build() .execute_with(|| { let local_asset_id = 1; + let foreign_asset_id_multilocation = + MultiLocation { parents: 1, interior: X2(Parachain(1234), GeneralIndex(12345)) }; // check before assert_eq!(Assets::balance(local_asset_id, AccountId::from(ALICE)), 0); + assert_eq!( + ForeignAssets::balance(foreign_asset_id_multilocation, AccountId::from(ALICE)), + 0 + ); assert_eq!(Balances::free_balance(AccountId::from(ALICE)), 0); assert!(Runtime::query_account_balances(AccountId::from(ALICE)).unwrap().is_empty()); @@ -400,15 +413,37 @@ fn test_assets_balances_api_works() { minimum_asset_balance )); + // create foreign asset + let foreign_asset_minimum_asset_balance = 3333333_u128; + assert_ok!(ForeignAssets::force_create( + RuntimeHelper::::root_origin(), + foreign_asset_id_multilocation.clone().into(), + AccountId::from(SOME_ASSET_ADMIN).into(), + false, + foreign_asset_minimum_asset_balance + )); + + // We first mint enough asset for the account to exist for assets + assert_ok!(ForeignAssets::mint( + RuntimeHelper::::origin_of(AccountId::from(SOME_ASSET_ADMIN)), + foreign_asset_id_multilocation.clone().into(), + AccountId::from(ALICE).into(), + 6 * foreign_asset_minimum_asset_balance + )); + // check after assert_eq!( Assets::balance(local_asset_id, AccountId::from(ALICE)), minimum_asset_balance ); + assert_eq!( + ForeignAssets::balance(foreign_asset_id_multilocation, AccountId::from(ALICE)), + 6 * minimum_asset_balance + ); assert_eq!(Balances::free_balance(AccountId::from(ALICE)), some_currency); let result = Runtime::query_account_balances(AccountId::from(ALICE)).unwrap(); - assert_eq!(result.len(), 2); + assert_eq!(result.len(), 3); // check currency assert!(result.iter().any(|asset| asset.eq( @@ -423,56 +458,165 @@ fn test_assets_balances_api_works() { minimum_asset_balance ) .into()))); + // check foreign asset + assert!(result.iter().any(|asset| asset.eq(&( + Identity::reverse_ref(foreign_asset_id_multilocation).unwrap(), + 6 * foreign_asset_minimum_asset_balance + ) + .into()))); }); } -#[test] -fn receive_teleported_asset_works() { - ExtBuilder::::default() - .with_collators(vec![AccountId::from(ALICE)]) - .with_session_keys(vec![( - AccountId::from(ALICE), - AccountId::from(ALICE), - SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) }, - )]) - .build() - .execute_with(|| { - let xcm = Xcm(vec![ - ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }])), - ClearOrigin, - BuyExecution { - fees: MultiAsset { - id: Concrete(MultiLocation { parents: 1, interior: Here }), - fun: Fungible(10000000000000), - }, - weight_limit: Limited(Weight::from_parts(303531000, 65536)), - }, - DepositAsset { - assets: Wild(AllCounted(1)), - beneficiary: MultiLocation { - parents: 0, - interior: X1(AccountId32 { - network: None, - id: [ - 18, 153, 85, 112, 1, 245, 88, 21, 211, 252, 181, 60, 116, 70, 58, - 203, 12, 246, 209, 77, 70, 57, 179, 64, 152, 44, 96, 135, 127, 56, - 70, 9, - ], - }), - }, - }, - ]); - let hash = xcm.using_encoded(sp_io::hashing::blake2_256); - - let weight_limit = ReservedDmpWeight::get(); - - let outcome = XcmExecutor::::execute_xcm(Parent, xcm, hash, weight_limit); - assert_eq!(outcome.ensure_complete(), Ok(())); - }) -} +asset_test_utils::include_teleports_for_native_asset_works!( + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + ParachainSystem, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }) +); + +asset_test_utils::include_teleports_for_foreign_assets_works!( + Runtime, + XcmConfig, + CheckingAccount, + WeightToFee, + ParachainSystem, + ForeignCreatorsSovereignAccountOf, + ForeignAssetsInstance, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_local_consensus_currency_works!( + Runtime, + XcmConfig, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_pallet_assets_instance_works!( + asset_transactor_transfer_with_trust_backed_assets_works, + Runtime, + XcmConfig, + TrustBackedAssetsInstance, + AssetIdForTrustBackedAssets, + AssetIdForTrustBackedAssetsConvert, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + 12345, + Box::new(|| { + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_asset_transactor_transfer_with_pallet_assets_instance_works!( + asset_transactor_transfer_with_foreign_assets_works, + Runtime, + XcmConfig, + ForeignAssetsInstance, + MultiLocation, + JustTry, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + MultiLocation { parents: 1, interior: X2(Parachain(1313), GeneralIndex(12345)) }, + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + }) +); + +asset_test_utils::include_create_and_manage_foreign_assets_for_local_consensus_parachain_assets_works!( + Runtime, + XcmConfig, + WeightToFee, + ForeignCreatorsSovereignAccountOf, + ForeignAssetsInstance, + MultiLocation, + JustTry, + asset_test_utils::CollatorSessionKeys::new( + AccountId::from(ALICE), + AccountId::from(ALICE), + SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) } + ), + ExistentialDeposit::get(), + AssetDeposit::get(), + MetadataDepositBase::get(), + MetadataDepositPerByte::get(), + Box::new(|pallet_asset_call| RuntimeCall::ForeignAssets(pallet_asset_call).encode()), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::ForeignAssets(pallet_asset_event)) => Some(pallet_asset_event), + _ => None, + } + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert!(ForeignAssets::asset_ids().collect::>().is_empty()); + }), + Box::new(|| { + assert!(Assets::asset_ids().collect::>().is_empty()); + assert_eq!(ForeignAssets::asset_ids().collect::>().len(), 1); + }) +); #[test] fn plain_receive_teleported_asset_works() { @@ -494,10 +638,8 @@ fn plain_receive_teleported_asset_works() { ) .map(xcm::v3::Xcm::::try_from).expect("failed").expect("failed"); - let weight_limit = ReservedDmpWeight::get(); - let outcome = - XcmExecutor::::execute_xcm(Parent, maybe_msg, message_id, weight_limit); + XcmExecutor::::execute_xcm(Parent, maybe_msg, message_id, RuntimeHelper::::xcm_max_weight(XcmReceivedFrom::Parent)); assert_eq!(outcome.ensure_complete(), Ok(())); }) } diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/xcm_config.rs b/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/xcm_config.rs index e211355abbb..cfe8662cb7c 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/xcm_config.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/xcm_config.rs @@ -74,7 +74,7 @@ pub type CurrencyTransactor = CurrencyAdapter< LocationToAccountId, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, - // We don't track any teleports. + // We don't track any teleports of `Balances`. (), >; diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs b/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs index d85edffe8d8..189e7c74f81 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/xcm_config.rs @@ -74,7 +74,7 @@ pub type CurrencyTransactor = CurrencyAdapter< LocationToAccountId, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, - // We don't track any teleports. + // We don't track any teleports of `Balances`. (), >; diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs index 66377877de4..17ac293c62b 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs @@ -72,7 +72,7 @@ pub type CurrencyTransactor = CurrencyAdapter< LocationToAccountId, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, - // We don't track any teleports. + // We don't track any teleports of `Balances`. (), >; diff --git a/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs b/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs index f9aa843cf8f..3e726b5b4b7 100644 --- a/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs +++ b/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs @@ -46,7 +46,6 @@ parameter_types! { pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(ParachainInfo::parachain_id().into())); - pub const Local: MultiLocation = Here.into_location(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); pub const GovernanceLocation: MultiLocation = MultiLocation::parent(); }