referrerpolicy=no-referrer-when-downgrade

pallet_transaction_payment/
payment.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18/// ! Traits and default implementation for paying transaction fees.
19use crate::{Config, Pallet, TxPaymentCredit, LOG_TARGET};
20
21use codec::{DecodeWithMemTracking, FullCodec, MaxEncodedLen};
22use core::marker::PhantomData;
23use frame_support::{
24	traits::{
25		fungible::{Balanced, Credit, Inspect},
26		tokens::{Precision, WithdrawConsequence},
27		Currency, ExistenceRequirement, Imbalance, NoDrop, OnUnbalanced, SuppressedDrop,
28		WithdrawReasons,
29	},
30	unsigned::TransactionValidityError,
31};
32use scale_info::TypeInfo;
33use sp_runtime::{
34	traits::{CheckedSub, DispatchInfoOf, PostDispatchInfoOf, Saturating, Zero},
35	transaction_validity::InvalidTransaction,
36};
37
38type NegativeImbalanceOf<C, T> =
39	<C as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;
40
41/// Handle withdrawing, refunding and depositing of transaction fees.
42pub trait OnChargeTransaction<T: Config>: TxCreditHold<T> {
43	/// The underlying integer type in which fees are calculated.
44	type Balance: frame_support::traits::tokens::Balance;
45
46	type LiquidityInfo: Default;
47
48	/// Before the transaction is executed the payment of the transaction fees
49	/// need to be secured.
50	///
51	/// Returns the tip credit
52	fn withdraw_fee(
53		who: &T::AccountId,
54		call: &T::RuntimeCall,
55		dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
56		fee_with_tip: Self::Balance,
57		tip: Self::Balance,
58	) -> Result<Self::LiquidityInfo, TransactionValidityError>;
59
60	/// Check if the predicted fee from the transaction origin can be withdrawn.
61	fn can_withdraw_fee(
62		who: &T::AccountId,
63		call: &T::RuntimeCall,
64		dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
65		fee_with_tip: Self::Balance,
66		tip: Self::Balance,
67	) -> Result<(), TransactionValidityError>;
68
69	/// After the transaction was executed the actual fee can be calculated.
70	/// This function should refund any overpaid fees and optionally deposit
71	/// the corrected amount.
72	///
73	/// Note: The `fee` already includes the `tip`.
74	fn correct_and_deposit_fee(
75		who: &T::AccountId,
76		dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
77		post_info: &PostDispatchInfoOf<T::RuntimeCall>,
78		corrected_fee_with_tip: Self::Balance,
79		tip: Self::Balance,
80		liquidity_info: Self::LiquidityInfo,
81	) -> Result<(), TransactionValidityError>;
82
83	#[cfg(feature = "runtime-benchmarks")]
84	fn endow_account(who: &T::AccountId, amount: Self::Balance);
85
86	#[cfg(feature = "runtime-benchmarks")]
87	fn minimum_balance() -> Self::Balance;
88}
89
90/// Needs to be implemented for every [`OnChargeTransaction`].
91///
92/// Cannot be added to `OnChargeTransaction` directly as this would
93/// cause cycles in trait resolution.
94pub trait TxCreditHold<T: Config> {
95	/// The credit that is used to represent the withdrawn transaction fees.
96	///
97	/// The pallet will put this into a temporary storage item in order to
98	/// make it available to other pallets during tx application.
99	///
100	/// Is only used within a transaction. Hence changes to the encoding of this
101	/// type **won't** require a storage migration.
102	///
103	/// Set to `()` if your `OnChargeTransaction` impl does not store the credit.
104	type Credit: FullCodec + DecodeWithMemTracking + MaxEncodedLen + TypeInfo + SuppressedDrop;
105}
106
107/// Implements transaction payment for a pallet implementing the [`frame_support::traits::fungible`]
108/// trait (eg. pallet_balances) using an unbalance handler (implementing
109/// [`OnUnbalanced`]).
110///
111/// The unbalance handler is given 2 unbalanceds in [`OnUnbalanced::on_unbalanceds`]: `fee` and
112/// then `tip`.
113pub struct FungibleAdapter<F, OU>(PhantomData<(F, OU)>);
114
115impl<T, F, OU> OnChargeTransaction<T> for FungibleAdapter<F, OU>
116where
117	T: Config,
118	T::OnChargeTransaction: TxCreditHold<T, Credit = NoDrop<Credit<T::AccountId, F>>>,
119	F: Balanced<T::AccountId> + 'static,
120	OU: OnUnbalanced<<Self::Credit as SuppressedDrop>::Inner>,
121{
122	type LiquidityInfo = Option<<Self::Credit as SuppressedDrop>::Inner>;
123	type Balance = <F as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
124
125	fn withdraw_fee(
126		who: &<T>::AccountId,
127		_call: &<T>::RuntimeCall,
128		_dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>,
129		fee_with_tip: Self::Balance,
130		tip: Self::Balance,
131	) -> Result<Self::LiquidityInfo, TransactionValidityError> {
132		if fee_with_tip.is_zero() {
133			return Ok(None)
134		}
135
136		let credit = F::withdraw(
137			who,
138			fee_with_tip,
139			Precision::Exact,
140			frame_support::traits::tokens::Preservation::Preserve,
141			frame_support::traits::tokens::Fortitude::Polite,
142		)
143		.map_err(|_| InvalidTransaction::Payment)?;
144
145		let (tip_credit, inclusion_fee) = credit.split(tip);
146
147		<Pallet<T>>::deposit_txfee(inclusion_fee);
148
149		Ok(Some(tip_credit))
150	}
151
152	fn can_withdraw_fee(
153		who: &T::AccountId,
154		_call: &T::RuntimeCall,
155		_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
156		fee_with_tip: Self::Balance,
157		_tip: Self::Balance,
158	) -> Result<(), TransactionValidityError> {
159		if fee_with_tip.is_zero() {
160			return Ok(())
161		}
162
163		match F::can_withdraw(who, fee_with_tip) {
164			WithdrawConsequence::Success => Ok(()),
165			_ => Err(InvalidTransaction::Payment.into()),
166		}
167	}
168
169	fn correct_and_deposit_fee(
170		who: &<T>::AccountId,
171		_dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>,
172		_post_info: &PostDispatchInfoOf<<T>::RuntimeCall>,
173		corrected_fee_with_tip: Self::Balance,
174		tip: Self::Balance,
175		tip_credit: Self::LiquidityInfo,
176	) -> Result<(), TransactionValidityError> {
177		let corrected_fee = corrected_fee_with_tip.saturating_sub(tip);
178
179		let remaining_credit = <TxPaymentCredit<T>>::take()
180			.map(|stored_credit| stored_credit.into_inner())
181			.unwrap_or_default();
182
183		// If pallets take away too much it makes the transaction invalid. They need to make
184		// sure that this does not happen. We do not invalide the transaction because we already
185		// executed it and we rather collect too little fees than none at all.
186		if remaining_credit.peek() < corrected_fee {
187			log::error!(target: LOG_TARGET, "Not enough balance on hold to pay tx fees. This is a bug.");
188		}
189
190		// skip refund if account was killed by the tx
191		let fee_credit = if frame_system::Pallet::<T>::account_exists(who) {
192			let (mut fee_credit, refund_credit) = remaining_credit.split(corrected_fee);
193			// resolve might fail if refund is below the ed and account
194			// is kept alive by other providers
195			if !refund_credit.peek().is_zero() {
196				if let Err(not_refunded) = F::resolve(who, refund_credit) {
197					fee_credit.subsume(not_refunded);
198				}
199			}
200			fee_credit
201		} else {
202			remaining_credit
203		};
204
205		OU::on_unbalanceds(Some(fee_credit).into_iter().chain(tip_credit));
206
207		Ok(())
208	}
209
210	#[cfg(feature = "runtime-benchmarks")]
211	fn endow_account(who: &T::AccountId, amount: Self::Balance) {
212		let _ = F::deposit(who, amount, Precision::BestEffort);
213	}
214
215	#[cfg(feature = "runtime-benchmarks")]
216	fn minimum_balance() -> Self::Balance {
217		F::minimum_balance()
218	}
219}
220
221impl<T, F, OU> TxCreditHold<T> for FungibleAdapter<F, OU>
222where
223	T: Config,
224	F: Balanced<T::AccountId> + 'static,
225{
226	type Credit = NoDrop<Credit<<T as frame_system::Config>::AccountId, F>>;
227}
228
229/// Implements the transaction payment for a pallet implementing the [`Currency`]
230/// trait (eg. the pallet_balances) using an unbalance handler (implementing
231/// [`OnUnbalanced`]).
232///
233/// The unbalance handler is given 2 unbalanceds in [`OnUnbalanced::on_unbalanceds`]: `fee` and
234/// then `tip`.
235#[deprecated(
236	note = "Please use the fungible trait and FungibleAdapter. This struct will be removed some time after March 2024."
237)]
238pub struct CurrencyAdapter<C, OU>(PhantomData<(C, OU)>);
239
240/// Default implementation for a Currency and an OnUnbalanced handler.
241///
242/// The unbalance handler is given 2 unbalanceds in [`OnUnbalanced::on_unbalanceds`]: `fee` and
243/// then `tip`.
244#[allow(deprecated)]
245impl<T, C, OU> OnChargeTransaction<T> for CurrencyAdapter<C, OU>
246where
247	T: Config,
248	C: Currency<<T as frame_system::Config>::AccountId>,
249	C::PositiveImbalance: Imbalance<
250		<C as Currency<<T as frame_system::Config>::AccountId>>::Balance,
251		Opposite = C::NegativeImbalance,
252	>,
253	C::NegativeImbalance: Imbalance<
254		<C as Currency<<T as frame_system::Config>::AccountId>>::Balance,
255		Opposite = C::PositiveImbalance,
256	>,
257	OU: OnUnbalanced<NegativeImbalanceOf<C, T>>,
258{
259	type LiquidityInfo = Option<NegativeImbalanceOf<C, T>>;
260	type Balance = <C as Currency<<T as frame_system::Config>::AccountId>>::Balance;
261
262	/// Withdraw the predicted fee from the transaction origin.
263	///
264	/// Note: The `fee` already includes the `tip`.
265	fn withdraw_fee(
266		who: &T::AccountId,
267		_call: &T::RuntimeCall,
268		_info: &DispatchInfoOf<T::RuntimeCall>,
269		fee_with_tip: Self::Balance,
270		tip: Self::Balance,
271	) -> Result<Self::LiquidityInfo, TransactionValidityError> {
272		if fee_with_tip.is_zero() {
273			return Ok(None)
274		}
275
276		let withdraw_reason = if tip.is_zero() {
277			WithdrawReasons::TRANSACTION_PAYMENT
278		} else {
279			WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
280		};
281
282		match C::withdraw(who, fee_with_tip, withdraw_reason, ExistenceRequirement::KeepAlive) {
283			Ok(imbalance) => Ok(Some(imbalance)),
284			Err(_) => Err(InvalidTransaction::Payment.into()),
285		}
286	}
287
288	/// Check if the predicted fee from the transaction origin can be withdrawn.
289	///
290	/// Note: The `fee` already includes the `tip`.
291	fn can_withdraw_fee(
292		who: &T::AccountId,
293		_call: &T::RuntimeCall,
294		_info: &DispatchInfoOf<T::RuntimeCall>,
295		fee_with_tip: Self::Balance,
296		tip: Self::Balance,
297	) -> Result<(), TransactionValidityError> {
298		if fee_with_tip.is_zero() {
299			return Ok(())
300		}
301
302		let withdraw_reason = if tip.is_zero() {
303			WithdrawReasons::TRANSACTION_PAYMENT
304		} else {
305			WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
306		};
307
308		let new_balance = C::free_balance(who)
309			.checked_sub(&fee_with_tip)
310			.ok_or(InvalidTransaction::Payment)?;
311		C::ensure_can_withdraw(who, fee_with_tip, withdraw_reason, new_balance)
312			.map(|_| ())
313			.map_err(|_| InvalidTransaction::Payment.into())
314	}
315
316	/// Hand the fee and the tip over to the `[OnUnbalanced]` implementation.
317	/// Since the predicted fee might have been too high, parts of the fee may
318	/// be refunded.
319	///
320	/// Note: The `corrected_fee` already includes the `tip`.
321	fn correct_and_deposit_fee(
322		who: &T::AccountId,
323		_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
324		_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
325		corrected_fee_with_tip: Self::Balance,
326		tip: Self::Balance,
327		already_withdrawn: Self::LiquidityInfo,
328	) -> Result<(), TransactionValidityError> {
329		if let Some(paid) = already_withdrawn {
330			// Calculate how much refund we should return
331			let refund_amount = paid.peek().saturating_sub(corrected_fee_with_tip);
332			// refund to the the account that paid the fees. If this fails, the
333			// account might have dropped below the existential balance. In
334			// that case we don't refund anything.
335			let refund_imbalance = C::deposit_into_existing(who, refund_amount)
336				.unwrap_or_else(|_| C::PositiveImbalance::zero());
337			// merge the imbalance caused by paying the fees and refunding parts of it again.
338			let adjusted_paid = paid
339				.offset(refund_imbalance)
340				.same()
341				.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
342			// Call someone else to handle the imbalance (fee and tip separately)
343			let (tip, fee) = adjusted_paid.split(tip);
344			OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip)));
345		}
346		Ok(())
347	}
348
349	#[cfg(feature = "runtime-benchmarks")]
350	fn endow_account(who: &T::AccountId, amount: Self::Balance) {
351		let _ = C::deposit_creating(who, amount);
352	}
353
354	#[cfg(feature = "runtime-benchmarks")]
355	fn minimum_balance() -> Self::Balance {
356		C::minimum_balance()
357	}
358}
359
360#[allow(deprecated)]
361impl<T: Config, C, OU> TxCreditHold<T> for CurrencyAdapter<C, OU> {
362	type Credit = ();
363}