Pay tx fee with assets by using the asset conversion pallet (#14340)

* Pay tx by swapping the assets

* Change liquidity structure

* Uncomment the event

* Update frame/transaction-payment/asset-tx-payment/src/payment.rs

Co-authored-by: Squirrel <gilescope@gmail.com>

* New approach

* Fix bounds

* Clearer version

* Change IsType with Into and From

* Enable event

* Check ED + fix the logic

* Add temp comments

* Rework the refund

* Clean up

* Improve readability

* Getting closer

* fix

* Use fungible instead of Currency

* Test account without ed

* Final push

* Fixed

* Rename to pallet-asset-conversion-tx-payment

* Bring back the old pallet

* Update versions

* Update docs

* Update readme

* Wrong readme updated

* Revert back doc change

* Fix import

* Fix kitchensink

* Fix

* One more time..

* Wait pls

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Squirrel <gilescope@gmail.com>

* Update frame/support/src/traits/tokens/fungibles/regular.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update docs/comments

* Docs improvement

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Payed -> paid

* Docs

* Update frame/transaction-payment/asset-conversion-tx-payment/README.md

Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com>

* Rewrite docs

* Try to clean the deps

* Add debug assert

* Return back frame-benchmarking

* Update cargo

* Update frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Rename

* clearer error message

* Docs for Pay by Swap (#14445)

* docs

* better error name

* more comments

* more docs on swap trait

* Fix compile errors

* Another fix

* Refactoring

* Update frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Emit an error if we fail to swap the refund back

* Add integrity_test

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Fmt

* Use defensive_ok_or

* child PR: Tidy swap event (#14441)

* Dedup raising swap event

* use expect rather than unwrap

* Additional checks for future defence.

* cargo fmt

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

---------

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

---------

Co-authored-by: Squirrel <gilescope@gmail.com>
Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Jegor Sidorenko
2023-06-23 21:17:52 +02:00
committed by GitHub
parent 3e2c73dfad
commit be7c654c42
21 changed files with 1803 additions and 85 deletions
+22
View File
@@ -4048,6 +4048,7 @@ dependencies = [
"node-primitives",
"pallet-alliance",
"pallet-asset-conversion",
"pallet-asset-conversion-tx-payment",
"pallet-asset-rate",
"pallet-asset-tx-payment",
"pallet-assets",
@@ -5398,6 +5399,7 @@ dependencies = [
"node-primitives",
"node-rpc",
"pallet-asset-conversion",
"pallet-asset-conversion-tx-payment",
"pallet-asset-tx-payment",
"pallet-assets",
"pallet-balances",
@@ -5680,6 +5682,7 @@ dependencies = [
"node-executor",
"node-primitives",
"pallet-asset-conversion",
"pallet-asset-conversion-tx-payment",
"pallet-asset-tx-payment",
"pallet-assets",
"pallet-transaction-payment",
@@ -5993,6 +5996,25 @@ dependencies = [
"sp-std",
]
[[package]]
name = "pallet-asset-conversion-tx-payment"
version = "4.0.0-dev"
dependencies = [
"frame-support",
"frame-system",
"pallet-asset-conversion",
"pallet-assets",
"pallet-balances",
"pallet-transaction-payment",
"parity-scale-codec",
"scale-info",
"sp-core",
"sp-io",
"sp-runtime",
"sp-std",
"sp-storage",
]
[[package]]
name = "pallet-asset-rate"
version = "4.0.0-dev"
+1
View File
@@ -169,6 +169,7 @@ members = [
"frame/system/rpc/runtime-api",
"frame/timestamp",
"frame/transaction-payment",
"frame/transaction-payment/asset-conversion-tx-payment",
"frame/transaction-payment/asset-tx-payment",
"frame/transaction-payment/rpc",
"frame/transaction-payment/rpc/runtime-api",
+2 -1
View File
@@ -90,7 +90,8 @@ frame-system-rpc-runtime-api = { version = "4.0.0-dev", path = "../../../frame/s
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" }
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets/" }
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment/" }
pallet-asset-conversion-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-conversion-tx-payment" }
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" }
# node-specific dependencies
+3 -2
View File
@@ -98,7 +98,7 @@ pub fn create_extrinsic(
)),
frame_system::CheckNonce::<kitchensink_runtime::Runtime>::from(nonce),
frame_system::CheckWeight::<kitchensink_runtime::Runtime>::new(),
pallet_asset_tx_payment::ChargeAssetTxPayment::<kitchensink_runtime::Runtime>::from(
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<kitchensink_runtime::Runtime>::from(
tip, None,
),
);
@@ -815,7 +815,8 @@ mod tests {
let check_era = frame_system::CheckEra::from(Era::Immortal);
let check_nonce = frame_system::CheckNonce::from(index);
let check_weight = frame_system::CheckWeight::new();
let tx_payment = pallet_asset_tx_payment::ChargeAssetTxPayment::from(0, None);
let tx_payment =
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::from(0, None);
let extra = (
check_non_zero_sender,
check_spec_version,
+4 -1
View File
@@ -121,7 +121,8 @@ pallet-treasury = { version = "4.0.0-dev", default-features = false, path = "../
pallet-utility = { version = "4.0.0-dev", default-features = false, path = "../../../frame/utility" }
pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-payment" }
pallet-transaction-payment-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-payment/rpc/runtime-api/" }
pallet-asset-tx-payment = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-payment/asset-tx-payment/" }
pallet-asset-conversion-tx-payment = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-payment/asset-conversion-tx-payment" }
pallet-asset-tx-payment = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-transaction-storage = { version = "4.0.0-dev", default-features = false, path = "../../../frame/transaction-storage" }
pallet-uniques = { version = "4.0.0-dev", default-features = false, path = "../../../frame/uniques" }
pallet-vesting = { version = "4.0.0-dev", default-features = false, path = "../../../frame/vesting" }
@@ -137,6 +138,7 @@ std = [
"pallet-whitelist/std",
"pallet-offences-benchmarking?/std",
"pallet-election-provider-support-benchmarking?/std",
"pallet-asset-conversion-tx-payment/std",
"pallet-asset-tx-payment/std",
"frame-system-benchmarking?/std",
"frame-election-provider-support/std",
@@ -355,6 +357,7 @@ try-runtime = [
"pallet-asset-rate/try-runtime",
"pallet-utility/try-runtime",
"pallet-transaction-payment/try-runtime",
"pallet-asset-conversion-tx-payment/try-runtime",
"pallet-asset-tx-payment/try-runtime",
"pallet-transaction-storage/try-runtime",
"pallet-uniques/try-runtime",
+10 -2
View File
@@ -493,6 +493,13 @@ impl pallet_asset_tx_payment::Config for Runtime {
>;
}
impl pallet_asset_conversion_tx_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Fungibles = Assets;
type OnChargeAssetTransaction =
pallet_asset_conversion_tx_payment::AssetConversionAdapter<Balances, AssetConversion>;
}
parameter_types! {
pub const MinimumPeriod: Moment = SLOT_DURATION / 2;
}
@@ -1292,7 +1299,7 @@ where
frame_system::CheckEra::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_asset_tx_payment::ChargeAssetTxPayment::<Runtime>::from(tip, None),
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<Runtime>::from(tip, None),
);
let raw_payload = SignedPayload::new(call, extra)
.map_err(|e| {
@@ -1876,6 +1883,7 @@ construct_runtime!(
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
AssetTxPayment: pallet_asset_tx_payment,
AssetConversionTxPayment: pallet_asset_conversion_tx_payment,
ElectionProviderMultiPhase: pallet_election_provider_multi_phase,
Staking: pallet_staking,
Session: pallet_session,
@@ -1960,7 +1968,7 @@ pub type SignedExtra = (
frame_system::CheckEra<Runtime>,
frame_system::CheckNonce<Runtime>,
frame_system::CheckWeight<Runtime>,
pallet_asset_tx_payment::ChargeAssetTxPayment<Runtime>,
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment<Runtime>,
);
/// Unchecked extrinsic type as expected by this runtime.
+1
View File
@@ -24,6 +24,7 @@ node-primitives = { version = "2.0.0", path = "../primitives" }
kitchensink-runtime = { version = "3.0.0-dev", path = "../runtime" }
pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" }
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets" }
pallet-asset-conversion-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-conversion-tx-payment" }
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
sc-block-builder = { version = "0.10.0-dev", path = "../../../client/block-builder" }
+1 -1
View File
@@ -77,7 +77,7 @@ pub fn signed_extra(nonce: Index, extra_fee: Balance) -> SignedExtra {
frame_system::CheckEra::from(Era::mortal(256, 0)),
frame_system::CheckNonce::from(nonce),
frame_system::CheckWeight::new(),
pallet_asset_tx_payment::ChargeAssetTxPayment::from(extra_fee, None),
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::from(extra_fee, None),
)
}
+3 -5
View File
@@ -13,12 +13,13 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] }
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true }
scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" }
sp-core = { version = "21.0.0", default-features = false, path = "../../primitives/core" }
sp-io = { version = "23.0.0", default-features = false, path = "../../primitives/io" }
sp-std = { version = "8.0.0", default-features = false, path = "../../primitives/std" }
sp-runtime = { version = "24.0.0", default-features = false, path = "../../primitives/runtime" }
@@ -28,9 +29,6 @@ sp-arithmetic = { version = "16.0.0", default-features = false, path = "../../pr
pallet-balances = { version = "4.0.0-dev", path = "../balances" }
pallet-assets = { version = "4.0.0-dev", path = "../assets" }
primitive-types = { version = "0.12.0", default-features = false, features = ["codec", "scale-info", "num-traits"] }
sp-std = { version = "8.0.0", path = "../../primitives/std" }
sp-core = { version = "21.0.0", path = "../../primitives/core" }
sp-io = { version = "23.0.0", path = "../../primitives/io" }
[features]
default = ["std"]
+124 -56
View File
@@ -53,7 +53,7 @@
//! (This can be run against the kitchen sync node in the `node` folder of this repo.)
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::traits::Incrementable;
use frame_support::traits::{DefensiveOption, Incrementable};
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
@@ -307,7 +307,10 @@ pub mod pallet {
WrongDesiredAmount,
/// Provided amount should be greater than or equal to the existential deposit/asset's
/// minimal amount.
AmountLessThanMinimal,
AmountOneLessThanMinimal,
/// Provided amount should be greater than or equal to the existential deposit/asset's
/// minimal amount.
AmountTwoLessThanMinimal,
/// Reserve needs to always be greater than or equal to the existential deposit/asset's
/// minimal amount.
ReserveLeftLessThanMinimal,
@@ -347,6 +350,20 @@ pub mod pallet {
PathError,
/// The provided path must consists of unique assets.
NonUniquePath,
/// Unable to find an element in an array/vec that should have one-to-one correspondence
/// with another. For example, an array of assets constituting a `path` should have a
/// corresponding array of `amounts` along the path.
CorrespondenceError,
}
#[pallet::hooks]
impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {
fn integrity_test() {
assert!(
T::MaxSwapPathLength::get() > 1,
"the `MaxSwapPathLength` should be greater than 1",
);
}
}
/// Pallet's callable functions.
@@ -488,9 +505,9 @@ pub mod pallet {
}
Self::validate_minimal_amount(amount1.saturating_add(reserve1), &asset1)
.map_err(|_| Error::<T>::AmountLessThanMinimal)?;
.map_err(|_| Error::<T>::AmountOneLessThanMinimal)?;
Self::validate_minimal_amount(amount2.saturating_add(reserve2), &asset2)
.map_err(|_| Error::<T>::AmountLessThanMinimal)?;
.map_err(|_| Error::<T>::AmountTwoLessThanMinimal)?;
Self::transfer(&asset1, &sender, &pool_account, amount1, true)?;
Self::transfer(&asset2, &sender, &pool_account, amount2, true)?;
@@ -635,16 +652,7 @@ pub mod pallet {
let amount_out = *amounts.last().expect("Has always more than 1 element");
ensure!(amount_out >= amount_out_min, Error::<T>::ProvidedMinimumNotSufficientForSwap);
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(())
}
@@ -676,21 +684,13 @@ pub mod pallet {
let amount_in = *amounts.first().expect("Always has more than one element");
ensure!(amount_in <= amount_in_max, Error::<T>::ProvidedMaximumNotSufficientForSwap);
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
/// Transfer an `amount` of `asset_id`, respecting the `keep_alive` requirements.
fn transfer(
asset_id: &T::MultiAssetId,
from: &T::AccountId,
@@ -709,8 +709,13 @@ pub mod pallet {
true => Preserve,
false => Expendable,
};
let amount = Self::asset_to_native(amount)?;
Ok(Self::native_to_asset(T::Currency::transfer(from, to, amount, preservation)?)?)
let amount = Self::convert_asset_balance_to_native_balance(amount)?;
Ok(Self::convert_native_balance_to_asset_balance(T::Currency::transfer(
from,
to,
amount,
preservation,
)?)?)
} else {
T::Assets::transfer(
T::MultiAssetIdConverter::try_convert(&asset_id)
@@ -723,31 +728,40 @@ pub mod pallet {
}
}
pub(crate) fn native_to_asset(amount: T::Balance) -> Result<T::AssetBalance, Error<T>> {
/// Convert a `Balance` type to an `AssetBalance`.
pub(crate) fn convert_native_balance_to_asset_balance(
amount: T::Balance,
) -> Result<T::AssetBalance, Error<T>> {
T::HigherPrecisionBalance::from(amount)
.try_into()
.map_err(|_| Error::<T>::Overflow)
}
pub(crate) fn asset_to_native(amount: T::AssetBalance) -> Result<T::Balance, Error<T>> {
/// Convert an `AssetBalance` type to a `Balance`.
pub(crate) fn convert_asset_balance_to_native_balance(
amount: T::AssetBalance,
) -> Result<T::Balance, Error<T>> {
T::HigherPrecisionBalance::from(amount)
.try_into()
.map_err(|_| Error::<T>::Overflow)
}
/// Swap assets along a `path`, depositing in `send_to`.
pub(crate) fn do_swap(
sender: &T::AccountId,
sender: T::AccountId,
amounts: &Vec<T::AssetBalance>,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
send_to: &T::AccountId,
path: BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
send_to: T::AccountId,
keep_alive: bool,
) -> Result<(), DispatchError> {
if let Some([asset1, asset2]) = path.get(0..2) {
ensure!(amounts.len() > 1, Error::<T>::CorrespondenceError);
if let Some([asset1, asset2]) = &path.get(0..2) {
let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone());
let pool_account = Self::get_pool_account(&pool_id);
let first_amount = amounts.first().expect("Always has more than one element");
// amounts should always contain a corresponding element to path.
let first_amount = amounts.first().ok_or(Error::<T>::CorrespondenceError)?;
Self::transfer(asset1, sender, &pool_account, *first_amount, keep_alive)?;
Self::transfer(asset1, &sender, &pool_account, *first_amount, keep_alive)?;
let mut i = 0;
let path_len = path.len() as u32;
@@ -757,7 +771,7 @@ pub mod pallet {
let pool_account = Self::get_pool_account(&pool_id);
let amount_out =
amounts.get((i + 1) as usize).ok_or(Error::<T>::PathError)?;
amounts.get((i + 1) as usize).ok_or(Error::<T>::CorrespondenceError)?;
let to = if i < path_len - 2 {
let asset3 = path.get((i + 2) as usize).ok_or(Error::<T>::PathError)?;
@@ -778,6 +792,15 @@ pub mod pallet {
}
i.saturating_inc();
}
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in: *first_amount,
amount_out: *amounts.last().expect("Always has more than 1 element"),
});
} else {
return Err(Error::<T>::InvalidPath.into())
}
Ok(())
}
@@ -793,14 +816,16 @@ pub mod pallet {
.expect("infinite length input; no invalid inputs for type; qed")
}
/// Get the `owner`'s balance of `asset`, which could be the chain's native asset or another
/// fungible. Returns a value in the form of an `AssetBalance`.
fn get_balance(
owner: &T::AccountId,
asset: &T::MultiAssetId,
) -> Result<T::AssetBalance, Error<T>> {
if T::MultiAssetIdConverter::is_native(asset) {
Self::native_to_asset(<<T as Config>::Currency>::reducible_balance(
owner, Expendable, Polite,
))
Self::convert_native_balance_to_asset_balance(
<<T as Config>::Currency>::reducible_balance(owner, Expendable, Polite),
)
} else {
Ok(<<T as Config>::Assets>::reducible_balance(
T::MultiAssetIdConverter::try_convert(asset)
@@ -841,6 +866,7 @@ pub mod pallet {
Ok((balance1, balance2))
}
/// Leading to an amount at the end of a `path`, get the required amounts in.
pub(crate) fn get_amounts_in(
amount_out: &T::AssetBalance,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
@@ -860,6 +886,7 @@ pub mod pallet {
Ok(amounts)
}
/// Following an amount into a `path`, get the corresponding amounts out.
pub(crate) fn get_amounts_out(
amount_in: &T::AssetBalance,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
@@ -969,10 +996,10 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Calculates amount out
/// Calculates amount out.
///
/// Given an input amount of an asset and pair reserves, returns the maximum output amount
/// of the other asset
/// of the other asset.
pub fn get_amount_out(
amount_in: &T::AssetBalance,
reserve_in: &T::AssetBalance,
@@ -1004,10 +1031,10 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Calculates amount in
/// Calculates amount in.
///
/// Given an output amount of an asset and pair reserves, returns a required input amount
/// of the other asset
/// of the other asset.
pub fn get_amount_in(
amount_out: &T::AssetBalance,
reserve_in: &T::AssetBalance,
@@ -1046,6 +1073,7 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Ensure that a `value` meets the minimum balance requirements of an `asset` class.
fn validate_minimal_amount(
value: T::AssetBalance,
asset: &T::MultiAssetId,
@@ -1064,6 +1092,7 @@ pub mod pallet {
Ok(())
}
/// Ensure that a path is valid.
fn validate_swap_path(
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
) -> Result<(), DispatchError> {
@@ -1092,7 +1121,7 @@ pub mod pallet {
}
impl<T: Config>
frame_support::traits::tokens::fungibles::SwapForNative<
frame_support::traits::tokens::fungibles::SwapNative<
T::RuntimeOrigin,
T::AccountId,
T::Balance,
@@ -1103,7 +1132,11 @@ where
<T as pallet::Config>::Currency:
frame_support::traits::tokens::fungible::Inspect<<T as frame_system::Config>::AccountId>,
{
// If successful returns the amount in.
/// Take an `asset_id` and swap some amount for `amount_out` of the chain's native asset. If an
/// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be
/// too costly.
///
/// If successful returns the amount of the `asset_id` taken to provide `amount_out`.
fn swap_tokens_for_exact_native(
sender: T::AccountId,
asset_id: T::AssetId,
@@ -1121,26 +1154,59 @@ where
path.push(T::MultiAssetIdConverter::get_native());
let path = path.try_into().unwrap();
let amount_out = Self::native_to_asset(amount_out)?;
// convert `amount_out` from native balance type, to asset balance type
let amount_out = Self::convert_native_balance_to_asset_balance(amount_out)?;
// calculate the amount we need to provide
let amounts = Self::get_amounts_in(&amount_out, &path)?;
let amount_in = *amounts.first().expect("Always has more than one element");
let amount_in =
*amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?;
if let Some(amount_in_max) = amount_in_max {
ensure!(amount_in <= amount_in_max, Error::<T>::ProvidedMaximumNotSufficientForSwap);
}
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(amount_in)
}
/// Take an `asset_id` and swap `amount_in` of the chain's native asset for it. If an
/// `amount_out_min` is specified, it will return an error if it is unable to acquire the amount
/// desired.
///
/// If successful, returns the amount of `asset_id` acquired for the `amount_in`.
fn swap_exact_native_for_tokens(
sender: T::AccountId,
asset_id: T::AssetId,
amount_in: T::Balance,
amount_out_min: Option<T::AssetBalance>,
send_to: T::AccountId,
keep_alive: bool,
) -> Result<T::AssetBalance, DispatchError> {
ensure!(amount_in > Zero::zero(), Error::<T>::ZeroAmount);
if let Some(amount_out_min) = amount_out_min {
ensure!(amount_out_min > Zero::zero(), Error::<T>::ZeroAmount);
}
let mut path = sp_std::vec::Vec::new();
path.push(T::MultiAssetIdConverter::get_native());
path.push(T::MultiAssetIdConverter::into_multiasset_id(&asset_id));
let path = path.try_into().expect(
"`MaxSwapPathLength` is ensured by to be greater than 2; pushed only twice; qed",
);
// convert `amount_in` from native balance type, to asset balance type
let amount_in = Self::convert_native_balance_to_asset_balance(amount_in)?;
// calculate the amount we should receive
let amounts = Self::get_amounts_out(&amount_in, &path)?;
let amount_out =
*amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?;
if let Some(amount_out_min) = amount_out_min {
ensure!(amount_out >= amount_out_min, Error::<T>::ProvidedMaximumNotSufficientForSwap);
}
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(amount_out)
}
}
sp_api::decl_runtime_apis! {
@@ -1167,3 +1233,5 @@ sp_api::decl_runtime_apis! {
fn get_reserves(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)>;
}
}
sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $);
@@ -356,7 +356,7 @@ fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() {
1,
user
),
Error::<Test>::AmountLessThanMinimal
Error::<Test>::AmountOneLessThanMinimal
);
assert_noop!(
@@ -36,5 +36,5 @@ pub use hold::{
pub use imbalance::{Credit, Debt, HandleImbalanceDrop, Imbalance};
pub use lifetime::{Create, Destroy};
pub use regular::{
Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, SwapForNative, Unbalanced,
Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, SwapNative, Unbalanced,
};
@@ -584,9 +584,16 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> {
fn done_withdraw(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {}
}
/// Use an on-chain exchange to convert the asset to the equivalent in native tokens.
pub trait SwapForNative<Origin, AccountId, Balance, AssetBalance, AssetId> {
// If successful returns the amount in native tokens.
/// Trait for providing methods to swap between the chain's native token and other asset classes.
pub trait SwapNative<Origin, AccountId, Balance, AssetBalance, AssetId> {
/// Take an `asset_id` and swap some amount for `amount_out` of the chain's native asset. If an
/// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be
/// too costly.
///
/// Withdraws `asset_id` from `sender`, deposits native asset to `send_to`, respecting
/// `keep_alive`.
///
/// If successful returns the amount of the `asset_id` taken to provide `amount_out`.
fn swap_tokens_for_exact_native(
sender: AccountId,
asset_id: AssetId,
@@ -595,4 +602,21 @@ pub trait SwapForNative<Origin, AccountId, Balance, AssetBalance, AssetId> {
send_to: AccountId,
keep_alive: bool,
) -> Result<AssetBalance, DispatchError>;
/// Take an `asset_id` and swap `amount_in` of the chain's native asset for it. If an
/// `amount_out_min` is specified, it will return an error if it is unable to acquire the amount
/// desired.
///
/// Withdraws native asset from `sender`, deposits `asset_id` to `send_to`, respecting
/// `keep_alive`.
///
/// If successful, returns the amount of `asset_id` acquired for the `amount_in`.
fn swap_exact_native_for_tokens(
sender: AccountId,
asset_id: AssetId,
amount_in: Balance,
amount_out_min: Option<AssetBalance>,
send_to: AccountId,
keep_alive: bool,
) -> Result<AssetBalance, DispatchError>;
}
@@ -0,0 +1,46 @@
[package]
name = "pallet-asset-conversion-tx-payment"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "Pallet to manage transaction payments in assets by converting them to native assets."
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
# Substrate dependencies
sp-runtime = { version = "24.0.0", default-features = false, path = "../../../primitives/runtime" }
sp-std = { version = "8.0.0", default-features = false, path = "../../../primitives/std" }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" }
pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, path = ".." }
codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] }
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
[dev-dependencies]
sp-core = { version = "21.0.0", default-features = false, path = "../../../primitives/core" }
sp-io = { version = "23.0.0", default-features = false, path = "../../../primitives/io" }
sp-storage = { version = "13.0.0", default-features = false, path = "../../../primitives/storage" }
pallet-assets = { version = "4.0.0-dev", path = "../../assets" }
pallet-balances = { version = "4.0.0-dev", path = "../../balances" }
pallet-asset-conversion = { version = "4.0.0-dev", path = "../../asset-conversion" }
[features]
default = ["std"]
std = [
"scale-info/std",
"codec/std",
"sp-std/std",
"sp-runtime/std",
"frame-support/std",
"frame-system/std",
"sp-io/std",
"sp-core/std",
"pallet-transaction-payment/std",
]
try-runtime = ["frame-support/try-runtime"]
@@ -0,0 +1,21 @@
# pallet-asset-conversion-tx-payment
## Asset Conversion Transaction Payment Pallet
This pallet allows runtimes that include it to pay for transactions in assets other than the
native token of the chain.
### Overview
It does this by extending transactions to include an optional `AssetId` that specifies the asset
to be used for payment (defaulting to the native token on `None`). It expects an
[`OnChargeAssetTransaction`] implementation analogously to [`pallet-transaction-payment`]. The
included [`AssetConversionAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the fee
amount by converting the fee calculated by [`pallet-transaction-payment`] into the desired
asset.
### Integration
This pallet wraps FRAME's transaction payment pallet and functions as a replacement. This means
you should include both pallets in your `construct_runtime` macro, but only include this
pallet's [`SignedExtension`] ([`ChargeAssetTxPayment`]).
License: Apache-2.0
@@ -0,0 +1,351 @@
// 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.
//! # Asset Conversion Transaction Payment Pallet
//!
//! This pallet allows runtimes that include it to pay for transactions in assets other than the
//! chain's native asset.
//!
//! ## Overview
//!
//! This pallet provides a `SignedExtension` with an optional `AssetId` that specifies the asset
//! to be used for payment (defaulting to the native token on `None`). It expects an
//! [`OnChargeAssetTransaction`] implementation analogous to [`pallet-transaction-payment`]. The
//! included [`AssetConversionAdapter`] (implementing [`OnChargeAssetTransaction`]) determines the
//! fee amount by converting the fee calculated by [`pallet-transaction-payment`] in the native
//! asset into the amount required of the specified asset.
//!
//! ## Pallet API
//!
//! This pallet does not have any dispatchable calls or storage. It wraps FRAME's Transaction
//! Payment pallet and functions as a replacement. This means you should include both pallets in
//! your `construct_runtime` macro, but only include this pallet's [`SignedExtension`]
//! ([`ChargeAssetTxPayment`]).
//!
//! ## Terminology
//!
//! - Native Asset or Native Currency: The asset that a chain considers native, as in its default
//! for transaction fee payment, deposits, inflation, etc.
//! - Other assets: Other assets that may exist on chain, for example under the Assets pallet.
#![cfg_attr(not(feature = "std"), no_std)]
use sp_std::prelude::*;
use codec::{Decode, Encode};
use frame_support::{
dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
traits::{
tokens::fungibles::{Balanced, Inspect},
IsType,
},
DefaultNoBound,
};
use pallet_transaction_payment::OnChargeTransaction;
use scale_info::TypeInfo;
use sp_runtime::{
traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero},
transaction_validity::{
InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,
},
FixedPointOperand,
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod payment;
pub use payment::*;
/// Type aliases used for interaction with `OnChargeTransaction`.
pub(crate) type OnChargeTransactionOf<T> =
<T as pallet_transaction_payment::Config>::OnChargeTransaction;
/// Balance type alias for balances of the chain's native asset.
pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
/// Liquidity info type alias.
pub(crate) type LiquidityInfoOf<T> =
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
/// Balance type alias for balances of assets that implement the `fungibles` trait.
pub(crate) type AssetBalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
/// Type alias for Asset IDs.
pub(crate) type AssetIdOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
/// Type alias for the interaction of balances with `OnChargeAssetTransaction`.
pub(crate) type ChargeAssetBalanceOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::Balance;
/// Type alias for Asset IDs in their interaction with `OnChargeAssetTransaction`.
pub(crate) type ChargeAssetIdOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::AssetId;
/// Liquidity info type alias for interaction with `OnChargeAssetTransaction`.
pub(crate) type ChargeAssetLiquidityOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
/// Used to pass the initial payment info from pre- to post-dispatch.
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
pub enum InitialPayment<T: Config> {
/// No initial fee was paid.
#[default]
Nothing,
/// The initial fee was paid in the native currency.
Native(LiquidityInfoOf<T>),
/// The initial fee was paid in an asset.
Asset((LiquidityInfoOf<T>, BalanceOf<T>, AssetBalanceOf<T>)),
}
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The fungibles instance used to pay for transactions in assets.
type Fungibles: Balanced<Self::AccountId>;
/// The actual transaction charging logic that charges the fees.
type OnChargeAssetTransaction: OnChargeAssetTransaction<Self>;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A transaction fee `actual_fee`, of which `tip` was added to the minimum inclusion fee,
/// has been paid by `who` in an asset `asset_id`.
AssetTxFeePaid {
who: T::AccountId,
actual_fee: AssetBalanceOf<T>,
tip: BalanceOf<T>,
asset_id: ChargeAssetIdOf<T>,
},
/// A swap of the refund in native currency back to asset failed.
AssetRefundFailed { native_amount_kept: BalanceOf<T> },
}
}
/// Require payment for transaction inclusion and optionally include a tip to gain additional
/// priority in the queue. Allows paying via both `Currency` as well as `fungibles::Balanced`.
///
/// Wraps the transaction logic in [`pallet_transaction_payment`] and extends it with assets.
/// An asset ID of `None` falls back to the underlying transaction payment logic via the native
/// currency.
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeAssetTxPayment<T: Config> {
#[codec(compact)]
tip: BalanceOf<T>,
asset_id: Option<ChargeAssetIdOf<T>>,
}
impl<T: Config> ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
AssetBalanceOf<T>: Send + Sync + FixedPointOperand,
BalanceOf<T>: Send
+ Sync
+ FixedPointOperand
+ Into<ChargeAssetBalanceOf<T>>
+ From<ChargeAssetLiquidityOf<T>>,
ChargeAssetIdOf<T>: Send + Sync,
{
/// Utility constructor. Used only in client/factory code.
pub fn from(tip: BalanceOf<T>, asset_id: Option<ChargeAssetIdOf<T>>) -> Self {
Self { tip, asset_id }
}
/// Fee withdrawal logic that dispatches to either `OnChargeAssetTransaction` or
/// `OnChargeTransaction`.
fn withdraw_fee(
&self,
who: &T::AccountId,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
if fee.is_zero() {
Ok((fee, InitialPayment::Nothing))
} else if let Some(asset_id) = self.asset_id {
T::OnChargeAssetTransaction::withdraw_fee(
who,
call,
info,
asset_id,
fee.into(),
self.tip.into(),
)
.map(|(used_for_fee, received_exchanged, asset_consumed)| {
(
fee,
InitialPayment::Asset((
used_for_fee.into(),
received_exchanged.into(),
asset_consumed.into(),
)),
)
})
} else {
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee(
who, call, info, fee, self.tip,
)
.map(|i| (fee, InitialPayment::Native(i)))
.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
}
}
}
impl<T: Config> sp_std::fmt::Debug for ChargeAssetTxPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}
impl<T: Config> SignedExtension for ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
AssetBalanceOf<T>: Send + Sync + FixedPointOperand,
BalanceOf<T>: Send
+ Sync
+ From<u64>
+ FixedPointOperand
+ Into<ChargeAssetBalanceOf<T>>
+ Into<ChargeAssetLiquidityOf<T>>
+ From<ChargeAssetLiquidityOf<T>>,
ChargeAssetIdOf<T>: Send + Sync,
{
const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
type AccountId = T::AccountId;
type Call = T::RuntimeCall;
type AdditionalSigned = ();
type Pre = (
// tip
BalanceOf<T>,
// who paid the fee
Self::AccountId,
// imbalance resulting from withdrawing the fee
InitialPayment<T>,
// asset_id for the transaction payment
Option<ChargeAssetIdOf<T>>,
);
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {
Ok(())
}
fn validate(
&self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
use pallet_transaction_payment::ChargeTransactionPayment;
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, fee);
Ok(ValidTransaction { priority, ..Default::default() })
}
fn pre_dispatch(
self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?;
Ok((self.tip, who.clone(), initial_payment, self.asset_id))
}
fn post_dispatch(
pre: Option<Self::Pre>,
info: &DispatchInfoOf<Self::Call>,
post_info: &PostDispatchInfoOf<Self::Call>,
len: usize,
result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
if let Some((tip, who, initial_payment, asset_id)) = pre {
match initial_payment {
InitialPayment::Native(already_withdrawn) => {
debug_assert!(
asset_id.is_none(),
"For that payment type the `asset_id` should be None"
);
pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch(
Some((tip, who, already_withdrawn)),
info,
post_info,
len,
result,
)?;
},
InitialPayment::Asset(already_withdrawn) => {
debug_assert!(
asset_id.is_some(),
"For that payment type the `asset_id` should be set"
);
let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
len as u32, info, post_info, tip,
);
if let Some(asset_id) = asset_id {
let (used_for_fee, received_exchanged, asset_consumed) = already_withdrawn;
let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
&who,
info,
post_info,
actual_fee.into(),
tip.into(),
used_for_fee.into(),
received_exchanged.into(),
asset_id,
asset_consumed.into(),
)?;
Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
who,
actual_fee: converted_fee,
tip,
asset_id,
});
}
},
InitialPayment::Nothing => {
// `actual_fee` should be zero here for any signed extrinsic. It would be
// non-zero here in case of unsigned extrinsics as they don't pay fees but
// `compute_actual_fee` is not aware of them. In both cases it's fine to just
// move ahead without adjusting the fee, though, so we do nothing.
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
},
}
}
Ok(())
}
}
@@ -0,0 +1,274 @@
// 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.
use super::*;
use crate as pallet_asset_conversion_tx_payment;
use codec;
use frame_support::{
dispatch::DispatchClass,
instances::Instance2,
ord_parameter_types,
pallet_prelude::*,
parameter_types,
traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, ConstU8, Imbalance, OnUnbalanced},
weights::{Weight, WeightToFee as WeightToFeeT},
PalletId,
};
use frame_system as system;
use frame_system::{EnsureRoot, EnsureSignedBy};
use pallet_asset_conversion::{NativeOrAssetId, NativeOrAssetIdConverter};
use pallet_transaction_payment::CurrencyAdapter;
use sp_core::H256;
use sp_runtime::{
testing::Header,
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, SaturatedConversion},
Permill,
};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
type Block = frame_system::mocking::MockBlock<Runtime>;
type Balance = u64;
type AccountId = u64;
frame_support::construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: system,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
Assets: pallet_assets,
PoolAssets: pallet_assets::<Instance2>,
AssetConversion: pallet_asset_conversion,
AssetTxPayment: pallet_asset_conversion_tx_payment,
}
);
parameter_types! {
pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero();
}
pub struct BlockWeights;
impl Get<frame_system::limits::BlockWeights> for BlockWeights {
fn get() -> frame_system::limits::BlockWeights {
frame_system::limits::BlockWeights::builder()
.base_block(Weight::zero())
.for_class(DispatchClass::all(), |weights| {
weights.base_extrinsic = ExtrinsicBaseWeight::get().into();
})
.for_class(DispatchClass::non_mandatory(), |weights| {
weights.max_total = Weight::from_parts(1024, u64::MAX).into();
})
.build_or_panic()
}
}
parameter_types! {
pub static WeightToFee: u64 = 1;
pub static TransactionByteFee: u64 = 1;
}
impl frame_system::Config for Runtime {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = BlockWeights;
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type Index = u64;
type BlockNumber = u64;
type RuntimeCall = RuntimeCall;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
parameter_types! {
pub const ExistentialDeposit: u64 = 10;
}
impl pallet_balances::Config for Runtime {
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<10>;
type AccountStore = System;
type MaxLocks = ();
type WeightInfo = ();
type MaxReserves = ConstU32<50>;
type ReserveIdentifier = [u8; 8];
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type MaxHolds = ();
}
impl WeightToFeeT for WeightToFee {
type Balance = u64;
fn weight_to_fee(weight: &Weight) -> Self::Balance {
Self::Balance::saturated_from(weight.ref_time())
.saturating_mul(WEIGHT_TO_FEE.with(|v| *v.borrow()))
}
}
impl WeightToFeeT for TransactionByteFee {
type Balance = u64;
fn weight_to_fee(weight: &Weight) -> Self::Balance {
Self::Balance::saturated_from(weight.ref_time())
.saturating_mul(TRANSACTION_BYTE_FEE.with(|v| *v.borrow()))
}
}
parameter_types! {
pub(crate) static TipUnbalancedAmount: u64 = 0;
pub(crate) static FeeUnbalancedAmount: u64 = 0;
}
pub struct DealWithFees;
impl OnUnbalanced<pallet_balances::NegativeImbalance<Runtime>> for DealWithFees {
fn on_unbalanceds<B>(
mut fees_then_tips: impl Iterator<Item = pallet_balances::NegativeImbalance<Runtime>>,
) {
if let Some(fees) = fees_then_tips.next() {
FeeUnbalancedAmount::mutate(|a| *a += fees.peek());
if let Some(tips) = fees_then_tips.next() {
TipUnbalancedAmount::mutate(|a| *a += tips.peek());
}
}
}
}
impl pallet_transaction_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type OnChargeTransaction = CurrencyAdapter<Balances, DealWithFees>;
type WeightToFee = WeightToFee;
type LengthToFee = TransactionByteFee;
type FeeMultiplierUpdate = ();
type OperationalFeeMultiplier = ConstU8<5>;
}
type AssetId = u32;
impl pallet_assets::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type AssetId = AssetId;
type AssetIdParameter = codec::Compact<AssetId>;
type Currency = Balances;
type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
type ForceOrigin = EnsureRoot<AccountId>;
type AssetDeposit = ConstU64<2>;
type AssetAccountDeposit = ConstU64<2>;
type MetadataDepositBase = ConstU64<0>;
type MetadataDepositPerByte = ConstU64<0>;
type ApprovalDeposit = ConstU64<0>;
type StringLimit = ConstU32<20>;
type Freezer = ();
type Extra = ();
type CallbackHandle = ();
type WeightInfo = ();
type RemoveItemsLimit = ConstU32<1000>;
pallet_assets::runtime_benchmarks_enabled! {
type BenchmarkHelper = ();
}
}
impl pallet_assets::Config<Instance2> for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = u64;
type RemoveItemsLimit = ConstU32<1000>;
type AssetId = u32;
type AssetIdParameter = u32;
type Currency = Balances;
type CreateOrigin = AsEnsureOriginWithArg<EnsureSignedBy<AssetConversionOrigin, u64>>;
type ForceOrigin = frame_system::EnsureRoot<u64>;
type AssetDeposit = ConstU64<0>;
type AssetAccountDeposit = ConstU64<0>;
type MetadataDepositBase = ConstU64<0>;
type MetadataDepositPerByte = ConstU64<0>;
type ApprovalDeposit = ConstU64<0>;
type StringLimit = ConstU32<50>;
type Freezer = ();
type Extra = ();
type WeightInfo = ();
type CallbackHandle = ();
pallet_assets::runtime_benchmarks_enabled! {
type BenchmarkHelper = ();
}
}
parameter_types! {
pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon");
pub storage AllowMultiAssetPools: bool = false;
// should be non-zero if AllowMultiAssetPools is true, otherwise can be zero
pub storage LiquidityWithdrawalFee: Permill = Permill::from_percent(0);
pub const MaxSwapPathLength: u32 = 4;
}
ord_parameter_types! {
pub const AssetConversionOrigin: u64 = AccountIdConversion::<u64>::into_account_truncating(&AssetConversionPalletId::get());
}
impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type AssetBalance = <Self as pallet_balances::Config>::Balance;
type AssetId = u32;
type PoolAssetId = u32;
type Assets = Assets;
type PoolAssets = PoolAssets;
type PalletId = AssetConversionPalletId;
type WeightInfo = ();
type LPFee = ConstU32<3>; // means 0.3%
type PoolSetupFee = ConstU64<100>; // should be more or equal to the existential deposit
type PoolSetupFeeReceiver = AssetConversionOrigin;
type LiquidityWithdrawalFee = LiquidityWithdrawalFee;
type AllowMultiAssetPools = AllowMultiAssetPools;
type MaxSwapPathLength = MaxSwapPathLength;
type MintMinLiquidity = ConstU64<100>; // 100 is good enough when the main currency has 12 decimals.
type Balance = u64;
type HigherPrecisionBalance = u128;
type MultiAssetId = NativeOrAssetId<u32>;
type MultiAssetIdConverter = NativeOrAssetIdConverter<u32>;
pallet_asset_conversion::runtime_benchmarks_enabled! {
type BenchmarkHelper = ();
}
}
impl Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Fungibles = Assets;
type OnChargeAssetTransaction = AssetConversionAdapter<Balances, AssetConversion>;
}
@@ -0,0 +1,191 @@
// 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 codec::FullCodec;
use frame_support::{
ensure,
traits::{fungible::Inspect, fungibles::SwapNative, tokens::Balance},
unsigned::TransactionValidityError,
};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{DispatchInfoOf, MaybeSerializeDeserialize, PostDispatchInfoOf, Zero},
transaction_validity::InvalidTransaction,
Saturating,
};
use sp_std::{fmt::Debug, marker::PhantomData};
/// Handle withdrawing, refunding and depositing of transaction fees.
pub trait OnChargeAssetTransaction<T: Config> {
/// 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: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo;
/// 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<T::RuntimeCall>,
asset_id: Self::AssetId,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<
(LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>),
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<T::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
corrected_fee: Self::Balance,
tip: Self::Balance,
fee_paid: LiquidityInfoOf<T>,
received_exchanged: Self::LiquidityInfo,
asset_id: Self::AssetId,
initial_asset_consumed: AssetBalanceOf<T>,
) -> Result<AssetBalanceOf<T>, TransactionValidityError>;
}
/// Implements the asset transaction for a balance to asset converter (implementing
/// [`SwapNative`]).
///
/// The converter is given the complete fee in terms of the asset used for the transaction.
pub struct AssetConversionAdapter<C, CON>(PhantomData<(C, CON)>);
/// Default implementation for a runtime instantiating this pallet, an asset to native swapper.
impl<T, C, CON> OnChargeAssetTransaction<T> for AssetConversionAdapter<C, CON>
where
T: Config,
C: Inspect<<T as frame_system::Config>::AccountId>,
CON: SwapNative<T::RuntimeOrigin, T::AccountId, BalanceOf<T>, AssetBalanceOf<T>, AssetIdOf<T>>,
AssetIdOf<T>: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo,
BalanceOf<T>: IsType<<C as Inspect<<T as frame_system::Config>::AccountId>>::Balance>,
{
type Balance = BalanceOf<T>;
type AssetId = AssetIdOf<T>;
type LiquidityInfo = BalanceOf<T>;
/// 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<T::RuntimeCall>,
asset_id: Self::AssetId,
fee: BalanceOf<T>,
tip: BalanceOf<T>,
) -> Result<
(LiquidityInfoOf<T>, Self::LiquidityInfo, AssetBalanceOf<T>),
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_native(
who.clone(),
asset_id,
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
<T::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip)
.map(|r| (r, native_asset_required, asset_consumed))
}
/// 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<T::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
corrected_fee: BalanceOf<T>,
tip: BalanceOf<T>,
fee_paid: LiquidityInfoOf<T>,
received_exchanged: Self::LiquidityInfo,
asset_id: Self::AssetId,
initial_asset_consumed: AssetBalanceOf<T>,
) -> Result<AssetBalanceOf<T>, TransactionValidityError> {
// Refund the native asset to the account that paid the fees (`who`).
// The `who` account will receive the "fee_paid - corrected_fee" refund.
<T::OnChargeTransaction>::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_native_for_tokens(
who.clone(), // we already deposited the native to `who`
asset_id, // 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,
None => {
Pallet::<T>::deposit_event(Event::<T>::AssetRefundFailed {
native_amount_kept: swap_back,
});
},
}
}
let actual_paid = initial_asset_consumed.saturating_sub(asset_refund);
Ok(actual_paid)
}
}
@@ -0,0 +1,708 @@
// 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.
use super::*;
use frame_support::{
assert_ok,
dispatch::{DispatchInfo, PostDispatchInfo},
pallet_prelude::*,
traits::{fungible::Inspect, fungibles::Mutate},
weights::Weight,
};
use frame_system as system;
use mock::{ExtrinsicBaseWeight, *};
use pallet_asset_conversion::NativeOrAssetId;
use pallet_balances::Call as BalancesCall;
use sp_runtime::traits::StaticLookup;
const CALL: &<Runtime as frame_system::Config>::RuntimeCall =
&RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 });
pub struct ExtBuilder {
balance_factor: u64,
base_weight: Weight,
byte_fee: u64,
weight_to_fee: u64,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
balance_factor: 1,
base_weight: Weight::from_parts(0, 0),
byte_fee: 1,
weight_to_fee: 1,
}
}
}
impl ExtBuilder {
pub fn base_weight(mut self, base_weight: Weight) -> Self {
self.base_weight = base_weight;
self
}
pub fn balance_factor(mut self, factor: u64) -> Self {
self.balance_factor = factor;
self
}
fn set_constants(&self) {
ExtrinsicBaseWeight::mutate(|v| *v = self.base_weight);
TRANSACTION_BYTE_FEE.with(|v| *v.borrow_mut() = self.byte_fee);
WEIGHT_TO_FEE.with(|v| *v.borrow_mut() = self.weight_to_fee);
}
pub fn build(self) -> sp_io::TestExternalities {
self.set_constants();
let mut t = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
pallet_balances::GenesisConfig::<Runtime> {
balances: if self.balance_factor > 0 {
vec![
(1, 10 * self.balance_factor),
(2, 20 * self.balance_factor),
(3, 30 * self.balance_factor),
(4, 40 * self.balance_factor),
(5, 50 * self.balance_factor),
(6, 60 * self.balance_factor),
]
} else {
vec![]
},
}
.assimilate_storage(&mut t)
.unwrap();
t.into()
}
}
/// create a transaction info struct from weight. Handy to avoid building the whole struct.
pub fn info_from_weight(w: Weight) -> DispatchInfo {
// pays_fee: Pays::Yes -- class: DispatchClass::Normal
DispatchInfo { weight: w, ..Default::default() }
}
fn post_info_from_weight(w: Weight) -> PostDispatchInfo {
PostDispatchInfo { actual_weight: Some(w), pays_fee: Default::default() }
}
fn info_from_pays(p: Pays) -> DispatchInfo {
DispatchInfo { pays_fee: p, ..Default::default() }
}
fn post_info_from_pays(p: Pays) -> PostDispatchInfo {
PostDispatchInfo { actual_weight: None, pays_fee: p }
}
fn default_post_info() -> PostDispatchInfo {
PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }
}
fn setup_lp(asset_id: u32, balance_factor: u64) {
let lp_provider = 5;
assert_ok!(Balances::force_set_balance(
RuntimeOrigin::root(),
lp_provider,
10_000 * balance_factor
));
let lp_provider_account = <Runtime as system::Config>::Lookup::unlookup(lp_provider);
assert_ok!(Assets::mint_into(asset_id.into(), &lp_provider_account, 10_000 * balance_factor));
let token_1 = NativeOrAssetId::Native;
let token_2 = NativeOrAssetId::Asset(asset_id);
assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(lp_provider), token_1, token_2));
assert_ok!(AssetConversion::add_liquidity(
RuntimeOrigin::signed(lp_provider),
token_1,
token_2,
1_000 * balance_factor, // 1 desired
10_000 * balance_factor, // 2 desired
1, // 1 min
1, // 2 min
lp_provider_account,
));
}
const WEIGHT_5: Weight = Weight::from_parts(5, 0);
const WEIGHT_50: Weight = Weight::from_parts(50, 0);
const WEIGHT_100: Weight = Weight::from_parts(100, 0);
#[test]
fn transaction_payment_in_native_possible() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
let len = 10;
let pre = ChargeAssetTxPayment::<Runtime>::from(0, None)
.pre_dispatch(&1, CALL, &info_from_weight(WEIGHT_5), len)
.unwrap();
let initial_balance = 10 * balance_factor;
assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_5),
&default_post_info(),
len,
&Ok(())
));
assert_eq!(Balances::free_balance(1), initial_balance - 5 - 5 - 10);
let pre = ChargeAssetTxPayment::<Runtime>::from(5 /* tipped */, None)
.pre_dispatch(&2, CALL, &info_from_weight(WEIGHT_100), len)
.unwrap();
let initial_balance_for_2 = 20 * balance_factor;
assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 100 - 5);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_100),
&post_info_from_weight(WEIGHT_50),
len,
&Ok(())
));
assert_eq!(Balances::free_balance(2), initial_balance_for_2 - 5 - 10 - 50 - 5);
});
}
#[test]
fn transaction_payment_in_asset_possible() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 2;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance
));
// mint into the caller account
let caller = 1;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let len = 10;
let tx_weight = 5;
setup_lp(asset_id, balance_factor);
let fee_in_native = base_weight + tx_weight + len as u64;
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
NativeOrAssetId::Asset(asset_id),
NativeOrAssetId::Native,
fee_in_native,
true,
);
assert_eq!(input_quote, Some(201));
let fee_in_asset = input_quote.unwrap();
assert_eq!(Assets::balance(asset_id, caller), balance);
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
.unwrap();
// assert that native balance is not used
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
// check that fee was charged in the given asset
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_5), // estimated tx weight
&default_post_info(), // weight actually used == estimated
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
assert_eq!(TipUnbalancedAmount::get(), 0);
assert_eq!(FeeUnbalancedAmount::get(), fee_in_native);
});
}
#[test]
fn transaction_payment_in_asset_fails_if_no_pool_for_that_asset() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 2;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance
));
// mint into the caller account
let caller = 1;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let len = 10;
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id)).pre_dispatch(
&caller,
CALL,
&info_from_weight(WEIGHT_5),
len,
);
// As there is no pool in the dex set up for this asset, conversion should fail.
assert!(pre.is_err());
});
}
#[test]
fn transaction_payment_without_fee() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
let caller = 1;
// create the asset
let asset_id = 1;
let balance = 1000;
let min_balance = 2;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance,
));
setup_lp(asset_id, balance_factor);
// mint into the caller account
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let weight = 5;
let len = 10;
let fee_in_native = base_weight + weight + len as u64;
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
NativeOrAssetId::Asset(asset_id),
NativeOrAssetId::Native,
fee_in_native,
true,
);
assert_eq!(input_quote, Some(201));
let fee_in_asset = input_quote.unwrap();
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
.unwrap();
// assert that native balance is not used
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
// check that fee was charged in the given asset
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
let refund = AssetConversion::quote_price_exact_tokens_for_tokens(
NativeOrAssetId::Native,
NativeOrAssetId::Asset(asset_id),
fee_in_native,
true,
)
.unwrap();
assert_eq!(refund, 199);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_5),
&post_info_from_pays(Pays::No),
len,
&Ok(())
));
// caller should get refunded
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund);
assert_eq!(Balances::free_balance(caller), 10 * balance_factor);
});
}
#[test]
fn asset_transaction_payment_with_tip_and_refund() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 2;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance,
));
setup_lp(asset_id, balance_factor);
// mint into the caller account
let caller = 2;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 10000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let weight = 100;
let tip = 5;
let len = 10;
let fee_in_native = base_weight + weight + len as u64 + tip;
let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens(
NativeOrAssetId::Asset(asset_id),
NativeOrAssetId::Native,
fee_in_native,
true,
);
assert_eq!(input_quote, Some(1206));
let fee_in_asset = input_quote.unwrap();
let pre = ChargeAssetTxPayment::<Runtime>::from(tip, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_100), len)
.unwrap();
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
let final_weight = 50;
let expected_fee = fee_in_native - final_weight - tip;
let expected_token_refund = AssetConversion::quote_price_exact_tokens_for_tokens(
NativeOrAssetId::Native,
NativeOrAssetId::Asset(asset_id),
fee_in_native - expected_fee - tip,
true,
)
.unwrap();
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_100),
&post_info_from_weight(WEIGHT_50),
len,
&Ok(())
));
assert_eq!(TipUnbalancedAmount::get(), tip);
assert_eq!(FeeUnbalancedAmount::get(), expected_fee);
// caller should get refunded
assert_eq!(
Assets::balance(asset_id, caller),
balance - fee_in_asset + expected_token_refund
);
assert_eq!(Balances::free_balance(caller), 20 * balance_factor);
});
}
#[test]
fn payment_from_account_with_only_assets() {
let base_weight = 5;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 2;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance,
));
setup_lp(asset_id, balance_factor);
// mint into the caller account
let caller = 333;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
// assert that native balance is not necessary
assert_eq!(Balances::free_balance(caller), 0);
let weight = 5;
let len = 10;
let fee_in_native = base_weight + weight + len as u64;
let ed = Balances::minimum_balance();
let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens(
NativeOrAssetId::Asset(asset_id),
NativeOrAssetId::Native,
fee_in_native + ed,
true,
)
.unwrap();
assert_eq!(fee_in_asset, 301);
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_weight(WEIGHT_5), len)
.unwrap();
assert_eq!(Balances::free_balance(caller), ed);
// check that fee was charged in the given asset
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
let refund = AssetConversion::quote_price_exact_tokens_for_tokens(
NativeOrAssetId::Native,
NativeOrAssetId::Asset(asset_id),
ed,
true,
)
.unwrap();
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(WEIGHT_5),
&default_post_info(),
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset + refund);
assert_eq!(Balances::free_balance(caller), 0);
assert_eq!(TipUnbalancedAmount::get(), 0);
assert_eq!(FeeUnbalancedAmount::get(), fee_in_native);
});
}
#[test]
fn converted_fee_is_never_zero_if_input_fee_is_not() {
let base_weight = 1;
let balance_factor = 100;
ExtBuilder::default()
.balance_factor(balance_factor)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 1;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance
));
setup_lp(asset_id, balance_factor);
// mint into the caller account
let caller = 2;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let weight = 1;
let len = 1;
// there will be no conversion when the fee is zero
{
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len)
.unwrap();
// `Pays::No` implies there are no fees
assert_eq!(Assets::balance(asset_id, caller), balance);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_pays(Pays::No),
&post_info_from_pays(Pays::No),
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance);
}
// validate even a small fee gets converted to asset.
let fee_in_native = base_weight + weight + len as u64;
let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens(
NativeOrAssetId::Asset(asset_id),
NativeOrAssetId::Native,
fee_in_native,
true,
)
.unwrap();
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_weight(Weight::from_parts(weight, 0)), len)
.unwrap();
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_weight(Weight::from_parts(weight, 0)),
&default_post_info(),
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset);
});
}
#[test]
fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() {
let base_weight = 1;
ExtBuilder::default()
.balance_factor(100)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 100;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance
));
// mint into the caller account
let caller = 333;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let weight = 1;
let len = 1;
let fee = base_weight + weight + len as u64;
// calculated fee is greater than 0
assert!(fee > 0);
let pre = ChargeAssetTxPayment::<Runtime>::from(0, Some(asset_id))
.pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len)
.unwrap();
// `Pays::No` implies no pre-dispatch fees
assert_eq!(Assets::balance(asset_id, caller), balance);
let (_tip, _who, initial_payment, _asset_id) = &pre;
let not_paying = match initial_payment {
&InitialPayment::Nothing => true,
_ => false,
};
assert!(not_paying, "initial payment should be Nothing if we pass Pays::No");
// `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the
// initial fee)
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
Some(pre),
&info_from_pays(Pays::No),
&post_info_from_pays(Pays::Yes),
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance);
});
}
#[test]
fn post_dispatch_fee_is_zero_if_unsigned_pre_dispatch_fee_is_zero() {
let base_weight = 1;
ExtBuilder::default()
.balance_factor(100)
.base_weight(Weight::from_parts(base_weight, 0))
.build()
.execute_with(|| {
// create the asset
let asset_id = 1;
let min_balance = 100;
assert_ok!(Assets::force_create(
RuntimeOrigin::root(),
asset_id.into(),
42, /* owner */
true, /* is_sufficient */
min_balance
));
// mint into the caller account
let caller = 333;
let beneficiary = <Runtime as system::Config>::Lookup::unlookup(caller);
let balance = 1000;
assert_ok!(Assets::mint_into(asset_id.into(), &beneficiary, balance));
assert_eq!(Assets::balance(asset_id, caller), balance);
let weight = 1;
let len = 1;
ChargeAssetTxPayment::<Runtime>::pre_dispatch_unsigned(
CALL,
&info_from_weight(Weight::from_parts(weight, 0)),
len,
)
.unwrap();
assert_eq!(Assets::balance(asset_id, caller), balance);
// `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the
// initial fee)
assert_ok!(ChargeAssetTxPayment::<Runtime>::post_dispatch(
None,
&info_from_weight(Weight::from_parts(weight, 0)),
&post_info_from_pays(Pays::Yes),
len,
&Ok(())
));
assert_eq!(Assets::balance(asset_id, caller), balance);
});
}
@@ -67,17 +67,17 @@ mod tests;
mod payment;
pub use payment::*;
// Type aliases used for interaction with `OnChargeTransaction`.
/// Type aliases used for interaction with `OnChargeTransaction`.
pub(crate) type OnChargeTransactionOf<T> =
<T as pallet_transaction_payment::Config>::OnChargeTransaction;
// Balance type alias.
/// Balance type alias.
pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
// Liquity info type alias.
/// Liquidity info type alias.
pub(crate) type LiquidityInfoOf<T> =
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
// Type alias used for interaction with fungibles (assets).
// Balance type alias.
/// Type alias used for interaction with fungibles (assets).
/// Balance type alias.
pub(crate) type AssetBalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
/// Asset id type alias.
@@ -85,25 +85,25 @@ pub(crate) type AssetIdOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
// Type aliases used for interaction with `OnChargeAssetTransaction`.
// Balance type alias.
/// Balance type alias.
pub(crate) type ChargeAssetBalanceOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::Balance;
// Asset id type alias.
/// Asset id type alias.
pub(crate) type ChargeAssetIdOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::AssetId;
// Liquity info type alias.
/// Liquidity info type alias.
pub(crate) type ChargeAssetLiquidityOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
/// Used to pass the initial payment info from pre- to post-dispatch.
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
pub enum InitialPayment<T: Config> {
/// No initial fee was payed.
/// No initial fee was paid.
#[default]
Nothing,
/// The initial fee was payed in the native currency.
/// The initial fee was paid in the native currency.
Native(LiquidityInfoOf<T>),
/// The initial fee was payed in an asset.
/// The initial fee was paid in an asset.
Asset(Credit<T::AccountId, T::Fungibles>),
}
@@ -126,7 +126,7 @@ where
.max(min_converted_fee);
let can_withdraw =
<T::Fungibles as Inspect<T::AccountId>>::can_withdraw(asset_id, who, converted_fee);
if !matches!(can_withdraw, WithdrawConsequence::Success) {
if can_withdraw != WithdrawConsequence::Success {
return Err(InvalidTransaction::Payment.into())
}
<T::Fungibles as Balanced<T::AccountId>>::withdraw(