From 81d125fe2a938ba336716c8b1d5e0faff0487d50 Mon Sep 17 00:00:00 2001 From: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> Date: Fri, 23 Jun 2023 21:17:52 +0200 Subject: [PATCH] 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 * 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 * 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 * 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 Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: Muharem Ismailov Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- Cargo.lock | 22 + Cargo.toml | 1 + bin/node/cli/Cargo.toml | 3 +- bin/node/cli/src/service.rs | 5 +- bin/node/runtime/Cargo.toml | 5 +- bin/node/runtime/src/lib.rs | 12 +- bin/node/testing/Cargo.toml | 1 + bin/node/testing/src/keyring.rs | 2 +- frame/asset-conversion/Cargo.toml | 8 +- frame/asset-conversion/src/lib.rs | 178 +++-- frame/asset-conversion/src/tests.rs | 2 +- .../src/traits/tokens/fungibles/mod.rs | 2 +- .../src/traits/tokens/fungibles/regular.rs | 30 +- .../asset-conversion-tx-payment/Cargo.toml | 46 ++ .../asset-conversion-tx-payment/README.md | 21 + .../asset-conversion-tx-payment/src/lib.rs | 351 +++++++++ .../asset-conversion-tx-payment/src/mock.rs | 274 +++++++ .../src/payment.rs | 191 +++++ .../asset-conversion-tx-payment/src/tests.rs | 708 ++++++++++++++++++ .../asset-tx-payment/src/lib.rs | 22 +- .../asset-tx-payment/src/payment.rs | 2 +- 21 files changed, 1802 insertions(+), 84 deletions(-) create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/README.md create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs create mode 100644 frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 4a4413905a40e..b59f18c3c4c54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ce93421bc9ff1..e0887b0007289 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index adbf9e85a9fba..197c0771c1c34 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -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 diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index 8fc44c7c5eddf..81f4575384473 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -98,7 +98,7 @@ pub fn create_extrinsic( )), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), - pallet_asset_tx_payment::ChargeAssetTxPayment::::from( + pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::::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, diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 55c21b920c3d7..30f0052c6eaa6 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -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", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 7d16a1afa1f2d..2270cd0145353 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -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; +} + parameter_types! { pub const MinimumPeriod: Moment = SLOT_DURATION / 2; } @@ -1292,7 +1299,7 @@ where frame_system::CheckEra::::from(era), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), - pallet_asset_tx_payment::ChargeAssetTxPayment::::from(tip, None), + pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::::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, frame_system::CheckNonce, frame_system::CheckWeight, - pallet_asset_tx_payment::ChargeAssetTxPayment, + pallet_asset_conversion_tx_payment::ChargeAssetTxPayment, ); /// Unchecked extrinsic type as expected by this runtime. diff --git a/bin/node/testing/Cargo.toml b/bin/node/testing/Cargo.toml index c5a9b018e0dab..81a15f528057a 100644 --- a/bin/node/testing/Cargo.toml +++ b/bin/node/testing/Cargo.toml @@ -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" } diff --git a/bin/node/testing/src/keyring.rs b/bin/node/testing/src/keyring.rs index e16502bf17554..c3f8d8e6d9485 100644 --- a/bin/node/testing/src/keyring.rs +++ b/bin/node/testing/src/keyring.rs @@ -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), ) } diff --git a/frame/asset-conversion/Cargo.toml b/frame/asset-conversion/Cargo.toml index 4025be0db9a9c..0a7f48b0047f5 100644 --- a/frame/asset-conversion/Cargo.toml +++ b/frame/asset-conversion/Cargo.toml @@ -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"] diff --git a/frame/asset-conversion/src/lib.rs b/frame/asset-conversion/src/lib.rs index c97ab537a945f..68a0210b40058 100644 --- a/frame/asset-conversion/src/lib.rs +++ b/frame/asset-conversion/src/lib.rs @@ -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 Hooks for Pallet { + 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::::AmountLessThanMinimal)?; + .map_err(|_| Error::::AmountOneLessThanMinimal)?; Self::validate_minimal_amount(amount2.saturating_add(reserve2), &asset2) - .map_err(|_| Error::::AmountLessThanMinimal)?; + .map_err(|_| Error::::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::::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::::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 Pallet { + /// 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> { + /// Convert a `Balance` type to an `AssetBalance`. + pub(crate) fn convert_native_balance_to_asset_balance( + amount: T::Balance, + ) -> Result> { T::HigherPrecisionBalance::from(amount) .try_into() .map_err(|_| Error::::Overflow) } - pub(crate) fn asset_to_native(amount: T::AssetBalance) -> Result> { + /// Convert an `AssetBalance` type to a `Balance`. + pub(crate) fn convert_asset_balance_to_native_balance( + amount: T::AssetBalance, + ) -> Result> { T::HigherPrecisionBalance::from(amount) .try_into() .map_err(|_| Error::::Overflow) } + /// Swap assets along a `path`, depositing in `send_to`. pub(crate) fn do_swap( - sender: &T::AccountId, + sender: T::AccountId, amounts: &Vec, - path: &BoundedVec, - send_to: &T::AccountId, + path: BoundedVec, + send_to: T::AccountId, keep_alive: bool, ) -> Result<(), DispatchError> { - if let Some([asset1, asset2]) = path.get(0..2) { + ensure!(amounts.len() > 1, Error::::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::::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::::PathError)?; + amounts.get((i + 1) as usize).ok_or(Error::::CorrespondenceError)?; let to = if i < path_len - 2 { let asset3 = path.get((i + 2) as usize).ok_or(Error::::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::::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> { if T::MultiAssetIdConverter::is_native(asset) { - Self::native_to_asset(<::Currency>::reducible_balance( - owner, Expendable, Polite, - )) + Self::convert_native_balance_to_asset_balance( + <::Currency>::reducible_balance(owner, Expendable, Polite), + ) } else { Ok(<::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, @@ -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, @@ -969,10 +996,10 @@ pub mod pallet { result.try_into().map_err(|_| Error::::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::::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::::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, ) -> Result<(), DispatchError> { @@ -1092,7 +1121,7 @@ pub mod pallet { } impl - frame_support::traits::tokens::fungibles::SwapForNative< + frame_support::traits::tokens::fungibles::SwapNative< T::RuntimeOrigin, T::AccountId, T::Balance, @@ -1103,7 +1132,11 @@ where ::Currency: frame_support::traits::tokens::fungible::Inspect<::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,25 +1154,58 @@ 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::::ProvidedMaximumNotSufficientForSwap); } - Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?; + Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Ok(amount_in) + } - Self::deposit_event(Event::SwapExecuted { - who: sender, - send_to, - path, - amount_in, - amount_out, - }); + /// 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, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + ensure!(amount_in > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_out_min) = amount_out_min { + ensure!(amount_out_min > Zero::zero(), Error::::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::::ProvidedMaximumNotSufficientForSwap); + } - Ok(amount_in) + Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Ok(amount_out) } } @@ -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", $); diff --git a/frame/asset-conversion/src/tests.rs b/frame/asset-conversion/src/tests.rs index 4b166c5cb44c5..a4c02f365bc08 100644 --- a/frame/asset-conversion/src/tests.rs +++ b/frame/asset-conversion/src/tests.rs @@ -356,7 +356,7 @@ fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() { 1, user ), - Error::::AmountLessThanMinimal + Error::::AmountOneLessThanMinimal ); assert_noop!( diff --git a/frame/support/src/traits/tokens/fungibles/mod.rs b/frame/support/src/traits/tokens/fungibles/mod.rs index 10ca82b874c5b..8070d916fb330 100644 --- a/frame/support/src/traits/tokens/fungibles/mod.rs +++ b/frame/support/src/traits/tokens/fungibles/mod.rs @@ -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, }; diff --git a/frame/support/src/traits/tokens/fungibles/regular.rs b/frame/support/src/traits/tokens/fungibles/regular.rs index 089dc38ad690e..3df0a6b6d3def 100644 --- a/frame/support/src/traits/tokens/fungibles/regular.rs +++ b/frame/support/src/traits/tokens/fungibles/regular.rs @@ -584,9 +584,16 @@ pub trait Balanced: Inspect + Unbalanced { 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 { - // 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 { + /// 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 { send_to: AccountId, keep_alive: bool, ) -> Result; + + /// 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, + send_to: AccountId, + keep_alive: bool, + ) -> Result; } diff --git a/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml b/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml new file mode 100644 index 0000000000000..8f9e1af04a76a --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-asset-conversion-tx-payment" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +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"] diff --git a/frame/transaction-payment/asset-conversion-tx-payment/README.md b/frame/transaction-payment/asset-conversion-tx-payment/README.md new file mode 100644 index 0000000000000..eccba773673e6 --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/README.md @@ -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 diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs new file mode 100644 index 0000000000000..7f6da90dfe95f --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs @@ -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 = + ::OnChargeTransaction; +/// Balance type alias for balances of the chain's native asset. +pub(crate) type BalanceOf = as OnChargeTransaction>::Balance; +/// Liquidity info type alias. +pub(crate) type LiquidityInfoOf = + as OnChargeTransaction>::LiquidityInfo; + +/// Balance type alias for balances of assets that implement the `fungibles` trait. +pub(crate) type AssetBalanceOf = + <::Fungibles as Inspect<::AccountId>>::Balance; +/// Type alias for Asset IDs. +pub(crate) type AssetIdOf = + <::Fungibles as Inspect<::AccountId>>::AssetId; + +/// Type alias for the interaction of balances with `OnChargeAssetTransaction`. +pub(crate) type ChargeAssetBalanceOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::Balance; +/// Type alias for Asset IDs in their interaction with `OnChargeAssetTransaction`. +pub(crate) type ChargeAssetIdOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::AssetId; +/// Liquidity info type alias for interaction with `OnChargeAssetTransaction`. +pub(crate) type ChargeAssetLiquidityOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::LiquidityInfo; + +/// Used to pass the initial payment info from pre- to post-dispatch. +#[derive(Encode, Decode, DefaultNoBound, TypeInfo)] +pub enum InitialPayment { + /// No initial fee was paid. + #[default] + Nothing, + /// The initial fee was paid in the native currency. + Native(LiquidityInfoOf), + /// The initial fee was paid in an asset. + Asset((LiquidityInfoOf, BalanceOf, AssetBalanceOf)), +} + +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> + IsType<::RuntimeEvent>; + /// The fungibles instance used to pay for transactions in assets. + type Fungibles: Balanced; + /// The actual transaction charging logic that charges the fees. + type OnChargeAssetTransaction: OnChargeAssetTransaction; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// 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, + tip: BalanceOf, + asset_id: ChargeAssetIdOf, + }, + /// A swap of the refund in native currency back to asset failed. + AssetRefundFailed { native_amount_kept: BalanceOf }, + } +} + +/// 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 { + #[codec(compact)] + tip: BalanceOf, + asset_id: Option>, +} + +impl ChargeAssetTxPayment +where + T::RuntimeCall: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + + Sync + + FixedPointOperand + + Into> + + From>, + ChargeAssetIdOf: Send + Sync, +{ + /// Utility constructor. Used only in client/factory code. + pub fn from(tip: BalanceOf, asset_id: Option>) -> 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, + len: usize, + ) -> Result<(BalanceOf, InitialPayment), TransactionValidityError> { + let fee = pallet_transaction_payment::Pallet::::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 { + as OnChargeTransaction>::withdraw_fee( + who, call, info, fee, self.tip, + ) + .map(|i| (fee, InitialPayment::Native(i))) + .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() }) + } + } +} + +impl sp_std::fmt::Debug for ChargeAssetTxPayment { + #[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 SignedExtension for ChargeAssetTxPayment +where + T::RuntimeCall: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + + Sync + + From + + FixedPointOperand + + Into> + + Into> + + From>, + ChargeAssetIdOf: Send + Sync, +{ + const IDENTIFIER: &'static str = "ChargeAssetTxPayment"; + type AccountId = T::AccountId; + type Call = T::RuntimeCall; + type AdditionalSigned = (); + type Pre = ( + // tip + BalanceOf, + // who paid the fee + Self::AccountId, + // imbalance resulting from withdrawing the fee + InitialPayment, + // asset_id for the transaction payment + Option>, + ); + + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> TransactionValidity { + use pallet_transaction_payment::ChargeTransactionPayment; + let (fee, _) = self.withdraw_fee(who, call, info, len)?; + let priority = ChargeTransactionPayment::::get_priority(info, len, self.tip, fee); + Ok(ValidTransaction { priority, ..Default::default() }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + 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, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + 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::::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::::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::::deposit_event(Event::::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(()) + } +} diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs new file mode 100644 index 0000000000000..6e05ec6163ddb --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs @@ -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; +type Block = frame_system::mocking::MockBlock; +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::, + AssetConversion: pallet_asset_conversion, + AssetTxPayment: pallet_asset_conversion_tx_payment, + } +); + +parameter_types! { + pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero(); +} + +pub struct BlockWeights; +impl Get 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; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + 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> for DealWithFees { + fn on_unbalanceds( + mut fees_then_tips: impl Iterator>, + ) { + 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; + 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; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + 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 for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + 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::::into_account_truncating(&AssetConversionPalletId::get()); +} + +impl pallet_asset_conversion::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type AssetBalance = ::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; + type MultiAssetIdConverter = NativeOrAssetIdConverter; + + pallet_asset_conversion::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Fungibles = Assets; + type OnChargeAssetTransaction = AssetConversionAdapter; +} diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs new file mode 100644 index 0000000000000..86b2c09e541ac --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs @@ -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 { + /// 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, + 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 +/// [`SwapNative`]). +/// +/// The converter is given the complete fee in terms of the asset used for the transaction. +pub struct AssetConversionAdapter(PhantomData<(C, CON)>); + +/// Default implementation for a runtime instantiating this pallet, an asset to native swapper. +impl OnChargeAssetTransaction for AssetConversionAdapter +where + T: Config, + C: Inspect<::AccountId>, + CON: SwapNative, AssetBalanceOf, AssetIdOf>, + AssetIdOf: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo, + 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_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 + ::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, + 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_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::::deposit_event(Event::::AssetRefundFailed { + native_amount_kept: swap_back, + }); + }, + } + } + + let actual_paid = initial_asset_consumed.saturating_sub(asset_refund); + Ok(actual_paid) + } +} diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs new file mode 100644 index 0000000000000..7cd22e305831b --- /dev/null +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs @@ -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: &::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::().unwrap(); + pallet_balances::GenesisConfig:: { + 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 = ::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::::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::::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::::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::::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 = ::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::::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::::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 = ::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::::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 = ::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::::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::::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 = ::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::::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::::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 = ::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::::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::::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 = ::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::::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::::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::::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::::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 = ::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::::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) = ⪯ + 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::::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 = ::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::::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::::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); + }); +} diff --git a/frame/transaction-payment/asset-tx-payment/src/lib.rs b/frame/transaction-payment/asset-tx-payment/src/lib.rs index 4e83d8b489b70..86f3025cc313c 100644 --- a/frame/transaction-payment/asset-tx-payment/src/lib.rs +++ b/frame/transaction-payment/asset-tx-payment/src/lib.rs @@ -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 = ::OnChargeTransaction; -// Balance type alias. +/// Balance type alias. pub(crate) type BalanceOf = as OnChargeTransaction>::Balance; -// Liquity info type alias. +/// Liquidity info type alias. pub(crate) type LiquidityInfoOf = as OnChargeTransaction>::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 = <::Fungibles as Inspect<::AccountId>>::Balance; /// Asset id type alias. @@ -85,25 +85,25 @@ pub(crate) type AssetIdOf = <::Fungibles as Inspect<::AccountId>>::AssetId; // Type aliases used for interaction with `OnChargeAssetTransaction`. -// Balance type alias. +/// Balance type alias. pub(crate) type ChargeAssetBalanceOf = <::OnChargeAssetTransaction as OnChargeAssetTransaction>::Balance; -// Asset id type alias. +/// Asset id type alias. pub(crate) type ChargeAssetIdOf = <::OnChargeAssetTransaction as OnChargeAssetTransaction>::AssetId; -// Liquity info type alias. +/// Liquidity info type alias. pub(crate) type ChargeAssetLiquidityOf = <::OnChargeAssetTransaction as OnChargeAssetTransaction>::LiquidityInfo; /// Used to pass the initial payment info from pre- to post-dispatch. #[derive(Encode, Decode, DefaultNoBound, TypeInfo)] pub enum InitialPayment { - /// 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), - /// The initial fee was payed in an asset. + /// The initial fee was paid in an asset. Asset(Credit), } diff --git a/frame/transaction-payment/asset-tx-payment/src/payment.rs b/frame/transaction-payment/asset-tx-payment/src/payment.rs index 49e78fb8bce01..717114ab6bd03 100644 --- a/frame/transaction-payment/asset-tx-payment/src/payment.rs +++ b/frame/transaction-payment/asset-tx-payment/src/payment.rs @@ -126,7 +126,7 @@ where .max(min_converted_fee); let can_withdraw = >::can_withdraw(asset_id, who, converted_fee); - if !matches!(can_withdraw, WithdrawConsequence::Success) { + if can_withdraw != WithdrawConsequence::Success { return Err(InvalidTransaction::Payment.into()) } >::withdraw(