diff --git a/Cargo.lock b/Cargo.lock index 0d08c175bde5..bab7484bce02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "sp-runtime", "sp-std", "staging-xcm", + "staging-xcm-builder", "staging-xcm-executor", "substrate-wasm-builder", ] diff --git a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/pallet_xcm.rs index c2ed67d2f5d4..474ad208f501 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/pallet_xcm.rs @@ -92,6 +92,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-kusama/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-kusama/tests/tests.rs index 7d49b56e461a..88ec38b7ac96 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-kusama/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-kusama/tests/tests.rs @@ -18,13 +18,14 @@ //! Tests for the Statemine (Kusama Assets Hub) chain. use asset_hub_kusama_runtime::xcm_config::{ - AssetFeeAsExistentialDepositMultiplierFeeCharger, KsmLocation, TrustBackedAssetsPalletLocation, + AssetFeeAsExistentialDepositMultiplierFeeCharger, KsmLocation, LocationToAccountId, + TrustBackedAssetsPalletLocation, }; pub use asset_hub_kusama_runtime::{ xcm_config::{CheckingAccount, ForeignCreatorsSovereignAccountOf, XcmConfig}, AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, Runtime, - RuntimeCall, RuntimeEvent, SessionKeys, System, TrustBackedAssetsInstance, + RuntimeCall, RuntimeEvent, SessionKeys, System, TrustBackedAssetsInstance, XcmpQueue, }; use asset_test_utils::{CollatorSessionKeys, ExtBuilder}; use codec::{Decode, Encode}; @@ -632,3 +633,62 @@ asset_test_utils::include_create_and_manage_foreign_assets_for_local_consensus_p assert_eq!(ForeignAssets::asset_ids().collect::>().len(), 1); }) ); + +#[test] +fn reserve_transfer_native_asset_works() { + asset_test_utils::test_cases::reserve_transfer_native_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} + +#[test] +fn reserve_withdraw_foreign_asset_works() { + asset_test_utils::test_cases::reserve_withdraw_foreign_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ForeignAssetsInstance, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/pallet_xcm.rs index bd7615895e2e..48a9f859c255 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/pallet_xcm.rs @@ -92,6 +92,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/tests/tests.rs index 7200ebc16a28..a1f2e4bc1eac 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/tests/tests.rs @@ -19,12 +19,13 @@ use asset_hub_polkadot_runtime::xcm_config::{ AssetFeeAsExistentialDepositMultiplierFeeCharger, CheckingAccount, DotLocation, - ForeignCreatorsSovereignAccountOf, TrustBackedAssetsPalletLocation, XcmConfig, + ForeignCreatorsSovereignAccountOf, LocationToAccountId, TrustBackedAssetsPalletLocation, + XcmConfig, }; pub use asset_hub_polkadot_runtime::{ AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, Runtime, - RuntimeCall, RuntimeEvent, SessionKeys, System, TrustBackedAssetsInstance, + RuntimeCall, RuntimeEvent, SessionKeys, System, TrustBackedAssetsInstance, XcmpQueue, }; use asset_test_utils::{CollatorSessionKeys, ExtBuilder}; use codec::{Decode, Encode}; @@ -657,3 +658,62 @@ asset_test_utils::include_create_and_manage_foreign_assets_for_local_consensus_p assert_eq!(ForeignAssets::asset_ids().collect::>().len(), 1); }) ); + +#[test] +fn reserve_transfer_native_asset_works() { + asset_test_utils::test_cases::reserve_transfer_native_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} + +#[test] +fn reserve_withdraw_foreign_asset_works() { + asset_test_utils::test_cases::reserve_withdraw_foreign_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ForeignAssetsInstance, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs index 1cc4c2d0e24c..bbf09bee42a9 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_xcm.rs @@ -92,6 +92,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } fn execute() -> Weight { // Proof Size summary in bytes: // Measured: `0` diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs index 599ff90e254a..1c3af637ea21 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs @@ -20,10 +20,10 @@ use asset_hub_westend_runtime::{ xcm_config::{ AssetFeeAsExistentialDepositMultiplierFeeCharger, ForeignCreatorsSovereignAccountOf, - WestendLocation, + LocationToAccountId, WestendLocation, }, AllPalletsWithoutSystem, MetadataDepositBase, MetadataDepositPerByte, RuntimeCall, - RuntimeEvent, + RuntimeEvent, XcmpQueue, }; pub use asset_hub_westend_runtime::{ xcm_config::{CheckingAccount, TrustBackedAssetsPalletLocation, XcmConfig}, @@ -666,3 +666,62 @@ fn plain_receive_teleported_asset_works() { assert_eq!(outcome.ensure_complete(), Ok(())); }) } + +#[test] +fn reserve_transfer_native_asset_works() { + asset_test_utils::test_cases::reserve_transfer_native_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} + +#[test] +fn reserve_withdraw_foreign_asset_works() { + asset_test_utils::test_cases::reserve_withdraw_foreign_asset_works::< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ForeignAssetsInstance, + ParachainSystem, + XcmpQueue, + LocationToAccountId, + >( + collator_session_keys(), + ExistentialDeposit::get(), + AccountId::from(ALICE), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::PolkadotXcm(event)) => Some(event), + _ => None, + } + }), + Box::new(|runtime_event_encoded: Vec| { + match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) { + Ok(RuntimeEvent::XcmpQueue(event)) => Some(event), + _ => None, + } + }), + WeightLimit::Unlimited, + ); +} diff --git a/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml b/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml index 7fee710f000c..691314419102 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/test-utils/Cargo.toml @@ -36,6 +36,7 @@ parachains-runtimes-test-utils = { path = "../../test-utils", default-features = # Polkadot xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false} +xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false} xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false} pallet-xcm = { path = "../../../../../polkadot/xcm/pallet-xcm", default-features = false} polkadot-parachain-primitives = { path = "../../../../../polkadot/parachain", default-features = false} @@ -72,6 +73,7 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-builder/std", "xcm-executor/std", "xcm/std", ] diff --git a/cumulus/parachains/runtimes/assets/test-utils/src/lib.rs b/cumulus/parachains/runtimes/assets/test-utils/src/lib.rs index 7177726e0704..d0121193a258 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/test-utils/src/lib.rs @@ -17,4 +17,79 @@ //! Module contains predefined test-case scenarios for `Runtime` with various assets. pub mod test_cases; + +use frame_support::traits::ProcessMessageError; pub use parachains_runtimes_test_utils::*; + +use xcm::latest::prelude::*; +use xcm_builder::{CreateMatcher, MatchXcm}; + +/// Helper function to verify `xcm` contains all relevant instructions expected on destination +/// chain as part of a reserve-asset-transfer. +pub(crate) fn assert_matches_reserve_asset_deposited_instructions( + xcm: &mut Xcm, + expected_reserve_assets_deposited: &MultiAssets, + expected_beneficiary: &MultiLocation, +) { + let _ = xcm + .0 + .matcher() + .skip_inst_while(|inst| !matches!(inst, ReserveAssetDeposited(..))) + .expect("no instruction ReserveAssetDeposited?") + .match_next_inst(|instr| match instr { + ReserveAssetDeposited(reserve_assets) + if reserve_assets == expected_reserve_assets_deposited => + Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction ReserveAssetDeposited") + .match_next_inst(|instr| match instr { + ClearOrigin => Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction ClearOrigin") + .match_next_inst(|instr| match instr { + BuyExecution { .. } => Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction BuyExecution") + .match_next_inst(|instr| match instr { + DepositAsset { assets: _, beneficiary } if beneficiary == expected_beneficiary => + Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction DepositAsset"); +} + +/// Helper function to verify `xcm` contains all relevant instructions expected on destination +/// chain as part of a reserve-asset-withdrawal. +pub(crate) fn assert_matches_reserve_assets_withdrawal_instructions( + xcm: &mut Xcm, + expected_reserve_assets: &MultiAssets, + expected_beneficiary: &MultiLocation, +) { + let _ = xcm + .0 + .matcher() + .match_next_inst(|instr| match instr { + WithdrawAsset(reserve_assets) if reserve_assets == expected_reserve_assets => Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction WithdrawAsset for reserve asset") + .match_next_inst(|instr| match instr { + ClearOrigin => Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction ClearOrigin") + .match_next_inst(|instr| match instr { + BuyExecution { .. } => Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction BuyExecution") + .match_next_inst(|instr| match instr { + DepositAsset { assets: _, beneficiary } if beneficiary == expected_beneficiary => + Ok(()), + _ => Err(ProcessMessageError::BadFormat), + }) + .expect("expected instruction DepositAsset"); +} diff --git a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs index 7a8d571403c6..d831113c1560 100644 --- a/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs +++ b/cumulus/parachains/runtimes/assets/test-utils/src/test_cases.rs @@ -15,23 +15,28 @@ //! Module contains predefined test-case scenarios for `Runtime` with various assets. +use crate::{ + assert_matches_reserve_asset_deposited_instructions, + assert_matches_reserve_assets_withdrawal_instructions, +}; use codec::Encode; +use cumulus_primitives_core::XcmpMessageSource; use frame_support::{ assert_noop, assert_ok, - traits::{fungibles::InspectEnumerable, Get, OnFinalize, OnInitialize, OriginTrait}, + traits::{fungibles::InspectEnumerable, Currency, Get, OnFinalize, OnInitialize, OriginTrait}, weights::Weight, }; use frame_system::pallet_prelude::BlockNumberFor; use parachains_common::{AccountId, Balance}; use parachains_runtimes_test_utils::{ - assert_metadata, assert_total, AccountIdOf, BalanceOf, CollatorSessionKeys, ExtBuilder, - ValidatorIdOf, XcmReceivedFrom, + assert_metadata, assert_total, mock_open_hrmp_channel, AccountIdOf, BalanceOf, + CollatorSessionKeys, ExtBuilder, ValidatorIdOf, XcmReceivedFrom, }; use sp_runtime::{ traits::{MaybeEquivalence, StaticLookup, Zero}, DispatchError, Saturating, }; -use xcm::latest::prelude::*; +use xcm::{latest::prelude::*, VersionedMultiAssets}; use xcm_executor::{traits::ConvertLocation, XcmExecutor}; type RuntimeHelper = @@ -1339,3 +1344,415 @@ macro_rules! include_create_and_manage_foreign_assets_for_local_consensus_parach } } ); + +/// Test-case makes sure that `Runtime` can reserve-transfer asset to other parachains +pub fn reserve_transfer_native_asset_works< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + HrmpChannelOpener, + HrmpChannelSource, + LocationToAccountId, +>( + collator_session_keys: CollatorSessionKeys, + existential_deposit: BalanceOf, + alice_account: AccountIdOf, + unwrap_pallet_xcm_event: Box) -> Option>>, + unwrap_xcmp_queue_event: Box< + dyn Fn(Vec) -> Option>, + >, + weight_limit: WeightLimit, +) where + Runtime: frame_system::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + cumulus_pallet_xcmp_queue::Config, + AllPalletsWithoutSystem: + OnInitialize> + OnFinalize>, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + ::Balance: From + Into, + XcmConfig: xcm_executor::Config, + LocationToAccountId: ConvertLocation>, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + ::AccountId: From, + HrmpChannelOpener: frame_support::inherent::ProvideInherent< + Call = cumulus_pallet_parachain_system::Call, + >, + HrmpChannelSource: XcmpMessageSource, +{ + let runtime_para_id = 1000; + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_tracing() + .with_safe_xcm_version(3) + .with_para_id(runtime_para_id.into()) + .build() + .execute_with(|| { + let mut alice = [0u8; 32]; + alice[0] = 1; + let included_head = RuntimeHelper::::run_to_block( + 2, + AccountId::from(alice).into(), + ); + + // reserve-transfer native asset with local reserve to remote parachain (1234) + + let other_para_id = 1234; + let native_asset = MultiLocation::parent(); + let dest = MultiLocation::new(1, X1(Parachain(other_para_id))); + let dest_beneficiary = MultiLocation::new(1, X1(Parachain(other_para_id))) + .appended_with(AccountId32 { + network: None, + id: sp_runtime::AccountId32::new([3; 32]).into(), + }) + .unwrap(); + + let reserve_account = LocationToAccountId::convert_location(&dest) + .expect("Sovereign account for reserves"); + let balance_to_transfer = 1_000_000_000_000_u128; + + // open HRMP to other parachain + mock_open_hrmp_channel::( + runtime_para_id.into(), + other_para_id.into(), + included_head, + &alice, + ); + + // drip ED to account + let alice_account_init_balance = existential_deposit + balance_to_transfer.into(); + let _ = >::deposit_creating( + &alice_account, + alice_account_init_balance, + ); + // SA of target location needs to have at least ED, otherwise making reserve fails + let _ = >::deposit_creating( + &reserve_account, + existential_deposit, + ); + + // we just check here, that user retains enough balance after withdrawal + // and also we check if `balance_to_transfer` is more than `existential_deposit`, + assert!( + (>::free_balance(&alice_account) - + balance_to_transfer.into()) >= + existential_deposit + ); + // SA has just ED + assert_eq!( + >::free_balance(&reserve_account), + existential_deposit + ); + + // local native asset (pallet_balances) + let asset_to_transfer = MultiAsset { + fun: Fungible(balance_to_transfer.into()), + id: Concrete(native_asset), + }; + + // pallet_xcm call reserve transfer + assert_ok!(>::limited_reserve_transfer_assets( + RuntimeHelper::::origin_of(alice_account.clone()), + Box::new(dest.into_versioned()), + Box::new(dest_beneficiary.into_versioned()), + Box::new(VersionedMultiAssets::from(MultiAssets::from(asset_to_transfer))), + 0, + weight_limit, + )); + + // check alice account decreased by balance_to_transfer + // TODO:check-parameter: change and assert in tests when (https://github.com/paritytech/polkadot/pull/7005) merged + assert_eq!( + >::free_balance(&alice_account), + alice_account_init_balance - balance_to_transfer.into() + ); + + // check reserve account + // check reserve account increased by balance_to_transfer + assert_eq!( + >::free_balance(&reserve_account), + existential_deposit + balance_to_transfer.into() + ); + + // check events + // check pallet_xcm attempted + RuntimeHelper::::assert_pallet_xcm_event_outcome( + &unwrap_pallet_xcm_event, + |outcome| { + assert_ok!(outcome.ensure_complete()); + }, + ); + + // check that xcm was sent + let xcm_sent_message_hash = >::events() + .into_iter() + .filter_map(|e| unwrap_xcmp_queue_event(e.event.encode())) + .find_map(|e| match e { + cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { message_hash } => + Some(message_hash), + _ => None, + }); + + // read xcm + let xcm_sent = RuntimeHelper::::take_xcm( + other_para_id.into(), + ) + .unwrap(); + + assert_eq!( + xcm_sent_message_hash, + Some(xcm_sent.using_encoded(sp_io::hashing::blake2_256)) + ); + let mut xcm_sent: Xcm<()> = xcm_sent.try_into().expect("versioned xcm"); + + // check sent XCM Program to other parachain + println!("reserve_transfer_native_asset_works sent xcm: {:?}", xcm_sent); + let reserve_assets_deposited = MultiAssets::from(vec![MultiAsset { + id: Concrete(MultiLocation { parents: 1, interior: Here }), + fun: Fungible(1000000000000), + }]); + + assert_matches_reserve_asset_deposited_instructions( + &mut xcm_sent, + &reserve_assets_deposited, + &dest_beneficiary, + ); + }) +} + +/// Test-case makes sure that `Runtime` can initiate withdrawal of reserve-based foreign asset +pub fn reserve_withdraw_foreign_asset_works< + Runtime, + AllPalletsWithoutSystem, + XcmConfig, + ForeignAssetsPalletInstance, + HrmpChannelOpener, + HrmpChannelSource, + LocationToAccountId, +>( + collator_session_keys: CollatorSessionKeys, + existential_deposit: BalanceOf, + alice_account: AccountIdOf, + unwrap_pallet_xcm_event: Box) -> Option>>, + unwrap_xcmp_queue_event: Box< + dyn Fn(Vec) -> Option>, + >, + weight_limit: WeightLimit, +) where + Runtime: frame_system::Config + + pallet_assets::Config + + pallet_balances::Config + + pallet_session::Config + + pallet_xcm::Config + + parachain_info::Config + + pallet_collator_selection::Config + + cumulus_pallet_parachain_system::Config + + cumulus_pallet_xcmp_queue::Config, + ForeignAssetsPalletInstance: 'static, + AllPalletsWithoutSystem: + OnInitialize> + OnFinalize>, + AccountIdOf: Into<[u8; 32]>, + ValidatorIdOf: From>, + BalanceOf: From, + ::Balance: From + Into, + XcmConfig: xcm_executor::Config, + LocationToAccountId: ConvertLocation>, + ::AccountId: + Into<<::RuntimeOrigin as OriginTrait>::AccountId>, + <::Lookup as StaticLookup>::Source: + From<::AccountId>, + ::AccountId: From, + >::AssetId: From, + >::AssetIdParameter: + From, + >::Balance: + From + Into, + HrmpChannelOpener: frame_support::inherent::ProvideInherent< + Call = cumulus_pallet_parachain_system::Call, + >, + HrmpChannelSource: XcmpMessageSource, +{ + let runtime_para_id = 1000; + ExtBuilder::::default() + .with_collators(collator_session_keys.collators()) + .with_session_keys(collator_session_keys.session_keys()) + .with_tracing() + .with_safe_xcm_version(3) + .with_para_id(runtime_para_id.into()) + .build() + .execute_with(|| { + let mut alice = [0u8; 32]; + alice[0] = 1; + let included_head = RuntimeHelper::::run_to_block( + 2, + AccountId::from(alice).into(), + ); + + // Let's assume remote parachain (1234) has and acts as a reserve for its own + // native asset (`BLA`). + // Local AssetHub user Alice has previously received `wBLA` as reserve-based foreign + // asset with its reserve on parachain 1234. + // Verify Alice can withdraw wBLA here to send BLAs to some beneficiary on para 1234. + + let other_para_id = 1234; + let foreign_asset_id = MultiLocation::new(1, X1(Parachain(other_para_id))); + let dest = MultiLocation::new(1, X1(Parachain(other_para_id))); + let dest_beneficiary = MultiLocation::new(1, X1(Parachain(other_para_id))) + .appended_with(AccountId32 { + network: None, + id: sp_runtime::AccountId32::new([3; 32]).into(), + }) + .unwrap(); + + let reserve_account = LocationToAccountId::convert_location(&dest) + .expect("Sovereign account for reserves"); + let balance_to_transfer = 1_000_000_000_000_u128; + + // open HRMP to other parachain + mock_open_hrmp_channel::( + runtime_para_id.into(), + other_para_id.into(), + included_head, + &alice, + ); + + // drip ED to alice account + let _ = >::deposit_creating( + &alice_account, + existential_deposit, + ); + // SA of target location needs to have at least ED, otherwise making reserve fails + let _ = >::deposit_creating( + &reserve_account, + existential_deposit, + ); + let existential_deposit: u128 = existential_deposit.into(); + + // create foreign assets with remote reserve (what we want to test withdraw for) + assert_ok!( + >::force_create( + RuntimeHelper::::root_origin(), + foreign_asset_id.into(), + reserve_account.clone().into(), + false, + existential_deposit.into() + ) + ); + // drip foreign asset to alice + assert_ok!(>::mint( + RuntimeHelper::::origin_of(reserve_account.clone()), + foreign_asset_id.into(), + alice_account.clone().into(), + (existential_deposit + balance_to_transfer).into() + )); + + assert_eq!( + >::free_balance(&alice_account), + existential_deposit.into() + ); + assert_eq!( + >::free_balance(&reserve_account), + existential_deposit.into() + ); + assert_eq!( + >::balance( + foreign_asset_id.into(), + alice_account.clone() + ), + (existential_deposit + balance_to_transfer).into() + ); + + // foreign asset to withdraw + let asset_to_withdraw = MultiAsset { + fun: Fungible(balance_to_transfer.into()), + id: Concrete(foreign_asset_id), + }; + + // pallet_xcm call reserve withdraw + assert_ok!(>::limited_reserve_withdraw_assets( + RuntimeHelper::::origin_of(alice_account.clone()), + Box::new(dest.into_versioned()), + Box::new(dest_beneficiary.into_versioned()), + Box::new(VersionedMultiAssets::from(MultiAssets::from(asset_to_withdraw))), + 0, + weight_limit, + )); + + // check alice account (balances not changed) + assert_eq!( + >::free_balance(&alice_account), + existential_deposit.into() + ); + // check reserve account (balances not changed) + assert_eq!( + >::free_balance(&reserve_account), + existential_deposit.into() + ); + // `ForeignAssets` for alice account is decreased + // TODO:check-parameter: change and assert in tests when (https://github.com/paritytech/polkadot/pull/7005) merged + assert_eq!( + >::balance( + foreign_asset_id.into(), + alice_account.clone() + ), + existential_deposit.into() + ); + + // check events + // check pallet_xcm attempted + RuntimeHelper::::assert_pallet_xcm_event_outcome( + &unwrap_pallet_xcm_event, + |outcome| { + assert_ok!(outcome.ensure_complete()); + }, + ); + + // check that xcm was sent + let xcm_sent_message_hash = >::events() + .into_iter() + .filter_map(|e| unwrap_xcmp_queue_event(e.event.encode())) + .find_map(|e| match e { + cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { message_hash } => + Some(message_hash), + _ => None, + }); + + // read xcm + let xcm_sent = RuntimeHelper::::take_xcm( + other_para_id.into(), + ) + .unwrap(); + + assert_eq!( + xcm_sent_message_hash, + Some(xcm_sent.using_encoded(sp_io::hashing::blake2_256)) + ); + let mut xcm_sent: Xcm<()> = xcm_sent.try_into().expect("versioned xcm"); + + // check sent XCM Program to other parachain + println!("reserve_withdraw_foreign_asset_works sent xcm: {:?}", xcm_sent); + + // The reserve assets (BLA) as seen by their native parachain. + // We expect the destination parachain to execute `WithdrawAsset()` on this. + let expected_reanchored_reserve_assets = MultiAssets::from(vec![MultiAsset { + id: Concrete(MultiLocation { parents: 0, interior: Here }), + fun: Fungible(1000000000000), + }]); + + assert_matches_reserve_assets_withdrawal_instructions( + &mut xcm_sent, + &expected_reanchored_reserve_assets, + &dest_beneficiary, + ); + }) +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/pallet_xcm.rs index 71bc58307710..664041448a24 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/pallet_xcm.rs @@ -91,6 +91,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(18_446_744_073_709_551_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/pallet_xcm.rs index ffc5fa2fc23e..5a6816af4559 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/pallet_xcm.rs @@ -91,6 +91,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(18_446_744_073_709_551_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs index 72bdb282585d..b9065327a4cb 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/pallet_xcm.rs @@ -91,6 +91,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(18_446_744_073_709_551_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/cumulus/parachains/runtimes/collectives/collectives-polkadot/src/weights/pallet_xcm.rs b/cumulus/parachains/runtimes/collectives/collectives-polkadot/src/weights/pallet_xcm.rs index 738742b6108b..0b60425c0f51 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-polkadot/src/weights/pallet_xcm.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-polkadot/src/weights/pallet_xcm.rs @@ -91,6 +91,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(18_446_744_073_709_551_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) fn execute() -> Weight { diff --git a/polkadot/runtime/kusama/src/weights/pallet_xcm.rs b/polkadot/runtime/kusama/src/weights/pallet_xcm.rs index 4b1a790a57a1..15c9734d206b 100644 --- a/polkadot/runtime/kusama/src/weights/pallet_xcm.rs +++ b/polkadot/runtime/kusama/src/weights/pallet_xcm.rs @@ -89,6 +89,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(22_407_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } fn execute() -> Weight { // Proof Size summary in bytes: // Measured: `0` diff --git a/polkadot/runtime/polkadot/src/weights/pallet_xcm.rs b/polkadot/runtime/polkadot/src/weights/pallet_xcm.rs index abbd5b1f2b96..52eba6b98f08 100644 --- a/polkadot/runtime/polkadot/src/weights/pallet_xcm.rs +++ b/polkadot/runtime/polkadot/src/weights/pallet_xcm.rs @@ -89,6 +89,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(23_138_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: Benchmark Override (r:0 w:0) /// Proof Skipped: Benchmark Override (max_values: None, max_size: None, mode: Measured) fn execute() -> Weight { diff --git a/polkadot/runtime/rococo/src/weights/pallet_xcm.rs b/polkadot/runtime/rococo/src/weights/pallet_xcm.rs index 43b4358b8903..8eac9bd76461 100644 --- a/polkadot/runtime/rococo/src/weights/pallet_xcm.rs +++ b/polkadot/runtime/rococo/src/weights/pallet_xcm.rs @@ -85,6 +85,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(21_768_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } fn execute() -> Weight { // Proof Size summary in bytes: // Measured: `0` diff --git a/polkadot/runtime/westend/src/weights/pallet_xcm.rs b/polkadot/runtime/westend/src/weights/pallet_xcm.rs index 7f2a1de44e93..07fc6bcb0625 100644 --- a/polkadot/runtime/westend/src/weights/pallet_xcm.rs +++ b/polkadot/runtime/westend/src/weights/pallet_xcm.rs @@ -89,6 +89,10 @@ impl pallet_xcm::WeightInfo for WeightInfo { Weight::from_parts(21_942_000, 0) .saturating_add(Weight::from_parts(0, 0)) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(23_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } /// Storage: Benchmark Override (r:0 w:0) /// Proof Skipped: Benchmark Override (max_values: None, max_size: None, mode: Measured) fn execute() -> Weight { diff --git a/polkadot/xcm/pallet-xcm/src/benchmarking.rs b/polkadot/xcm/pallet-xcm/src/benchmarking.rs index aca4bd1fb3fa..71771f2acf45 100644 --- a/polkadot/xcm/pallet-xcm/src/benchmarking.rs +++ b/polkadot/xcm/pallet-xcm/src/benchmarking.rs @@ -79,6 +79,22 @@ benchmarks! { let versioned_assets: VersionedMultiAssets = asset.into(); }: _>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0) + reserve_withdraw_assets { + let send_origin = + T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let recipient = [0u8; 32]; + let dest = T::ReachableDest::get().ok_or( + BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)), + )?; + // For reserve_withdraw_assets we'd use destination native asset `(dest.clone(), 10).into()`, + // but for weighing it doesn't matter, and it's easier to use Balances asset here. + let asset: MultiAsset = (Here, 10).into(); + let versioned_dest: VersionedMultiLocation = dest.into(); + let versioned_beneficiary: VersionedMultiLocation = + AccountId32 { network: None, id: recipient.into() }.into(); + let versioned_assets: VersionedMultiAssets = asset.into(); + }: _>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0) + execute { let execute_origin = T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs index 9dfb63c2c9c6..962e19d30973 100644 --- a/polkadot/xcm/pallet-xcm/src/lib.rs +++ b/polkadot/xcm/pallet-xcm/src/lib.rs @@ -60,6 +60,7 @@ pub trait WeightInfo { fn send() -> Weight; fn teleport_assets() -> Weight; fn reserve_transfer_assets() -> Weight; + fn reserve_withdraw_assets() -> Weight; fn execute() -> Weight; fn force_xcm_version() -> Weight; fn force_default_xcm_version() -> Weight; @@ -90,6 +91,10 @@ impl WeightInfo for TestWeightInfo { Weight::from_parts(100_000_000, 0) } + fn reserve_withdraw_assets() -> Weight { + Weight::from_parts(100_000_000, 0) + } + fn execute() -> Weight { Weight::from_parts(100_000_000, 0) } @@ -1124,6 +1129,117 @@ pub mod pallet { XcmExecutionSuspended::::set(suspended); Ok(()) } + + /// Burn some reserve-based `assets` from local chain and forward a notification XCM to + /// `dest` reserve chain to transfer equivalent assets from reserve to `beneficiary`. + /// + /// Fee payment on the destination side is made from the asset in the `assets` vector of + /// index `fee_asset_item`. The weight limit for fees is not provided and thus is unlimited, + /// with all fees taken as needed from the asset. + /// + /// - `origin`: Must be capable of withdrawing the `assets` and executing XCM. + /// - `dest`: Destination context for the assets. Will typically be `(Parent, + /// Parachain(..))` to claim reserve assets from a sibling parachain, or `(Parent,)` to + /// claim the reserve assets from the Relay chain. + /// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will + /// generally be an `AccountId32` value. + /// - `assets`: The assets to be withdrawn. This should include the assets used to pay the + /// fee on the `dest` side. + /// - `fee_asset_item`: The index into `assets` of the item which should be used to pay + /// fees. + #[pallet::call_index(11)] + #[pallet::weight({ + let maybe_assets: Result = (*assets.clone()).try_into(); + let maybe_dest: Result = (*dest.clone()).try_into(); + match (maybe_assets, maybe_dest) { + (Ok(assets), Ok(dest)) => { + use sp_std::vec; + let all_assets = AllCounted(assets.len() as u32); + let mut message = Xcm(vec![ + WithdrawAsset(assets), + InitiateReserveWithdraw { assets: Wild(all_assets), reserve: dest, xcm: Xcm(vec![]) }, + ]); + T::Weigher::weight(&mut message).map_or( + Weight::MAX, + |w| T::WeightInfo::reserve_withdraw_assets().saturating_add(w) + ) + } + _ => Weight::MAX, + } + })] + pub fn reserve_withdraw_assets( + origin: OriginFor, + dest: Box, + beneficiary: Box, + assets: Box, + fee_asset_item: u32, + ) -> DispatchResult { + Self::do_reserve_withdraw_assets( + origin, + dest, + beneficiary, + assets, + fee_asset_item, + None, + ) + } + + /// Burn some reserve-based `assets` from local chain and forward a notification XCM to + /// `dest` reserve chain to transfer equivalent assets from reserve to `beneficiary`. + /// + /// Fee payment on the destination side is made from the asset in the `assets` vector of + /// index `fee_asset_item`, up to enough to pay for `weight_limit` of weight. If more weight + /// is needed than `weight_limit`, then the operation will fail and the assets send may be + /// at risk. + /// + /// - `origin`: Must be capable of withdrawing the `assets` and executing XCM. + /// - `dest`: Destination context for the assets. Will typically be `(Parent, + /// Parachain(..))` to claim reserve assets from a sibling parachain, or `(Parent,)` to + /// claim the reserve assets from the Relay chain. + /// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will + /// generally be an `AccountId32` value. + /// - `assets`: The assets to be withdrawn. This should include the assets used to pay the + /// fee on the `dest` side. + /// - `fee_asset_item`: The index into `assets` of the item which should be used to pay + /// fees. + /// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase. + #[pallet::call_index(12)] + #[pallet::weight({ + let maybe_assets: Result = (*assets.clone()).try_into(); + let maybe_dest: Result = (*dest.clone()).try_into(); + match (maybe_assets, maybe_dest) { + (Ok(assets), Ok(dest)) => { + use sp_std::vec; + let all_assets = AllCounted(assets.len() as u32); + let mut message = Xcm(vec![ + WithdrawAsset(assets), + InitiateReserveWithdraw { assets: Wild(all_assets), reserve: dest, xcm: Xcm(vec![]) }, + ]); + T::Weigher::weight(&mut message).map_or( + Weight::MAX, + |w| T::WeightInfo::reserve_withdraw_assets().saturating_add(w) + ) + } + _ => Weight::MAX, + } + })] + pub fn limited_reserve_withdraw_assets( + origin: OriginFor, + dest: Box, + beneficiary: Box, + assets: Box, + fee_asset_item: u32, + weight_limit: WeightLimit, + ) -> DispatchResult { + Self::do_reserve_withdraw_assets( + origin, + dest, + beneficiary, + assets, + fee_asset_item, + Some(weight_limit), + ) + } } } @@ -1251,6 +1367,65 @@ impl Pallet { Ok(()) } + fn do_reserve_withdraw_assets( + origin: OriginFor, + dest: Box, + beneficiary: Box, + assets: Box, + fee_asset_item: u32, + maybe_weight_limit: Option, + ) -> DispatchResult { + let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?; + let dest = (*dest).try_into().map_err(|()| Error::::BadVersion)?; + let beneficiary: MultiLocation = + (*beneficiary).try_into().map_err(|()| Error::::BadVersion)?; + let assets: MultiAssets = (*assets).try_into().map_err(|()| Error::::BadVersion)?; + + ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::::TooManyAssets); + let value = (origin_location, assets.into_inner()); + let (origin_location, assets) = value; + let context = T::UniversalLocation::get(); + let fees = assets + .get(fee_asset_item as usize) + .ok_or(Error::::Empty)? + .clone() + .reanchored(&dest, context) + .map_err(|_| Error::::CannotReanchor)?; + let max_assets = assets.len() as u32; + let assets: MultiAssets = assets.into(); + let weight_limit = match maybe_weight_limit { + Some(weight_limit) => weight_limit, + None => { + let fees = fees.clone(); + let mut remote_message = Xcm(vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees, weight_limit: Limited(Weight::zero()) }, + DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }, + ]); + // use local weight for remote message and hope for the best. + let remote_weight = T::Weigher::weight(&mut remote_message) + .map_err(|()| Error::::UnweighableMessage)?; + Limited(remote_weight) + }, + }; + let xcm = Xcm(vec![ + BuyExecution { fees, weight_limit }, + DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary }, + ]); + let mut message = Xcm(vec![ + WithdrawAsset(assets), + InitiateReserveWithdraw { assets: Wild(AllCounted(max_assets)), reserve: dest, xcm }, + ]); + let weight = + T::Weigher::weight(&mut message).map_err(|()| Error::::UnweighableMessage)?; + let hash = message.using_encoded(sp_io::hashing::blake2_256); + let outcome = + T::XcmExecutor::execute_xcm_in_credit(origin_location, message, hash, weight, weight); + Self::deposit_event(Event::Attempted { outcome }); + Ok(()) + } + fn do_teleport_assets( origin: OriginFor, dest: Box, diff --git a/polkadot/xcm/pallet-xcm/src/tests.rs b/polkadot/xcm/pallet-xcm/src/tests.rs index 486887598c78..7c91df552e88 100644 --- a/polkadot/xcm/pallet-xcm/src/tests.rs +++ b/polkadot/xcm/pallet-xcm/src/tests.rs @@ -616,6 +616,53 @@ fn unlimited_reserve_transfer_assets_works() { }); } +/// Test `limited_reserve_withdraw_assets` with unlimited weight purchasing +/// +/// 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 unlimited_reserve_withdraw_assets_works() { + let balances = vec![ + (ALICE, INITIAL_BALANCE), + (ParaId::from(PARA_ID).into_account_truncating(), INITIAL_BALANCE), + ]; + new_test_ext_with_balances(balances).execute_with(|| { + let weight = BaseXcmWeight::get() * 2; + let dest: MultiLocation = Junction::AccountId32 { network: None, id: ALICE.into() }.into(); + assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE); + // TODO: for a more accurate test, use derivative/synthetic (pallet_assets) asset instead of + // native (pallet_balances) `Here` asset, but mock doesn't have `pallet_assets` and not sure + // it's worth adding just for this. + assert_ok!(XcmPallet::limited_reserve_withdraw_assets( + RuntimeOrigin::signed(ALICE), + Box::new(Parachain(PARA_ID).into()), + Box::new(dest.into()), + Box::new((Here, SEND_AMOUNT).into()), + 0, + WeightLimit::Unlimited, + )); + // Alice spent amount + assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - SEND_AMOUNT); + // Check destination XCM program + assert_eq!( + sent_xcm(), + vec![( + Parachain(PARA_ID).into(), + Xcm(vec![ + WithdrawAsset((Parent, SEND_AMOUNT).into()), + ClearOrigin, + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: AllCounted(1).into(), beneficiary: dest }, + ]), + )] + ); + assert_eq!( + last_event(), + RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(weight) }) + ); + }); +} + /// Test local execution of XCM /// /// Asserts that the sender's balance is decreased and the beneficiary's balance