diff --git a/polkadot/Cargo.lock b/polkadot/Cargo.lock index 35e1a44b2a..95501d6b72 100644 --- a/polkadot/Cargo.lock +++ b/polkadot/Cargo.lock @@ -5450,11 +5450,17 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-balances", "parity-scale-codec", + "polkadot-parachain", + "polkadot-runtime-parachains", "serde", + "sp-core", + "sp-io", "sp-runtime", "sp-std", "xcm", + "xcm-builder", "xcm-executor", ] diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml index 17f4de5395..094d773c50 100644 --- a/polkadot/xcm/pallet-xcm/Cargo.toml +++ b/polkadot/xcm/pallet-xcm/Cargo.toml @@ -17,6 +17,14 @@ frame-system = { git = "https://github.com/paritytech/substrate", default-featur xcm = { path = "..", default-features = false } xcm-executor = { path = "../xcm-executor", default-features = false } +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" } +polkadot-runtime-parachains = { path = "../../runtime/parachains" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +xcm-builder = { path = "../xcm-builder" } +polkadot-parachain = { path = "../../parachain" } + [features] default = ["std"] std = [ diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs index bcd175f4bd..ce5155ac6b 100644 --- a/polkadot/xcm/pallet-xcm/src/lib.rs +++ b/polkadot/xcm/pallet-xcm/src/lib.rs @@ -18,7 +18,11 @@ #![cfg_attr(not(feature = "std"), no_std)] -use codec::{Decode, Encode}; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + use frame_support::traits::{Contains, EnsureOrigin, Filter, Get, OriginTrait}; use sp_runtime::{traits::BadOrigin, RuntimeDebug}; use sp_std::{boxed::Box, convert::TryInto, marker::PhantomData, prelude::*, vec}; diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs new file mode 100644 index 0000000000..bd8375dec3 --- /dev/null +++ b/polkadot/xcm/pallet-xcm/src/mock.rs @@ -0,0 +1,214 @@ +// Copyright 2020 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 frame_support::{ + construct_runtime, parameter_types, + traits::{All, AllowAll}, + weights::Weight, +}; +use polkadot_parachain::primitives::Id as ParaId; +use polkadot_runtime_parachains::origin; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; +pub use sp_std::{cell::RefCell, fmt::Debug, marker::PhantomData}; +use xcm::{ + opaque::v0::{Error as XcmError, MultiAsset, Result as XcmResult, SendXcm, Xcm}, + v0::{MultiLocation, NetworkId, Order}, +}; +use xcm_builder::{ + AccountId32Aliases, AllowTopLevelPaidExecutionFrom, ChildParachainAsNative, + ChildParachainConvertsVia, ChildSystemParachainAsSuperuser, + CurrencyAdapter as XcmCurrencyAdapter, FixedRateOfConcreteFungible, FixedWeightBounds, + IsConcrete, LocationInverter, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, TakeWeightCredit, +}; +use xcm_executor::XcmExecutor; + +use crate as pallet_xcm; + +pub type AccountId = AccountId32; +pub type Balance = u128; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + ParasOrigin: origin::{Pallet, Origin}, + XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event}, + } +); + +thread_local! { + pub static SENT_XCM: RefCell> = RefCell::new(Vec::new()); +} +pub fn sent_xcm() -> Vec<(MultiLocation, Xcm)> { + SENT_XCM.with(|q| (*q.borrow()).clone()) +} +/// Sender that never returns error, always sends +pub struct TestSendXcm; +impl SendXcm for TestSendXcm { + fn send_xcm(dest: MultiLocation, msg: Xcm) -> XcmResult { + SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); + Ok(()) + } +} +/// Sender that returns error if `X8` junction and stops routing +pub struct TestSendXcmErrX8; +impl SendXcm for TestSendXcmErrX8 { + fn send_xcm(dest: MultiLocation, msg: Xcm) -> XcmResult { + if let MultiLocation::X8(..) = dest { + Err(XcmError::Undefined) + } else { + SENT_XCM.with(|q| q.borrow_mut().push((dest, msg))); + Ok(()) + } + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Test { + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = AllowAll; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type MaxLocks = MaxLocks; + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; +} + +parameter_types! { + pub const RelayLocation: MultiLocation = MultiLocation::Null; + pub const AnyNetwork: NetworkId = NetworkId::Any; + pub Ancestry: MultiLocation = MultiLocation::Null; + pub UnitWeightCost: Weight = 1_000; +} + +pub type SovereignAccountOf = + (ChildParachainConvertsVia, AccountId32Aliases); + +pub type LocalAssetTransactor = + XcmCurrencyAdapter, SovereignAccountOf, AccountId, ()>; + +type LocalOriginConverter = ( + SovereignSignedViaLocation, + ChildParachainAsNative, + SignedAccountId32AsNative, + ChildSystemParachainAsSuperuser, +); + +parameter_types! { + pub const BaseXcmWeight: Weight = 1_000; + pub CurrencyPerSecond: (MultiLocation, u128) = (RelayLocation::get(), 1); +} + +pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom>); + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type Call = Call; + type XcmSender = TestSendXcm; + type AssetTransactor = LocalAssetTransactor; + type OriginConverter = LocalOriginConverter; + type IsReserve = (); + type IsTeleporter = (); + type LocationInverter = LocationInverter; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = FixedRateOfConcreteFungible; + type ResponseHandler = (); +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +impl pallet_xcm::Config for Test { + type Event = Event; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = (TestSendXcmErrX8, TestSendXcm); + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = All<(MultiLocation, xcm::v0::Xcm)>; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = All<(MultiLocation, Vec)>; + type XcmReserveTransferFilter = All<(MultiLocation, Vec)>; + type Weigher = FixedWeightBounds; +} + +impl origin::Config for Test {} + +pub(crate) fn last_event() -> Event { + System::events().pop().expect("Event expected").event +} + +pub(crate) fn buy_execution(debt: Weight) -> Order { + use xcm::opaque::v0::prelude::*; + Order::BuyExecution { fees: All, weight: 0, debt, halt_on_error: false, xcm: vec![] } +} + +pub(crate) fn new_test_ext_with_balances( + balances: Vec<(AccountId, Balance)>, +) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/polkadot/xcm/pallet-xcm/src/tests.rs b/polkadot/xcm/pallet-xcm/src/tests.rs new file mode 100644 index 0000000000..f0727e619b --- /dev/null +++ b/polkadot/xcm/pallet-xcm/src/tests.rs @@ -0,0 +1,199 @@ +// Copyright 2020 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 crate::mock::*; +use frame_support::{assert_noop, assert_ok, traits::Currency}; +use polkadot_parachain::primitives::{AccountIdConversion, Id as ParaId}; +use xcm::{ + opaque::v0::prelude::*, + v0::{Junction, Xcm}, +}; + +const ALICE: AccountId = AccountId::new([0u8; 32]); +const BOB: AccountId = AccountId::new([1u8; 32]); +const PARA_ID: u32 = 2000; +const INITIAL_BALANCE: u128 = 100; +const SEND_AMOUNT: u128 = 10; + +/// Test sending an `XCM` message (`XCM::ReserveAssetDeposit`) +/// +/// Asserts that the expected message is sent and the event is emitted +#[test] +fn send_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = 2 * BaseXcmWeight::get(); + let sender: MultiLocation = + Junction::AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); + let message = Xcm::ReserveAssetDeposit { + assets: vec![ConcreteFungible { id: Parent.into(), amount: SEND_AMOUNT }], + effects: vec![ + buy_execution(weight), + DepositAsset { assets: vec![All], dest: sender.clone() }, + ], + }; + assert_ok!(XcmPallet::send(Origin::signed(ALICE), RelayLocation::get(), message.clone())); + assert_eq!( + sent_xcm(), + vec![( + MultiLocation::Null, + RelayedFrom { who: sender.clone(), message: Box::new(message.clone()) } + )] + ); + assert_eq!( + last_event(), + Event::XcmPallet(crate::Event::Sent(sender, RelayLocation::get(), message)) + ); + }); +} + +/// Test that sending an `XCM` message fails when the `XcmRouter` blocks the +/// matching message format +/// +/// Asserts that `send` fails with `Error::SendFailure` +#[test] +fn send_fails_when_xcm_router_blocks() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = 2 * BaseXcmWeight::get(); + let sender: MultiLocation = + Junction::AccountId32 { network: AnyNetwork::get(), id: ALICE.into() }.into(); + let message = Xcm::ReserveAssetDeposit { + assets: vec![ConcreteFungible { id: Parent.into(), amount: SEND_AMOUNT }], + effects: vec![ + buy_execution(weight), + DepositAsset { assets: vec![All], dest: sender.clone() }, + ], + }; + assert_noop!( + XcmPallet::send( + Origin::signed(ALICE), + X8( + Junction::Parent, + Junction::Parent, + Junction::Parent, + Junction::Parent, + Junction::Parent, + Junction::Parent, + Junction::Parent, + Junction::Parent + ), + message.clone() + ), + crate::Error::::SendFailure + ); + }); +} + +/// Test `teleport_assets` +/// +/// Asserts that the sender's balance is decreased as a result of execution of +/// local effects. +#[test] +fn teleport_assets_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = 2 * BaseXcmWeight::get(); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE); + assert_ok!(XcmPallet::teleport_assets( + Origin::signed(ALICE), + RelayLocation::get(), + MultiLocation::X1(Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() }), + vec![ConcreteFungible { id: MultiLocation::Null, amount: SEND_AMOUNT }], + weight, + )); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT); + assert_eq!( + last_event(), + Event::XcmPallet(crate::Event::Attempted(Outcome::Complete(weight))) + ); + }); +} + +/// Test `reserve_transfer_assets` +/// +/// Asserts that the sender's balance is decreased and the beneficiary's balance +/// is increased. Verifies the correct message is sent and event is emitted. +#[test] +fn reserve_transfer_assets_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = BaseXcmWeight::get(); + let dest: MultiLocation = + Junction::AccountId32 { network: NetworkId::Any, id: ALICE.into() }.into(); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE); + assert_ok!(XcmPallet::reserve_transfer_assets( + Origin::signed(ALICE), + Parachain(PARA_ID).into(), + dest.clone(), + vec![ConcreteFungible { id: MultiLocation::Null, amount: SEND_AMOUNT }], + weight + )); + // Alice spent amount + assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - SEND_AMOUNT); + // Destination account (parachain account) has amount + let para_acc: AccountId = ParaId::from(PARA_ID).into_account(); + assert_eq!(Balances::free_balance(para_acc), INITIAL_BALANCE + SEND_AMOUNT); + assert_eq!( + sent_xcm(), + vec![( + Parachain(PARA_ID).into(), + Xcm::ReserveAssetDeposit { + assets: vec![ConcreteFungible { id: Parent.into(), amount: SEND_AMOUNT }], + effects: vec![buy_execution(weight), DepositAsset { assets: vec![All], dest },] + } + )] + ); + assert_eq!( + last_event(), + Event::XcmPallet(crate::Event::Attempted(Outcome::Complete(weight))) + ); + }); +} + +/// Test local execution of XCM +/// +/// Asserts that the sender's balance is decreased and the beneficiary's balance +/// is increased. Verifies the expected event is emitted. +#[test] +fn execute_withdraw_to_deposit_works() { + let balances = + vec![(ALICE, INITIAL_BALANCE), (ParaId::from(PARA_ID).into_account(), INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = 3 * BaseXcmWeight::get(); + let dest: MultiLocation = + Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() }.into(); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE); + assert_ok!(XcmPallet::execute( + Origin::signed(ALICE), + Box::new(Xcm::WithdrawAsset { + assets: vec![ConcreteFungible { id: MultiLocation::Null, amount: SEND_AMOUNT }], + effects: vec![buy_execution(weight), DepositAsset { assets: vec![All], dest }], + }), + weight + )); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT); + assert_eq!(Balances::total_balance(&BOB), SEND_AMOUNT); + assert_eq!( + last_event(), + Event::XcmPallet(crate::Event::Attempted(Outcome::Complete(weight))) + ); + }); +}