referrerpolicy=no-referrer-when-downgrade

pallet_asset_conversion_tx_payment/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! # Asset Conversion Transaction Payment Pallet
17//!
18//! This pallet allows runtimes that include it to pay for transactions in assets other than the
19//! chain's native asset.
20//!
21//! ## Overview
22//!
23//! This pallet provides a `TransactionExtension` with an optional `AssetId` that specifies the
24//! asset to be used for payment (defaulting to the native token on `None`). It expects an
25//! [`OnChargeAssetTransaction`] implementation analogous to [`pallet-transaction-payment`]. The
26//! included [`SwapAssetAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the
27//! fee amount by converting the fee calculated by [`pallet-transaction-payment`] in the native
28//! asset into the amount required of the specified asset.
29//!
30//! ## Pallet API
31//!
32//! This pallet does not have any dispatchable calls or storage. It wraps FRAME's Transaction
33//! Payment pallet and functions as a replacement. This means you should include both pallets in
34//! your `construct_runtime` macro, but only include this pallet's [`TransactionExtension`]
35//! ([`ChargeAssetTxPayment`]).
36//!
37//! ## Terminology
38//!
39//! - Native Asset or Native Currency: The asset that a chain considers native, as in its default
40//!   for transaction fee payment, deposits, inflation, etc.
41//! - Other assets: Other assets that may exist on chain, for example under the Assets pallet.
42
43#![cfg_attr(not(feature = "std"), no_std)]
44
45extern crate alloc;
46
47use codec::{Decode, DecodeWithMemTracking, Encode};
48use frame_support::{
49	dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
50	pallet_prelude::TransactionSource,
51	traits::IsType,
52	DefaultNoBound,
53};
54use pallet_transaction_payment::{ChargeTransactionPayment, OnChargeTransaction};
55use scale_info::TypeInfo;
56use sp_runtime::{
57	traits::{
58		AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, RefundWeight,
59		TransactionExtension, ValidateResult, Zero,
60	},
61	transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction},
62};
63
64#[cfg(test)]
65mod mock;
66#[cfg(test)]
67mod tests;
68pub mod weights;
69
70#[cfg(feature = "runtime-benchmarks")]
71mod benchmarking;
72
73mod payment;
74use frame_support::{pallet_prelude::Weight, traits::tokens::AssetId};
75pub use payment::*;
76pub use weights::WeightInfo;
77
78/// Balance type alias for balances of the chain's native asset.
79pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
80
81/// Type aliases used for interaction with `OnChargeTransaction`.
82pub(crate) type OnChargeTransactionOf<T> =
83	<T as pallet_transaction_payment::Config>::OnChargeTransaction;
84
85/// Liquidity info type alias for the chain's native asset.
86pub(crate) type NativeLiquidityInfoOf<T> =
87	<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
88
89/// Liquidity info type alias for the chain's assets.
90pub(crate) type AssetLiquidityInfoOf<T> =
91	<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
92
93/// Used to pass the initial payment info from pre- to post-dispatch.
94#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
95pub enum InitialPayment<T: Config> {
96	/// No initial fee was paid.
97	#[default]
98	Nothing,
99	/// The initial fee was paid in the native currency.
100	Native(NativeLiquidityInfoOf<T>),
101	/// The initial fee was paid in an asset.
102	Asset((T::AssetId, AssetLiquidityInfoOf<T>)),
103}
104
105pub use pallet::*;
106
107#[frame_support::pallet]
108pub mod pallet {
109	use super::*;
110
111	#[pallet::config]
112	pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
113		/// The overarching event type.
114		#[allow(deprecated)]
115		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
116		/// The asset ID type that can be used for transaction payments in addition to a
117		/// native asset.
118		type AssetId: AssetId;
119		/// The actual transaction charging logic that charges the fees.
120		type OnChargeAssetTransaction: OnChargeAssetTransaction<
121			Self,
122			Balance = BalanceOf<Self>,
123			AssetId = Self::AssetId,
124		>;
125		/// The weight information of this pallet.
126		type WeightInfo: WeightInfo;
127		#[cfg(feature = "runtime-benchmarks")]
128		/// Benchmark helper
129		type BenchmarkHelper: BenchmarkHelperTrait<
130			Self::AccountId,
131			Self::AssetId,
132			<<Self as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<Self>>::AssetId,
133		>;
134	}
135
136	#[pallet::pallet]
137	pub struct Pallet<T>(_);
138
139	#[cfg(feature = "runtime-benchmarks")]
140	/// Helper trait to benchmark the `ChargeAssetTxPayment` transaction extension.
141	pub trait BenchmarkHelperTrait<AccountId, FunAssetIdParameter, AssetIdParameter> {
142		/// Returns the `AssetId` to be used in the liquidity pool by the benchmarking code.
143		fn create_asset_id_parameter(id: u32) -> (FunAssetIdParameter, AssetIdParameter);
144		/// Create a liquidity pool for a given asset and sufficiently endow accounts to benchmark
145		/// the extension.
146		fn setup_balances_and_pool(asset_id: FunAssetIdParameter, account: AccountId);
147	}
148
149	#[pallet::event]
150	#[pallet::generate_deposit(pub(super) fn deposit_event)]
151	pub enum Event<T: Config> {
152		/// A transaction fee `actual_fee`, of which `tip` was added to the minimum inclusion fee,
153		/// has been paid by `who` in an asset `asset_id`.
154		AssetTxFeePaid {
155			who: T::AccountId,
156			actual_fee: BalanceOf<T>,
157			tip: BalanceOf<T>,
158			asset_id: T::AssetId,
159		},
160		/// A swap of the refund in native currency back to asset failed.
161		AssetRefundFailed { native_amount_kept: BalanceOf<T> },
162	}
163}
164
165/// Require payment for transaction inclusion and optionally include a tip to gain additional
166/// priority in the queue.
167///
168/// Wraps the transaction logic in [`pallet_transaction_payment`] and extends it with assets.
169/// An asset ID of `None` falls back to the underlying transaction payment logic via the native
170/// currency.
171///
172/// Transaction payments are processed using different handlers based on the asset type:
173/// - Payments with a native asset are charged by
174///   [pallet_transaction_payment::Config::OnChargeTransaction].
175/// - Payments with other assets are charged by [Config::OnChargeAssetTransaction].
176#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
177#[scale_info(skip_type_params(T))]
178pub struct ChargeAssetTxPayment<T: Config> {
179	#[codec(compact)]
180	tip: BalanceOf<T>,
181	asset_id: Option<T::AssetId>,
182}
183
184impl<T: Config> ChargeAssetTxPayment<T>
185where
186	T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
187{
188	/// Utility constructor. Used only in client/factory code.
189	pub fn from(tip: BalanceOf<T>, asset_id: Option<T::AssetId>) -> Self {
190		Self { tip, asset_id }
191	}
192
193	/// Fee withdrawal logic that dispatches to either [`Config::OnChargeAssetTransaction`] or
194	/// [`pallet_transaction_payment::Config::OnChargeTransaction`].
195	fn withdraw_fee(
196		&self,
197		who: &T::AccountId,
198		call: &T::RuntimeCall,
199		info: &DispatchInfoOf<T::RuntimeCall>,
200		fee: BalanceOf<T>,
201	) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
202		debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
203		if fee.is_zero() {
204			Ok((fee, InitialPayment::Nothing))
205		} else if let Some(asset_id) = &self.asset_id {
206			T::OnChargeAssetTransaction::withdraw_fee(
207				who,
208				call,
209				info,
210				asset_id.clone(),
211				fee,
212				self.tip,
213			)
214			.map(|payment| (fee, InitialPayment::Asset((asset_id.clone(), payment))))
215		} else {
216			T::OnChargeTransaction::withdraw_fee(who, call, info, fee, self.tip)
217				.map(|payment| (fee, InitialPayment::Native(payment)))
218		}
219	}
220
221	/// Fee withdrawal logic dry-run that dispatches to either `OnChargeAssetTransaction` or
222	/// `OnChargeTransaction`.
223	fn can_withdraw_fee(
224		&self,
225		who: &T::AccountId,
226		call: &T::RuntimeCall,
227		info: &DispatchInfoOf<T::RuntimeCall>,
228		fee: BalanceOf<T>,
229	) -> Result<(), TransactionValidityError> {
230		debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
231		if fee.is_zero() {
232			Ok(())
233		} else if let Some(asset_id) = &self.asset_id {
234			T::OnChargeAssetTransaction::can_withdraw_fee(who, asset_id.clone(), fee.into())
235		} else {
236			<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::can_withdraw_fee(
237				who, call, info, fee, self.tip,
238			)
239			.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
240		}
241	}
242}
243
244impl<T: Config> core::fmt::Debug for ChargeAssetTxPayment<T> {
245	#[cfg(feature = "std")]
246	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
247		write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
248	}
249	#[cfg(not(feature = "std"))]
250	fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
251		Ok(())
252	}
253}
254
255/// The info passed between the validate and prepare steps for the `ChargeAssetTxPayment` extension.
256pub enum Val<T: Config> {
257	Charge {
258		tip: BalanceOf<T>,
259		// who paid the fee
260		who: T::AccountId,
261		// transaction fee
262		fee: BalanceOf<T>,
263	},
264	NoCharge,
265}
266
267/// The info passed between the prepare and post-dispatch steps for the `ChargeAssetTxPayment`
268/// extension.
269pub enum Pre<T: Config> {
270	Charge {
271		tip: BalanceOf<T>,
272		// who paid the fee
273		who: T::AccountId,
274		// imbalance resulting from withdrawing the fee
275		initial_payment: InitialPayment<T>,
276		// weight used by the extension
277		weight: Weight,
278	},
279	NoCharge {
280		// weight initially estimated by the extension, to be refunded
281		refund: Weight,
282	},
283}
284
285impl<T: Config> TransactionExtension<T::RuntimeCall> for ChargeAssetTxPayment<T>
286where
287	T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
288	BalanceOf<T>: Send + Sync + From<u64>,
289	T::AssetId: Send + Sync,
290	<T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
291{
292	const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
293	type Implicit = ();
294	type Val = Val<T>;
295	type Pre = Pre<T>;
296
297	fn weight(&self, _: &T::RuntimeCall) -> Weight {
298		if self.asset_id.is_some() {
299			<T as Config>::WeightInfo::charge_asset_tx_payment_asset()
300		} else {
301			<T as Config>::WeightInfo::charge_asset_tx_payment_native()
302		}
303	}
304
305	fn validate(
306		&self,
307		origin: <T::RuntimeCall as Dispatchable>::RuntimeOrigin,
308		call: &T::RuntimeCall,
309		info: &DispatchInfoOf<T::RuntimeCall>,
310		len: usize,
311		_self_implicit: Self::Implicit,
312		_inherited_implication: &impl Encode,
313		_source: TransactionSource,
314	) -> ValidateResult<Self::Val, T::RuntimeCall> {
315		let Some(who) = origin.as_system_origin_signer() else {
316			return Ok((ValidTransaction::default(), Val::NoCharge, origin))
317		};
318		// Non-mutating call of `compute_fee` to calculate the fee used in the transaction priority.
319		let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
320		self.can_withdraw_fee(&who, call, info, fee)?;
321		let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, fee);
322		let validity = ValidTransaction { priority, ..Default::default() };
323		let val = Val::Charge { tip: self.tip, who: who.clone(), fee };
324		Ok((validity, val, origin))
325	}
326
327	fn prepare(
328		self,
329		val: Self::Val,
330		_origin: &<T::RuntimeCall as Dispatchable>::RuntimeOrigin,
331		call: &T::RuntimeCall,
332		info: &DispatchInfoOf<T::RuntimeCall>,
333		_len: usize,
334	) -> Result<Self::Pre, TransactionValidityError> {
335		match val {
336			Val::Charge { tip, who, fee } => {
337				// Mutating call of `withdraw_fee` to actually charge for the transaction.
338				let (_fee, initial_payment) = self.withdraw_fee(&who, call, info, fee)?;
339				Ok(Pre::Charge { tip, who, initial_payment, weight: self.weight(call) })
340			},
341			Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
342		}
343	}
344
345	fn post_dispatch_details(
346		pre: Self::Pre,
347		info: &DispatchInfoOf<T::RuntimeCall>,
348		post_info: &PostDispatchInfoOf<T::RuntimeCall>,
349		len: usize,
350		_result: &DispatchResult,
351	) -> Result<Weight, TransactionValidityError> {
352		let (tip, who, initial_payment, extension_weight) = match pre {
353			Pre::Charge { tip, who, initial_payment, weight } =>
354				(tip, who, initial_payment, weight),
355			Pre::NoCharge { refund } => {
356				// No-op: Refund everything
357				return Ok(refund)
358			},
359		};
360
361		match initial_payment {
362			InitialPayment::Native(already_withdrawn) => {
363				// Take into account the weight used by this extension before calculating the
364				// refund.
365				let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_native();
366				let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
367				let mut actual_post_info = *post_info;
368				actual_post_info.refund(unspent_weight);
369				let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
370					len as u32,
371					info,
372					&actual_post_info,
373					tip,
374				);
375				T::OnChargeTransaction::correct_and_deposit_fee(
376					&who,
377					info,
378					&actual_post_info,
379					actual_fee,
380					tip,
381					already_withdrawn,
382				)?;
383				pallet_transaction_payment::Pallet::<T>::deposit_fee_paid_event(
384					who, actual_fee, tip,
385				);
386				Ok(unspent_weight)
387			},
388			InitialPayment::Asset((asset_id, already_withdrawn)) => {
389				// Take into account the weight used by this extension before calculating the
390				// refund.
391				let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_asset();
392				let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
393				let mut actual_post_info = *post_info;
394				actual_post_info.refund(unspent_weight);
395				let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
396					len as u32,
397					info,
398					&actual_post_info,
399					tip,
400				);
401				let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
402					&who,
403					info,
404					&actual_post_info,
405					actual_fee,
406					tip,
407					asset_id.clone(),
408					already_withdrawn,
409				)?;
410
411				Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
412					who,
413					actual_fee: converted_fee,
414					tip,
415					asset_id,
416				});
417
418				Ok(unspent_weight)
419			},
420			InitialPayment::Nothing => {
421				// `actual_fee` should be zero here for any signed extrinsic. It would be
422				// non-zero here in case of unsigned extrinsics as they don't pay fees but
423				// `compute_actual_fee` is not aware of them. In both cases it's fine to just
424				// move ahead without adjusting the fee, though, so we do nothing.
425				debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
426				Ok(extension_weight
427					.saturating_sub(<T as Config>::WeightInfo::charge_asset_tx_payment_zero()))
428			},
429		}
430	}
431}