Acala uses their own pallet, but unlike the previous two solutions, Acala actually performs a swap into their native currency. Similarly to Hydra, they have extrinsics to set the used currency (and swap path), but they also have a single-shot with_fee_path function. Their implementation is overkill for our purposes - they have this thing called a fee pool (in addition to the dex) and they allow fees to be paid by other accounts. They also reimplement a lot of code provided by substrate.
For the swap to actually work a swap pool is needed, which would add a lot of uneccessary complexity and consumes a lot of time to maintain. The requirements don't imply to charge transaction fees necessarily in the native token ZTG. But obviously this would create demand for the ZTG token and foreign tokens would be sold for being able to pay transaction fees.
Hydra uses a custom pallet. Like the substrate one, it does not do any swaps, and just pays the block author in a different currency. The documentation states otherwise, but it is incorrect - I checked on-chain behavior: see e.g. block 2363593. Unlike the substrate pallet, they configure the fee currency not on a per-extrinsic level, but instead they configure the used currency in a dedicated extrinsic (and as a consequence, it's persistent). Implementation detail: they use the same pallet_transaction_payment that we do, but with OnChargeTransaction that they implement in their transaction-multi-payment pallet.
With this approach it's necessary to add a custom pallet by HydraDX. The fee currency is set by an extrinsic. So it's part of the runtime storage. It's also worth mentioning whenever something changes inside the Substrate repository, this custom transaction-multi-payment
pallet needs to be maintained by HydraDX too. For a native substrate pallet, it's likely to be maintained instantly.
This approach could be easier for the front end to implement, because the fee tokens can just be queried on-chain and don't need to be stored by separately.
https://github.com/galacticcouncil/HydraDX-node/tree/master/pallets/transaction-multi-payment
The path of the requirements leads to the usage of the pallet-asset-tx-payment
pallet, because there is no other pre-written way inside of Substrate to handle transaction fees in different assets. The fees are charged in the foreign currency and would be hold by the Zeitgeist treasury in that currency. For example if the user pays with DOT, the fees wouldn't be paid in ZTG anymore, but directly in DOT. So, we wouldn't need any swap functionality here to swap from DOT to ZTG to pay the native fees.
This pallet is used by e.g. statemine and composable. The support in polkadot.js explorer is not good. Signing will work but it doesn't allow you to select different curriences. I think we could select the currency in our our front-end code, though.
For front end integration regard this:
https://substrate.stackexchange.com/questions/8063/where-do-i-add-a-signedextension-in-a-subxt-call
As the Zeitgeist protocol does not have an internal swap mechanism for currencies and the overhead of implementing one just for the sake of providing the basis for one of the transaction fee solutions is an unacceptable overhead, the solution provided by Acala is automatically eliminated.
Maintenance and long term support are important factors in the decision as they determine the future effort that is necessary to operate this solution. It is not guaranteed that this is given using the solution from HydraDX. Furthermore, the solution from HydraDX is unflexible in that it does only allow to pay is one currency at a time. To be precise, when an account is created for the first time, a currency is mapped to that account that was used to create that account. Now other currencies might be added to the account. Once the initial currency is depleted and the account is removed, the initial currency is removed as a fee payment currency. Now the system only allows to pay in the native currency until the account sets a new prefered currency, although other currencies are available in the account.
While the Substrate pallet solution puts additional strain on the front-end developers, it also has the benefit that the front-end is always capable to determine a currency that fees can be paid with.
The long-term support and the flexibility during currency selection make the Substrate pallet solution an ideal candidate for Zeitgeist.
To understand how the pallet pallet-asset-tx-payment
works, it's important to note that it's a wrapper around the pallet-transaction-payment
pallet. This means that pallet-asset-tx-payment
internally calls the functionality of pallet-transaction-payment
for the native currency and therefore depends on it.
To use the pallet-asset-tx-payment
pallet the following trait has to be implemented:
/// Handle withdrawing, refunding and depositing of transaction fees.
pub trait OnChargeAssetTransaction<T: Config> {
/// The underlying integer type in which fees are calculated.
type Balance: Balance;
/// The type used to identify the assets used for transaction payment.
type AssetId: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo;
/// The type used to store the intermediate values between pre- and post-dispatch.
type LiquidityInfo;
/// Before the transaction is executed the payment of the transaction fees needs to be secured.
///
/// Note: The `fee` already includes the `tip`.
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<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`.
///
/// Returns the fee and tip in the asset used for payment as (fee, tip).
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,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(AssetBalanceOf<T>, AssetBalanceOf<T>), TransactionValidityError>;
}
The functions of this trait for withdraw_fee
and correct_and_deposit_fee
are meant to implement the charge of transaction fees in all foreign currencies. The types Balance
, AssetId
are pretty straigth-forward to assign, since the internal Balance
type and CurrencyId
type of the zeitgeist node can be used.
Because LiquidityInfo
is returned from withdraw_fee
and passed as function parameter to correct_and_deposit_fee
as already_withdrawn
it's expected to represent a form of Imbalance
. It actually can be seen here, which turns out to be an imbalance from here.
To actually use the fee payment functionality of the newly configured pallet, it has to be integrated in the following way:
you should include both pallets in your
construct_runtime
macro, but only include this pallet's [SignedExtension
] ([ChargeAssetTxPayment
])
The integration information was found here.