referrerpolicy=no-referrer-when-downgrade

pallet_asset_conversion_tx_payment/
payment.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///! Traits and default implementation for paying transaction fees in assets.
17use super::*;
18use crate::Config;
19
20use alloc::vec;
21use core::marker::PhantomData;
22use frame_support::{
23	defensive, ensure,
24	traits::{
25		fungibles,
26		tokens::{Balance, Fortitude, Precision, Preservation, WithdrawConsequence},
27		Defensive, OnUnbalanced, SameOrOther,
28	},
29	unsigned::TransactionValidityError,
30};
31use pallet_asset_conversion::{QuotePrice, SwapCredit};
32use sp_runtime::{
33	traits::{DispatchInfoOf, Get, PostDispatchInfoOf, Zero},
34	transaction_validity::InvalidTransaction,
35	Saturating,
36};
37
38/// Handle withdrawing, refunding and depositing of transaction fees.
39pub trait OnChargeAssetTransaction<T: Config> {
40	/// The underlying integer type in which fees are calculated.
41	type Balance: Balance;
42	/// The type used to identify the assets used for transaction payment.
43	type AssetId: AssetId;
44	/// The type used to store the intermediate values between pre- and post-dispatch.
45	type LiquidityInfo;
46
47	/// Secure the payment of the transaction fees before the transaction is executed.
48	///
49	/// Note: The `fee` already includes the `tip`.
50	fn withdraw_fee(
51		who: &T::AccountId,
52		call: &T::RuntimeCall,
53		dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
54		asset_id: Self::AssetId,
55		fee: Self::Balance,
56		tip: Self::Balance,
57	) -> Result<Self::LiquidityInfo, TransactionValidityError>;
58
59	/// Ensure payment of the transaction fees can be withdrawn.
60	///
61	/// Note: The `fee` already includes the tip.
62	fn can_withdraw_fee(
63		who: &T::AccountId,
64		asset_id: Self::AssetId,
65		fee: Self::Balance,
66	) -> Result<(), TransactionValidityError>;
67
68	/// Refund any overpaid fees and deposit the corrected amount.
69	/// The actual fee gets calculated once the transaction is executed.
70	///
71	/// Note: The `fee` already includes the `tip`.
72	///
73	/// Returns the amount of `asset_id` that was used for the payment.
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: Self::Balance,
79		tip: Self::Balance,
80		asset_id: Self::AssetId,
81		already_withdraw: Self::LiquidityInfo,
82	) -> Result<BalanceOf<T>, TransactionValidityError>;
83}
84
85/// Means to withdraw, correct and deposit fees in the asset accepted by the system.
86///
87/// The type uses the [`SwapCredit`] implementation to swap the asset used by a user for the fee
88/// payment for the asset accepted as a fee payment be the system.
89///
90/// Parameters:
91/// - `A`: The asset identifier that system accepts as a fee payment (eg. native asset).
92/// - `F`: The fungibles registry that can handle assets provided by user and the `A` asset.
93/// - `S`: The swap implementation that can swap assets provided by user for the `A` asset.
94/// - OU: The handler for withdrawn `fee` and `tip`, passed in the respective order to
95///   [OnUnbalanced::on_unbalanceds].
96/// - `T`: The pallet's configuration.
97pub struct SwapAssetAdapter<A, F, S, OU>(PhantomData<(A, F, S, OU)>);
98
99impl<A, F, S, OU, T> OnChargeAssetTransaction<T> for SwapAssetAdapter<A, F, S, OU>
100where
101	A: Get<T::AssetId>,
102	F: fungibles::Balanced<T::AccountId, Balance = BalanceOf<T>, AssetId = T::AssetId>,
103	S: SwapCredit<
104			T::AccountId,
105			Balance = BalanceOf<T>,
106			AssetKind = T::AssetId,
107			Credit = fungibles::Credit<T::AccountId, F>,
108		> + QuotePrice<Balance = BalanceOf<T>, AssetKind = T::AssetId>,
109	OU: OnUnbalanced<fungibles::Credit<T::AccountId, F>>,
110	T: Config,
111{
112	type AssetId = T::AssetId;
113	type Balance = BalanceOf<T>;
114	type LiquidityInfo = (fungibles::Credit<T::AccountId, F>, BalanceOf<T>);
115
116	fn withdraw_fee(
117		who: &T::AccountId,
118		_call: &T::RuntimeCall,
119		_dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>,
120		asset_id: Self::AssetId,
121		fee: Self::Balance,
122		_tip: Self::Balance,
123	) -> Result<Self::LiquidityInfo, TransactionValidityError> {
124		if asset_id == A::get() {
125			// The `asset_id` is the target asset, we do not need to swap.
126			let fee_credit = F::withdraw(
127				asset_id.clone(),
128				who,
129				fee,
130				Precision::Exact,
131				Preservation::Preserve,
132				Fortitude::Polite,
133			)
134			.map_err(|_| InvalidTransaction::Payment)?;
135
136			return Ok((fee_credit, fee));
137		}
138
139		// Quote the amount of the `asset_id` needed to pay the fee in the asset `A`.
140		let asset_fee =
141			S::quote_price_tokens_for_exact_tokens(asset_id.clone(), A::get(), fee, true)
142				.ok_or(InvalidTransaction::Payment)?;
143
144		// Withdraw the `asset_id` credit for the swap.
145		let asset_fee_credit = F::withdraw(
146			asset_id.clone(),
147			who,
148			asset_fee,
149			Precision::Exact,
150			Preservation::Preserve,
151			Fortitude::Polite,
152		)
153		.map_err(|_| InvalidTransaction::Payment)?;
154
155		let (fee_credit, change) = match S::swap_tokens_for_exact_tokens(
156			vec![asset_id, A::get()],
157			asset_fee_credit,
158			fee,
159		) {
160			Ok((fee_credit, change)) => (fee_credit, change),
161			Err((credit_in, _)) => {
162				defensive!("Fee swap should pass for the quoted amount");
163				let _ = F::resolve(who, credit_in).defensive_proof("Should resolve the credit");
164				return Err(InvalidTransaction::Payment.into())
165			},
166		};
167
168		// Since the exact price for `fee` has been quoted, the change should be zero.
169		ensure!(change.peek().is_zero(), InvalidTransaction::Payment);
170
171		Ok((fee_credit, asset_fee))
172	}
173
174	/// Dry run of swap & withdraw the predicted fee from the transaction origin.
175	///
176	/// Note: The `fee` already includes the tip.
177	///
178	/// Returns an error if the total amount in native currency can't be exchanged for `asset_id`.
179	fn can_withdraw_fee(
180		who: &T::AccountId,
181		asset_id: Self::AssetId,
182		fee: BalanceOf<T>,
183	) -> Result<(), TransactionValidityError> {
184		if asset_id == A::get() {
185			// The `asset_id` is the target asset, we do not need to swap.
186			match F::can_withdraw(asset_id.clone(), who, fee) {
187				WithdrawConsequence::BalanceLow |
188				WithdrawConsequence::UnknownAsset |
189				WithdrawConsequence::Underflow |
190				WithdrawConsequence::Overflow |
191				WithdrawConsequence::Frozen =>
192					return Err(TransactionValidityError::from(InvalidTransaction::Payment)),
193				WithdrawConsequence::Success |
194				WithdrawConsequence::ReducedToZero(_) |
195				WithdrawConsequence::WouldDie => return Ok(()),
196			}
197		}
198
199		let asset_fee =
200			S::quote_price_tokens_for_exact_tokens(asset_id.clone(), A::get(), fee, true)
201				.ok_or(InvalidTransaction::Payment)?;
202
203		// Ensure we can withdraw enough `asset_id` for the swap.
204		match F::can_withdraw(asset_id.clone(), who, asset_fee) {
205			WithdrawConsequence::BalanceLow |
206			WithdrawConsequence::UnknownAsset |
207			WithdrawConsequence::Underflow |
208			WithdrawConsequence::Overflow |
209			WithdrawConsequence::Frozen =>
210				return Err(TransactionValidityError::from(InvalidTransaction::Payment)),
211			WithdrawConsequence::Success |
212			WithdrawConsequence::ReducedToZero(_) |
213			WithdrawConsequence::WouldDie => {},
214		};
215
216		Ok(())
217	}
218
219	fn correct_and_deposit_fee(
220		who: &T::AccountId,
221		_dispatch_info: &DispatchInfoOf<<T>::RuntimeCall>,
222		_post_info: &PostDispatchInfoOf<<T>::RuntimeCall>,
223		corrected_fee: Self::Balance,
224		tip: Self::Balance,
225		asset_id: Self::AssetId,
226		already_withdrawn: Self::LiquidityInfo,
227	) -> Result<BalanceOf<T>, TransactionValidityError> {
228		let (fee_paid, initial_asset_consumed) = already_withdrawn;
229		let refund_amount = fee_paid.peek().saturating_sub(corrected_fee);
230		let (fee_in_asset, adjusted_paid) = if refund_amount.is_zero() ||
231			F::total_balance(asset_id.clone(), who).is_zero()
232		{
233			// Nothing to refund or the account was removed be the dispatched function.
234			(initial_asset_consumed, fee_paid)
235		} else if asset_id == A::get() {
236			// The `asset_id` is the target asset, we do not need to swap.
237			let (refund, fee_paid) = fee_paid.split(refund_amount);
238			if let Err(refund) = F::resolve(who, refund) {
239				let fee_paid = fee_paid.merge(refund).map_err(|_| {
240					defensive!("`fee_paid` and `refund` are credits of the same asset.");
241					InvalidTransaction::Payment
242				})?;
243				(initial_asset_consumed, fee_paid)
244			} else {
245				(fee_paid.peek().saturating_sub(refund_amount), fee_paid)
246			}
247		} else {
248			// Check if the refund amount can be swapped back into the asset used by `who` for fee
249			// payment.
250			let refund_asset_amount = S::quote_price_exact_tokens_for_tokens(
251				A::get(),
252				asset_id.clone(),
253				refund_amount,
254				true,
255			)
256			// No refund given if it cannot be swapped back.
257			.unwrap_or(Zero::zero());
258
259			let debt = if refund_asset_amount.is_zero() {
260				fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone())
261			} else {
262				// Deposit the refund before the swap to ensure it can be processed.
263				match F::deposit(asset_id.clone(), &who, refund_asset_amount, Precision::BestEffort)
264				{
265					Ok(debt) => debt,
266					// No refund given since it cannot be deposited.
267					Err(_) => fungibles::Debt::<T::AccountId, F>::zero(asset_id.clone()),
268				}
269			};
270
271			if debt.peek().is_zero() {
272				// No refund given.
273				(initial_asset_consumed, fee_paid)
274			} else {
275				let (refund, adjusted_paid) = fee_paid.split(refund_amount);
276				match S::swap_exact_tokens_for_tokens(
277					vec![A::get(), asset_id],
278					refund,
279					Some(refund_asset_amount),
280				) {
281					Ok(refund_asset) => {
282						match refund_asset.offset(debt) {
283							Ok(SameOrOther::None) => {},
284							// This arm should never be reached, as the  amount of `debt` is
285							// expected to be exactly equal to the amount of `refund_asset` credit.
286							_ => {
287								defensive!("Debt should be equal to the refund credit");
288								return Err(InvalidTransaction::Payment.into())
289							},
290						};
291						(
292							initial_asset_consumed.saturating_sub(refund_asset_amount.into()),
293							adjusted_paid,
294						)
295					},
296					// The error should not occur since swap was quoted before.
297					Err((refund, _)) => {
298						defensive!("Refund swap should pass for the quoted amount");
299						match F::settle(who, debt, Preservation::Expendable) {
300							Ok(dust) => ensure!(dust.peek().is_zero(), InvalidTransaction::Payment),
301							// The error should not occur as the `debt` was just withdrawn above.
302							Err(_) => {
303								defensive!("Should settle the debt");
304								return Err(InvalidTransaction::Payment.into())
305							},
306						};
307						let adjusted_paid = adjusted_paid.merge(refund).map_err(|_| {
308							// The error should never occur since `adjusted_paid` and `refund` are
309							// credits of the same asset.
310							InvalidTransaction::Payment
311						})?;
312						(initial_asset_consumed, adjusted_paid)
313					},
314				}
315			}
316		};
317
318		// Handle the imbalance (fee and tip separately).
319		let (tip, fee) = adjusted_paid.split(tip);
320		OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip)));
321		Ok(fee_in_asset)
322	}
323}