diff --git a/Cargo.lock b/Cargo.lock index 9ce52f3fe3f..6c3eac10beb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,26 +327,35 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" name = "asset-test-utils" version = "1.0.0" dependencies = [ + "assets-common", + "cumulus-pallet-parachain-system", "frame-support", "frame-system", "hex-literal", + "pallet-assets", "pallet-balances", "pallet-collator-selection", "pallet-session", "parachains-common", + "parity-scale-codec", "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/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..fbf19b89914 100644 --- a/parachains/runtimes/assets/common/src/lib.rs +++ b/parachains/runtimes/assets/common/src/lib.rs @@ -15,39 +15,78 @@ #![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 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 +106,146 @@ 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 TrustBackAssetsConvert = + 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))); + } + + // setup convert + type Convert = ForeignAssetsConvertedConcreteId, 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 + (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), + ), + // 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)), + ), + ]; + + 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..05acaff3990 --- /dev/null +++ b/parachains/runtimes/assets/common/src/matching.rs @@ -0,0 +1,78 @@ +// 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() + } +} + +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`, so means that `MultiLocation-a' starts with `MultiLocation-b' +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 0f0921976a8..bdcd3b550e9 100644 --- a/parachains/runtimes/assets/statemine/Cargo.toml +++ b/parachains/runtimes/assets/statemine/Cargo.toml @@ -108,6 +108,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 de7ec75e88c..4f957ab7e0d 100644 --- a/parachains/runtimes/assets/statemine/src/lib.rs +++ b/parachains/runtimes/assets/statemine/src/lib.rs @@ -65,19 +65,22 @@ use parachains_common::{ NORMAL_DISPATCH_RATIO, SLOT_DURATION, }; use xcm_config::{ - ForeignCreators, KsmLocation, MultiLocationForAssetId, TrustBackedAssetsConvertedConcreteId, - XcmConfig, + ForeignAssetsConvertedConcreteId, 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, IsMajorityOfBody, 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! { @@ -261,29 +264,43 @@ 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. -type ForeignAssetsInstance = pallet_assets::Instance2; +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; + type CreateOrigin = ForeignCreators< + (FromSiblingParachain>,), + ForeignCreatorsSovereignAccountOf, + AccountId, + >; type ForceOrigin = AssetsForceOrigin; - type AssetDeposit = AssetDeposit; - type MetadataDepositBase = MetadataDepositBase; - type MetadataDepositPerByte = MetadataDepositPerByte; - type ApprovalDeposit = ApprovalDeposit; - type StringLimit = AssetsStringLimit; + 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 = AssetAccountDeposit; + type AssetAccountDeposit = ForeignAssetsAssetAccountDeposit; type RemoveItemsLimit = frame_support::traits::ConstU32<1000>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; @@ -361,6 +378,7 @@ impl Default for ProxyType { Self::Any } } + impl InstanceFilter for ProxyType { fn filter(&self, c: &RuntimeCall) -> bool { match self { @@ -884,11 +902,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 f3b5405cc40..3476da44a47 100644 --- a/parachains/runtimes/assets/statemine/src/xcm_config.rs +++ b/parachains/runtimes/assets/statemine/src/xcm_config.rs @@ -14,39 +14,34 @@ // 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}; use frame_support::{ match_types, parameter_types, - traits::{ - ConstU32, Contains, EnsureOrigin, EnsureOriginWithArg, Everything, Nothing, - PalletInfoAccess, - }, + traits::{ConstU32, Contains, Everything, Nothing, PalletInfoAccess}, }; -use pallet_xcm::{EnsureXcm, XcmPassthrough}; +use pallet_xcm::XcmPassthrough; use parachains_common::{ impls::ToStakingPot, xcm_config::{ AssetFeeAsExistentialDepositMultiplier, DenyReserveTransferToRelayChain, DenyThenTry, }, }; -use polkadot_parachain::primitives::{Id as ParaId, Sibling}; +use polkadot_parachain::primitives::Sibling; use sp_runtime::traits::ConvertInto; 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, }; -use xcm_executor::{ - traits::{Convert, WithOriginFilter}, - XcmExecutor, -}; +use xcm_executor::{traits::WithOriginFilter, XcmExecutor}; parameter_types! { pub const KsmLocation: MultiLocation = MultiLocation::parent(); @@ -54,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 = Here.into_location(); pub TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); @@ -86,7 +80,7 @@ pub type CurrencyTransactor = CurrencyAdapter< (), >; -/// `AssetId/Balancer` converter for `TrustBackedAssets` +/// `AssetId/Balance` converter for `TrustBackedAssets` pub type TrustBackedAssetsConvertedConcreteId = assets_common::TrustBackedAssetsConvertedConcreteId; @@ -106,8 +100,31 @@ pub type FungiblesTransactor = FungiblesAdapter< // The account to use for tracking teleports. CheckingAccount, >; + +/// `AssetId/Balance` converter for `TrustBackedAssets` +pub type ForeignAssetsConvertedConcreteId = assets_common::ForeignAssetsConvertedConcreteId< + StartsWith, + 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, + // TODO:check-parameter - no teleports + 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 @@ -216,6 +233,34 @@ impl Contains for SafeCallFilter { pallet_assets::Call::touch { .. } | pallet_assets::Call::refund { .. }, ) | + RuntimeCall::ForeignAssets( + /* avoided: mint, burn */ + 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::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::Uniques( pallet_uniques::Call::create { .. } | pallet_uniques::Call::force_create { .. } | @@ -287,7 +332,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< @@ -380,37 +431,12 @@ impl cumulus_pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; } -pub type MultiLocationForAssetId = MultiLocation; - -pub type SovereignAccountOf = ( - SiblingParachainConvertsVia, +pub type ForeignCreatorsSovereignAccountOf = ( + SiblingParachainConvertsVia, AccountId32Aliases, ParentIsPreset, ); -// `EnsureOriginWithArg` impl for `CreateOrigin` that allows only XCM origins that are locations -// containing the class location. -pub struct ForeignCreators; -impl EnsureOriginWithArg for ForeignCreators { - type Success = AccountId; - - fn try_origin( - o: RuntimeOrigin, - a: &MultiLocation, - ) -> sp_std::result::Result { - let origin_location = EnsureXcm::::try_origin(o.clone())?; - if !a.starts_with(&origin_location) { - return Err(o) - } - SovereignAccountOf::convert(origin_location).map_err(|_| o) - } - - #[cfg(feature = "runtime-benchmarks")] - fn try_successful_origin(a: &MultiLocation) -> Result { - Ok(pallet_xcm::Origin::Xcm(a.clone()).into()) - } -} - /// Simple conversion of `u32` into an `AssetId` for use in benchmarking. pub struct XcmBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] diff --git a/parachains/runtimes/assets/statemine/tests/tests.rs b/parachains/runtimes/assets/statemine/tests/tests.rs index b696e4ef1e3..7d19a88618c 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::{ForeignCreatorsSovereignAccountOf, XcmConfig}, + AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, + MetadataDepositBase, 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,131 @@ 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_receive_teleported_asset_for_native_asset_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)) } + ) +); + +asset_test_utils::include_receive_teleported_asset_from_foreign_creator_works!( + Runtime, + XcmConfig, + WeightToFee, + 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() +); + +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(), + 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 f6d54cbd85f..02407e23d5c 100644 --- a/parachains/runtimes/assets/statemint/Cargo.toml +++ b/parachains/runtimes/assets/statemint/Cargo.toml @@ -100,6 +100,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 0bb2c5ff5a7..11ef984483d 100644 --- a/parachains/runtimes/assets/statemint/src/xcm_config.rs +++ b/parachains/runtimes/assets/statemint/src/xcm_config.rs @@ -48,7 +48,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(); @@ -80,7 +79,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..2e070dc1aec 100644 --- a/parachains/runtimes/assets/statemint/tests/tests.rs +++ b/parachains/runtimes/assets/statemint/tests/tests.rs @@ -1,25 +1,24 @@ use asset_test_utils::{ExtBuilder, RuntimeHelper}; -use codec::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, Balance, StatemintAuraId as AuraId}; +use parachains_common::{ + AccountId, AssetIdForTrustBackedAssets, Balance, StatemintAuraId as AuraId, +}; use statemint_runtime::xcm_config::{ AssetFeeAsExistentialDepositMultiplierFeeCharger, 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, Runtime, 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 +437,47 @@ 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_receive_teleported_asset_for_native_asset_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)) } + ) +); + +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..c80bb7f67af 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,14 @@ 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 } pallet-collator-selection = { path = "../../../../pallets/collator-selection", default-features = false } parachains-common = { path = "../../../common", default-features = false } +assets-common = { path = "../common", 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" } [dev-dependencies] hex-literal = "0.3.4" @@ -34,12 +41,17 @@ default = [ "std" ] 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", "sp-consensus-aura/std", "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm/std", + "xcm-executor/std", ] diff --git a/parachains/runtimes/assets/test-utils/src/lib.rs b/parachains/runtimes/assets/test-utils/src/lib.rs index fb4750bae9e..94435365bd0 100644 --- a/parachains/runtimes/assets/test-utils/src/lib.rs +++ b/parachains/runtimes/assets/test-utils/src/lib.rs @@ -1,11 +1,19 @@ use frame_support::traits::GenesisBuild; use sp_std::marker::PhantomData; -use frame_support::traits::OriginTrait; +use frame_support::{traits::OriginTrait, weights::Weight}; use parachains_common::AccountId; use sp_consensus_aura::AURA_ENGINE_ID; use sp_core::Encode; use sp_runtime::{Digest, DigestItem}; +use xcm::{ + latest::{MultiAsset, MultiLocation, XcmContext}, + prelude::{Concrete, Fungible, XcmError}, +}; +use xcm_executor::{traits::TransactAsset, Assets}; + +pub mod test_cases; +pub use test_cases::CollatorSessionKeys; pub type BalanceOf = ::Balance; pub type AccountIdOf = ::AccountId; @@ -56,6 +64,11 @@ impl Self { + frame_support::sp_tracing::try_init_simple(); + self + } + pub fn build(self) -> sp_io::TestExternalities where Runtime: @@ -132,3 +145,49 @@ 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]), + ) + } +} + +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(), + } + } +} + +pub fn assert_metadata( + asset_id: &Assets::AssetId, + expected_name: &str, + expected_symbol: &str, + expected_decimals: u8, +) where + Assets: frame_support::traits::tokens::fungibles::InspectMetadata + + frame_support::traits::tokens::fungibles::Inspect, +{ + assert_eq!(Assets::name(asset_id), Vec::from(expected_name),); + assert_eq!(Assets::symbol(asset_id), Vec::from(expected_symbol),); + assert_eq!(Assets::decimals(asset_id), expected_decimals); +} 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..c6f2f5f8e91 --- /dev/null +++ b/parachains/runtimes/assets/test-utils/src/test_cases.rs @@ -0,0 +1,1016 @@ +// 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, AccountIdOf, BalanceOf, ExtBuilder, RuntimeHelper, SessionKeysOf, + ValidatorIdOf, XcmReceivedFrom, +}; +use codec::Encode; +use frame_support::{ + assert_noop, assert_ok, + traits::{fungibles::InspectEnumerable, OriginTrait}, + weights::Weight, +}; +use parachains_common::Balance; +use sp_runtime::{ + traits::{StaticLookup, Zero}, + DispatchError, +}; +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 teleported native assets from relay chain +pub fn receive_teleported_asset_for_native_asset_works( + collator_session_keys: CollatorSessionKeys, + target_account: AccountIdOf, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + XcmConfig: xcm_executor::Config, +{ + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .build() + .execute_with(|| { + // check Balances before + assert_eq!(>::free_balance(&target_account), 0.into()); + + let native_asset_id = MultiLocation::parent(); + + let xcm = Xcm(vec![ + ReceiveTeleportedAsset(MultiAssets::from(vec![MultiAsset { + id: Concrete(native_asset_id), + fun: Fungible(10000000000000), + }])), + ClearOrigin, + BuyExecution { + fees: MultiAsset { + id: Concrete(native_asset_id), + 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: 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()); + }) +} + +#[macro_export] +macro_rules! include_receive_teleported_asset_for_native_asset_works( + ( + $runtime:path, + $xcm_config:path, + $collator_session_key:expr + ) => { + #[test] + fn receive_teleported_asset_for_native_asset_works() { + const BOB: [u8; 32] = [2u8; 32]; + let target_account = parachains_common::AccountId::from(BOB); + + asset_test_utils::test_cases::receive_teleported_asset_for_native_asset_works::< + $runtime, + $xcm_config + >($collator_session_key, target_account) + } + } +); + +/// Test-case makes sure that `Runtime` can receive teleported assets from sibling parachain relay chain +pub fn receive_teleported_asset_from_foreign_creator_works< + Runtime, + XcmConfig, + WeightToFee, + SovereignAccountOf, + ForeignAssetsPalletInstance, +>( + collator_session_keys: CollatorSessionKeys, + target_account: AccountIdOf, + existential_deposit: BalanceOf, + asset_owner: AccountIdOf, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::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, +{ + // foreign parachain with the same consenus currency as asset + let foreign_asset_id_multilocation = + MultiLocation { parents: 1, interior: X2(Parachain(2222), GeneralIndex(1234567)) }; + + // 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.into()), + }; + + let teleported_foreign_asset_amount = 10000000000000; + + 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), + ]) + .with_tracing() + .build() + .execute_with(|| { + // checks before + assert_eq!( + >::free_balance(&target_account), + existential_deposit + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &target_account + ), + 0.into() + ); + + // create foreign asset + let asset_minimum_asset_balance = 3333333_u128; + assert_ok!( + >::force_create( + RuntimeHelper::::root_origin(), + foreign_asset_id_multilocation.clone().into(), + asset_owner.into(), + false, + asset_minimum_asset_balance.into() + ) + ); + assert!(teleported_foreign_asset_amount > asset_minimum_asset_balance); + + // prepare xcm + 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, 1024)), + }, + // 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 after + assert_eq!( + >::free_balance(&target_account), + existential_deposit + ); + assert_eq!( + >::balance( + foreign_asset_id_multilocation.into(), + &target_account + ), + teleported_foreign_asset_amount.into() + ); + }) +} + +#[macro_export] +macro_rules! include_receive_teleported_asset_from_foreign_creator_works( + ( + $runtime:path, + $xcm_config:path, + $weight_to_fee:path, + $sovereign_account_of:path, + $assets_pallet_instance:path, + $collator_session_key:expr, + $existential_deposit:expr + ) => { + #[test] + fn receive_teleported_asset_from_foreign_creator_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::receive_teleported_asset_from_foreign_creator_works::< + $runtime, + $xcm_config, + $weight_to_fee, + $sovereign_account_of, + $assets_pallet_instance + >($collator_session_key, target_account, $existential_deposit, asset_owner) + } + } +); + +/// 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_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.clone()), + (BalanceOf::::from(10_u128) * unit) + ); + assert_eq!( + >::free_balance(target_account.clone()), + (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_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, + 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.clone().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.clone().into(), + alice_account.clone().into(), + (6 * asset_minimum_asset_balance).into() + )); + + // check Assets before + assert_eq!( + >::balance( + asset_id.clone().into(), + &alice_account + ), + (6 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.clone().into(), + &bob_account + ), + 0.into() + ); + assert_eq!( + >::balance( + asset_id.clone().into(), + &charlie_account + ), + 0.into() + ); + assert_eq!( + >::balance( + asset_id.clone().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.clone().into(), + &alice_account + ), + (5 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.clone().into(), + &bob_account + ), + (1 * asset_minimum_asset_balance).into() + ); + assert_eq!( + >::balance( + asset_id.clone().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, + 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_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, + 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), + }; + + 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 + + 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 + + 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.clone().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.clone().into(), + name: Vec::from("My super coin"), + symbol: Vec::from("MY_S_COIN"), + 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.clone().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.clone().into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::admin( + asset_id.clone().into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::issuer( + asset_id.clone().into() + ), + Some(foreign_creator_as_account_id.clone()) + ); + assert_eq!( + >::freezer( + asset_id.clone().into() + ), + Some(bob_account.clone()) + ); + assert!( + >::free_balance(&foreign_creator_as_account_id) < + existential_deposit + + buy_execution_fee_amount.into() + + buy_execution_fee_amount.into() + ); + assert_metadata::< + pallet_assets::Pallet, + AccountIdOf, + >(&asset_id.clone().into(), "My super coin", "MY_S_COIN", 12); + + // check if changed freezer, can freeze + assert_noop!( + >::freeze( + RuntimeHelper::::origin_of(bob_account), + asset_id.clone().into(), + alice_account.clone().into() + ), + pallet_assets::Error::::NoAccount + ); + assert_noop!( + >::freeze( + RuntimeHelper::::origin_of(foreign_creator_as_account_id.clone()), + asset_id.clone().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.clone().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, + $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, + 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 591c7699f8b..1707a17e191 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 e3d36588b0c..c9e8143beaf 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::{ - ForeignCreators, MultiLocationForAssetId, 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! { @@ -246,29 +249,43 @@ 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. -type ForeignAssetsInstance = pallet_assets::Instance2; +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; + type CreateOrigin = ForeignCreators< + (FromSiblingParachain>,), + ForeignCreatorsSovereignAccountOf, + AccountId, + >; type ForceOrigin = AssetsForceOrigin; - type AssetDeposit = AssetDeposit; - type MetadataDepositBase = MetadataDepositBase; - type MetadataDepositPerByte = MetadataDepositPerByte; - type ApprovalDeposit = ApprovalDeposit; - type StringLimit = AssetsStringLimit; + 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 = AssetAccountDeposit; + type AssetAccountDeposit = ForeignAssetsAssetAccountDeposit; type RemoveItemsLimit = frame_support::traits::ConstU32<1000>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; @@ -346,6 +363,7 @@ impl Default for ProxyType { Self::Any } } + impl InstanceFilter for ProxyType { fn filter(&self, c: &RuntimeCall) -> bool { match self { @@ -942,11 +960,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 e604315467b..b6587c7dc97 100644 --- a/parachains/runtimes/assets/westmint/src/xcm_config.rs +++ b/parachains/runtimes/assets/westmint/src/xcm_config.rs @@ -18,35 +18,31 @@ use super::{ ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, TrustBackedAssetsInstance, WeightToFee, XcmpQueue, }; +use crate::ForeignAssets; +use assets_common::matching::{FromSiblingParachain, IsForeignConcreteAsset, StartsWith}; use frame_support::{ match_types, parameter_types, - traits::{ - ConstU32, Contains, EnsureOrigin, EnsureOriginWithArg, Everything, Nothing, - PalletInfoAccess, - }, + traits::{ConstU32, Contains, Everything, Nothing, PalletInfoAccess}, }; -use pallet_xcm::{EnsureXcm, XcmPassthrough}; +use pallet_xcm::XcmPassthrough; use parachains_common::{ impls::ToStakingPot, xcm_config::{ AssetFeeAsExistentialDepositMultiplier, DenyReserveTransferToRelayChain, DenyThenTry, }, }; -use polkadot_parachain::primitives::{Id as ParaId, Sibling}; +use polkadot_parachain::primitives::Sibling; use sp_runtime::traits::ConvertInto; 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, }; -use xcm_executor::{ - traits::{Convert, WithOriginFilter}, - XcmExecutor, -}; +use xcm_executor::{traits::WithOriginFilter, XcmExecutor}; parameter_types! { pub const WestendLocation: MultiLocation = MultiLocation::parent(); @@ -54,7 +50,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 TrustBackedAssetsPalletLocation: MultiLocation = PalletInstance(::index() as u8).into(); pub CheckingAccount: AccountId = PolkadotXcm::check_account(); @@ -86,7 +81,7 @@ pub type CurrencyTransactor = CurrencyAdapter< (), >; -/// `AssetId/Balancer` converter for `TrustBackedAssets` +/// `AssetId/Balance` converter for `TrustBackedAssets` pub type TrustBackedAssetsConvertedConcreteId = assets_common::TrustBackedAssetsConvertedConcreteId; @@ -106,8 +101,31 @@ pub type FungiblesTransactor = FungiblesAdapter< // The account to use for tracking teleports. CheckingAccount, >; + +/// `AssetId/Balance` converter for `TrustBackedAssets` +pub type ForeignAssetsConvertedConcreteId = assets_common::ForeignAssetsConvertedConcreteId< + StartsWith, + 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, + // TODO:check-parameter - no teleports + 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 @@ -182,7 +200,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 { .. } | @@ -211,6 +233,34 @@ impl Contains for SafeCallFilter { pallet_assets::Call::touch { .. } | pallet_assets::Call::refund { .. }, ) | + RuntimeCall::ForeignAssets( + /* avoided: mint, burn */ + 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::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::Uniques( pallet_uniques::Call::create { .. } | pallet_uniques::Call::force_create { .. } | @@ -282,7 +332,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< @@ -370,37 +426,12 @@ impl cumulus_pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; } -pub type MultiLocationForAssetId = MultiLocation; - -pub type SovereignAccountOf = ( - SiblingParachainConvertsVia, +pub type ForeignCreatorsSovereignAccountOf = ( + SiblingParachainConvertsVia, AccountId32Aliases, ParentIsPreset, ); -// `EnsureOriginWithArg` impl for `CreateOrigin` that allows only XCM origins that are locations -// containing the class location. -pub struct ForeignCreators; -impl EnsureOriginWithArg for ForeignCreators { - type Success = AccountId; - - fn try_origin( - o: RuntimeOrigin, - a: &MultiLocation, - ) -> sp_std::result::Result { - let origin_location = EnsureXcm::::try_origin(o.clone())?; - if !a.starts_with(&origin_location) { - return Err(o) - } - SovereignAccountOf::convert(origin_location).map_err(|_| o) - } - - #[cfg(feature = "runtime-benchmarks")] - fn try_successful_origin(a: &MultiLocation) -> Result { - Ok(pallet_xcm::Origin::Xcm(a.clone()).into()) - } -} - /// Simple conversion of `u32` into an `AssetId` for use in benchmarking. pub struct XcmBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] diff --git a/parachains/runtimes/assets/westmint/tests/tests.rs b/parachains/runtimes/assets/westmint/tests/tests.rs index 5d7a6187869..77125a4884c 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, + AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, + Runtime, SessionKeys, System, TrustBackedAssetsInstance, }; use westmint_runtime::{ - xcm_config::{AssetFeeAsExistentialDepositMultiplierFeeCharger, WestendLocation}, - RuntimeCall, + xcm_config::{ + AssetFeeAsExistentialDepositMultiplierFeeCharger, ForeignCreatorsSovereignAccountOf, + WestendLocation, + }, + MetadataDepositBase, 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,134 @@ 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_receive_teleported_asset_for_native_asset_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)) } + ) +); + +asset_test_utils::include_receive_teleported_asset_from_foreign_creator_works!( + Runtime, + XcmConfig, + WeightToFee, + 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() +); + +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(), + 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 +607,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/collectives/collectives-polkadot/src/xcm_config.rs b/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs index 5c6332472aa..661b8403238 100644 --- a/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs +++ b/parachains/runtimes/collectives/collectives-polkadot/src/xcm_config.rs @@ -45,7 +45,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(); }