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