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

Use MultiLocation for tx fee payment instead of u32 #1403

Merged
merged 4 commits into from
Jan 30, 2025
Merged
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
8 changes: 1 addition & 7 deletions primitives/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022-2024 Forecasting Technologies LTD.
// Copyright 2022-2025 Forecasting Technologies LTD.
// Copyright 2021-2022 Zeitgeist PM LLC.
//
// This file is part of Zeitgeist.
Expand Down Expand Up @@ -83,12 +83,6 @@ pub type BasicCurrencyAdapter<R, B> = orml_currencies::BasicCurrencyAdapter<R, B

pub type CurrencyId = Asset<MarketId>;

/// The asset id specifically used for pallet_assets_tx_payment for
/// paying transaction fees in different assets.
/// Since the polkadot extension and wallets can't handle custom asset ids other than just u32,
/// we are using a u32 as on the asset-hubs here.
pub type TxPaymentAssetId = u32;

/// Index of a transaction in the chain.
pub type Nonce = u64;

Expand Down
199 changes: 118 additions & 81 deletions runtime/common/src/fees.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022-2024 Forecasting Technologies LTD.
// Copyright 2022-2025 Forecasting Technologies LTD.
// Copyright 2021-2022 Zeitgeist PM LLC.
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
//
Expand Down Expand Up @@ -65,6 +65,7 @@ macro_rules! impl_fee_types {
macro_rules! impl_foreign_fees {
() => {
use frame_support::{
ensure,
pallet_prelude::InvalidTransaction,
traits::{
fungibles::{Credit, Inspect},
Expand All @@ -85,10 +86,7 @@ macro_rules! impl_foreign_fees {
traits::{Convert, DispatchInfoOf, PostDispatchInfoOf},
Perbill,
};
use zeitgeist_primitives::{
math::fixed::{FixedDiv, FixedMul},
types::TxPaymentAssetId,
};
use zeitgeist_primitives::math::fixed::{FixedDiv, FixedMul};

#[repr(u8)]
pub enum CustomTxError {
Expand All @@ -97,70 +95,77 @@ macro_rules! impl_foreign_fees {
NoAssetMetadata = 2,
NoFeeFactor = 3,
NonForeignAssetPaid = 4,
InvalidFeeAsset = 5,
NonNativeFeeAssetOnStandaloneChain = 6,
}

// It does calculate foreign fees by extending transactions to include an optional
// `AssetId` that specifies the asset to be used for payment (defaulting to the native
// token on `None`), such that for each transaction the asset id can be specified.
// For real ZTG `None` is used and for DOT `Some(Asset::Foreign(0))` is used.
cfg_if::cfg_if! {
if #[cfg(feature = "parachain")] {
/// The asset id specifically used for pallet_assets_tx_payment for
/// paying transaction fees in different assets.
/// Since the polkadot API extension assumes the same type as on the asset-hubs,
/// we use it too.
/// https://github.com/polkadot-fellows/runtimes/blob/20ac6ff4dc4c488ad08f507c14b899adc6cb4394/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs#L767
pub type TxPaymentAssetId = xcm::latest::MultiLocation;

pub(crate) fn convert_asset_to_currency_id(
value: TxPaymentAssetId,
) -> Result<CurrencyId, TransactionValidityError> {
// value (TxPaymentAssetId) is a MultiLocation as defined above
let currency_id = AssetRegistry::location_to_asset_id(value).ok_or(
TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::InvalidFeeAsset as u8,
)),
)?;
Ok(currency_id)
}

pub(crate) fn calculate_fee(
native_fee: Balance,
fee_factor: Balance,
) -> Result<Balance, TransactionValidityError> {
// Assume a fee_factor of 143_120_520 for DOT, now divide by
// BASE (10^10) = 0.0143120520 DOT per ZTG.
// Keep in mind that ZTG BASE is 10_000_000_000, and because fee_factor is below that,
// less DOT than ZTG is paid for fees.
// Assume a fee_factor of 20_000_000_000, then the fee would result in
// 20_000_000_000 / 10_000_000_000 = 2 units per ZTG
let converted_fee = native_fee.bmul(fee_factor).map_err(|_| {
TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::FeeConversionArith as u8,
))
})?;

Ok(converted_fee)
}
pub(crate) fn get_fee_factor(
currency_id: CurrencyId,
) -> Result<Balance, TransactionValidityError> {
let metadata = <AssetRegistry as AssetRegistryInspect>::metadata(&currency_id).ok_or(
TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NoAssetMetadata as u8,
)),
)?;
let fee_factor =
metadata.additional.xcm.fee_factor.ok_or(TransactionValidityError::Invalid(
InvalidTransaction::Custom(CustomTxError::NoFeeFactor as u8),
))?;
Ok(fee_factor)
}

#[cfg(feature = "parachain")]
pub(crate) fn get_fee_factor(
currency_id: CurrencyId,
) -> Result<Balance, TransactionValidityError> {
let metadata = <AssetRegistry as AssetRegistryInspect>::metadata(&currency_id).ok_or(
TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NoAssetMetadata as u8,
)),
)?;
let fee_factor =
metadata.additional.xcm.fee_factor.ok_or(TransactionValidityError::Invalid(
InvalidTransaction::Custom(CustomTxError::NoFeeFactor as u8),
))?;
Ok(fee_factor)
}
pub(crate) fn calculate_fee(
native_fee: Balance,
fee_factor: Balance,
) -> Result<Balance, TransactionValidityError> {
// Assume a fee_factor of 143_120_520 for DOT, now divide by
// BASE (10^10) = 0.0143120520 DOT per ZTG.
// Keep in mind that ZTG BASE is 10_000_000_000, and because fee_factor is below that,
// less DOT than ZTG is paid for fees.
// Assume a fee_factor of 20_000_000_000, then the fee would result in
// 20_000_000_000 / 10_000_000_000 = 2 units per ZTG
let converted_fee = native_fee.bmul(fee_factor).map_err(|_| {
TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::FeeConversionArith as u8,
))
})?;

pub struct TTCBalanceToAssetBalance;
impl ConversionToAssetBalance<Balance, TxPaymentAssetId, Balance>
for TTCBalanceToAssetBalance
{
type Error = TransactionValidityError;

fn to_asset_balance(
native_fee: Balance,
asset_id: TxPaymentAssetId,
) -> Result<Balance, Self::Error> {
#[cfg(feature = "parachain")]
{
let currency_id = Asset::ForeignAsset(asset_id);
let fee_factor = get_fee_factor(currency_id)?;
let converted_fee = calculate_fee(native_fee, fee_factor)?;
Ok(converted_fee)
}
#[cfg(not(feature = "parachain"))]
{
Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NoForeignAssetsOnStandaloneChain as u8,
)))

pub struct TTCBalanceToAssetBalance;
impl ConversionToAssetBalance<Balance, CurrencyId, Balance> for TTCBalanceToAssetBalance {
type Error = TransactionValidityError;

fn to_asset_balance(
native_fee: Balance,
asset_id: CurrencyId,
) -> Result<Balance, Self::Error> {
let fee_factor = get_fee_factor(asset_id)?;
let converted_fee = calculate_fee(native_fee, fee_factor)?;
Ok(converted_fee)
}
}
}
}
Expand All @@ -173,7 +178,13 @@ macro_rules! impl_foreign_fees {
}
}

// It does calculate foreign fees by extending transactions to include an optional
// `AssetId` that specifies the asset to be used for payment (defaulting to the native
// token on `None`), such that for each transaction the asset id can be specified.
// For real ZTG `None` is used and for DOT `Some(Asset::Foreign(0))` is used.
pub struct TokensTxCharger;

#[cfg(feature = "parachain")]
impl pallet_asset_tx_payment::OnChargeAssetTransaction<Runtime> for TokensTxCharger {
type AssetId = TxPaymentAssetId;
type Balance = Balance;
Expand All @@ -187,15 +198,15 @@ macro_rules! impl_foreign_fees {
native_fee: Self::Balance,
_tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
let currency_id = convert_asset_to_currency_id(asset_id)?;
// We don't know the precision of the underlying asset. Because the converted fee
// could be less than one (e.g. 0.5) but gets rounded down by integer division we
// introduce a minimum fee.
let min_converted_fee =
if native_fee.is_zero() { Zero::zero() } else { One::one() };
let converted_fee =
TTCBalanceToAssetBalance::to_asset_balance(native_fee, asset_id)?
TTCBalanceToAssetBalance::to_asset_balance(native_fee, currency_id)?
.max(min_converted_fee);
let currency_id = Asset::ForeignAsset(asset_id);
let can_withdraw =
<Tokens as Inspect<AccountId>>::can_withdraw(currency_id, who, converted_fee);
if can_withdraw != WithdrawConsequence::Success {
Expand All @@ -222,19 +233,11 @@ macro_rules! impl_foreign_fees {
) -> Result<(Self::Balance, Self::Balance), TransactionValidityError> {
let min_converted_fee =
if corrected_native_fee.is_zero() { Zero::zero() } else { One::one() };
let asset_id = match paid.asset() {
Asset::ForeignAsset(asset_id) => asset_id,
_ => {
return Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NonForeignAssetPaid as u8,
)));
}
};
// Convert the corrected fee and tip into the asset used for payment.
let converted_fee =
TTCBalanceToAssetBalance::to_asset_balance(corrected_native_fee, asset_id)?
TTCBalanceToAssetBalance::to_asset_balance(corrected_native_fee, paid.asset())?
.max(min_converted_fee);
let converted_tip = TTCBalanceToAssetBalance::to_asset_balance(tip, asset_id)?;
let converted_tip = TTCBalanceToAssetBalance::to_asset_balance(tip, paid.asset())?;

// Calculate how much refund we should return.
let (final_fee, refund) = paid.split(converted_fee);
Expand All @@ -250,6 +253,41 @@ macro_rules! impl_foreign_fees {
Ok((final_fee_peek, tip))
}
}

#[cfg(not(feature = "parachain"))]
impl pallet_asset_tx_payment::OnChargeAssetTransaction<Runtime> for TokensTxCharger {
// used `u32` since we don't care about decoding in the polkadot API, because it would throw an error anyways
// additionally, we don't want to add the `xcm` dependency to the standalone chain (without parachain feature)
type AssetId = u32;
type Balance = Balance;
type LiquidityInfo = Credit<AccountId, Tokens>;

fn withdraw_fee(
_who: &AccountId,
_call: &RuntimeCall,
_dispatch_info: &DispatchInfoOf<RuntimeCall>,
_asset_id: Self::AssetId,
_native_fee: Self::Balance,
_tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NonNativeFeeAssetOnStandaloneChain as u8,
)))
}

fn correct_and_deposit_fee(
_who: &AccountId,
_dispatch_info: &DispatchInfoOf<RuntimeCall>,
_post_info: &PostDispatchInfoOf<RuntimeCall>,
_corrected_native_fee: Self::Balance,
_tip: Self::Balance,
_paid: Self::LiquidityInfo,
) -> Result<(Self::Balance, Self::Balance), TransactionValidityError> {
Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(
CustomTxError::NonNativeFeeAssetOnStandaloneChain as u8,
)))
}
}
};
}

Expand Down Expand Up @@ -536,8 +574,7 @@ macro_rules! fee_tests {
)),
additional: custom_metadata,
};
let dot_asset_id = 0u32;
let dot = Asset::ForeignAsset(dot_asset_id);
let dot = Asset::ForeignAsset(0u32);

assert_ok!(AssetRegistry::register_asset(
RuntimeOrigin::root(),
Expand Down Expand Up @@ -596,21 +633,21 @@ macro_rules! fee_tests {
let fee_factor = 143_120_520;
let custom_metadata = CustomMetadata {
xcm: XcmMetadata { fee_factor: Some(fee_factor) },
..Default::default()
allow_as_base_asset: true,
};
let dot_location = xcm::latest::MultiLocation::parent();
let meta: AssetMetadata<Balance, CustomMetadata, AssetRegistryStringLimit> =
AssetMetadata {
decimals: 10,
name: "Polkadot".as_bytes().to_vec().try_into().unwrap(),
symbol: "DOT".as_bytes().to_vec().try_into().unwrap(),
existential_deposit: 5 * MILLI,
location: Some(xcm::VersionedMultiLocation::V3(
xcm::latest::MultiLocation::parent(),
dot_location,
)),
additional: custom_metadata,
};
let dot_asset_id = 0u32;
let dot = Asset::ForeignAsset(dot_asset_id);
let dot = Asset::ForeignAsset(0u32);

assert_ok!(AssetRegistry::register_asset(
RuntimeOrigin::root(),
Expand All @@ -637,7 +674,7 @@ macro_rules! fee_tests {
&Treasury::account_id(),
&mock_call,
&mock_dispatch_info,
dot_asset_id,
dot_location,
BASE / 2,
0,
)
Expand Down
Loading