diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index f09647854cd0132751889ef2b33bca1c5ec16e74..83bc12cf9b41fc887451fe672ca65f6bf056257f 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -71,7 +71,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, EnsureSigned, EnsureSignedBy, }; -use pallet_asset_conversion_tx_payment::AssetConversionAdapter; +use pallet_asset_conversion_tx_payment::SwapAssetAdapter; use pallet_nfts::PalletFeatures; use parachains_common::{ impls::DealWithFees, @@ -798,11 +798,19 @@ impl pallet_collator_selection::Config for Runtime { type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>; } +parameter_types! { + pub StakingPot: AccountId = CollatorSelection::account_id(); +} + impl pallet_asset_conversion_tx_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type Fungibles = LocalAndForeignAssets; - type OnChargeAssetTransaction = - AssetConversionAdapter<Balances, AssetConversion, TokenLocationV3>; + type AssetId = xcm::v3::Location; + type OnChargeAssetTransaction = SwapAssetAdapter< + TokenLocationV3, + NativeAndAssets, + AssetConversion, + ResolveAssetTo<StakingPot, NativeAndAssets>, + >; } parameter_types! { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 178b886fc3e84b741eb6721fec1b4e493626795b..2d9de07f251f9b157f01cb3c05c63b7bb55ba5f6 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -55,7 +55,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, EnsureSigned, EnsureSignedBy, }; -use pallet_asset_conversion_tx_payment::AssetConversionAdapter; +use pallet_asset_conversion_tx_payment::SwapAssetAdapter; use pallet_nfts::{DestroyWitness, PalletFeatures}; use pallet_xcm::EnsureXcm; use parachains_common::{ @@ -787,11 +787,19 @@ impl pallet_collator_selection::Config for Runtime { type WeightInfo = weights::pallet_collator_selection::WeightInfo<Runtime>; } +parameter_types! { + pub StakingPot: AccountId = CollatorSelection::account_id(); +} + impl pallet_asset_conversion_tx_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type Fungibles = LocalAndForeignAssets; - type OnChargeAssetTransaction = - AssetConversionAdapter<Balances, AssetConversion, WestendLocationV3>; + type AssetId = xcm::v3::Location; + type OnChargeAssetTransaction = SwapAssetAdapter< + WestendLocationV3, + NativeAndAssets, + AssetConversion, + ResolveAssetTo<StakingPot, NativeAndAssets>, + >; } parameter_types! { diff --git a/prdoc/pr_4488.prdoc b/prdoc/pr_4488.prdoc new file mode 100644 index 0000000000000000000000000000000000000000..d0b6a877be6b1ecbdfbf94e364cc9c836cb8dd32 --- /dev/null +++ b/prdoc/pr_4488.prdoc @@ -0,0 +1,25 @@ +title: "Tx Payment: drop ED requirements for tx payments with exchangeable asset" + +doc: + - audience: Runtime Dev + description: | + Drop the Existential Deposit requirement for the asset amount exchangeable for the fee asset + (eg. DOT/KSM) during transaction payments. + + This achieved by using `SwapCredit` implementation of asset conversion, which works with + imbalances and does not require a temporary balance account within the transaction payment. + + This is a breaking change for the `pallet-asset-conversion-tx-payment` pallet, use examples + from PR for the migration. + +crates: + - name: pallet-asset-conversion-tx-payment + bump: major + - name: pallet-transaction-payment + bump: patch + - name: pallet-asset-conversion + bump: patch + - name: asset-hub-rococo-runtime + bump: patch + - name: asset-hub-westend-runtime + bump: patch diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index cad2cc119f0d5dcb2d9b7bc6fa66250c971d42af..5d046539b0362b003b8d3aec23e17207a0b8dc84 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -69,6 +69,7 @@ use frame_system::{ pub use node_primitives::{AccountId, Signature}; use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Moment, Nonce}; use pallet_asset_conversion::{AccountIdConverter, Ascending, Chain, WithFirstAsset}; +use pallet_asset_conversion_tx_payment::SwapAssetAdapter; use pallet_broker::{CoreAssignment, CoreIndex, CoretimeInterface, PartsOf57600}; use pallet_election_provider_multi_phase::{GeometricDepositBase, SolutionAccuracyOf}; use pallet_identity::legacy::IdentityInfo; @@ -123,7 +124,7 @@ pub use sp_runtime::BuildStorage; pub mod impls; #[cfg(not(feature = "runtime-benchmarks"))] use impls::AllianceIdentityVerifier; -use impls::{AllianceProposalProvider, Author, CreditToBlockAuthor}; +use impls::{AllianceProposalProvider, Author}; /// Constant values used within the runtime. pub mod constants; @@ -574,22 +575,14 @@ impl pallet_transaction_payment::Config for Runtime { >; } -impl pallet_asset_tx_payment::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Fungibles = Assets; - type OnChargeAssetTransaction = pallet_asset_tx_payment::FungiblesAdapter< - pallet_assets::BalanceToAssetBalance<Balances, Runtime, ConvertInto, Instance1>, - CreditToBlockAuthor, - >; -} - impl pallet_asset_conversion_tx_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type Fungibles = Assets; - type OnChargeAssetTransaction = pallet_asset_conversion_tx_payment::AssetConversionAdapter< - Balances, - AssetConversion, + type AssetId = NativeOrWithId<u32>; + type OnChargeAssetTransaction = SwapAssetAdapter< Native, + NativeAndAssets, + AssetConversion, + ResolveAssetTo<TreasuryAccount, NativeAndAssets>, >; } @@ -1705,12 +1698,15 @@ parameter_types! { pub const Native: NativeOrWithId<u32> = NativeOrWithId::Native; } +pub type NativeAndAssets = + UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, AccountId>; + impl pallet_asset_conversion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = u128; type HigherPrecisionBalance = sp_core::U256; type AssetKind = NativeOrWithId<u32>; - type Assets = UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, AccountId>; + type Assets = NativeAndAssets; type PoolId = (Self::AssetKind, Self::AssetKind); type PoolLocator = Chain< WithFirstAsset< @@ -2259,9 +2255,6 @@ mod runtime { #[runtime::pallet_index(7)] pub type TransactionPayment = pallet_transaction_payment::Pallet<Runtime>; - #[runtime::pallet_index(8)] - pub type AssetTxPayment = pallet_asset_tx_payment::Pallet<Runtime>; - #[runtime::pallet_index(9)] pub type AssetConversionTxPayment = pallet_asset_conversion_tx_payment::Pallet<Runtime>; diff --git a/substrate/frame/asset-conversion/src/swap.rs b/substrate/frame/asset-conversion/src/swap.rs index a6154e29414767550106544585b592903c3a6f2a..1485e7166f30eda340139ea5b889b359ca904230 100644 --- a/substrate/frame/asset-conversion/src/swap.rs +++ b/substrate/frame/asset-conversion/src/swap.rs @@ -112,6 +112,37 @@ pub trait SwapCredit<AccountId> { ) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)>; } +/// Trait providing methods to quote swap prices between asset classes. +/// +/// The quoted price is only guaranteed if no other swaps are made after the price is quoted and +/// before the target swap (e.g., the swap is made immediately within the same transaction). +pub trait QuotePrice { + /// Measurement units of the asset classes for pricing. + type Balance: Balance; + /// Type representing the kind of assets for which the price is being quoted. + type AssetKind; + /// Quotes the amount of `asset1` required to obtain the exact `amount` of `asset2`. + /// + /// If `include_fee` is set to `true`, the price will include the pool's fee. + /// If the pool does not exist or the swap cannot be made, `None` is returned. + fn quote_price_tokens_for_exact_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option<Self::Balance>; + /// Quotes the amount of `asset2` resulting from swapping the exact `amount` of `asset1`. + /// + /// If `include_fee` is set to `true`, the price will include the pool's fee. + /// If the pool does not exist or the swap cannot be made, `None` is returned. + fn quote_price_exact_tokens_for_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option<Self::Balance>; +} + impl<T: Config> Swap<T::AccountId> for Pallet<T> { type Balance = T::Balance; type AssetKind = T::AssetKind; @@ -210,3 +241,24 @@ impl<T: Config> SwapCredit<T::AccountId> for Pallet<T> { .map_err(|_| (Self::Credit::zero(credit_asset), DispatchError::Corruption))? } } + +impl<T: Config> QuotePrice for Pallet<T> { + type Balance = T::Balance; + type AssetKind = T::AssetKind; + fn quote_price_exact_tokens_for_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option<Self::Balance> { + Self::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee) + } + fn quote_price_tokens_for_exact_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option<Self::Balance> { + Self::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee) + } +} diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs index 538d88bfacfaa6d5138ff7fca33e9b45cf1ee56c..825a35e621382d65ba60c804b258b866be6b8f1a 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs @@ -23,7 +23,7 @@ //! This pallet provides a `SignedExtension` with an optional `AssetId` that specifies the asset //! to be used for payment (defaulting to the native token on `None`). It expects an //! [`OnChargeAssetTransaction`] implementation analogous to [`pallet-transaction-payment`]. The -//! included [`AssetConversionAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the +//! included [`SwapAssetAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the //! fee amount by converting the fee calculated by [`pallet-transaction-payment`] in the native //! asset into the amount required of the specified asset. //! @@ -47,19 +47,14 @@ extern crate alloc; use codec::{Decode, Encode}; use frame_support::{ dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, - traits::{ - fungibles::{Balanced, Inspect}, - IsType, - }, + traits::IsType, DefaultNoBound, }; use pallet_transaction_payment::OnChargeTransaction; use scale_info::TypeInfo; use sp_runtime::{ traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero}, - transaction_validity::{ - InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, - }, + transaction_validity::{TransactionValidity, TransactionValidityError, ValidTransaction}, }; #[cfg(test)] @@ -71,30 +66,19 @@ mod payment; use frame_support::traits::tokens::AssetId; pub use payment::*; +/// Balance type alias for balances of the chain's native asset. +pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance; + /// Type aliases used for interaction with `OnChargeTransaction`. pub(crate) type OnChargeTransactionOf<T> = <T as pallet_transaction_payment::Config>::OnChargeTransaction; -/// Balance type alias for balances of the chain's native asset. -pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance; -/// Liquidity info type alias. -pub(crate) type LiquidityInfoOf<T> = - <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo; -/// Balance type alias for balances of assets that implement the `fungibles` trait. -pub(crate) type AssetBalanceOf<T> = - <<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance; -/// Type alias for Asset IDs. -pub(crate) type AssetIdOf<T> = - <<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId; +/// Liquidity info type alias for the chain's native asset. +pub(crate) type NativeLiquidityInfoOf<T> = + <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo; -/// Type alias for the interaction of balances with `OnChargeAssetTransaction`. -pub(crate) type ChargeAssetBalanceOf<T> = - <<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::Balance; -/// Type alias for Asset IDs in their interaction with `OnChargeAssetTransaction`. -pub(crate) type ChargeAssetIdOf<T> = - <<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::AssetId; -/// Liquidity info type alias for interaction with `OnChargeAssetTransaction`. -pub(crate) type ChargeAssetLiquidityOf<T> = +/// Liquidity info type alias for the chain's assets. +pub(crate) type AssetLiquidityInfoOf<T> = <<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo; /// Used to pass the initial payment info from pre- to post-dispatch. @@ -104,9 +88,9 @@ pub enum InitialPayment<T: Config> { #[default] Nothing, /// The initial fee was paid in the native currency. - Native(LiquidityInfoOf<T>), + Native(NativeLiquidityInfoOf<T>), /// The initial fee was paid in an asset. - Asset((LiquidityInfoOf<T>, BalanceOf<T>, AssetBalanceOf<T>)), + Asset((T::AssetId, AssetLiquidityInfoOf<T>)), } pub use pallet::*; @@ -116,15 +100,18 @@ pub mod pallet { use super::*; #[pallet::config] - pub trait Config: - frame_system::Config + pallet_transaction_payment::Config + pallet_asset_conversion::Config - { + pub trait Config: frame_system::Config + pallet_transaction_payment::Config { /// The overarching event type. type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; - /// The fungibles instance used to pay for transactions in assets. - type Fungibles: Balanced<Self::AccountId>; + /// The asset ID type that can be used for transaction payments in addition to a + /// native asset. + type AssetId: AssetId; /// The actual transaction charging logic that charges the fees. - type OnChargeAssetTransaction: OnChargeAssetTransaction<Self>; + type OnChargeAssetTransaction: OnChargeAssetTransaction< + Self, + Balance = BalanceOf<Self>, + AssetId = Self::AssetId, + >; } #[pallet::pallet] @@ -137,9 +124,9 @@ pub mod pallet { /// has been paid by `who` in an asset `asset_id`. AssetTxFeePaid { who: T::AccountId, - actual_fee: AssetBalanceOf<T>, + actual_fee: BalanceOf<T>, tip: BalanceOf<T>, - asset_id: ChargeAssetIdOf<T>, + asset_id: T::AssetId, }, /// A swap of the refund in native currency back to asset failed. AssetRefundFailed { native_amount_kept: BalanceOf<T> }, @@ -147,33 +134,35 @@ pub mod pallet { } /// Require payment for transaction inclusion and optionally include a tip to gain additional -/// priority in the queue. Allows paying via both `Currency` as well as `fungibles::Balanced`. +/// priority in the queue. /// /// Wraps the transaction logic in [`pallet_transaction_payment`] and extends it with assets. /// An asset ID of `None` falls back to the underlying transaction payment logic via the native /// currency. +/// +/// Transaction payments are processed using different handlers based on the asset type: +/// - Payments with a native asset are charged by +/// [pallet_transaction_payment::Config::OnChargeTransaction]. +/// - Payments with other assets are charged by [Config::OnChargeAssetTransaction]. #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct ChargeAssetTxPayment<T: Config> { #[codec(compact)] tip: BalanceOf<T>, - asset_id: Option<ChargeAssetIdOf<T>>, + asset_id: Option<T::AssetId>, } impl<T: Config> ChargeAssetTxPayment<T> where T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>, - AssetBalanceOf<T>: Send + Sync, - BalanceOf<T>: Send + Sync + Into<ChargeAssetBalanceOf<T>> + From<ChargeAssetLiquidityOf<T>>, - ChargeAssetIdOf<T>: Send + Sync, { /// Utility constructor. Used only in client/factory code. - pub fn from(tip: BalanceOf<T>, asset_id: Option<ChargeAssetIdOf<T>>) -> Self { + pub fn from(tip: BalanceOf<T>, asset_id: Option<T::AssetId>) -> Self { Self { tip, asset_id } } - /// Fee withdrawal logic that dispatches to either `OnChargeAssetTransaction` or - /// `OnChargeTransaction`. + /// Fee withdrawal logic that dispatches to either [`Config::OnChargeAssetTransaction`] or + /// [`pallet_transaction_payment::Config::OnChargeTransaction`]. fn withdraw_fee( &self, who: &T::AccountId, @@ -191,25 +180,13 @@ where call, info, asset_id.clone(), - fee.into(), - self.tip.into(), + fee, + self.tip, ) - .map(|(used_for_fee, received_exchanged, asset_consumed)| { - ( - fee, - InitialPayment::Asset(( - used_for_fee.into(), - received_exchanged.into(), - asset_consumed.into(), - )), - ) - }) + .map(|payment| (fee, InitialPayment::Asset((asset_id.clone(), payment)))) } else { - <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee( - who, call, info, fee, self.tip, - ) - .map(|i| (fee, InitialPayment::Native(i))) - .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() }) + T::OnChargeTransaction::withdraw_fee(who, call, info, fee, self.tip) + .map(|payment| (fee, InitialPayment::Native(payment))) } } } @@ -228,14 +205,8 @@ impl<T: Config> core::fmt::Debug for ChargeAssetTxPayment<T> { impl<T: Config> SignedExtension for ChargeAssetTxPayment<T> where T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>, - AssetBalanceOf<T>: Send + Sync, - BalanceOf<T>: Send - + Sync - + From<u64> - + Into<ChargeAssetBalanceOf<T>> - + Into<ChargeAssetLiquidityOf<T>> - + From<ChargeAssetLiquidityOf<T>>, - ChargeAssetIdOf<T>: Send + Sync, + BalanceOf<T>: Send + Sync, + T::AssetId: Send + Sync, { const IDENTIFIER: &'static str = "ChargeAssetTxPayment"; type AccountId = T::AccountId; @@ -248,8 +219,6 @@ where Self::AccountId, // imbalance resulting from withdrawing the fee InitialPayment<T>, - // asset_id for the transaction payment - Option<ChargeAssetIdOf<T>>, ); fn additional_signed(&self) -> core::result::Result<(), TransactionValidityError> { @@ -277,7 +246,7 @@ where len: usize, ) -> Result<Self::Pre, TransactionValidityError> { let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?; - Ok((self.tip, who.clone(), initial_payment, self.asset_id)) + Ok((self.tip, who.clone(), initial_payment)) } fn post_dispatch( @@ -285,53 +254,45 @@ where info: &DispatchInfoOf<Self::Call>, post_info: &PostDispatchInfoOf<Self::Call>, len: usize, - result: &DispatchResult, + _result: &DispatchResult, ) -> Result<(), TransactionValidityError> { - if let Some((tip, who, initial_payment, asset_id)) = pre { + if let Some((tip, who, initial_payment)) = pre { match initial_payment { InitialPayment::Native(already_withdrawn) => { - debug_assert!( - asset_id.is_none(), - "For that payment type the `asset_id` should be None" + let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee( + len as u32, info, post_info, tip, ); - pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch( - Some((tip, who, already_withdrawn)), + T::OnChargeTransaction::correct_and_deposit_fee( + &who, info, post_info, - len, - result, + actual_fee, + tip, + already_withdrawn, )?; - }, - InitialPayment::Asset(already_withdrawn) => { - debug_assert!( - asset_id.is_some(), - "For that payment type the `asset_id` should be set" + pallet_transaction_payment::Pallet::<T>::deposit_fee_paid_event( + who, actual_fee, tip, ); + }, + InitialPayment::Asset((asset_id, already_withdrawn)) => { let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee( len as u32, info, post_info, tip, ); - - if let Some(asset_id) = asset_id { - let (used_for_fee, received_exchanged, asset_consumed) = already_withdrawn; - let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee( - &who, - info, - post_info, - actual_fee.into(), - tip.into(), - used_for_fee.into(), - received_exchanged.into(), - asset_id.clone(), - asset_consumed.into(), - )?; - - Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid { - who, - actual_fee: converted_fee, - tip, - asset_id, - }); - } + let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee( + &who, + info, + post_info, + actual_fee, + tip, + asset_id.clone(), + already_withdrawn, + )?; + Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid { + who, + actual_fee: converted_fee, + tip, + asset_id, + }); }, InitialPayment::Nothing => { // `actual_fee` should be zero here for any signed extrinsic. It would be diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs index 245900760de9d31f5540b83b5b3a5789b72d31cc..acfd43d0a7cb9520ac14974f439eca7530e7b3a8 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs @@ -24,7 +24,7 @@ use frame_support::{ pallet_prelude::*, parameter_types, traits::{ - fungible, + fungible, fungibles, tokens::{ fungible::{NativeFromLeft, NativeOrWithId, UnionOf}, imbalance::ResolveAssetTo, @@ -145,6 +145,22 @@ impl OnUnbalanced<fungible::Credit<<Runtime as frame_system::Config>::AccountId, } } +pub struct DealWithFungiblesFees; +impl OnUnbalanced<fungibles::Credit<AccountId, NativeAndAssets>> for DealWithFungiblesFees { + fn on_unbalanceds( + mut fees_then_tips: impl Iterator< + Item = fungibles::Credit<<Runtime as frame_system::Config>::AccountId, NativeAndAssets>, + >, + ) { + if let Some(fees) = fees_then_tips.next() { + FeeUnbalancedAmount::mutate(|a| *a += fees.peek()); + if let Some(tips) = fees_then_tips.next() { + TipUnbalancedAmount::mutate(|a| *a += tips.peek()); + } + } + } +} + #[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig)] impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; @@ -221,12 +237,14 @@ pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter< (NativeOrWithId<u32>, NativeOrWithId<u32>), >; +type NativeAndAssets = UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, AccountId>; + impl pallet_asset_conversion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type HigherPrecisionBalance = u128; type AssetKind = NativeOrWithId<u32>; - type Assets = UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, AccountId>; + type Assets = NativeAndAssets; type PoolId = (Self::AssetKind, Self::AssetKind); type PoolLocator = Chain< WithFirstAsset<Native, AccountId, NativeOrWithId<u32>, PoolIdToAccountId>, @@ -250,6 +268,7 @@ impl pallet_asset_conversion::Config for Runtime { impl Config for Runtime { type RuntimeEvent = RuntimeEvent; - type Fungibles = Assets; - type OnChargeAssetTransaction = AssetConversionAdapter<Balances, AssetConversion, Native>; + type AssetId = NativeOrWithId<u32>; + type OnChargeAssetTransaction = + SwapAssetAdapter<Native, NativeAndAssets, AssetConversion, DealWithFungiblesFees>; } diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs index 0ef3fb1111439e0020bc4797fb0cd85ed71521b7..dc7faecd56084d924c6e0a57fa0c6052acb4bd38 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs @@ -20,11 +20,15 @@ use crate::Config; use alloc::vec; use core::marker::PhantomData; use frame_support::{ - ensure, - traits::{fungible::Inspect, tokens::Balance}, + defensive, ensure, + traits::{ + fungibles, + tokens::{Balance, Fortitude, Precision, Preservation}, + Defensive, OnUnbalanced, SameOrOther, + }, unsigned::TransactionValidityError, }; -use pallet_asset_conversion::Swap; +use pallet_asset_conversion::{QuotePrice, SwapCredit}; use sp_runtime::{ traits::{DispatchInfoOf, Get, PostDispatchInfoOf, Zero}, transaction_validity::InvalidTransaction, @@ -50,150 +54,216 @@ pub trait OnChargeAssetTransaction<T: Config> { asset_id: Self::AssetId, fee: Self::Balance, tip: Self::Balance, - ) -> Result< - (LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>), - TransactionValidityError, - >; + ) -> Result<Self::LiquidityInfo, TransactionValidityError>; /// Refund any overpaid fees and deposit the corrected amount. /// The actual fee gets calculated once the transaction is executed. /// /// Note: The `fee` already includes the `tip`. /// - /// Returns the fee and tip in the asset used for payment as (fee, tip). + /// Returns the amount of `asset_id` that was used for the payment. fn correct_and_deposit_fee( who: &T::AccountId, dispatch_info: &DispatchInfoOf<T::RuntimeCall>, post_info: &PostDispatchInfoOf<T::RuntimeCall>, corrected_fee: Self::Balance, tip: Self::Balance, - fee_paid: LiquidityInfoOf<T>, - received_exchanged: Self::LiquidityInfo, asset_id: Self::AssetId, - initial_asset_consumed: AssetBalanceOf<T>, - ) -> Result<AssetBalanceOf<T>, TransactionValidityError>; + already_withdraw: Self::LiquidityInfo, + ) -> Result<BalanceOf<T>, TransactionValidityError>; } -/// Implements the asset transaction for a balance to asset converter (implementing [`Swap`]). +/// Means to withdraw, correct and deposit fees in the asset accepted by the system. /// -/// The converter is given the complete fee in terms of the asset used for the transaction. -pub struct AssetConversionAdapter<C, CON, N>(PhantomData<(C, CON, N)>); +/// The type uses the [`SwapCredit`] implementation to swap the asset used by a user for the fee +/// payment for the asset accepted as a fee payment be the system. +/// +/// Parameters: +/// - `A`: The asset identifier that system accepts as a fee payment (eg. native asset). +/// - `F`: The fungibles registry that can handle assets provided by user and the `A` asset. +/// - `S`: The swap implementation that can swap assets provided by user for the `A` asset. +/// - OU: The handler for withdrawn `fee` and `tip`, passed in the respective order to +/// [OnUnbalanced::on_unbalanceds]. +/// - `T`: The pallet's configuration. +pub struct SwapAssetAdapter<A, F, S, OU>(PhantomData<(A, F, S, OU)>); -/// Default implementation for a runtime instantiating this pallet, an asset to native swapper. -impl<T, C, CON, N> OnChargeAssetTransaction<T> for AssetConversionAdapter<C, CON, N> +impl<A, F, S, OU, T> OnChargeAssetTransaction<T> for SwapAssetAdapter<A, F, S, OU> where - N: Get<CON::AssetKind>, + A: Get<T::AssetId>, + F: fungibles::Balanced<T::AccountId, Balance = BalanceOf<T>, AssetId = T::AssetId>, + S: SwapCredit< + T::AccountId, + Balance = BalanceOf<T>, + AssetKind = T::AssetId, + Credit = fungibles::Credit<T::AccountId, F>, + > + QuotePrice<Balance = BalanceOf<T>, AssetKind = T::AssetId>, + OU: OnUnbalanced<fungibles::Credit<T::AccountId, F>>, T: Config, - C: Inspect<<T as frame_system::Config>::AccountId>, - CON: Swap<T::AccountId, Balance = BalanceOf<T>, AssetKind = T::AssetKind>, - BalanceOf<T>: Into<AssetBalanceOf<T>>, - T::AssetKind: From<AssetIdOf<T>>, - BalanceOf<T>: IsType<<C as Inspect<<T as frame_system::Config>::AccountId>>::Balance>, { + type AssetId = T::AssetId; type Balance = BalanceOf<T>; - type AssetId = AssetIdOf<T>; - type LiquidityInfo = BalanceOf<T>; + type LiquidityInfo = (fungibles::Credit<T::AccountId, F>, BalanceOf<T>); - /// Swap & withdraw the predicted fee from the transaction origin. - /// - /// Note: The `fee` already includes the `tip`. - /// - /// Returns the total amount in native currency received by exchanging the `asset_id` and the - /// amount in native currency used to pay the fee. fn withdraw_fee( who: &T::AccountId, - call: &T::RuntimeCall, - info: &DispatchInfoOf<T::RuntimeCall>, + _call: &T::RuntimeCall, + _dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>, asset_id: Self::AssetId, - fee: BalanceOf<T>, - tip: BalanceOf<T>, - ) -> Result< - (LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>), - TransactionValidityError, - > { - // convert the asset into native currency - let ed = C::minimum_balance(); - let native_asset_required = - if C::balance(&who) >= ed.saturating_add(fee.into()) { fee } else { fee + ed.into() }; - - let asset_consumed = CON::swap_tokens_for_exact_tokens( - who.clone(), - vec![asset_id.into(), N::get()], - native_asset_required, - None, - who.clone(), - true, + fee: Self::Balance, + _tip: Self::Balance, + ) -> Result<Self::LiquidityInfo, TransactionValidityError> { + if asset_id == A::get() { + // The `asset_id` is the target asset, we do not need to swap. + let fee_credit = F::withdraw( + asset_id.clone(), + who, + fee, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) + .map_err(|_| InvalidTransaction::Payment)?; + + return Ok((fee_credit, fee)); + } + + // Quote the amount of the `asset_id` needed to pay the fee in the asset `A`. + let asset_fee = + S::quote_price_tokens_for_exact_tokens(asset_id.clone(), A::get(), fee, true) + .ok_or(InvalidTransaction::Payment)?; + + // Withdraw the `asset_id` credit for the swap. + let asset_fee_credit = F::withdraw( + asset_id.clone(), + who, + asset_fee, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, ) - .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; + .map_err(|_| InvalidTransaction::Payment)?; - ensure!(asset_consumed > Zero::zero(), InvalidTransaction::Payment); + let (fee_credit, change) = match S::swap_tokens_for_exact_tokens( + vec![asset_id, A::get()], + asset_fee_credit, + fee, + ) { + Ok((fee_credit, change)) => (fee_credit, change), + Err((credit_in, _)) => { + defensive!("Fee swap should pass for the quoted amount"); + let _ = F::resolve(who, credit_in).defensive_proof("Should resolve the credit"); + return Err(InvalidTransaction::Payment.into()) + }, + }; - // charge the fee in native currency - <T::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip) - .map(|r| (r, native_asset_required, asset_consumed.into())) + // Since the exact price for `fee` has been quoted, the change should be zero. + ensure!(change.peek().is_zero(), InvalidTransaction::Payment); + + Ok((fee_credit, asset_fee)) } - /// Correct the fee and swap the refund back to asset. - /// - /// Note: The `corrected_fee` already includes the `tip`. - /// Note: Is the ED wasn't needed, the `received_exchanged` will be equal to `fee_paid`, or - /// `fee_paid + ed` otherwise. fn correct_and_deposit_fee( who: &T::AccountId, - dispatch_info: &DispatchInfoOf<T::RuntimeCall>, - post_info: &PostDispatchInfoOf<T::RuntimeCall>, - corrected_fee: BalanceOf<T>, - tip: BalanceOf<T>, - fee_paid: LiquidityInfoOf<T>, - received_exchanged: Self::LiquidityInfo, + _dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>, + _post_info: &PostDispatchInfoOf<<T>::RuntimeCall>, + corrected_fee: Self::Balance, + tip: Self::Balance, asset_id: Self::AssetId, - initial_asset_consumed: AssetBalanceOf<T>, - ) -> Result<AssetBalanceOf<T>, TransactionValidityError> { - // Refund the native asset to the account that paid the fees (`who`). - // The `who` account will receive the "fee_paid - corrected_fee" refund. - <T::OnChargeTransaction>::correct_and_deposit_fee( - who, - dispatch_info, - post_info, - corrected_fee, - tip, - fee_paid, - )?; - - // calculate the refund in native asset, to swap back to the desired `asset_id` - let swap_back = received_exchanged.saturating_sub(corrected_fee); - let mut asset_refund = Zero::zero(); - if !swap_back.is_zero() { - // If this fails, the account might have dropped below the existential balance or there - // is not enough liquidity left in the pool. In that case we don't throw an error and - // the account will keep the native currency. - match CON::swap_exact_tokens_for_tokens( - who.clone(), // we already deposited the native to `who` - vec![ - N::get(), // we provide the native - asset_id.into(), // we want asset_id back - ], - swap_back, // amount of the native asset to convert to `asset_id` - None, // no minimum amount back - who.clone(), // we will refund to `who` - false, // no need to keep alive + already_withdrawn: Self::LiquidityInfo, + ) -> Result<BalanceOf<T>, TransactionValidityError> { + let (fee_paid, initial_asset_consumed) = already_withdrawn; + let refund_amount = fee_paid.peek().saturating_sub(corrected_fee); + let (fee_in_asset, adjusted_paid) = if refund_amount.is_zero() || + F::total_balance(asset_id.clone(), who).is_zero() + { + // Nothing to refund or the account was removed be the dispatched function. + (initial_asset_consumed, fee_paid) + } else if asset_id == A::get() { + // The `asset_id` is the target asset, we do not need to swap. + let (refund, fee_paid) = fee_paid.split(refund_amount); + if let Err(refund) = F::resolve(who, refund) { + let fee_paid = fee_paid.merge(refund).map_err(|_| { + defensive!("`fee_paid` and `refund` are credits of the same asset."); + InvalidTransaction::Payment + })?; + (initial_asset_consumed, fee_paid) + } else { + (fee_paid.peek().saturating_sub(refund_amount), fee_paid) + } + } else { + // Check if the refund amount can be swapped back into the asset used by `who` for fee + // payment. + let refund_asset_amount = S::quote_price_exact_tokens_for_tokens( + A::get(), + asset_id.clone(), + refund_amount, + true, ) - .ok() - { - Some(acquired) => { - asset_refund = acquired - .try_into() - .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; - }, - None => { - Pallet::<T>::deposit_event(Event::<T>::AssetRefundFailed { - native_amount_kept: swap_back, - }); - }, + // No refund given if it cannot be swapped back. + .unwrap_or(Zero::zero()); + + let debt = if refund_asset_amount.is_zero() { + fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone()) + } else { + // Deposit the refund before the swap to ensure it can be processed. + match F::deposit(asset_id.clone(), &who, refund_asset_amount, Precision::BestEffort) + { + Ok(debt) => debt, + // No refund given since it cannot be deposited. + Err(_) => fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone()), + } + }; + + if debt.peek().is_zero() { + // No refund given. + (initial_asset_consumed, fee_paid) + } else { + let (refund, adjusted_paid) = fee_paid.split(refund_amount); + match S::swap_exact_tokens_for_tokens( + vec![A::get(), asset_id], + refund, + Some(refund_asset_amount), + ) { + Ok(refund_asset) => { + match refund_asset.offset(debt) { + Ok(SameOrOther::None) => {}, + // This arm should never be reached, as the amount of `debt` is + // expected to be exactly equal to the amount of `refund_asset` credit. + _ => { + defensive!("Debt should be equal to the refund credit"); + return Err(InvalidTransaction::Payment.into()) + }, + }; + ( + initial_asset_consumed.saturating_sub(refund_asset_amount.into()), + adjusted_paid, + ) + }, + // The error should not occur since swap was quoted before. + Err((refund, _)) => { + defensive!("Refund swap should pass for the quoted amount"); + match F::settle(who, debt, Preservation::Expendable) { + Ok(dust) => ensure!(dust.peek().is_zero(), InvalidTransaction::Payment), + // The error should not occur as the `debt` was just withdrawn above. + Err(_) => { + defensive!("Should settle the debt"); + return Err(InvalidTransaction::Payment.into()) + }, + }; + let adjusted_paid = adjusted_paid.merge(refund).map_err(|_| { + // The error should never occur since `adjusted_paid` and `refund` are + // credits of the same asset. + InvalidTransaction::Payment + })?; + (initial_asset_consumed, adjusted_paid) + }, + } } - } + }; - let actual_paid = initial_asset_consumed.saturating_sub(asset_refund); - Ok(actual_paid) + // Handle the imbalance (fee and tip separately). + let (tip, fee) = adjusted_paid.split(tip); + OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip))); + Ok(fee_in_asset) } } diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs index aa2f26f3a6a8d5ce0edc45e1694c51a2c12da44c..aab657199533ec2714ee2b14813044e02e83d205 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs @@ -22,6 +22,7 @@ use frame_support::{ traits::{ fungible::{Inspect, NativeOrWithId}, fungibles::{Inspect as FungiblesInspect, Mutate}, + tokens::{Fortitude, Precision, Preservation}, }, weights::Weight, }; @@ -239,7 +240,7 @@ fn transaction_payment_in_asset_possible() { let fee_in_asset = input_quote.unwrap(); assert_eq!(Assets::balance(asset_id, caller), balance); - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); // assert that native balance is not used @@ -297,7 +298,7 @@ fn transaction_payment_in_asset_fails_if_no_pool_for_that_asset() { assert_eq!(Assets::balance(asset_id, caller), balance); let len = 10; - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)).pre_dispatch( + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())).pre_dispatch( &caller, CALL, &info_from_weight(WEIGHT_5), @@ -352,7 +353,7 @@ fn transaction_payment_without_fee() { assert_eq!(input_quote, Some(201)); let fee_in_asset = input_quote.unwrap(); - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); @@ -429,7 +430,7 @@ fn asset_transaction_payment_with_tip_and_refund() { assert_eq!(input_quote, Some(1206)); let fee_in_asset = input_quote.unwrap(); - let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_100), len) .unwrap(); assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); @@ -512,31 +513,21 @@ fn payment_from_account_with_only_assets() { let len = 10; let fee_in_native = base_weight + weight + len as u64; - let ed = Balances::minimum_balance(); let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens( NativeOrWithId::WithId(asset_id), NativeOrWithId::Native, - fee_in_native + ed, + fee_in_native, true, ) .unwrap(); - assert_eq!(fee_in_asset, 301); + assert_eq!(fee_in_asset, 201); - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); - assert_eq!(Balances::free_balance(caller), ed); // check that fee was charged in the given asset assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); - let refund = AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrWithId::Native, - NativeOrWithId::WithId(asset_id), - ed, - true, - ) - .unwrap(); - assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch( Some(pre), &info_from_weight(WEIGHT_5), @@ -544,7 +535,7 @@ fn payment_from_account_with_only_assets() { len, &Ok(()) )); - assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund); + assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); assert_eq!(Balances::free_balance(caller), 0); assert_eq!(TipUnbalancedAmount::get(), 0); @@ -587,7 +578,7 @@ fn converted_fee_is_never_zero_if_input_fee_is_not() { // there will be no conversion when the fee is zero { - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) .unwrap(); // `Pays::No` implies there are no fees @@ -613,7 +604,7 @@ fn converted_fee_is_never_zero_if_input_fee_is_not() { ) .unwrap(); - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len) .unwrap(); assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); @@ -663,14 +654,14 @@ fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() { // calculated fee is greater than 0 assert!(fee > 0); - let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) .unwrap(); // `Pays::No` implies no pre-dispatch fees assert_eq!(Assets::balance(asset_id, caller), balance); - let (_tip, _who, initial_payment, _asset_id) = ⪯ + let (_tip, _who, initial_payment) = ⪯ let not_paying = match initial_payment { &InitialPayment::Nothing => true, _ => false, @@ -740,3 +731,139 @@ fn post_dispatch_fee_is_zero_if_unsigned_pre_dispatch_fee_is_zero() { assert_eq!(Assets::balance(asset_id, caller), balance); }); } + +#[test] +fn fee_with_native_asset_passed_with_id() { + let base_weight = 5; + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + let caller = 1; + let caller_balance = 1000; + // native asset + let asset_id = NativeOrWithId::Native; + // assert that native balance is not necessary + assert_eq!(Balances::free_balance(caller), caller_balance); + + let tip = 10; + let weight = 100; + let len = 5; + let initial_fee = base_weight + weight + len as u64 + tip; + + let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id.into())) + .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_100), len) + .unwrap(); + assert_eq!(Balances::free_balance(caller), caller_balance - initial_fee); + + let final_weight = 50; + let expected_fee = initial_fee - final_weight; + + assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch( + Some(pre), + &info_from_weight(WEIGHT_100), + &post_info_from_weight(WEIGHT_50), + len, + &Ok(()) + )); + + assert_eq!(Balances::free_balance(caller), caller_balance - expected_fee); + + assert_eq!(TipUnbalancedAmount::get(), tip); + assert_eq!(FeeUnbalancedAmount::get(), expected_fee - tip); + }); +} + +#[test] +fn transfer_add_and_remove_account() { + let base_weight = 5; + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(Weight::from_parts(base_weight, 0)) + .build() + .execute_with(|| { + System::set_block_number(1); + + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + asset_id.into(), + 42, /* owner */ + true, /* is_sufficient */ + min_balance, + )); + + setup_lp(asset_id, balance_factor); + + // mint into the caller account + let caller = 222; + let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller); + let balance = 10000; + + assert_eq!(Balances::free_balance(caller), 0); + assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + + let weight = 100; + let tip = 5; + let len = 10; + let fee_in_native = base_weight + weight + len as u64 + tip; + let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::WithId(asset_id), + NativeOrWithId::Native, + fee_in_native, + true, + ); + assert!(!input_quote.unwrap().is_zero()); + + let fee_in_asset = input_quote.unwrap(); + let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id.into())) + .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_100), len) + .unwrap(); + + assert_eq!(Assets::balance(asset_id, &caller), balance - fee_in_asset); + + // remove caller account. + assert_ok!(Assets::burn_from( + asset_id, + &caller, + Assets::balance(asset_id, &caller), + Preservation::Expendable, + Precision::Exact, + Fortitude::Force + )); + + let final_weight = 50; + let final_fee_in_native = fee_in_native - final_weight - tip; + let token_refund = AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(asset_id), + fee_in_native - final_fee_in_native - tip, + true, + ) + .unwrap(); + + // make sure the refund amount is enough to create the account. + assert!(token_refund >= min_balance); + + assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch( + Some(pre), + &info_from_weight(WEIGHT_100), + &post_info_from_weight(WEIGHT_50), + len, + &Ok(()) + )); + + // fee paid with no refund. + assert_eq!(TipUnbalancedAmount::get(), tip); + assert_eq!(FeeUnbalancedAmount::get(), fee_in_native - tip); + + // caller account removed. + assert_eq!(Assets::balance(asset_id, caller), 0); + }); +} diff --git a/substrate/frame/transaction-payment/src/lib.rs b/substrate/frame/transaction-payment/src/lib.rs index 7df658a4732e65dfaf2a89dfba7ac08cb75c68a6..c17ab393b5d3b2a05cc3a52745a83183e164205f 100644 --- a/substrate/frame/transaction-payment/src/lib.rs +++ b/substrate/frame/transaction-payment/src/lib.rs @@ -667,6 +667,11 @@ impl<T: Config> Pallet<T> { let capped_weight = weight.min(T::BlockWeights::get().max_block); T::WeightToFee::weight_to_fee(&capped_weight) } + + /// Deposit the [`Event::TransactionFeePaid`] event. + pub fn deposit_fee_paid_event(who: T::AccountId, actual_fee: BalanceOf<T>, tip: BalanceOf<T>) { + Self::deposit_event(Event::TransactionFeePaid { who, actual_fee, tip }); + } } impl<T> Convert<Weight, BalanceOf<T>> for Pallet<T>