diff --git a/xtokens/Cargo.toml b/xtokens/Cargo.toml index dd1b7e1c4..e86c24e01 100644 --- a/xtokens/Cargo.toml +++ b/xtokens/Cargo.toml @@ -11,6 +11,7 @@ edition = "2021" scale-info = { version = "2.1.2", default-features = false, features = ["derive"] } serde = { version = "1.0.136", optional = true } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +log = "0.4.16" # substrate sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "polkadot-v0.9.24" } @@ -59,6 +60,7 @@ std = [ "serde", "codec/std", "scale-info/std", + "log/std", "sp-runtime/std", "sp-std/std", "sp-io/std", diff --git a/xtokens/src/lib.rs b/xtokens/src/lib.rs index f5d552244..dfdd5ea59 100644 --- a/xtokens/src/lib.rs +++ b/xtokens/src/lib.rs @@ -59,6 +59,13 @@ enum TransferKind { } use TransferKind::*; +// Used for BoundedVector and has to be u32 because of Decode trait bounds. +// Since the BoundedVector is used for extrinsic arguments, it is required to +// implement Encode/Decode, and it can't be less than u32, as that is the +// smallest type that implements the required traits. Therefore it is not a +// configurable pallet type to avoid huge transact messages being attempted. +type MaxEncodedDataLen = ConstU32<256>; + #[frame_support::pallet] pub mod module { use super::*; @@ -118,6 +125,11 @@ pub mod module { /// The way to retreave the reserve of a MultiAsset. This can be /// configured to accept absolute or relative paths for self tokens type ReserveProvider: Reserve; + + /// Used to bypass the xcm-executor for sending message including + /// Transact instruction that need to use specific descended location + /// for origin + type XcmSender: SendXcm; } #[pallet::event] @@ -176,6 +188,7 @@ pub mod module { NotSupportedMultiLocation, /// MinXcmFee not registered for certain reserve location MinXcmFeeNotDefined, + SendFailure, } #[pallet::hooks] @@ -343,6 +356,117 @@ pub mod module { Self::do_transfer_multicurrencies(who, currencies, fee_item, dest, dest_weight) } + /// Transfer native currencies and use them to execute a Transact + /// instruction on the destination. + /// + /// Sends a XCM message that will descend to a user sovereign account on + /// the destination chain, created from the multilocation of the sender + /// from the perspective of the destination chain, usually by hashing. + /// From that origin some assets will be withdrawn to buy execution time + /// for a local extrinsic call. That call will be decoded from the + /// encoded call data in a `Transact` instruction. The call will then be + /// dispatched with the user sovereign account as origin. Any surplus + /// assets are deposited back into the user sovereign account. + /// + /// `transact_dest_weight` is the weight for XCM execution on the dest + /// chain, and it would be charged from the transferred assets. If set + /// below requirements, the execution may fail and assets wouldn't be + /// received. + /// + /// `encoded_call_data` is the encoded call data of the extrinsic on the + /// destination chain, with a bounded length. + /// + /// `transact_fee_amount` is the amount of assets that will be withdrawn + /// to execute the Transact call. Those assets must be pre-funded to the + /// user sovereign account in order to succeed. + /// + /// It's a no-op if any error on local XCM execution or message sending. + /// Note sending assets out per se doesn't guarantee they would be + /// received. Receiving depends on if the XCM message could be delivered + /// by the network, and if the receiving chain would handle + /// messages correctly. + #[pallet::weight(Pallet::::weight_of_send_transact())] + #[transactional] + pub fn transact( + origin: OriginFor, + transact_currency_id: T::CurrencyId, + dest_chain_id: u32, + transact_dest_weight: Weight, + encoded_call_data: BoundedVec, + transact_fee_amount: T::Balance, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_transact( + who, + transact_currency_id, + transact_fee_amount, + dest_chain_id, + transact_dest_weight, + encoded_call_data, + ) + } + + /// Transfer native currencies and use them to execute a Transact + /// instruction on the destination. + /// + /// Consists of sending two XCM messages: + /// 1. First message will transfer some assets to a user sovereign + /// account on the destination chain, created from the multilocation of + /// the sender from the perspective of the destination chain, usually by + /// hashing. + /// 2. Second message will descend to the user sovereign account origin + /// and will withdraw some of the transferred assets to buy execution + /// time for a local extrinsic call. That call will be decoded from the + /// encoded call data in a `Transact` instruction. The call will then be + /// dispatched with the user sovereign account as origin. Any surplus + /// assets are deposited back into the user sovereign account. + /// + /// `transact_dest_weight` is the weight for XCM execution on the dest + /// chain, and it would be charged from the transferred assets. If set + /// below requirements, the execution may fail and assets wouldn't be + /// received. + /// + /// `encoded_call_data` is the encoded call data of the extrinsic on the + /// destination chain with a bounded length. + /// + /// `transact_fee_amount` is the amount of assets that will be withdrawn + /// to execute the Transact call. Those assets must be less than or + /// equal to the transferred amount minus any other execution costs of + /// the instructions, like the initial deposit of the assets. + /// + /// It's a no-op if any error on local XCM execution or message sending. + /// Note sending assets out per se doesn't guarantee they would be + /// received. Receiving depends on if the XCM message could be delivered + /// by the network, and if the receiving chain would handle + /// messages correctly. + #[pallet::weight(Pallet::::weight_of_transfer_with_transact(currency_id.clone(), *transfer_amount, *dest_chain_id))] + #[transactional] + pub fn transfer_with_transact( + origin: OriginFor, + currency_id: T::CurrencyId, + transfer_amount: T::Balance, + dest_chain_id: u32, + transfer_dest_weight: Weight, + encoded_call_data: BoundedVec, + transact_fee_amount: T::Balance, + transact_dest_weight: Weight, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(transfer_amount >= transact_fee_amount, Error::::FeeNotEnough); + + Self::do_transfer_with_transact( + who, + currency_id, + transfer_amount, + dest_chain_id, + transfer_dest_weight, + encoded_call_data, + transact_fee_amount, + transact_dest_weight, + ) + } + /// Transfer several `MultiAsset` specifying the item to be used as fee /// /// `dest_weight` is the weight for XCM execution on the dest chain, and @@ -374,7 +498,7 @@ pub mod module { // We first grab the fee let fee: &MultiAsset = assets.get(fee_item as usize).ok_or(Error::::AssetIndexNonExistent)?; - Self::do_transfer_multiassets(who, assets.clone(), fee.clone(), dest, dest_weight) + Self::do_transfer_multiassets(who, assets.clone(), fee.clone(), dest, dest_weight, None) } } @@ -396,7 +520,7 @@ pub mod module { ); let asset: MultiAsset = (location, amount.into()).into(); - Self::do_transfer_multiassets(who, vec![asset.clone()].into(), asset, dest, dest_weight) + Self::do_transfer_multiassets(who, vec![asset.clone()].into(), asset, dest, dest_weight, None) } fn do_transfer_with_fee( @@ -425,7 +549,7 @@ pub mod module { assets.push(asset); assets.push(fee_asset.clone()); - Self::do_transfer_multiassets(who, assets, fee_asset, dest, dest_weight) + Self::do_transfer_multiassets(who, assets, fee_asset, dest, dest_weight, None) } fn do_transfer_multiasset( @@ -434,7 +558,7 @@ pub mod module { dest: MultiLocation, dest_weight: Weight, ) -> DispatchResult { - Self::do_transfer_multiassets(who, vec![asset.clone()].into(), asset, dest, dest_weight) + Self::do_transfer_multiassets(who, vec![asset.clone()].into(), asset, dest, dest_weight, None) } fn do_transfer_multiasset_with_fee( @@ -449,7 +573,7 @@ pub mod module { assets.push(asset); assets.push(fee.clone()); - Self::do_transfer_multiassets(who, assets, fee, dest, dest_weight)?; + Self::do_transfer_multiassets(who, assets, fee, dest, dest_weight, None)?; Ok(()) } @@ -493,7 +617,131 @@ pub mod module { let fee: MultiAsset = (fee_location, (*fee_amount).into()).into(); - Self::do_transfer_multiassets(who, assets, fee, dest, dest_weight) + Self::do_transfer_multiassets(who, assets, fee, dest, dest_weight, None) + } + + fn do_transact( + who: T::AccountId, + transact_currency_id: T::CurrencyId, + transact_fee_amount: T::Balance, + dest_chain_id: u32, + transact_dest_weight: Weight, + encoded_call_data: BoundedVec, + ) -> DispatchResult { + let transact_fee_location: MultiLocation = T::CurrencyIdConvert::convert(transact_currency_id) + .ok_or(Error::::NotCrossChainTransferableCurrency)?; + + let origin_location_interior = T::AccountIdToMultiLocation::convert(who).interior; + let dest_chain_location: MultiLocation = (1, Parachain(dest_chain_id)).into(); + let refund_recipient = T::SelfLocation::get(); + Self::send_transact_message( + transact_fee_location, + transact_fee_amount, + dest_chain_location, + origin_location_interior, + transact_dest_weight, + encoded_call_data, + refund_recipient, + )?; + + Ok(()) + } + + fn do_transfer_with_transact( + who: T::AccountId, + currency_id: T::CurrencyId, + transfer_amount: T::Balance, + dest_chain_id: u32, + transfer_dest_weight: Weight, + encoded_call_data: BoundedVec, + transact_fee_amount: T::Balance, + transact_dest_weight: Weight, + ) -> DispatchResult { + ensure!(!transfer_amount.is_zero(), Error::::ZeroAmount); + + let origin_location = T::AccountIdToMultiLocation::convert(who.clone()); + let mut dest_chain_location: MultiLocation = (1, Parachain(dest_chain_id)).into(); + let origin_location_interior = origin_location.clone().interior; + // Need to append some interior to pass the `contains()` check and later the + // `is_valid()` check in `do_transfer_multiassets()`. Only the chain part is + // needed afterwards. + let _ = dest_chain_location.append_with(origin_location_interior.clone()); + ensure!( + T::MultiLocationsFilter::contains(&dest_chain_location), + Error::::NotSupportedMultiLocation + ); + + let transact_fee_location: MultiLocation = + T::CurrencyIdConvert::convert(currency_id).ok_or(Error::::NotCrossChainTransferableCurrency)?; + let transfer_asset: MultiAsset = (transact_fee_location.clone(), transfer_amount.into()).into(); + let mut override_recipient = T::SelfLocation::get(); + let _ = override_recipient.append_with(origin_location_interior.clone()); + Self::do_transfer_multiassets( + who, + vec![transfer_asset.clone()].into(), + transfer_asset, + dest_chain_location.clone(), + transfer_dest_weight, + Some(override_recipient.clone()), + )?; + + let dest_chain_location = dest_chain_location.chain_part().ok_or(Error::::InvalidDest)?; + Self::send_transact_message( + transact_fee_location, + transact_fee_amount, + dest_chain_location, + origin_location_interior, + transact_dest_weight, + encoded_call_data, + override_recipient, + )?; + + Ok(()) + } + + fn send_transact_message( + transact_fee_location: MultiLocation, + transact_fee_amount: T::Balance, + dest_chain_location: MultiLocation, + origin_location_interior: Junctions, + transact_dest_weight: Weight, + encoded_call_data: BoundedVec, + refund_recipient: MultiLocation, + ) -> DispatchResult { + let ancestry = T::LocationInverter::ancestry(); + let mut transact_fee_asset: MultiAsset = (transact_fee_location, transact_fee_amount.into()).into(); + transact_fee_asset = transact_fee_asset + .clone() + .reanchored(&dest_chain_location, &ancestry) + .map_err(|_| Error::::CannotReanchor)?; + + let mut transact_fee_assets = MultiAssets::new(); + transact_fee_assets.push(transact_fee_asset.clone()); + let transact_fee_assets_len = transact_fee_assets.len(); + let instructions = vec![ + DescendOrigin(origin_location_interior), + WithdrawAsset(transact_fee_assets), + BuyExecution { + fees: transact_fee_asset, + weight_limit: WeightLimit::Limited(transact_dest_weight), + }, + Transact { + // SovereignAccount of the user, not the chain + origin_type: OriginKind::SovereignAccount, + require_weight_at_most: transact_dest_weight, + call: encoded_call_data.into_inner().into(), + }, + RefundSurplus, + DepositAsset { + assets: All.into(), + max_assets: transact_fee_assets_len as u32, + beneficiary: refund_recipient, + }, + ]; + + T::XcmSender::send_xcm(dest_chain_location, Xcm(instructions)).map_err(|_| Error::::SendFailure)?; + + Ok(()) } fn do_transfer_multiassets( @@ -502,6 +750,7 @@ pub mod module { fee: MultiAsset, dest: MultiLocation, dest_weight: Weight, + override_recipient: Option, ) -> DispatchResult { ensure!( assets.len() <= T::MaxAssetsForTransfer::get(), @@ -603,7 +852,7 @@ pub mod module { fee.clone(), non_fee_reserve, &dest, - None, + override_recipient, dest_weight, false, )?; @@ -860,6 +1109,29 @@ pub mod module { } } + /// Returns weight of `send_transact` call. + fn weight_of_send_transact() -> Weight { + let mut msg = Xcm(vec![]); + return T::Weigher::weight(&mut msg) + .map_or(Weight::max_value(), |w| T::BaseXcmWeight::get().saturating_add(w)); + } + + /// Returns weight of `transfer_with_transact` call. + fn weight_of_transfer_with_transact( + currency_id: T::CurrencyId, + amount: T::Balance, + dest_chain_id: u32, + ) -> Weight { + let dest_chain_location: MultiLocation = (1, Parachain(dest_chain_id)).into(); + if let Some(location) = T::CurrencyIdConvert::convert(currency_id) { + let asset = (location, amount.into()).into(); + Self::weight_of_transfer_multiasset(&asset, &VersionedMultiLocation::V1(dest_chain_location)) + + Self::weight_of_send_transact() + } else { + 0 + } + } + /// Returns weight of `transfer` call. fn weight_of_transfer_multicurrencies( currencies: &[(T::CurrencyId, T::Balance)], diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index 850d7eca6..2420c7c89 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -2,8 +2,8 @@ use super::{Amount, Balance, CurrencyId, CurrencyIdConvert, ParachainXcmRouter}; use crate as orml_xtokens; use frame_support::{ - construct_runtime, match_types, parameter_types, - traits::{ConstU128, ConstU32, ConstU64, Everything, Get, Nothing}, + construct_runtime, ensure, match_types, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Contains, Everything, Get, Nothing}, weights::{constants::WEIGHT_PER_SECOND, Weight}, }; use frame_system::EnsureRoot; @@ -13,17 +13,19 @@ use sp_runtime::{ traits::{Convert, IdentityLookup}, AccountId32, }; +use sp_std::marker::PhantomData; use cumulus_primitives_core::{ChannelStatus, GetChannelInfo, ParaId}; use pallet_xcm::XcmPassthrough; use polkadot_parachain::primitives::Sibling; use xcm::latest::prelude::*; use xcm_builder::{ - AccountId32Aliases, AllowTopLevelPaidExecutionFrom, EnsureXcmOrigin, FixedWeightBounds, LocationInverter, - NativeAsset, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, + Account32Hash, AccountId32Aliases, AllowTopLevelPaidExecutionFrom, EnsureXcmOrigin, FixedWeightBounds, + LocationInverter, NativeAsset, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, + TakeWeightCredit, }; -use xcm_executor::{Config, XcmExecutor}; +use xcm_executor::{traits::ShouldExecute, Config, XcmExecutor}; use crate::mock::AllTokensAreCreatedEqualToWeight; use orml_traits::{location::AbsoluteReserveProvider, parameter_type_with_key}; @@ -106,10 +108,12 @@ parameter_types! { pub Ancestry: MultiLocation = Parachain(ParachainInfo::parachain_id().into()).into(); } +pub type ParaAccount32Hash = Account32Hash; pub type LocationToAccountId = ( ParentIsPreset, SiblingParachainConvertsVia, AccountId32Aliases, + ParaAccount32Hash, ); pub type XcmOriginToCallOrigin = ( @@ -131,8 +135,92 @@ pub type LocalAssetTransactor = MultiCurrencyAdapter< (), >; +match_types! { + pub type ParentOrFriends: impl Contains = { + MultiLocation { parents: 1, interior: Here } | + MultiLocation { parents: 1, interior: X1(Parachain(1)) } | + MultiLocation { parents: 1, interior: X1(Parachain(2)) } | + MultiLocation { parents: 1, interior: X1(Parachain(3)) } | + MultiLocation { parents: 1, interior: X1(Parachain(4)) } + }; +} + pub type XcmRouter = ParachainXcmRouter; -pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); + +/// Allows execution of a Transact instruction from `origin` if it is contained +/// in `T` (i.e. `T::Contains(origin)`) taking payments into account. +/// +/// Only allows for a specific set of instructions: +/// DescendOrigin + WithdrawAsset + BuyExecution + Transact + RefundSurplus + +/// DepositAsset +pub struct AllowTransactFrom(PhantomData); +impl> ShouldExecute for AllowTransactFrom { + fn should_execute( + origin: &MultiLocation, + message: &mut Xcm, + max_weight: Weight, + _weight_credit: &mut Weight, + ) -> Result<(), ()> { + log::trace!( + target: "xcm::barriers", + "AllowTransactFrom origin: {:?}, message: {:?}, max_weight: + {:?}, weight_credit: {:?}", origin, message, max_weight, _weight_credit, + ); + + ensure!(T::contains(origin), ()); + + let mut iter = message.0.iter_mut(); + let next_instruction = iter.next().ok_or(())?; + // TODO: maybe check that we're not descending into Here + match next_instruction { + DescendOrigin(..) => (), + _ => return Err(()), + } + let next_instruction = iter.next().ok_or(())?; + match next_instruction { + WithdrawAsset(..) => (), + _ => return Err(()), + } + let next_instruction = iter.next().ok_or(())?; + match next_instruction { + BuyExecution { + ref mut weight_limit, .. + } => { + *weight_limit = Limited(max_weight); + } + _ => { + return Err(()); + } + } + let next_instruction = iter.next().ok_or(())?; + match next_instruction { + // TODO: can at least check the length of the encoded vec, need a PR in polkadot for that. + // Also the allowed length could be configurable. + // TODO: if possible decode the call and match against a configurable set of allowed calls + Transact { .. } => (), + _ => return Err(()), + } + let next_instruction = iter.next().ok_or(())?; + match next_instruction { + RefundSurplus { .. } => (), + _ => return Err(()), + } + let next_instruction = iter.next().ok_or(())?; + match next_instruction { + DepositAsset { .. } => (), + _ => return Err(()), + } + + Ok(()) + } +} +pub type Barrier = ( + TakeWeightCredit, + AllowTopLevelPaidExecutionFrom, + AllowTransactFrom, +); + +pub type ParaWeigher = FixedWeightBounds, Call, ConstU32<100>>; pub struct XcmConfig; impl Config for XcmConfig { @@ -144,7 +232,7 @@ impl Config for XcmConfig { type IsTeleporter = NativeAsset; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Weigher = ParaWeigher; type Trader = AllTokensAreCreatedEqualToWeight; type ResponseHandler = (); type AssetTrap = PolkadotXcm; @@ -195,7 +283,7 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Nothing; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Weigher = ParaWeigher; type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; @@ -239,6 +327,7 @@ parameter_type_with_key! { _ => None, } }; + } impl orml_xtokens::Config for Runtime { @@ -251,11 +340,12 @@ impl orml_xtokens::Config for Runtime { type MultiLocationsFilter = ParentOrParachains; type MinXcmFee = ParachainMinFee; type XcmExecutor = XcmExecutor; - type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Weigher = ParaWeigher; type BaseXcmWeight = ConstU64<100_000_000>; type LocationInverter = LocationInverter; type MaxAssetsForTransfer = MaxAssetsForTransfer; type ReserveProvider = AbsoluteReserveProvider; + type XcmSender = XcmRouter; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/mock/para_relative_view.rs b/xtokens/src/mock/para_relative_view.rs index 6045a8fe3..092903e1d 100644 --- a/xtokens/src/mock/para_relative_view.rs +++ b/xtokens/src/mock/para_relative_view.rs @@ -328,6 +328,7 @@ impl orml_xtokens::Config for Runtime { type LocationInverter = LocationInverter; type MaxAssetsForTransfer = MaxAssetsForTransfer; type ReserveProvider = RelativeReserveProvider; + type XcmSender = XcmRouter; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/mock/para_teleport.rs b/xtokens/src/mock/para_teleport.rs index 6b320ca31..a4510a44a 100644 --- a/xtokens/src/mock/para_teleport.rs +++ b/xtokens/src/mock/para_teleport.rs @@ -247,6 +247,7 @@ impl orml_xtokens::Config for Runtime { type LocationInverter = LocationInverter; type MaxAssetsForTransfer = MaxAssetsForTransfer; type ReserveProvider = AbsoluteReserveProvider; + type XcmSender = XcmRouter; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/mock/relay.rs b/xtokens/src/mock/relay.rs index 02df04f52..e8879d2cd 100644 --- a/xtokens/src/mock/relay.rs +++ b/xtokens/src/mock/relay.rs @@ -88,6 +88,8 @@ type LocalOriginConverter = ( pub type XcmRouter = super::RelayChainXcmRouter; pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); +pub type RelayWeigher = FixedWeightBounds, Call, ConstU32<100>>; + parameter_types! { pub const Kusama: MultiAssetFilter = Wild(AllOf { fun: WildFungible, id: Concrete(KsmLocation::get()) }); pub const Statemine: MultiLocation = Parachain(3).into(); @@ -106,7 +108,7 @@ impl Config for XcmConfig { type IsTeleporter = TrustedTeleporters; type LocationInverter = LocationInverter; type Barrier = Barrier; - type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Weigher = RelayWeigher; type Trader = UsingComponents, KsmLocation, AccountId, Balances, ()>; type ResponseHandler = (); type AssetTrap = (); @@ -126,7 +128,7 @@ impl pallet_xcm::Config for Runtime { type XcmExecutor = XcmExecutor; type XcmTeleportFilter = Everything; type XcmReserveTransferFilter = Everything; - type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Weigher = RelayWeigher; type LocationInverter = LocationInverter; type Origin = Origin; type Call = Call; diff --git a/xtokens/src/tests.rs b/xtokens/src/tests.rs index 51831365e..7f6b1e79d 100644 --- a/xtokens/src/tests.rs +++ b/xtokens/src/tests.rs @@ -3,11 +3,17 @@ use super::*; use codec::Encode; use cumulus_primitives_core::ParaId; -use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; -use mock::*; +use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency, BoundedVec}; +use mock::{ + para::{ParaAccount32Hash, ParaWeigher}, + relay::RelayWeigher, + *, +}; use orml_traits::{ConcreteFungibleAsset, MultiCurrency}; use polkadot_parachain::primitives::Sibling; use sp_runtime::{traits::AccountIdConversion, AccountId32}; +use xcm::latest::OriginKind::SovereignAccount; +use xcm_executor::traits::Convert; use xcm_simulator::TestExt; fn para_a_account() -> AccountId32 { @@ -300,6 +306,244 @@ fn send_sibling_asset_to_reserve_sibling() { }); } +#[test] +fn transfer_with_transact_self_reserve_sibling() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::A, &ALICE, 100_000)); + }); + + let remark = para::Call::System(frame_system::Call::::remark_with_event { remark: vec![1, 2, 3] }); + let encoded_call: Vec = remark.encode().into(); + let bounded_vec = BoundedVec::try_from(encoded_call).unwrap(); + + let transfer_amount = 50_000u128; + let transact_fee_amount = 10_000u128; + let transfer_dest_weight = 4_000; + let transact_dest_weight = 4_000; + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer_with_transact( + Some(ALICE).into(), + CurrencyId::A, + transfer_amount, + 2, // PARA_B_ID, + transfer_dest_weight, + bounded_vec, + transact_fee_amount, + transact_dest_weight, + )); + + assert_eq!(ParaTokens::free_balance(CurrencyId::A, &ALICE), transfer_amount); + }); + + ParaB::execute_with(|| { + assert!(para::System::events() + .iter() + .any(|r| matches!(r.event, para::Event::System(frame_system::Event::Remarked { .. })))); + let user_multilocaiton = ( + Parent, + Parachain(1), + Junction::AccountId32 { + network: NetworkId::Any, + id: ALICE.into(), + }, + ) + .into(); + + let mut token_transfer_msg = Xcm(vec![ClearOrigin, ClearOrigin, ClearOrigin, ClearOrigin]); + let token_transfer_weight = ParaWeigher::weight(&mut token_transfer_msg).unwrap() as u128; + + let mut transact_msg = Xcm(vec![ + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + Transact { + origin_type: SovereignAccount, + require_weight_at_most: transact_dest_weight, + call: vec![].into(), + }, + ]); + let transact_msg_weight = ParaWeigher::weight(&mut transact_msg).unwrap() as u128; + + let user_sovereign_account = ParaAccount32Hash::convert_ref(&user_multilocaiton).unwrap(); + + assert_eq!( + ParaTokens::free_balance(CurrencyId::A, &user_sovereign_account), + // AllTokensAreCreatedEqualToWeight means 1 to 1 mapping of the weight to fee + transfer_amount - token_transfer_weight - transact_msg_weight + ); + }); +} + +#[test] +fn transfer_with_transact_to_reserve_sibling() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &ALICE, 100_000)); + assert_ok!(ParaTokens::deposit(CurrencyId::A, &sibling_b_account(), 100_000)); + }); + + ParaB::execute_with(|| { + assert_ok!(ParaTokens::deposit(CurrencyId::B, &sibling_a_account(), 100_000)); + assert_ok!(ParaTokens::deposit(CurrencyId::A, &BOB, 100_000)); + }); + + let remark = para::Call::System(frame_system::Call::::remark_with_event { remark: vec![1, 2, 3] }); + let encoded_call: Vec = remark.encode().into(); + let bounded_vec = BoundedVec::try_from(encoded_call).unwrap(); + + let transfer_amount = 50_000u128; + let transact_fee_amount = 10_000u128; + let transfer_dest_weight = 4_000; + let transact_dest_weight = 4_000; + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer_with_transact( + Some(ALICE).into(), + CurrencyId::B, + transfer_amount, + 2, // PARA_B_ID, + transfer_dest_weight, + bounded_vec.clone(), + transact_fee_amount, + transact_dest_weight, + )); + + assert_eq!(ParaTokens::free_balance(CurrencyId::B, &ALICE), transfer_amount); + }); + + ParaB::execute_with(|| { + assert!(para::System::events() + .iter() + .any(|r| matches!(r.event, para::Event::System(frame_system::Event::Remarked { .. })))); + + assert_eq!( + ParaTokens::free_balance(CurrencyId::B, &sibling_a_account()), + transfer_amount + ); + + let user_multilocaiton = ( + Parent, + Parachain(1), + Junction::AccountId32 { + network: NetworkId::Any, + id: ALICE.into(), + }, + ) + .into(); + + let mut token_transfer_msg = Xcm(vec![ClearOrigin, ClearOrigin, ClearOrigin, ClearOrigin]); + let token_transfer_weight = ParaWeigher::weight(&mut token_transfer_msg).unwrap() as u128; + + let mut transact_msg = Xcm(vec![ + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + Transact { + origin_type: SovereignAccount, + require_weight_at_most: transact_dest_weight, + call: vec![].into(), + }, + ]); + let transact_msg_weight = ParaWeigher::weight(&mut transact_msg).unwrap() as u128; + + let user_sovereign_account = ParaAccount32Hash::convert_ref(&user_multilocaiton).unwrap(); + assert_eq!( + ParaTokens::free_balance(CurrencyId::B, &user_sovereign_account), + // AllTokensAreCreatedEqualToWeight means 1 to 1 mapping of the weight to fee + transfer_amount - token_transfer_weight - transact_msg_weight + ); + }); +} + +#[test] +fn transfer_with_transact_to_non_reserve_sibling() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_eq!(ParaTokens::free_balance(CurrencyId::R, &ALICE), 1_000); + assert_ok!(ParaTokens::deposit(CurrencyId::R, &ALICE, 100_000)); + }); + + Relay::execute_with(|| { + let _ = RelayBalances::deposit_creating(¶_a_account(), 100_000); + }); + + let remark = para::Call::System(frame_system::Call::::remark_with_event { remark: vec![1, 2, 3] }); + let encoded_call: Vec = remark.encode().into(); + let bounded_vec = BoundedVec::try_from(encoded_call).unwrap(); + + let transfer_amount = 50_000u128; + let transact_fee_amount = 10_000u128; + let transfer_dest_weight = 4_000; + let transact_dest_weight = 4_000; + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer_with_transact( + Some(ALICE).into(), + CurrencyId::R, + transfer_amount, + 2, // PARA_B_ID, + transfer_dest_weight, + bounded_vec, + transact_fee_amount, + transact_dest_weight, + )); + + assert_eq!(ParaTokens::free_balance(CurrencyId::R, &ALICE), transfer_amount + 1_000); + }); + + ParaB::execute_with(|| { + assert!(para::System::events() + .iter() + .any(|r| matches!(r.event, para::Event::System(frame_system::Event::Remarked { .. })))); + + let user_multilocaiton = ( + Parent, + Parachain(1), + Junction::AccountId32 { + network: NetworkId::Any, + id: ALICE.into(), + }, + ) + .into(); + + let mut token_transfer_msg = Xcm(vec![ClearOrigin, ClearOrigin, ClearOrigin, ClearOrigin]); + let token_transfer_weight = ParaWeigher::weight(&mut token_transfer_msg).unwrap() as u128; + let mut token_transfer_msg_on_relay = Xcm(vec![ClearOrigin, ClearOrigin, ClearOrigin, ClearOrigin]); + let token_transfer_weight_on_relay = RelayWeigher::weight(&mut token_transfer_msg_on_relay).unwrap() as u128; + + let mut transact_msg = Xcm(vec![ + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + ClearOrigin, + Transact { + origin_type: SovereignAccount, + require_weight_at_most: transact_dest_weight, + call: vec![].into(), + }, + ]); + let transact_msg_weight = ParaWeigher::weight(&mut transact_msg).unwrap() as u128; + + let user_sovereign_account = ParaAccount32Hash::convert_ref(&user_multilocaiton).unwrap(); + + assert_eq!( + ParaTokens::free_balance(CurrencyId::R, &user_sovereign_account), + // AllTokensAreCreatedEqualToWeight means 1 to 1 mapping of the weight to fee + transfer_amount - token_transfer_weight_on_relay - token_transfer_weight - transact_msg_weight + ); + }); +} + #[test] fn send_sibling_asset_to_reserve_sibling_with_fee() { TestNet::reset(); @@ -1021,8 +1265,6 @@ fn send_as_sovereign() { }); ParaA::execute_with(|| { - use xcm::latest::OriginKind::SovereignAccount; - let call = relay::Call::System(frame_system::Call::::remark_with_event { remark: vec![1, 1, 1] }); let assets: MultiAsset = (Here, 1_000_000_000_000).into();