Skip to content

Commit 41ad0b8

Browse files
weichweichatheikianenigma
authored
decouple transaction payment and currency (#6912)
* wip: setup types * fix types * make tx payment pallet independent from balances * fix dependent tests * comments * restructure a bit and include more info * clean up ugly phantom * reduce complexity * minor doc improvements * use shorthand * doc * fix line lenght and style * readd BalanceOf * some clarifications and readability improvements * move balance type to OnChargeTransaction * remove noise * fix style * Apply suggestions from code review improved documentation Co-authored-by: Alexander Theißen <[email protected]> * Improve naming and documentation Apply suggestions from code review Co-authored-by: Alexander Theißen <[email protected]> * Apply suggestions from code review Co-authored-by: Kian Paimani <[email protected]> * always call withdraw_fee * move NegativeImbalanceOf to payment module * fix unused import Co-authored-by: Alexander Theißen <[email protected]> Co-authored-by: Kian Paimani <[email protected]>
1 parent bc18b2a commit 41ad0b8

File tree

2 files changed

+175
-67
lines changed

2 files changed

+175
-67
lines changed

src/lib.rs

Lines changed: 48 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
//! - A means of updating the fee for the next block, via defining a multiplier, based on the
3030
//! final state of the chain at the end of the previous block. This can be configured via
3131
//! [`Trait::FeeMultiplierUpdate`]
32+
//! - How the fees are paid via [`Trait::OnChargeTransaction`].
3233
3334
#![cfg_attr(not(feature = "std"), no_std)]
3435

3536
use sp_std::prelude::*;
3637
use codec::{Encode, Decode};
3738
use frame_support::{
3839
decl_storage, decl_module,
39-
traits::{Currency, Get, OnUnbalanced, ExistenceRequirement, WithdrawReasons, Imbalance},
40+
traits::Get,
4041
weights::{
4142
Weight, DispatchInfo, PostDispatchInfo, GetDispatchInfo, Pays, WeightToFeePolynomial,
4243
WeightToFeeCoefficient,
@@ -46,23 +47,23 @@ use frame_support::{
4647
use sp_runtime::{
4748
FixedU128, FixedPointNumber, FixedPointOperand, Perquintill, RuntimeDebug,
4849
transaction_validity::{
49-
TransactionPriority, ValidTransaction, InvalidTransaction, TransactionValidityError,
50-
TransactionValidity,
50+
TransactionPriority, ValidTransaction, TransactionValidityError, TransactionValidity,
5151
},
5252
traits::{
53-
Zero, Saturating, SignedExtension, SaturatedConversion, Convert, Dispatchable,
53+
Saturating, SignedExtension, SaturatedConversion, Convert, Dispatchable,
5454
DispatchInfoOf, PostDispatchInfoOf,
5555
},
5656
};
5757
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
5858

59+
mod payment;
60+
pub use payment::*;
61+
5962
/// Fee multiplier.
6063
pub type Multiplier = FixedU128;
6164

6265
type BalanceOf<T> =
63-
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
64-
type NegativeImbalanceOf<T> =
65-
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::NegativeImbalance;
66+
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::Balance;
6667

6768
/// A struct to update the weight multiplier per block. It implements `Convert<Multiplier,
6869
/// Multiplier>`, meaning that it can convert the previous multiplier to the next one. This should
@@ -213,13 +214,13 @@ impl Default for Releases {
213214
}
214215

215216
pub trait Trait: frame_system::Trait {
216-
/// The currency type in which fees will be paid.
217-
type Currency: Currency<Self::AccountId> + Send + Sync;
218-
219-
/// Handler for the unbalanced reduction when taking transaction fees. This is either one or
220-
/// two separate imbalances, the first is the transaction fee paid, the second is the tip paid,
221-
/// if any.
222-
type OnTransactionPayment: OnUnbalanced<NegativeImbalanceOf<Self>>;
217+
/// Handler for withdrawing, refunding and depositing the transaction fee.
218+
/// Transaction fees are withdrawn before the transaction is executed.
219+
/// After the transaction was executed the transaction weight can be
220+
/// adjusted, depending on the used resources by the transaction. If the
221+
/// transaction weight is lower than expected, parts of the transaction fee
222+
/// might be refunded. In the end the fees can be deposited.
223+
type OnChargeTransaction: OnChargeTransaction<Self>;
223224

224225
/// The fee to be paid for making a transaction; the per-byte portion.
225226
type TransactionByteFee: Get<BalanceOf<Self>>;
@@ -442,30 +443,21 @@ impl<T: Trait + Send + Sync> ChargeTransactionPayment<T> where
442443
fn withdraw_fee(
443444
&self,
444445
who: &T::AccountId,
446+
call: &T::Call,
445447
info: &DispatchInfoOf<T::Call>,
446448
len: usize,
447-
) -> Result<(BalanceOf<T>, Option<NegativeImbalanceOf<T>>), TransactionValidityError> {
449+
) -> Result<
450+
(
451+
BalanceOf<T>,
452+
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::LiquidityInfo,
453+
),
454+
TransactionValidityError,
455+
> {
448456
let tip = self.0;
449457
let fee = Module::<T>::compute_fee(len as u32, info, tip);
450458

451-
// Only mess with balances if fee is not zero.
452-
if fee.is_zero() {
453-
return Ok((fee, None));
454-
}
455-
456-
match T::Currency::withdraw(
457-
who,
458-
fee,
459-
if tip.is_zero() {
460-
WithdrawReasons::TRANSACTION_PAYMENT
461-
} else {
462-
WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
463-
},
464-
ExistenceRequirement::KeepAlive,
465-
) {
466-
Ok(imbalance) => Ok((fee, Some(imbalance))),
467-
Err(_) => Err(InvalidTransaction::Payment.into()),
468-
}
459+
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::withdraw_fee(who, call, info, fee, tip)
460+
.map(|i| (fee, i))
469461
}
470462

471463
/// Get an appropriate priority for a transaction with the given length and info.
@@ -505,17 +497,24 @@ impl<T: Trait + Send + Sync> SignedExtension for ChargeTransactionPayment<T> whe
505497
type AccountId = T::AccountId;
506498
type Call = T::Call;
507499
type AdditionalSigned = ();
508-
type Pre = (BalanceOf<T>, Self::AccountId, Option<NegativeImbalanceOf<T>>, BalanceOf<T>);
500+
type Pre = (
501+
// tip
502+
BalanceOf<T>,
503+
// who paid the fee
504+
Self::AccountId,
505+
// imbalance resulting from withdrawing the fee
506+
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::LiquidityInfo,
507+
);
509508
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { Ok(()) }
510509

511510
fn validate(
512511
&self,
513512
who: &Self::AccountId,
514-
_call: &Self::Call,
513+
call: &Self::Call,
515514
info: &DispatchInfoOf<Self::Call>,
516515
len: usize,
517516
) -> TransactionValidity {
518-
let (fee, _) = self.withdraw_fee(who, info, len)?;
517+
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
519518
Ok(ValidTransaction {
520519
priority: Self::get_priority(len, info, fee),
521520
..Default::default()
@@ -525,12 +524,12 @@ impl<T: Trait + Send + Sync> SignedExtension for ChargeTransactionPayment<T> whe
525524
fn pre_dispatch(
526525
self,
527526
who: &Self::AccountId,
528-
_call: &Self::Call,
527+
call: &Self::Call,
529528
info: &DispatchInfoOf<Self::Call>,
530529
len: usize
531530
) -> Result<Self::Pre, TransactionValidityError> {
532-
let (fee, imbalance) = self.withdraw_fee(who, info, len)?;
533-
Ok((self.0, who.clone(), imbalance, fee))
531+
let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?;
532+
Ok((self.0, who.clone(), imbalance))
534533
}
535534

536535
fn post_dispatch(
@@ -540,32 +539,14 @@ impl<T: Trait + Send + Sync> SignedExtension for ChargeTransactionPayment<T> whe
540539
len: usize,
541540
_result: &DispatchResult,
542541
) -> Result<(), TransactionValidityError> {
543-
let (tip, who, imbalance, fee) = pre;
544-
if let Some(payed) = imbalance {
545-
let actual_fee = Module::<T>::compute_actual_fee(
546-
len as u32,
547-
info,
548-
post_info,
549-
tip,
550-
);
551-
let refund = fee.saturating_sub(actual_fee);
552-
let actual_payment = match T::Currency::deposit_into_existing(&who, refund) {
553-
Ok(refund_imbalance) => {
554-
// The refund cannot be larger than the up front payed max weight.
555-
// `PostDispatchInfo::calc_unspent` guards against such a case.
556-
match payed.offset(refund_imbalance) {
557-
Ok(actual_payment) => actual_payment,
558-
Err(_) => return Err(InvalidTransaction::Payment.into()),
559-
}
560-
}
561-
// We do not recreate the account using the refund. The up front payment
562-
// is gone in that case.
563-
Err(_) => payed,
564-
};
565-
let imbalances = actual_payment.split(tip);
566-
T::OnTransactionPayment::on_unbalanceds(Some(imbalances.0).into_iter()
567-
.chain(Some(imbalances.1)));
568-
}
542+
let (tip, who, imbalance) = pre;
543+
let actual_fee = Module::<T>::compute_actual_fee(
544+
len as u32,
545+
info,
546+
post_info,
547+
tip,
548+
);
549+
T::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?;
569550
Ok(())
570551
}
571552
}
@@ -580,6 +561,7 @@ mod tests {
580561
DispatchClass, DispatchInfo, PostDispatchInfo, GetDispatchInfo, Weight,
581562
WeightToFeePolynomial, WeightToFeeCoefficients, WeightToFeeCoefficient,
582563
},
564+
traits::Currency,
583565
};
584566
use pallet_balances::Call as BalancesCall;
585567
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
@@ -699,8 +681,7 @@ mod tests {
699681
}
700682

701683
impl Trait for Runtime {
702-
type Currency = pallet_balances::Module<Runtime>;
703-
type OnTransactionPayment = ();
684+
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
704685
type TransactionByteFee = TransactionByteFee;
705686
type WeightToFee = WeightToFee;
706687
type FeeMultiplierUpdate = ();

src/payment.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
///! Traits and default implementation for paying transaction fees.
2+
use crate::Trait;
3+
use codec::FullCodec;
4+
use frame_support::{
5+
traits::{Currency, ExistenceRequirement, Get, Imbalance, OnUnbalanced, WithdrawReasons},
6+
unsigned::TransactionValidityError,
7+
};
8+
use sp_runtime::{
9+
traits::{AtLeast32BitUnsigned, DispatchInfoOf, MaybeSerializeDeserialize, PostDispatchInfoOf, Saturating, Zero},
10+
transaction_validity::InvalidTransaction,
11+
};
12+
use sp_std::{fmt::Debug, marker::PhantomData};
13+
14+
type NegativeImbalanceOf<C, T> =
15+
<C as Currency<<T as frame_system::Trait>::AccountId>>::NegativeImbalance;
16+
17+
/// Handle withdrawing, refunding and depositing of transaction fees.
18+
pub trait OnChargeTransaction<T: Trait> {
19+
/// The underlying integer type in which fees are calculated.
20+
type Balance: AtLeast32BitUnsigned + FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default;
21+
type LiquidityInfo: Default;
22+
23+
/// Before the transaction is executed the payment of the transaction fees
24+
/// need to be secured.
25+
///
26+
/// Note: The `fee` already includes the `tip`.
27+
fn withdraw_fee(
28+
who: &T::AccountId,
29+
call: &T::Call,
30+
dispatch_info: &DispatchInfoOf<T::Call>,
31+
fee: Self::Balance,
32+
tip: Self::Balance,
33+
) -> Result<Self::LiquidityInfo, TransactionValidityError>;
34+
35+
/// After the transaction was executed the actual fee can be calculated.
36+
/// This function should refund any overpaid fees and optionally deposit
37+
/// the corrected amount.
38+
///
39+
/// Note: The `fee` already includes the `tip`.
40+
fn correct_and_deposit_fee(
41+
who: &T::AccountId,
42+
dispatch_info: &DispatchInfoOf<T::Call>,
43+
post_info: &PostDispatchInfoOf<T::Call>,
44+
corrected_fee: Self::Balance,
45+
tip: Self::Balance,
46+
already_withdrawn: Self::LiquidityInfo,
47+
) -> Result<(), TransactionValidityError>;
48+
}
49+
50+
/// Implements the transaction payment for a module implementing the `Currency`
51+
/// trait (eg. the pallet_balances) using an unbalance handler (implementing
52+
/// `OnUnbalanced`).
53+
pub struct CurrencyAdapter<C, OU>(PhantomData<(C, OU)>);
54+
55+
/// Default implementation for a Currency and an OnUnbalanced handler.
56+
impl<T, C, OU> OnChargeTransaction<T> for CurrencyAdapter<C, OU>
57+
where
58+
T: Trait,
59+
T::TransactionByteFee: Get<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance>,
60+
C: Currency<<T as frame_system::Trait>::AccountId>,
61+
C::PositiveImbalance:
62+
Imbalance<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance, Opposite = C::NegativeImbalance>,
63+
C::NegativeImbalance:
64+
Imbalance<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance, Opposite = C::PositiveImbalance>,
65+
OU: OnUnbalanced<NegativeImbalanceOf<C, T>>,
66+
{
67+
type LiquidityInfo = Option<NegativeImbalanceOf<C, T>>;
68+
type Balance = <C as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
69+
70+
/// Withdraw the predicted fee from the transaction origin.
71+
///
72+
/// Note: The `fee` already includes the `tip`.
73+
fn withdraw_fee(
74+
who: &T::AccountId,
75+
_call: &T::Call,
76+
_info: &DispatchInfoOf<T::Call>,
77+
fee: Self::Balance,
78+
tip: Self::Balance,
79+
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
80+
if fee.is_zero() {
81+
return Ok(None);
82+
}
83+
84+
let withdraw_reason = if tip.is_zero() {
85+
WithdrawReasons::TRANSACTION_PAYMENT
86+
} else {
87+
WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
88+
};
89+
90+
match C::withdraw(who, fee, withdraw_reason, ExistenceRequirement::KeepAlive) {
91+
Ok(imbalance) => Ok(Some(imbalance)),
92+
Err(_) => Err(InvalidTransaction::Payment.into()),
93+
}
94+
}
95+
96+
/// Hand the fee and the tip over to the `[OnUnbalanced]` implementation.
97+
/// Since the predicted fee might have been too high, parts of the fee may
98+
/// be refunded.
99+
///
100+
/// Note: The `fee` already includes the `tip`.
101+
fn correct_and_deposit_fee(
102+
who: &T::AccountId,
103+
_dispatch_info: &DispatchInfoOf<T::Call>,
104+
_post_info: &PostDispatchInfoOf<T::Call>,
105+
corrected_fee: Self::Balance,
106+
tip: Self::Balance,
107+
already_withdrawn: Self::LiquidityInfo,
108+
) -> Result<(), TransactionValidityError> {
109+
if let Some(paid) = already_withdrawn {
110+
// Calculate how much refund we should return
111+
let refund_amount = paid.peek().saturating_sub(corrected_fee);
112+
// refund to the the account that paid the fees. If this fails, the
113+
// account might have dropped below the existential balance. In
114+
// that case we don't refund anything.
115+
let refund_imbalance =
116+
C::deposit_into_existing(&who, refund_amount).unwrap_or_else(|_| C::PositiveImbalance::zero());
117+
// merge the imbalance caused by paying the fees and refunding parts of it again.
118+
let adjusted_paid = paid
119+
.offset(refund_imbalance)
120+
.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
121+
// Call someone else to handle the imbalance (fee and tip separately)
122+
let imbalances = adjusted_paid.split(tip);
123+
OU::on_unbalanceds(Some(imbalances.0).into_iter().chain(Some(imbalances.1)));
124+
}
125+
Ok(())
126+
}
127+
}

0 commit comments

Comments
 (0)