This guide assumes you're a parachain builder whose parachain has its own token for fee payment and will show you how to integrate DOT into your parachain so that it can also be used for fee payment.
We'll call the relay chain token DOT in this guide, even though it might be KSM/PAS/WND depending on the network your parachain is running on.
We'll also call the native token of your parachain PARA.
Like everything, in order to integrate DOT as a fee token on your parachain, there is an off-chain and an on-chain aspect.
The star of the show is a transaction extension called ChargeAssetTxPayment
.
This extension allows fees to be paid in any supported asset.
Wallets and apps interacting with your chain will need to add the asset id to the transaction.
This guide will assume you're using pallet-balances
to hold PARA.
Make sure to change it accordingly if you're using a different pallet.
First things first, you need a way of representing DOT on your chain.
In this guide, we'll assume a simple permissioned assets pallet instance with numeric ids.
You could also use pallet-asset-registry
to go with it.
If you use XCM Locations as ids for other assets, you can find an example of this in polimec or asset hub runtime.
One way of allowing DOT as a fee token is to just accept it for fee payment without swaps or anything.
The good thing about this method is there's no need to set up swaps (method 2).
Every parachain needs DOT for coretime, so accepting DOT directly and storing it in a treasury for example good for buying coretime later.
A potential downside is that DOT will take part of the utility from PARA since users now have a choice of what they want to use to actually pay for fees.
Additionally, you need a reliable way of knowing the exchange rate between DOT and PARA.
This could be using an oracle, like Polimec, or querying a DEX.
Another way of allowing DOT as a fee token in your runtime is to use liquidity pools to automatically swap it to pay fees.
pallet-asset-conversion
can be used for creating these liquidity pools.
WARNING: Having liquidity pools in your chain has it's own downsides. Such as fragmented liquidity and having too little liquidity means higher slippage when transacting, so less efficiency.
You will need to add 3 new pallets to your runtime:
PoolAssets
: an instance of pallet-assets
to hold liquidity pool tokens.pallet-asset-conversion
: the asset conversion pallet handling pools and swaps.pallet-asset-conversion-tx-payment
: handles automatically doing swaps for fee payment.use pallet_assets::{Instance1, Instance2};
construct_runtime! {
Assets: pallet_assets::<Instance1> = N;
PoolAssets: pallet_assets::<Instance2> = N + 1;
AssetConversion: pallet_asset_conversion = N + 2;
AssetConversionTxPayment: pallet_asset_conversion_tx_payment = N + 3;
}
Make sure to change the instances accordingly if you have another assets pallet instance.
Each of these new pallets needs to be configured.
type NumericAssetId = u32; // Put whatever id you use for assets.
impl pallet_assets::Config<InstanceX> for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type RemoveItemsLimit = ConstU32<1000>;
type AssetId = NumericAssetId;
type AssetIdParameter = NumericAssetId;
type Currency = Balances;
type CreateOrigin = NeverEnsureOrigin<AccountId>;
type ForceOrigin = EnsureRoot<AccountId>;
// Deposits are zero because creation/admin is limited to Root.
type AssetDeposit = ConstU128<0>;
type AssetAccountDeposit = ConstU128<0>;
type MetadataDepositBase = ConstU128<0>;
type MetadataDepositPerByte = ConstU128<0>;
type ApprovalDeposit = ExistentialDeposit; // Or whatever value you're using for the balances pallet.
type StringLimit = ConstU32<50>;
type Freezer = ();
type Extra = ();
type WeightInfo = (); // Make sure to benchmark this for production!
type CallbackHandle = ();
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
The asset conversion pallet needs a type that implements the fungibles::*
family of traits, this is usually something like pallet-assets.
pallet-balances, which tends to be used for managing the native token of a chain, implements the fungible::*
family of traits, note the missing “s”!
To unify the native token in the balances pallet and any other asset in the assets pallet, we use a type UnionOf
:
pub type NativeAndAssets = UnionOf<
Balances,
Assets,
NativeFromLeft,
NativeOrWithId<NumericAssetId>,
AccountId,
>;
The pallet also needs a way to derive an address for a pool given the pair of assets that go in it. This is called the PoolLocator
. The pallet itself provides some options for the locator:
WithFirstAsset
: Mandates the inclusion of a specific asset in a pair.Ascending
: Assets should be in ascending order.Chain
: Utility for chaining multiple locators.If you’re only allowing pools between PARA and other assets, then WithFirstAsset
is a good choice to make sure no pools without DOT can be created. Ascending
is an optional utility which can be good to Chain
together with the first one.
parameter_types! {
pub const Native: NativeOrWithId = NativeOrWithId::Native;
pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon");
// we charge no fee for liquidity withdrawal
pub const LiquidityWithdrawalFee: Permill = Permill::from_perthousand(0);
}
pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter<
AssetConversionPalletId,
(NativeOrWithId<NumericAssetId>, NativeOrWithId<NumericAssetId>),
>;
pub type AscendingLocator =
Ascending<AccountId, NativeOrWithId<NumericAssetId>, PoolIdToAccountId>;
pub type WithFirstAssetLocator = WithFirstAsset<
Native,
AccountId,
NativeOrWithId<NumericAssetId>,
PoolIdToAccountId,
>;
impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type HigherPrecisionBalance = sp_core::U256;
type AssetKind = NativeOrWithId<NumericAssetId>;
type Assets = NativeAndAssets; // The unified assets type built before.
type PoolId = (Self::AssetKind, Self::AssetKind); // The ID for a pool is the tuple of both assets that go in it.
type PoolLocator = Chain<WithFirstAssetLocator, AscendingLocator>;
type PoolAssetId = u32;
type PoolAssets = PoolAssets; // The instance of the assets pallet.
type PoolSetupFee = ConstU128<0>; // Asset class deposit fees are sufficient to prevent spam
type PoolSetupFeeAsset = Native;
type PoolSetupFeeTarget = ResolveAssetTo<AssetConversionOrigin, Self::Assets>;
type LiquidityWithdrawalFee = LiquidityWithdrawalFee;
type LPFee = ConstU32<3>; // 0.3% swap fee
type PalletId = AssetConversionPalletId;
type MaxSwapPathLength = ConstU32<3>;
type MintMinLiquidity = ConstU128<100>;
type WeightInfo = (); // Make sure to benchmark this for production!
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
The asset conversion tx payment pallet has a very small configuration.
Make sure to change the ChargeTransactionPayment
signed extension to ChargeAssetTxPayment
coming from this pallet.
impl pallet_asset_conversion_tx_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type AssetId = NativeOrWithId<AssetIdForTrustBackedAssets>;
type OnChargeAssetTransaction = pallet_asset_conversion_tx_payment::SwapAssetAdapter<
Native,
NativeAndAssets,
AssetConversion,
(), // Fees are burnt. You could use `ResolveAssetTo<TreasuryAccount, NativeAndAssets>` if your chain has a treasury.
>;
type WeightInfo = (); // Make sure to benchmark this for production!
}
type SignedExtra = (
--- pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
+++ pallet_asset_conversion_tx_payment::ChargeAssetTxPayment<Runtime>,
)