Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tx Payment: (tests check) drop ED requirements for tx payments with exchangeable asset #4455

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions substrate/frame/transaction-payment/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -39,6 +40,7 @@ std = [
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"pallet-assets/std",
"scale-info/std",
"serde",
"sp-core/std",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,19 @@ 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,
};
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},
Expand Down Expand Up @@ -151,32 +150,27 @@ impl WeightToFeeT for TransactionByteFee {
}

parameter_types! {
pub(crate) static TipUnbalancedAmount: u64 = 0;
pub(crate) static FeeUnbalancedAmount: u64 = 0;
}

pub struct DealWithFees;
impl OnUnbalanced<fungible::Credit<<Runtime as frame_system::Config>::AccountId, Balances>>
for DealWithFees
{
fn on_unbalanceds<B>(
mut fees_then_tips: impl Iterator<
Item = fungible::Credit<<Runtime as frame_system::Config>::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<fungibles::Credit<AccountId, NativeAndAssets>> for DealWithFungiblesFees {
fn on_nonzero_unbalanced(credit: fungibles::Credit<AccountId, NativeAndAssets>) {
if credit.asset() == Native::get() {
FeeUnbalancedAmount::mutate(|a| *a += credit.peek());
}
}
}

#[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig)]
impl pallet_transaction_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type OnChargeTransaction = FungibleAdapter<Balances, DealWithFees>;
// type OnChargeTransaction = FungibleAdapter<Balances, DealWithFees>;
type OnChargeTransaction = pallet_transaction_payment::FungiblesAdapter<
NativeAndAssets,
Native,
DealWithFungiblesFees,
>;
type WeightToFee = WeightToFee;
type LengthToFee = TransactionByteFee;
type FeeMultiplierUpdate = ();
Expand Down Expand Up @@ -249,12 +243,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>,
Expand All @@ -278,6 +274,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 Fungibles = NativeAndAssets;
// type OnChargeAssetTransaction = AssetConversionAdapter<Balances, AssetConversion, Native>;
type OnChargeAssetTransaction = SwapCreditAdapter<Native, AssetConversion>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<A, S>(PhantomData<(A, S)>);
impl<A, S, T> OnChargeAssetTransaction<T> for SwapCreditAdapter<A, S>
where
A: Get<S::AssetKind>,
S: SwapCredit<
T::AccountId,
Balance = T::Balance,
AssetKind = T::AssetKind,
Credit = fungibles::Credit<T::AccountId, T::Assets>,
>,

T: pallet_asset_conversion_tx_payment::Config,
T::Fungibles: fungibles::Inspect<T::AccountId, Balance = T::Balance, AssetId = T::AssetKind>,
T::OnChargeTransaction:
OnChargeTransaction<T, Balance = T::Balance, LiquidityInfo = Option<S::Credit>>,
{
type AssetId = T::AssetKind;
type Balance = T::Balance;
type LiquidityInfo = T::Balance;

fn withdraw_fee(
who: &<T>::AccountId,
_call: &<T>::RuntimeCall,
_dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>,
asset_id: Self::AssetId,
fee: Self::Balance,
_tip: Self::Balance,
) -> Result<(LiquidityInfoOf<T>, Self::LiquidityInfo, T::Balance), TransactionValidityError> {
let asset_fee = AssetConversion::<T>::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: &<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: T::Balance,
) -> Result<T::Balance, TransactionValidityError> {
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::<T>::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::<T::AccountId, T::Assets>::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)
}
}
Loading
Loading