// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. ///! Traits and default implementation for paying transaction fees in assets. use super::*; use crate::Config; use frame_support::{ ensure, traits::{fungible::Inspect, tokens::Balance}, unsigned::TransactionValidityError, }; use pallet_asset_conversion::Swap; use sp_runtime::{ traits::{DispatchInfoOf, Get, PostDispatchInfoOf, Zero}, transaction_validity::InvalidTransaction, Saturating, }; use sp_std::marker::PhantomData; /// Handle withdrawing, refunding and depositing of transaction fees. pub trait OnChargeAssetTransaction { /// The underlying integer type in which fees are calculated. type Balance: Balance; /// The type used to identify the assets used for transaction payment. type AssetId: AssetId; /// The type used to store the intermediate values between pre- and post-dispatch. type LiquidityInfo; /// Secure the payment of the transaction fees before the transaction is executed. /// /// Note: The `fee` already includes the `tip`. fn withdraw_fee( who: &T::AccountId, call: &T::RuntimeCall, dispatch_info: &DispatchInfoOf, asset_id: Self::AssetId, fee: Self::Balance, tip: Self::Balance, ) -> Result< (LiquidityInfoOf, Self::LiquidityInfo, AssetBalanceOf), TransactionValidityError, >; /// Refund any overpaid fees and deposit the corrected amount. /// The actual fee gets calculated once the transaction is executed. /// /// Note: The `fee` already includes the `tip`. /// /// Returns the fee and tip in the asset used for payment as (fee, tip). fn correct_and_deposit_fee( who: &T::AccountId, dispatch_info: &DispatchInfoOf, post_info: &PostDispatchInfoOf, corrected_fee: Self::Balance, tip: Self::Balance, fee_paid: LiquidityInfoOf, received_exchanged: Self::LiquidityInfo, asset_id: Self::AssetId, initial_asset_consumed: AssetBalanceOf, ) -> Result, TransactionValidityError>; } /// Implements the asset transaction for a balance to asset converter (implementing [`Swap`]). /// /// The converter is given the complete fee in terms of the asset used for the transaction. pub struct AssetConversionAdapter(PhantomData<(C, CON, N)>); /// Default implementation for a runtime instantiating this pallet, an asset to native swapper. impl OnChargeAssetTransaction for AssetConversionAdapter where N: Get, T: Config, C: Inspect<::AccountId>, CON: Swap, AssetKind = T::AssetKind>, BalanceOf: Into>, T::AssetKind: From>, BalanceOf: IsType<::AccountId>>::Balance>, { type Balance = BalanceOf; type AssetId = AssetIdOf; type LiquidityInfo = BalanceOf; /// Swap & withdraw the predicted fee from the transaction origin. /// /// Note: The `fee` already includes the `tip`. /// /// Returns the total amount in native currency received by exchanging the `asset_id` and the /// amount in native currency used to pay the fee. fn withdraw_fee( who: &T::AccountId, call: &T::RuntimeCall, info: &DispatchInfoOf, asset_id: Self::AssetId, fee: BalanceOf, tip: BalanceOf, ) -> Result< (LiquidityInfoOf, Self::LiquidityInfo, AssetBalanceOf), TransactionValidityError, > { // convert the asset into native currency let ed = C::minimum_balance(); let native_asset_required = if C::balance(&who) >= ed.saturating_add(fee.into()) { fee } else { fee + ed.into() }; let asset_consumed = CON::swap_tokens_for_exact_tokens( who.clone(), vec![asset_id.into(), N::get()], native_asset_required, None, who.clone(), true, ) .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; ensure!(asset_consumed > Zero::zero(), InvalidTransaction::Payment); // charge the fee in native currency ::withdraw_fee(who, call, info, fee, tip) .map(|r| (r, native_asset_required, asset_consumed.into())) } /// Correct the fee and swap the refund back to asset. /// /// Note: The `corrected_fee` already includes the `tip`. /// Note: Is the ED wasn't needed, the `received_exchanged` will be equal to `fee_paid`, or /// `fee_paid + ed` otherwise. fn correct_and_deposit_fee( who: &T::AccountId, dispatch_info: &DispatchInfoOf, post_info: &PostDispatchInfoOf, corrected_fee: BalanceOf, tip: BalanceOf, fee_paid: LiquidityInfoOf, received_exchanged: Self::LiquidityInfo, asset_id: Self::AssetId, initial_asset_consumed: AssetBalanceOf, ) -> Result, TransactionValidityError> { // Refund the native asset to the account that paid the fees (`who`). // The `who` account will receive the "fee_paid - corrected_fee" refund. ::correct_and_deposit_fee( who, dispatch_info, post_info, corrected_fee, tip, fee_paid, )?; // calculate the refund in native asset, to swap back to the desired `asset_id` let swap_back = received_exchanged.saturating_sub(corrected_fee); let mut asset_refund = Zero::zero(); if !swap_back.is_zero() { // If this fails, the account might have dropped below the existential balance or there // is not enough liquidity left in the pool. In that case we don't throw an error and // the account will keep the native currency. match CON::swap_exact_tokens_for_tokens( who.clone(), // we already deposited the native to `who` vec![ N::get(), // we provide the native asset_id.into(), // we want asset_id back ], swap_back, // amount of the native asset to convert to `asset_id` None, // no minimum amount back who.clone(), // we will refund to `who` false, // no need to keep alive ) .ok() { Some(acquired) => { asset_refund = acquired .try_into() .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; }, None => { Pallet::::deposit_event(Event::::AssetRefundFailed { native_amount_kept: swap_back, }); }, } } let actual_paid = initial_asset_consumed.saturating_sub(asset_refund); Ok(actual_paid) } }