diff --git a/Cargo.lock b/Cargo.lock index 0dda623c14ab..15f4a2218e80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11427,6 +11427,7 @@ version = "28.0.0" dependencies = [ "frame-support", "frame-system", + "pallet-assets", "pallet-balances", "parity-scale-codec", "scale-info", diff --git a/substrate/frame/transaction-payment/Cargo.toml b/substrate/frame/transaction-payment/Cargo.toml index 24e5a714f0fe..2f4e8a41e4d5 100644 --- a/substrate/frame/transaction-payment/Cargo.toml +++ b/substrate/frame/transaction-payment/Cargo.toml @@ -31,6 +31,7 @@ sp-std = { path = "../../primitives/std", default-features = false } [dev-dependencies] serde_json = { workspace = true, default-features = true } pallet-balances = { path = "../balances" } +pallet-assets = { path = "../assets" } [features] default = ["std"] @@ -39,6 +40,7 @@ std = [ "frame-support/std", "frame-system/std", "pallet-balances/std", + "pallet-assets/std", "scale-info/std", "serde", "sp-core/std", 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 9a2b22b81709..6ec3b8301878 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,12 +24,12 @@ use frame_support::{ pallet_prelude::*, parameter_types, traits::{ - fungible, + fungibles, tokens::{ fungible::{NativeFromLeft, NativeOrWithId, UnionOf}, imbalance::ResolveAssetTo, }, - AsEnsureOriginWithArg, ConstU32, ConstU64, ConstU8, Imbalance, OnUnbalanced, + AsEnsureOriginWithArg, ConstU32, ConstU64, ConstU8, OnUnbalanced, }, weights::{Weight, WeightToFee as WeightToFeeT}, PalletId, @@ -37,7 +37,6 @@ use frame_support::{ use frame_system as system; use frame_system::{EnsureRoot, EnsureSignedBy}; use pallet_asset_conversion::{Ascending, Chain, WithFirstAsset}; -use pallet_transaction_payment::FungibleAdapter; use sp_core::H256; use sp_runtime::{ traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, SaturatedConversion}, @@ -151,24 +150,14 @@ impl WeightToFeeT for TransactionByteFee { } parameter_types! { - pub(crate) static TipUnbalancedAmount: u64 = 0; pub(crate) static FeeUnbalancedAmount: u64 = 0; } -pub struct DealWithFees; -impl OnUnbalanced::AccountId, Balances>> - for DealWithFees -{ - fn on_unbalanceds( - mut fees_then_tips: impl Iterator< - Item = fungible::Credit<::AccountId, Balances>, - >, - ) { - 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()); - } +pub struct DealWithFungiblesFees; +impl OnUnbalanced> for DealWithFungiblesFees { + fn on_nonzero_unbalanced(credit: fungibles::Credit) { + if credit.asset() == Native::get() { + FeeUnbalancedAmount::mutate(|a| *a += credit.peek()); } } } @@ -176,7 +165,12 @@ impl OnUnbalanced::AccountId, #[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig)] impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter; + // type OnChargeTransaction = FungibleAdapter; + type OnChargeTransaction = pallet_transaction_payment::FungiblesAdapter< + NativeAndAssets, + Native, + DealWithFungiblesFees, + >; type WeightToFee = WeightToFee; type LengthToFee = TransactionByteFee; type FeeMultiplierUpdate = (); @@ -249,12 +243,14 @@ pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter< (NativeOrWithId, NativeOrWithId), >; +type NativeAndAssets = UnionOf, AccountId>; + impl pallet_asset_conversion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type HigherPrecisionBalance = u128; type AssetKind = NativeOrWithId; - type Assets = UnionOf, AccountId>; + type Assets = NativeAndAssets; type PoolId = (Self::AssetKind, Self::AssetKind); type PoolLocator = Chain< WithFirstAsset, PoolIdToAccountId>, @@ -278,6 +274,7 @@ impl pallet_asset_conversion::Config for Runtime { impl Config for Runtime { type RuntimeEvent = RuntimeEvent; - type Fungibles = Assets; - type OnChargeAssetTransaction = AssetConversionAdapter; + type Fungibles = NativeAndAssets; + // type OnChargeAssetTransaction = AssetConversionAdapter; + type OnChargeAssetTransaction = SwapCreditAdapter; } 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 f2f2c57bb376..deecd8bb65ad 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 @@ -15,14 +15,19 @@ ///! Traits and default implementation for paying transaction fees in assets. use super::*; -use crate::Config; use frame_support::{ ensure, - traits::{fungible::Inspect, tokens::Balance}, + traits::{ + fungible::Inspect, + fungibles, + fungibles::Inspect as FungiblesInspect, + tokens::{Balance, Fortitude, Precision, Preservation}, + Defensive, SameOrOther, + }, unsigned::TransactionValidityError, }; -use pallet_asset_conversion::Swap; +use pallet_asset_conversion::{Pallet as AssetConversion, Swap, SwapCredit}; use sp_runtime::{ traits::{DispatchInfoOf, Get, PostDispatchInfoOf, Zero}, transaction_validity::InvalidTransaction, @@ -196,3 +201,172 @@ where Ok(actual_paid) } } + +use super::pallet as pallet_asset_conversion_tx_payment; + +/// Implements [`OnChargeAssetTransaction`] for [`pallet_asset_conversion_tx_payment`], where +/// the asset class used to pay the fee is defined with the `A` type parameter (eg. DOT +/// location) and accessed via the type implementing the [`frame_support::traits::fungibles`] +/// trait. +pub struct SwapCreditAdapter(PhantomData<(A, S)>); +impl OnChargeAssetTransaction for SwapCreditAdapter +where + A: Get, + S: SwapCredit< + T::AccountId, + Balance = T::Balance, + AssetKind = T::AssetKind, + Credit = fungibles::Credit, + >, + + T: pallet_asset_conversion_tx_payment::Config, + T::Fungibles: fungibles::Inspect, + T::OnChargeTransaction: + OnChargeTransaction>, +{ + type AssetId = T::AssetKind; + type Balance = T::Balance; + type LiquidityInfo = T::Balance; + + fn withdraw_fee( + who: &::AccountId, + _call: &::RuntimeCall, + _dispatch_info: &DispatchInfoOf<::RuntimeCall>, + asset_id: Self::AssetId, + fee: Self::Balance, + _tip: Self::Balance, + ) -> Result<(LiquidityInfoOf, Self::LiquidityInfo, T::Balance), TransactionValidityError> { + let asset_fee = AssetConversion::::quote_price_tokens_for_exact_tokens( + asset_id.clone(), + A::get(), + fee, + true, + ) + .ok_or(InvalidTransaction::Payment)?; + + let asset_fee_credit = T::Assets::withdraw( + asset_id.clone(), + who, + asset_fee, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) + .map_err(|_| TransactionValidityError::from(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, _)) => { + // The swap should not error since the price quote was successful. + let _ = T::Assets::resolve(who, credit_in).defensive(); + return Err(InvalidTransaction::Payment.into()) + }, + }; + + // Should be always zero since the exact price was quoted before. + ensure!(change.peek().is_zero(), InvalidTransaction::Payment); + + Ok((Some(fee_credit), fee, asset_fee)) + } + fn correct_and_deposit_fee( + who: &::AccountId, + dispatch_info: &DispatchInfoOf<::RuntimeCall>, + post_info: &PostDispatchInfoOf<::RuntimeCall>, + corrected_fee: Self::Balance, + tip: Self::Balance, + fee_paid: LiquidityInfoOf, + _received_exchanged: Self::LiquidityInfo, + asset_id: Self::AssetId, + initial_asset_consumed: T::Balance, + ) -> Result { + let Some(fee_paid) = fee_paid else { + return Ok(Zero::zero()); + }; + // Try to refund if the fee paid is more than the corrected fee and the account was not + // removed by the dispatched function. + let (fee, fee_in_asset) = if fee_paid.peek() > corrected_fee && + !T::Assets::total_balance(asset_id.clone(), who).is_zero() + { + let refund_amount = fee_paid.peek().saturating_sub(corrected_fee); + // Check if the refund amount can be swapped back into the asset used by `who` for + // fee payment. + let refund_asset_amount = AssetConversion::::quote_price_exact_tokens_for_tokens( + A::get(), + asset_id.clone(), + refund_amount, + true, + ) + // No refund given if it cannot be swapped back. + .unwrap_or(Zero::zero()); + + // Deposit the refund before the swap to ensure it can be processed. + let debt = match T::Assets::deposit( + asset_id.clone(), + who, + refund_asset_amount, + Precision::BestEffort, + ) { + Ok(debt) => debt, + // No refund given since it cannot be deposited. + Err(_) => fungibles::Debt::::zero(asset_id.clone()), + }; + + if debt.peek().is_zero() { + // No refund given. + (fee_paid, initial_asset_consumed) + } else { + let (refund, fee_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. + _ => return Err(InvalidTransaction::Payment.into()), + }; + (fee_paid, initial_asset_consumed.saturating_sub(refund_asset_amount)) + }, + // The error should not occur since swap was quoted before. + Err((refund, _)) => { + match T::Assets::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(_) => return Err(InvalidTransaction::Payment.into()), + }; + let fee_paid = fee_paid.merge(refund).map_err(|_| { + // The error should never occur since `fee_paid` and `refund` are + // credits of the same asset. + TransactionValidityError::from(InvalidTransaction::Payment) + })?; + (fee_paid, initial_asset_consumed) + }, + } + } + } else { + (fee_paid, initial_asset_consumed) + }; + + // Refund is already processed. + let corrected_fee = fee.peek(); + // Deposit fee. + T::OnChargeTransaction::correct_and_deposit_fee( + who, + dispatch_info, + post_info, + corrected_fee, + tip, + Some(fee), + ) + .map(|_| 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 aa2f26f3a6a8..7db84e9a7d6f 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 @@ -239,7 +239,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::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); // assert that native balance is not used @@ -263,7 +263,6 @@ fn transaction_payment_in_asset_possible() { )); assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); - assert_eq!(TipUnbalancedAmount::get(), 0); assert_eq!(FeeUnbalancedAmount::get(), fee_in_native); }); } @@ -297,7 +296,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::::from(0, Some(asset_id)).pre_dispatch( + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id.into())).pre_dispatch( &caller, CALL, &info_from_weight(WEIGHT_5), @@ -352,7 +351,7 @@ fn transaction_payment_without_fee() { assert_eq!(input_quote, Some(201)); let fee_in_asset = input_quote.unwrap(); - let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); @@ -429,7 +428,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::::from(tip, Some(asset_id)) + let pre = ChargeAssetTxPayment::::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); @@ -458,8 +457,7 @@ fn asset_transaction_payment_with_tip_and_refund() { &Ok(()) )); - assert_eq!(TipUnbalancedAmount::get(), tip); - assert_eq!(FeeUnbalancedAmount::get(), expected_fee); + assert_eq!(FeeUnbalancedAmount::get(), expected_fee + tip); // caller should get refunded assert_eq!( @@ -512,30 +510,23 @@ 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::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len) .unwrap(); - assert_eq!(Balances::free_balance(caller), ed); + assert_eq!(Balances::free_balance(caller), 0); // 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(); + let refund = 0; assert_ok!(ChargeAssetTxPayment::::post_dispatch( Some(pre), @@ -547,7 +538,6 @@ fn payment_from_account_with_only_assets() { assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund); assert_eq!(Balances::free_balance(caller), 0); - assert_eq!(TipUnbalancedAmount::get(), 0); assert_eq!(FeeUnbalancedAmount::get(), fee_in_native); }); } @@ -587,7 +577,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::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::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 +603,7 @@ fn converted_fee_is_never_zero_if_input_fee_is_not() { ) .unwrap(); - let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::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,7 +653,7 @@ fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() { // calculated fee is greater than 0 assert!(fee > 0); - let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id.into())) .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) .unwrap(); // `Pays::No` implies no pre-dispatch fees diff --git a/substrate/frame/transaction-payment/src/mock.rs b/substrate/frame/transaction-payment/src/mock.rs index c1bb05ab5c7e..ce5ca907178e 100644 --- a/substrate/frame/transaction-payment/src/mock.rs +++ b/substrate/frame/transaction-payment/src/mock.rs @@ -25,11 +25,15 @@ use frame_support::{ derive_impl, dispatch::DispatchClass, parameter_types, - traits::{fungible, ConstU32, ConstU64, Imbalance, OnUnbalanced}, + traits::{ + fungible::{self, NativeFromLeft, NativeOrWithId}, + fungibles, AsEnsureOriginWithArg, ConstU32, ConstU64, OnUnbalanced, + }, weights::{Weight, WeightToFee as WeightToFeeT}, }; use frame_system as system; use pallet_balances::Call as BalancesCall; +use system::EnsureRoot; type Block = frame_system::mocking::MockBlock; @@ -38,6 +42,7 @@ frame_support::construct_runtime!( { System: system, Balances: pallet_balances, + Assets: pallet_assets, TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, } ); @@ -136,29 +141,56 @@ parameter_types! { pub(crate) static FeeUnbalancedAmount: u64 = 0; } -pub struct DealWithFees; -impl OnUnbalanced::AccountId, Balances>> - for DealWithFees +pub struct DealWithFungiblesFees; +impl OnUnbalanced::AccountId, NativeAndAssets>> + for DealWithFungiblesFees { - fn on_unbalanceds( - mut fees_then_tips: impl Iterator< - Item = fungible::Credit<::AccountId, Balances>, - >, + fn on_nonzero_unbalanced( + credit: fungibles::Credit<::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()); - } + if credit.asset() == Native::get() { + FeeUnbalancedAmount::mutate(|a| *a += credit.peek()); } } } +type NativeAndAssets = + fungible::UnionOf, u64>; + +parameter_types! { + pub Native: NativeOrWithId:: = NativeOrWithId::::Native; +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter; + // type OnChargeTransaction = FungibleAdapter; + type OnChargeTransaction = FungiblesAdapter; type OperationalFeeMultiplier = OperationalFeeMultiplier; type WeightToFee = WeightToFee; type LengthToFee = TransactionByteFee; type FeeMultiplierUpdate = (); } + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type AssetIdParameter = codec::Compact; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = ConstU64<2>; + type AssetAccountDeposit = ConstU64<2>; + type MetadataDepositBase = ConstU64<0>; + type MetadataDepositPerByte = ConstU64<0>; + type ApprovalDeposit = ConstU64<0>; + type StringLimit = ConstU32<20>; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} diff --git a/substrate/frame/transaction-payment/src/payment.rs b/substrate/frame/transaction-payment/src/payment.rs index 0fe616782903..e7c68619e329 100644 --- a/substrate/frame/transaction-payment/src/payment.rs +++ b/substrate/frame/transaction-payment/src/payment.rs @@ -19,16 +19,20 @@ use crate::Config; use core::marker::PhantomData; +use sp_core::Get; use sp_runtime::{ traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, Zero}, transaction_validity::InvalidTransaction, }; use frame_support::{ + ensure, traits::{ fungible::{Balanced, Credit, Debt, Inspect}, - tokens::Precision, - Currency, ExistenceRequirement, Imbalance, OnUnbalanced, WithdrawReasons, + fungibles, + tokens::{Fortitude, Precision, Preservation}, + Currency, Defensive, ExistenceRequirement, Imbalance, OnUnbalanced, SameOrOther, + WithdrawReasons, }, unsigned::TransactionValidityError, }; @@ -143,6 +147,90 @@ where } } +use super::pallet as pallet_transaction_payment; + +/// Implements [`OnChargeTransaction`] for [`pallet_transaction_payment`], where the asset class +/// used to pay the fee is defined with the `A` type parameter (eg. KSM location) and accessed +/// via the type implementing the [`frame_support::traits::fungibles`] trait. +pub struct FungiblesAdapter(PhantomData<(F, A, OU)>); + +impl OnChargeTransaction for FungiblesAdapter +where + T: pallet_transaction_payment::Config, + F: fungibles::Balanced, + A: Get, + OU: OnUnbalanced>, +{ + type LiquidityInfo = Option>; + type Balance = F::Balance; + + fn withdraw_fee( + who: &::AccountId, + _call: &::RuntimeCall, + _dispatch_info: &DispatchInfoOf<::RuntimeCall>, + fee: Self::Balance, + _tip: Self::Balance, + ) -> Result { + if fee.is_zero() { + return Ok(None) + } + + match F::withdraw( + A::get(), + who, + fee, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) { + Ok(imbalance) => Ok(Some(imbalance)), + Err(_) => Err(InvalidTransaction::Payment.into()), + } + } + + fn correct_and_deposit_fee( + who: &::AccountId, + _dispatch_info: &DispatchInfoOf<::RuntimeCall>, + _post_info: &PostDispatchInfoOf<::RuntimeCall>, + corrected_fee: Self::Balance, + _tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + let Some(paid) = already_withdrawn else { + return Ok(()); + }; + // Make sure the credit is in desired asset id. + ensure!(paid.asset() == A::get(), InvalidTransaction::Payment); + // Calculate how much refund we should return. + let refund_amount = paid.peek().saturating_sub(corrected_fee); + // Refund to the the account that paid the fees if it was not removed by the + // dispatched function. If fails for any reason (eg. ED requirement is not met) no + // refund given. + let refund_debt = if F::total_balance(A::get(), who).is_zero() || refund_amount.is_zero() { + fungibles::Debt::::zero(A::get()) + } else { + F::deposit(A::get(), who, refund_amount, Precision::BestEffort) + .unwrap_or_else(|_| fungibles::Debt::::zero(A::get())) + }; + // Merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid: fungibles::Credit = + match paid.offset(refund_debt).defensive_proof("credits should be identical") { + Ok(SameOrOther::Same(credit)) => credit, + // Paid amount is fully refunded. + Ok(SameOrOther::None) => fungibles::Credit::::zero(A::get()), + // Should never fail as at this point the asset id is always valid and the + // refund amount is not greater than paid amount. + _ => return Err(InvalidTransaction::Payment.into()), + }; + // No separation for simplicity. + // In our case the fees and the tips are deposited to the same pot. + // We cannot call [`OnUnbalanced::on_unbalanceds`] since fungibles credit does not + // implement `Imbalanced` trait. + OU::on_unbalanced(adjusted_paid); + Ok(()) + } +} + /// Implements the transaction payment for a pallet implementing the [`Currency`] /// trait (eg. the pallet_balances) using an unbalance handler (implementing /// [`OnUnbalanced`]). diff --git a/substrate/frame/transaction-payment/src/tests.rs b/substrate/frame/transaction-payment/src/tests.rs index bc0efd2d64a3..1194bf032082 100644 --- a/substrate/frame/transaction-payment/src/tests.rs +++ b/substrate/frame/transaction-payment/src/tests.rs @@ -150,7 +150,6 @@ fn signed_extension_transaction_payment_work() { )); assert_eq!(Balances::free_balance(1), 100 - 5 - 5 - 10); assert_eq!(FeeUnbalancedAmount::get(), 5 + 5 + 10); - assert_eq!(TipUnbalancedAmount::get(), 0); FeeUnbalancedAmount::mutate(|a| *a = 0); @@ -167,8 +166,7 @@ fn signed_extension_transaction_payment_work() { &Ok(()) )); assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 50 - 5); - assert_eq!(FeeUnbalancedAmount::get(), 5 + 10 + 50); - assert_eq!(TipUnbalancedAmount::get(), 5); + assert_eq!(FeeUnbalancedAmount::get(), 5 + 10 + 50 + 5); }); }