diff --git a/Cargo.lock b/Cargo.lock index 0cf0541fa5f7..959f6b0968c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6566,6 +6566,24 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-salary" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=master#96f8c97dff7f419a349ee01d02613445ba4a41f8" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-scheduler" version = "4.0.0-dev" @@ -15052,11 +15070,14 @@ dependencies = [ "frame-system", "impl-trait-for-tuples", "log", + "pallet-assets", "pallet-balances", + "pallet-salary", "pallet-transaction-payment", "pallet-xcm", "parity-scale-codec", "polkadot-parachain", + "polkadot-primitives", "polkadot-runtime-parachains", "polkadot-test-runtime", "primitive-types", diff --git a/xcm/xcm-builder/Cargo.toml b/xcm/xcm-builder/Cargo.toml index fec354d0caea..702d5bd7fa06 100644 --- a/xcm/xcm-builder/Cargo.toml +++ b/xcm/xcm-builder/Cargo.toml @@ -29,6 +29,9 @@ polkadot-parachain = { path = "../../parachain", default-features = false } primitive-types = "0.12.1" pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" } pallet-xcm = { path = "../pallet-xcm" } +pallet-salary = { git = "https://github.com/paritytech/substrate", branch = "master" } +pallet-assets = { git = "https://github.com/paritytech/substrate", branch = "master" } +primitives = { package = "polkadot-primitives", path = "../../primitives" } polkadot-runtime-parachains = { path = "../../runtime/parachains" } assert_matches = "1.5.0" polkadot-test-runtime = { path = "../../runtime/test-runtime" } diff --git a/xcm/xcm-builder/src/tests/mock.rs b/xcm/xcm-builder/src/tests/mock.rs index 9be034596f43..0e7b748106ef 100644 --- a/xcm/xcm-builder/src/tests/mock.rs +++ b/xcm/xcm-builder/src/tests/mock.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +//! Mock implementations to test XCM builder configuration types. + use crate::{ barriers::{AllowSubscriptionsFrom, RespectSuspension, TrailingSetTopicAsId}, test_utils::*, @@ -42,7 +44,7 @@ pub use sp_std::{ marker::PhantomData, }; pub use xcm::latest::{prelude::*, Weight}; -use xcm_executor::traits::Properties; +use xcm_executor::traits::{Properties, QueryHandler, QueryResponseStatus}; pub use xcm_executor::{ traits::{ AssetExchange, AssetLock, CheckSuspension, ConvertOrigin, Enact, ExportXcm, FeeManager, @@ -410,6 +412,63 @@ pub fn response(query_id: u64) -> Option { }) } +/// Mock implementation of the [`QueryHandler`] trait for creating XCM success queries and expecting +/// responses. +pub struct TestQueryHandler(core::marker::PhantomData<(T, BlockNumber)>); +impl QueryHandler + for TestQueryHandler +{ + type QueryId = u64; + type BlockNumber = BlockNumber; + type Error = XcmError; + type UniversalLocation = T::UniversalLocation; + + fn new_query( + responder: impl Into, + _timeout: Self::BlockNumber, + _match_querier: impl Into, + ) -> Self::QueryId { + let query_id = 1; + expect_response(query_id, responder.into()); + query_id + } + + fn report_outcome( + message: &mut Xcm<()>, + responder: impl Into, + timeout: Self::BlockNumber, + ) -> Result { + let responder = responder.into(); + let destination = Self::UniversalLocation::get() + .invert_target(&responder) + .map_err(|()| XcmError::LocationNotInvertible)?; + let query_id = Self::new_query(responder, timeout, Here); + let response_info = QueryResponseInfo { destination, query_id, max_weight: Weight::zero() }; + let report_error = Xcm(vec![ReportError(response_info)]); + message.0.insert(0, SetAppendix(report_error)); + Ok(query_id) + } + + fn take_response(query_id: Self::QueryId) -> QueryResponseStatus { + QUERIES + .with(|q| { + q.borrow().get(&query_id).and_then(|v| match v { + ResponseSlot::Received(r) => Some(QueryResponseStatus::Ready { + response: r.clone(), + at: Self::BlockNumber::zero(), + }), + _ => Some(QueryResponseStatus::NotFound), + }) + }) + .unwrap_or(QueryResponseStatus::NotFound) + } + + #[cfg(feature = "runtime-benchmarks")] + fn expect_response(_id: Self::QueryId, _response: xcm::latest::Response) { + // Unnecessary since it's only a test implementation + } +} + parameter_types! { pub static ExecutorUniversalLocation: InteriorMultiLocation = (ByGenesis([0; 32]), Parachain(42)).into(); diff --git a/xcm/xcm-builder/src/tests/mod.rs b/xcm/xcm-builder/src/tests/mod.rs index 6daf1872f055..e11caf6282be 100644 --- a/xcm/xcm-builder/src/tests/mod.rs +++ b/xcm/xcm-builder/src/tests/mod.rs @@ -34,6 +34,7 @@ mod bridging; mod expecting; mod locking; mod origins; +mod pay; mod querying; mod transacting; mod version_subscriptions; diff --git a/xcm/xcm-builder/src/tests/pay/mock.rs b/xcm/xcm-builder/src/tests/pay/mock.rs new file mode 100644 index 000000000000..3231611d3dd8 --- /dev/null +++ b/xcm/xcm-builder/src/tests/pay/mock.rs @@ -0,0 +1,334 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +use super::*; + +use frame_support::traits::{AsEnsureOriginWithArg, Nothing}; + +use frame_support::derive_impl; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU32, Everything}, +}; +use frame_system::{EnsureRoot, EnsureSigned}; +use polkadot_test_runtime::SignedExtra; +use primitives::{AccountIndex, BlakeTwo256, Signature}; +use sp_runtime::{generic, traits::MaybeEquivalence, AccountId32, BuildStorage}; +use xcm_executor::{traits::ConvertLocation, XcmExecutor}; + +pub type Address = sp_runtime::MultiAddress; +pub type UncheckedExtrinsic = + generic::UncheckedExtrinsic; +pub type Header = generic::Header; +pub type Block = generic::Block; + +pub type BlockNumber = u32; +pub type AccountId = AccountId32; + +construct_runtime!( + pub struct Test { + System: frame_system, + Balances: pallet_balances, + Assets: pallet_assets, + Salary: pallet_salary, + XcmPallet: pallet_xcm, + } +); + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type BlockHashCount = BlockHashCount; + type BaseCallFilter = frame_support::traits::Everything; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type PalletInfo = PalletInfo; + type OnSetCode = (); + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; +} + +pub type Balance = u128; + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<0>; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +parameter_types! { + pub const AssetDeposit: u128 = 1_000_000; + pub const MetadataDepositBase: u128 = 1_000_000; + pub const MetadataDepositPerByte: u128 = 100_000; + pub const AssetAccountDeposit: u128 = 1_000_000; + pub const ApprovalDeposit: u128 = 1_000_000; + pub const AssetsStringLimit: u32 = 50; + pub const RemoveItemsLimit: u32 = 50; +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetIdForAssets; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type AssetAccountDeposit = AssetAccountDeposit; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = AssetsStringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type RemoveItemsLimit = RemoveItemsLimit; + type AssetIdParameter = AssetIdForAssets; + type CallbackHandle = (); +} + +parameter_types! { + pub const RelayLocation: MultiLocation = Here.into_location(); + pub const AnyNetwork: Option = None; + pub UniversalLocation: InteriorMultiLocation = (ByGenesis([0; 32]), Parachain(42)).into(); + pub UnitWeightCost: u64 = 1_000; + pub static AdvertisedXcmVersion: u32 = 3; + pub const BaseXcmWeight: Weight = Weight::from_parts(1_000, 1_000); + pub CurrencyPerSecondPerByte: (AssetId, u128, u128) = (Concrete(RelayLocation::get()), 1, 1); + pub TrustedAssets: (MultiAssetFilter, MultiLocation) = (All.into(), Here.into()); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; + pub CheckingAccount: AccountId = XcmPallet::check_account(); +} + +type AssetIdForAssets = u128; + +pub struct FromMultiLocationToAsset( + core::marker::PhantomData<(MultiLocation, AssetId)>, +); +impl MaybeEquivalence + for FromMultiLocationToAsset +{ + fn convert(value: &MultiLocation) -> Option { + match value { + MultiLocation { parents: 0, interior: Here } => Some(0 as AssetIdForAssets), + MultiLocation { parents: 1, interior: Here } => Some(1 as AssetIdForAssets), + MultiLocation { parents: 0, interior: X2(PalletInstance(1), GeneralIndex(index)) } + if ![0, 1].contains(index) => + Some(*index as AssetIdForAssets), + _ => None, + } + } + + fn convert_back(value: &AssetIdForAssets) -> Option { + match value { + 0u128 => Some(MultiLocation { parents: 1, interior: Here }), + para_id @ 1..=1000 => + Some(MultiLocation { parents: 1, interior: X1(Parachain(*para_id as u32)) }), + _ => None, + } + } +} + +pub type LocalOriginToLocation = SignedToAccountId32; +pub type LocalAssetsTransactor = FungiblesAdapter< + Assets, + ConvertedConcreteId< + AssetIdForAssets, + Balance, + FromMultiLocationToAsset, + JustTry, + >, + SovereignAccountOf, + AccountId, + NoChecking, + CheckingAccount, +>; + +type OriginConverter = ( + pallet_xcm::XcmPassthrough, + SignedAccountId32AsNative, +); +type Barrier = AllowUnpaidExecutionFrom; + +pub struct DummyWeightTrader; +impl WeightTrader for DummyWeightTrader { + fn new() -> Self { + DummyWeightTrader + } + + fn buy_weight( + &mut self, + _weight: Weight, + _payment: xcm_executor::Assets, + _context: &XcmContext, + ) -> Result { + Ok(xcm_executor::Assets::default()) + } +} + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = TestMessageSender; + type AssetTransactor = LocalAssetsTransactor; + type OriginConverter = OriginConverter; + type IsReserve = (); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = DummyWeightTrader; + type ResponseHandler = XcmPallet; + type AssetTrap = XcmPallet; + type AssetLocker = (); + type AssetExchanger = (); + type AssetClaims = XcmPallet; + type SubscriptionService = XcmPallet; + type PalletInstancesInfo = (); + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; +} + +parameter_types! { + pub TreasuryAccountId: AccountId = AccountId::new([42u8; 32]); +} + +pub struct TreasuryToAccount; +impl ConvertLocation for TreasuryToAccount { + fn convert_location(location: &MultiLocation) -> Option { + match location { + MultiLocation { + parents: 1, + interior: + X2(Parachain(42), Plurality { id: BodyId::Treasury, part: BodyPart::Voice }), + } => Some(TreasuryAccountId::get()), // Hardcoded test treasury account id + _ => None, + } + } +} + +type SovereignAccountOf = ( + AccountId32Aliases, + TreasuryToAccount, + HashedDescription>, +); + +#[cfg(feature = "runtime-benchmarks")] +parameter_types! { + pub ReachableDest: Option = Some(Parachain(1000).into()); +} + +impl pallet_xcm::Config for Test { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = EnsureXcmOrigin; + type XcmRouter = TestMessageSender; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = AdvertisedXcmVersion; + type TrustedLockers = (); + type SovereignAccountOf = SovereignAccountOf; + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type MaxLockers = frame_support::traits::ConstU32<8>; + type MaxRemoteLockConsumers = frame_support::traits::ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type WeightInfo = pallet_xcm::TestWeightInfo; + #[cfg(feature = "runtime-benchmarks")] + type ReachableDest = ReachableDest; + type AdminOrigin = EnsureRoot; +} + +pub const UNITS: Balance = 1_000_000_000_000; +pub const INITIAL_BALANCE: Balance = 100 * UNITS; +pub const MINIMUM_BALANCE: Balance = 1 * UNITS; + +pub fn sibling_chain_account_id(para_id: u32, account: [u8; 32]) -> AccountId { + let location: MultiLocation = + (Parent, Parachain(para_id), Junction::AccountId32 { id: account, network: None }).into(); + SovereignAccountOf::convert_location(&location).unwrap() +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let admin_account: AccountId = AccountId::new([0u8; 32]); + pallet_assets::GenesisConfig:: { + assets: vec![ + (0, admin_account.clone(), true, MINIMUM_BALANCE), + (1, admin_account.clone(), true, MINIMUM_BALANCE), + (100, admin_account.clone(), true, MINIMUM_BALANCE), + ], + metadata: vec![ + (0, "Native token".encode(), "NTV".encode(), 12), + (1, "Relay token".encode(), "RLY".encode(), 12), + (100, "Test token".encode(), "TST".encode(), 12), + ], + accounts: vec![ + (0, sibling_chain_account_id(42, [3u8; 32]), INITIAL_BALANCE), + (1, TreasuryAccountId::get(), INITIAL_BALANCE), + (100, TreasuryAccountId::get(), INITIAL_BALANCE), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn next_block() { + System::set_block_number(System::block_number() + 1); +} + +pub fn run_to(block_number: BlockNumber) { + while System::block_number() < block_number { + next_block(); + } +} diff --git a/xcm/xcm-builder/src/tests/pay/mod.rs b/xcm/xcm-builder/src/tests/pay/mod.rs new file mode 100644 index 000000000000..8adf1ad2f5e1 --- /dev/null +++ b/xcm/xcm-builder/src/tests/pay/mod.rs @@ -0,0 +1,21 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +use super::*; + +mod mock; +mod pay; +mod salary; diff --git a/xcm/xcm-builder/src/tests/pay/pay.rs b/xcm/xcm-builder/src/tests/pay/pay.rs new file mode 100644 index 000000000000..28b2feec0c23 --- /dev/null +++ b/xcm/xcm-builder/src/tests/pay/pay.rs @@ -0,0 +1,158 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Tests for making sure `PayOverXcm::pay` generates the correct message and sends it to the +//! correct destination + +use super::{mock::*, *}; +use frame_support::{assert_ok, traits::tokens::Pay}; + +/// Type representing both a location and an asset that is held at that location. +/// The id of the held asset is relative to the location where it is being held. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq)] +pub struct AssetKind { + destination: MultiLocation, + asset_id: AssetId, +} + +pub struct LocatableAssetKindConverter; +impl sp_runtime::traits::Convert for LocatableAssetKindConverter { + fn convert(value: AssetKind) -> LocatableAssetId { + LocatableAssetId { asset_id: value.asset_id, location: value.destination } + } +} + +parameter_types! { + pub SenderAccount: AccountId = AccountId::new([3u8; 32]); + pub InteriorAccount: InteriorMultiLocation = AccountId32 { id: SenderAccount::get().into(), network: None }.into(); + pub InteriorBody: InteriorMultiLocation = Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into(); + pub Timeout: BlockNumber = 5; // 5 blocks +} + +/// Scenario: +/// Account #3 on the local chain, parachain 42, controls an amount of funds on parachain 2. +/// [`PayOverXcm::pay`] creates the correct message for account #3 to pay another account, account +/// #5, on parachain 2, remotely, in its native token. +#[test] +fn pay_over_xcm_works() { + let recipient = AccountId::new([5u8; 32]); + let asset_kind = + AssetKind { destination: (Parent, Parachain(2)).into(), asset_id: Here.into() }; + let amount = 10 * UNITS; + + new_test_ext().execute_with(|| { + // Check starting balance + assert_eq!(mock::Assets::balance(0, &recipient), 0); + + assert_ok!(PayOverXcm::< + InteriorAccount, + TestMessageSender, + TestQueryHandler, + Timeout, + AccountId, + AssetKind, + LocatableAssetKindConverter, + AliasesIntoAccountId32, + >::pay(&recipient, asset_kind, amount)); + + let expected_message = Xcm(vec![ + DescendOrigin(AccountId32 { id: SenderAccount::get().into(), network: None }.into()), + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + SetAppendix(Xcm(vec![ReportError(QueryResponseInfo { + destination: (Parent, Parachain(42)).into(), + query_id: 1, + max_weight: Weight::zero(), + })])), + TransferAsset { + assets: (Here, amount).into(), + beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(), + }, + ]); + let expected_hash = fake_message_hash(&expected_message); + + assert_eq!( + sent_xcm(), + vec![((Parent, Parachain(2)).into(), expected_message, expected_hash)] + ); + + let (_, message, hash) = sent_xcm()[0].clone(); + let message = + Xcm::<::RuntimeCall>::from(message.clone()); + + // Execute message in parachain 2 with parachain 42's origin + let origin = (Parent, Parachain(42)); + XcmExecutor::::execute_xcm(origin, message, hash, Weight::MAX); + assert_eq!(mock::Assets::balance(0, &recipient), amount); + }); +} + +/// Scenario: +/// A pluralistic body, a Treasury, on the local chain, parachain 42, controls an amount of funds +/// on parachain 2. [`PayOverXcm::pay`] creates the correct message for the treasury to pay +/// another account, account #7, on parachain 2, remotely, in the relay's token. +#[test] +fn pay_over_xcm_governance_body() { + let recipient = AccountId::new([7u8; 32]); + let asset_kind = + AssetKind { destination: (Parent, Parachain(2)).into(), asset_id: Parent.into() }; + let amount = 10 * UNITS; + + let relay_asset_index = 1; + + new_test_ext().execute_with(|| { + // Check starting balance + assert_eq!(mock::Assets::balance(relay_asset_index, &recipient), 0); + + assert_ok!(PayOverXcm::< + InteriorBody, + TestMessageSender, + TestQueryHandler, + Timeout, + AccountId, + AssetKind, + LocatableAssetKindConverter, + AliasesIntoAccountId32, + >::pay(&recipient, asset_kind, amount)); + + let expected_message = Xcm(vec![ + DescendOrigin(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into()), + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + SetAppendix(Xcm(vec![ReportError(QueryResponseInfo { + destination: (Parent, Parachain(42)).into(), + query_id: 1, + max_weight: Weight::zero(), + })])), + TransferAsset { + assets: (Parent, amount).into(), + beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(), + }, + ]); + let expected_hash = fake_message_hash(&expected_message); + assert_eq!( + sent_xcm(), + vec![((Parent, Parachain(2)).into(), expected_message, expected_hash)] + ); + + let (_, message, hash) = sent_xcm()[0].clone(); + let message = + Xcm::<::RuntimeCall>::from(message.clone()); + + // Execute message in parachain 2 with parachain 42's origin + let origin = (Parent, Parachain(42)); + XcmExecutor::::execute_xcm(origin, message, hash, Weight::MAX); + assert_eq!(mock::Assets::balance(relay_asset_index, &recipient), amount); + }); +} diff --git a/xcm/xcm-builder/src/tests/pay/salary.rs b/xcm/xcm-builder/src/tests/pay/salary.rs new file mode 100644 index 000000000000..1d40345f302a --- /dev/null +++ b/xcm/xcm-builder/src/tests/pay/salary.rs @@ -0,0 +1,172 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Tests for the integration between `PayOverXcm` and the salary pallet + +use super::{mock::*, *}; + +use frame_support::{ + assert_ok, + traits::{tokens::GetSalary, RankedMembers}, +}; +use sp_runtime::{traits::ConvertToValue, DispatchResult}; + +parameter_types! { + pub Interior: InteriorMultiLocation = Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into(); + pub Timeout: BlockNumber = 5; + pub AssetHub: MultiLocation = (Parent, Parachain(1)).into(); + pub AssetIdGeneralIndex: u128 = 100; + pub AssetHubAssetId: AssetId = (PalletInstance(1), GeneralIndex(AssetIdGeneralIndex::get())).into(); + pub LocatableAsset: LocatableAssetId = LocatableAssetId { asset_id: AssetHubAssetId::get(), location: AssetHub::get() }; +} + +type SalaryPayOverXcm = PayOverXcm< + Interior, + TestMessageSender, + TestQueryHandler, + Timeout, + AccountId, + (), + ConvertToValue, + AliasesIntoAccountId32, +>; + +type Rank = u128; + +thread_local! { + pub static CLUB: RefCell> = RefCell::new(BTreeMap::new()); +} + +pub struct TestClub; +impl RankedMembers for TestClub { + type AccountId = AccountId; + type Rank = Rank; + + fn min_rank() -> Self::Rank { + 0 + } + fn rank_of(who: &Self::AccountId) -> Option { + CLUB.with(|club| club.borrow().get(who).cloned()) + } + fn induct(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| club.borrow_mut().insert(who.clone(), 0)); + Ok(()) + } + fn promote(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| { + club.borrow_mut().entry(who.clone()).and_modify(|rank| *rank += 1); + }); + Ok(()) + } + fn demote(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| match club.borrow().get(who) { + None => Err(sp_runtime::DispatchError::Unavailable), + Some(&0) => { + club.borrow_mut().remove(&who); + Ok(()) + }, + Some(_) => { + club.borrow_mut().entry(who.clone()).and_modify(|rank| *rank += 1); + Ok(()) + }, + }) + } +} + +fn set_rank(who: AccountId, rank: u128) { + CLUB.with(|club| club.borrow_mut().insert(who, rank)); +} + +parameter_types! { + pub const RegistrationPeriod: BlockNumber = 2; + pub const PayoutPeriod: BlockNumber = 2; + pub const FixedSalaryAmount: Balance = 10 * UNITS; + pub static Budget: Balance = FixedSalaryAmount::get(); +} + +pub struct FixedSalary; +impl GetSalary for FixedSalary { + fn get_salary(_rank: Rank, _who: &AccountId) -> Balance { + FixedSalaryAmount::get() + } +} + +impl pallet_salary::Config for Test { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type Paymaster = SalaryPayOverXcm; + type Members = TestClub; + type Salary = FixedSalary; + type RegistrationPeriod = RegistrationPeriod; + type PayoutPeriod = PayoutPeriod; + type Budget = Budget; +} + +/// Scenario: +/// The salary pallet is used to pay a member over XCM. +/// The correct XCM message is generated and when executed in the remote chain, +/// the member receives the salary. +#[test] +fn salary_pay_over_xcm_works() { + let recipient = AccountId::new([1u8; 32]); + + new_test_ext().execute_with(|| { + // Set the recipient as a member of a ranked collective + set_rank(recipient.clone(), 1); + + // Check starting balance + assert_eq!(mock::Assets::balance(AssetIdGeneralIndex::get(), &recipient.clone()), 0); + + // Use salary pallet to call `PayOverXcm::pay` + assert_ok!(Salary::init(RuntimeOrigin::signed(recipient.clone()))); + run_to(5); + assert_ok!(Salary::induct(RuntimeOrigin::signed(recipient.clone()))); + assert_ok!(Salary::bump(RuntimeOrigin::signed(recipient.clone()))); + assert_ok!(Salary::register(RuntimeOrigin::signed(recipient.clone()))); + run_to(7); + assert_ok!(Salary::payout(RuntimeOrigin::signed(recipient.clone()))); + + // Get message from mock transport layer + let (_, message, hash) = sent_xcm()[0].clone(); + // Change type from `Xcm<()>` to `Xcm` to be able to execute later + let message = + Xcm::<::RuntimeCall>::from(message.clone()); + + let expected_message: Xcm = Xcm::(vec![ + DescendOrigin(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into()), + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + SetAppendix(Xcm(vec![ReportError(QueryResponseInfo { + destination: (Parent, Parachain(42)).into(), + query_id: 1, + max_weight: Weight::zero(), + })])), + TransferAsset { + assets: (AssetHubAssetId::get(), FixedSalaryAmount::get()).into(), + beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(), + }, + ]); + assert_eq!(message, expected_message); + + // Execute message as the asset hub + XcmExecutor::::execute_xcm((Parent, Parachain(42)), message, hash, Weight::MAX); + + // Recipient receives the payment + assert_eq!( + mock::Assets::balance(AssetIdGeneralIndex::get(), &recipient), + FixedSalaryAmount::get() + ); + }); +}