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 <alex.theissen@me.com>

* Improve naming and documentation

Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* Apply suggestions from code review

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* always call withdraw_fee

* move NegativeImbalanceOf to payment module

* fix unused import

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Albrecht
2020-10-30 13:27:04 +01:00
committed by GitHub
parent e1b56f8dd3
commit b7205d4ae0
7 changed files with 185 additions and 78 deletions
@@ -37,6 +37,7 @@ pub use frame_support::{
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
},
};
use pallet_transaction_payment::CurrencyAdapter;
/// Import the template pallet.
pub use template;
@@ -249,8 +250,7 @@ parameter_types! {
}
impl pallet_transaction_payment::Trait for Runtime {
type Currency = Balances;
type OnTransactionPayment = ();
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = IdentityFee<Balance>;
type FeeMultiplierUpdate = ();
+2 -3
View File
@@ -64,7 +64,7 @@ use pallet_grandpa::fg_primitives;
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId;
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
pub use pallet_transaction_payment::{Multiplier, TargetedFeeAdjustment};
pub use pallet_transaction_payment::{Multiplier, TargetedFeeAdjustment, CurrencyAdapter};
use pallet_session::{historical as pallet_session_historical};
use sp_inherents::{InherentData, CheckInherentsResult};
use static_assertions::const_assert;
@@ -360,8 +360,7 @@ parameter_types! {
}
impl pallet_transaction_payment::Trait for Runtime {
type Currency = Balances;
type OnTransactionPayment = DealWithFees;
type OnChargeTransaction = CurrencyAdapter<Balances, DealWithFees>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = IdentityFee<Balance>;
type FeeMultiplierUpdate =
@@ -29,6 +29,7 @@ use sp_io;
use frame_support::{impl_outer_origin, impl_outer_event, parameter_types};
use frame_support::traits::Get;
use frame_support::weights::{Weight, DispatchInfo, IdentityFee};
use pallet_transaction_payment::CurrencyAdapter;
use std::cell::RefCell;
use crate::{GenesisConfig, Module, Trait, decl_tests, tests::CallWithDispatchInfo};
@@ -97,8 +98,7 @@ parameter_types! {
pub const TransactionByteFee: u64 = 1;
}
impl pallet_transaction_payment::Trait for Test {
type Currency = Module<Test>;
type OnTransactionPayment = ();
type OnChargeTransaction = CurrencyAdapter<Module<Test>, ()>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = IdentityFee<u64>;
type FeeMultiplierUpdate = ();
+2 -2
View File
@@ -31,6 +31,7 @@ use frame_support::traits::{Get, StorageMapShim};
use frame_support::weights::{Weight, DispatchInfo, IdentityFee};
use std::cell::RefCell;
use crate::{GenesisConfig, Module, Trait, decl_tests, tests::CallWithDispatchInfo};
use pallet_transaction_payment::CurrencyAdapter;
use frame_system as system;
impl_outer_origin!{
@@ -97,8 +98,7 @@ parameter_types! {
pub const TransactionByteFee: u64 = 1;
}
impl pallet_transaction_payment::Trait for Test {
type Currency = Module<Test>;
type OnTransactionPayment = ();
type OnChargeTransaction = CurrencyAdapter<Module<Test>, ()>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = IdentityFee<u64>;
type FeeMultiplierUpdate = ();
+2 -2
View File
@@ -491,6 +491,7 @@ mod tests {
traits::{Currency, LockIdentifier, LockableCurrency, WithdrawReasons},
};
use frame_system::{Call as SystemCall, ChainContext, LastRuntimeUpgradeInfo};
use pallet_transaction_payment::CurrencyAdapter;
use pallet_balances::Call as BalancesCall;
use hex_literal::hex;
const TEST_KEY: &[u8] = &*b":test:key:";
@@ -632,8 +633,7 @@ mod tests {
pub const TransactionByteFee: Balance = 0;
}
impl pallet_transaction_payment::Trait for Runtime {
type Currency = Balances;
type OnTransactionPayment = ();
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = IdentityFee<Balance>;
type FeeMultiplierUpdate = ();
+48 -67
View File
@@ -29,6 +29,7 @@
//! - A means of updating the fee for the next block, via defining a multiplier, based on the
//! final state of the chain at the end of the previous block. This can be configured via
//! [`Trait::FeeMultiplierUpdate`]
//! - How the fees are paid via [`Trait::OnChargeTransaction`].
#![cfg_attr(not(feature = "std"), no_std)]
@@ -36,7 +37,7 @@ use sp_std::prelude::*;
use codec::{Encode, Decode};
use frame_support::{
decl_storage, decl_module,
traits::{Currency, Get, OnUnbalanced, ExistenceRequirement, WithdrawReasons, Imbalance},
traits::Get,
weights::{
Weight, DispatchInfo, PostDispatchInfo, GetDispatchInfo, Pays, WeightToFeePolynomial,
WeightToFeeCoefficient,
@@ -46,23 +47,23 @@ use frame_support::{
use sp_runtime::{
FixedU128, FixedPointNumber, FixedPointOperand, Perquintill, RuntimeDebug,
transaction_validity::{
TransactionPriority, ValidTransaction, InvalidTransaction, TransactionValidityError,
TransactionValidity,
TransactionPriority, ValidTransaction, TransactionValidityError, TransactionValidity,
},
traits::{
Zero, Saturating, SignedExtension, SaturatedConversion, Convert, Dispatchable,
Saturating, SignedExtension, SaturatedConversion, Convert, Dispatchable,
DispatchInfoOf, PostDispatchInfoOf,
},
};
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
mod payment;
pub use payment::*;
/// Fee multiplier.
pub type Multiplier = FixedU128;
type BalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
type NegativeImbalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::NegativeImbalance;
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::Balance;
/// A struct to update the weight multiplier per block. It implements `Convert<Multiplier,
/// Multiplier>`, meaning that it can convert the previous multiplier to the next one. This should
@@ -213,13 +214,13 @@ impl Default for Releases {
}
pub trait Trait: frame_system::Trait {
/// The currency type in which fees will be paid.
type Currency: Currency<Self::AccountId> + Send + Sync;
/// Handler for the unbalanced reduction when taking transaction fees. This is either one or
/// two separate imbalances, the first is the transaction fee paid, the second is the tip paid,
/// if any.
type OnTransactionPayment: OnUnbalanced<NegativeImbalanceOf<Self>>;
/// Handler for withdrawing, refunding and depositing the transaction fee.
/// Transaction fees are withdrawn before the transaction is executed.
/// After the transaction was executed the transaction weight can be
/// adjusted, depending on the used resources by the transaction. If the
/// transaction weight is lower than expected, parts of the transaction fee
/// might be refunded. In the end the fees can be deposited.
type OnChargeTransaction: OnChargeTransaction<Self>;
/// The fee to be paid for making a transaction; the per-byte portion.
type TransactionByteFee: Get<BalanceOf<Self>>;
@@ -442,30 +443,21 @@ impl<T: Trait + Send + Sync> ChargeTransactionPayment<T> where
fn withdraw_fee(
&self,
who: &T::AccountId,
call: &T::Call,
info: &DispatchInfoOf<T::Call>,
len: usize,
) -> Result<(BalanceOf<T>, Option<NegativeImbalanceOf<T>>), TransactionValidityError> {
) -> Result<
(
BalanceOf<T>,
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::LiquidityInfo,
),
TransactionValidityError,
> {
let tip = self.0;
let fee = Module::<T>::compute_fee(len as u32, info, tip);
// Only mess with balances if fee is not zero.
if fee.is_zero() {
return Ok((fee, None));
}
match T::Currency::withdraw(
who,
fee,
if tip.is_zero() {
WithdrawReasons::TRANSACTION_PAYMENT
} else {
WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
},
ExistenceRequirement::KeepAlive,
) {
Ok(imbalance) => Ok((fee, Some(imbalance))),
Err(_) => Err(InvalidTransaction::Payment.into()),
}
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::withdraw_fee(who, call, info, fee, tip)
.map(|i| (fee, i))
}
/// 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
type AccountId = T::AccountId;
type Call = T::Call;
type AdditionalSigned = ();
type Pre = (BalanceOf<T>, Self::AccountId, Option<NegativeImbalanceOf<T>>, BalanceOf<T>);
type Pre = (
// tip
BalanceOf<T>,
// who paid the fee
Self::AccountId,
// imbalance resulting from withdrawing the fee
<<T as Trait>::OnChargeTransaction as OnChargeTransaction<T>>::LiquidityInfo,
);
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { Ok(()) }
fn validate(
&self,
who: &Self::AccountId,
_call: &Self::Call,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
let (fee, _) = self.withdraw_fee(who, info, len)?;
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
Ok(ValidTransaction {
priority: Self::get_priority(len, info, fee),
..Default::default()
@@ -525,12 +524,12 @@ impl<T: Trait + Send + Sync> SignedExtension for ChargeTransactionPayment<T> whe
fn pre_dispatch(
self,
who: &Self::AccountId,
_call: &Self::Call,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize
) -> Result<Self::Pre, TransactionValidityError> {
let (fee, imbalance) = self.withdraw_fee(who, info, len)?;
Ok((self.0, who.clone(), imbalance, fee))
let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?;
Ok((self.0, who.clone(), imbalance))
}
fn post_dispatch(
@@ -540,32 +539,14 @@ impl<T: Trait + Send + Sync> SignedExtension for ChargeTransactionPayment<T> whe
len: usize,
_result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
let (tip, who, imbalance, fee) = pre;
if let Some(payed) = imbalance {
let actual_fee = Module::<T>::compute_actual_fee(
len as u32,
info,
post_info,
tip,
);
let refund = fee.saturating_sub(actual_fee);
let actual_payment = match T::Currency::deposit_into_existing(&who, refund) {
Ok(refund_imbalance) => {
// The refund cannot be larger than the up front payed max weight.
// `PostDispatchInfo::calc_unspent` guards against such a case.
match payed.offset(refund_imbalance) {
Ok(actual_payment) => actual_payment,
Err(_) => return Err(InvalidTransaction::Payment.into()),
}
}
// We do not recreate the account using the refund. The up front payment
// is gone in that case.
Err(_) => payed,
};
let imbalances = actual_payment.split(tip);
T::OnTransactionPayment::on_unbalanceds(Some(imbalances.0).into_iter()
.chain(Some(imbalances.1)));
}
let (tip, who, imbalance) = pre;
let actual_fee = Module::<T>::compute_actual_fee(
len as u32,
info,
post_info,
tip,
);
T::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?;
Ok(())
}
}
@@ -580,6 +561,7 @@ mod tests {
DispatchClass, DispatchInfo, PostDispatchInfo, GetDispatchInfo, Weight,
WeightToFeePolynomial, WeightToFeeCoefficients, WeightToFeeCoefficient,
},
traits::Currency,
};
use pallet_balances::Call as BalancesCall;
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
@@ -699,8 +681,7 @@ mod tests {
}
impl Trait for Runtime {
type Currency = pallet_balances::Module<Runtime>;
type OnTransactionPayment = ();
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = WeightToFee;
type FeeMultiplierUpdate = ();
@@ -0,0 +1,127 @@
///! Traits and default implementation for paying transaction fees.
use crate::Trait;
use codec::FullCodec;
use frame_support::{
traits::{Currency, ExistenceRequirement, Get, Imbalance, OnUnbalanced, WithdrawReasons},
unsigned::TransactionValidityError,
};
use sp_runtime::{
traits::{AtLeast32BitUnsigned, DispatchInfoOf, MaybeSerializeDeserialize, PostDispatchInfoOf, Saturating, Zero},
transaction_validity::InvalidTransaction,
};
use sp_std::{fmt::Debug, marker::PhantomData};
type NegativeImbalanceOf<C, T> =
<C as Currency<<T as frame_system::Trait>::AccountId>>::NegativeImbalance;
/// Handle withdrawing, refunding and depositing of transaction fees.
pub trait OnChargeTransaction<T: Trait> {
/// The underlying integer type in which fees are calculated.
type Balance: AtLeast32BitUnsigned + FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default;
type LiquidityInfo: Default;
/// Before the transaction is executed the payment of the transaction fees
/// need to be secured.
///
/// Note: The `fee` already includes the `tip`.
fn withdraw_fee(
who: &T::AccountId,
call: &T::Call,
dispatch_info: &DispatchInfoOf<T::Call>,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError>;
/// After the transaction was executed the actual fee can be calculated.
/// This function should refund any overpaid fees and optionally deposit
/// the corrected amount.
///
/// Note: The `fee` already includes the `tip`.
fn correct_and_deposit_fee(
who: &T::AccountId,
dispatch_info: &DispatchInfoOf<T::Call>,
post_info: &PostDispatchInfoOf<T::Call>,
corrected_fee: Self::Balance,
tip: Self::Balance,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(), TransactionValidityError>;
}
/// Implements the transaction payment for a module implementing the `Currency`
/// trait (eg. the pallet_balances) using an unbalance handler (implementing
/// `OnUnbalanced`).
pub struct CurrencyAdapter<C, OU>(PhantomData<(C, OU)>);
/// Default implementation for a Currency and an OnUnbalanced handler.
impl<T, C, OU> OnChargeTransaction<T> for CurrencyAdapter<C, OU>
where
T: Trait,
T::TransactionByteFee: Get<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance>,
C: Currency<<T as frame_system::Trait>::AccountId>,
C::PositiveImbalance:
Imbalance<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance, Opposite = C::NegativeImbalance>,
C::NegativeImbalance:
Imbalance<<C as Currency<<T as frame_system::Trait>::AccountId>>::Balance, Opposite = C::PositiveImbalance>,
OU: OnUnbalanced<NegativeImbalanceOf<C, T>>,
{
type LiquidityInfo = Option<NegativeImbalanceOf<C, T>>;
type Balance = <C as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
/// Withdraw the predicted fee from the transaction origin.
///
/// Note: The `fee` already includes the `tip`.
fn withdraw_fee(
who: &T::AccountId,
_call: &T::Call,
_info: &DispatchInfoOf<T::Call>,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
if fee.is_zero() {
return Ok(None);
}
let withdraw_reason = if tip.is_zero() {
WithdrawReasons::TRANSACTION_PAYMENT
} else {
WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
};
match C::withdraw(who, fee, withdraw_reason, ExistenceRequirement::KeepAlive) {
Ok(imbalance) => Ok(Some(imbalance)),
Err(_) => Err(InvalidTransaction::Payment.into()),
}
}
/// Hand the fee and the tip over to the `[OnUnbalanced]` implementation.
/// Since the predicted fee might have been too high, parts of the fee may
/// be refunded.
///
/// Note: The `fee` already includes the `tip`.
fn correct_and_deposit_fee(
who: &T::AccountId,
_dispatch_info: &DispatchInfoOf<T::Call>,
_post_info: &PostDispatchInfoOf<T::Call>,
corrected_fee: Self::Balance,
tip: Self::Balance,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(), TransactionValidityError> {
if let Some(paid) = already_withdrawn {
// 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 this fails, the
// account might have dropped below the existential balance. In
// that case we don't refund anything.
let refund_imbalance =
C::deposit_into_existing(&who, refund_amount).unwrap_or_else(|_| C::PositiveImbalance::zero());
// merge the imbalance caused by paying the fees and refunding parts of it again.
let adjusted_paid = paid
.offset(refund_imbalance)
.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
// Call someone else to handle the imbalance (fee and tip separately)
let imbalances = adjusted_paid.split(tip);
OU::on_unbalanceds(Some(imbalances.0).into_iter().chain(Some(imbalances.1)));
}
Ok(())
}
}