diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index f1e8b00c6e..32234cc4c9 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -4536,6 +4536,7 @@ dependencies = [ "node-primitives", "node-rpc", "node-runtime", + "pallet-asset-tx-payment", "pallet-balances", "pallet-im-online", "pallet-timestamp", @@ -4701,6 +4702,7 @@ dependencies = [ "hex-literal", "log 0.4.14", "node-primitives", + "pallet-asset-tx-payment", "pallet-assets", "pallet-authority-discovery", "pallet-authorship", @@ -4861,6 +4863,7 @@ dependencies = [ "node-executor", "node-primitives", "node-runtime", + "pallet-asset-tx-payment", "pallet-transaction-payment", "parity-scale-codec", "sc-block-builder", @@ -5104,6 +5107,28 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "pallet-asset-tx-payment" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "pallet-assets", + "pallet-authorship", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "serde", + "serde_json", + "smallvec 1.7.0", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-storage", +] + [[package]] name = "pallet-assets" version = "4.0.0-dev" @@ -10420,6 +10445,7 @@ dependencies = [ "node-cli", "node-primitives", "node-runtime", + "pallet-asset-tx-payment", "pallet-transaction-payment", "sc-consensus", "sc-consensus-babe", diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index e03f33a4d2..f30b223a9b 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -124,6 +124,7 @@ members = [ "frame/system/rpc/runtime-api", "frame/timestamp", "frame/transaction-payment", + "frame/transaction-payment/asset-tx-payment", "frame/transaction-payment/rpc", "frame/transaction-payment/rpc/runtime-api", "frame/transaction-storage", diff --git a/substrate/bin/node/cli/Cargo.toml b/substrate/bin/node/cli/Cargo.toml index 7529138c7f..5a9e76bccf 100644 --- a/substrate/bin/node/cli/Cargo.toml +++ b/substrate/bin/node/cli/Cargo.toml @@ -81,6 +81,7 @@ sc-sync-state-rpc = { version = "0.10.0-dev", path = "../../../client/sync-state frame-system = { version = "4.0.0-dev", path = "../../../frame/system" } frame-system-rpc-runtime-api = { version = "4.0.0-dev", path = "../../../frame/system/rpc/runtime-api" } pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-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/substrate/bin/node/cli/src/service.rs b/substrate/bin/node/cli/src/service.rs index e73b69153d..fec91a9b67 100644 --- a/substrate/bin/node/cli/src/service.rs +++ b/substrate/bin/node/cli/src/service.rs @@ -92,7 +92,7 @@ pub fn create_extrinsic( )), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + pallet_asset_tx_payment::ChargeAssetTxPayment::::from(tip, None), ); let raw_payload = node_runtime::SignedPayload::from_raw( @@ -725,7 +725,7 @@ 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 payment = pallet_transaction_payment::ChargeTransactionPayment::from(0); + let tx_payment = pallet_asset_tx_payment::ChargeAssetTxPayment::from(0, None); let extra = ( check_spec_version, check_tx_version, @@ -733,7 +733,7 @@ mod tests { check_era, check_nonce, check_weight, - payment, + tx_payment, ); let raw_payload = SignedPayload::from_raw( function, diff --git a/substrate/bin/node/runtime/Cargo.toml b/substrate/bin/node/runtime/Cargo.toml index 9a481120fd..f9ce4b0fca 100644 --- a/substrate/bin/node/runtime/Cargo.toml +++ b/substrate/bin/node/runtime/Cargo.toml @@ -95,6 +95,7 @@ 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-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" } diff --git a/substrate/bin/node/runtime/src/impls.rs b/substrate/bin/node/runtime/src/impls.rs index e315a45e69..cdd9f0900f 100644 --- a/substrate/bin/node/runtime/src/impls.rs +++ b/substrate/bin/node/runtime/src/impls.rs @@ -17,8 +17,12 @@ //! Some configurable implementations as associated type for the substrate runtime. -use crate::{Authorship, Balances, NegativeImbalance}; -use frame_support::traits::{Currency, OnUnbalanced}; +use crate::{AccountId, Assets, Authorship, Balances, NegativeImbalance, Runtime}; +use frame_support::traits::{ + fungibles::{Balanced, CreditOf}, + Currency, OnUnbalanced, +}; +use pallet_asset_tx_payment::HandleCredit; pub struct Author; impl OnUnbalanced for Author { @@ -27,6 +31,17 @@ impl OnUnbalanced for Author { } } +/// A `HandleCredit` implementation that naively transfers the fees to the block author. +/// Will drop and burn the assets in case the transfer fails. +pub struct CreditToBlockAuthor; +impl HandleCredit for CreditToBlockAuthor { + fn handle_credit(credit: CreditOf) { + let author = pallet_authorship::Pallet::::author(); + // Drop the result which will trigger the `OnDrop` of the imbalance in case of error. + let _ = Assets::resolve(&author, credit); + } +} + #[cfg(test)] mod multiplier_tests { use pallet_transaction_payment::{Multiplier, TargetedFeeAdjustment}; diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 6d04ca8fdc..5b3c0685d1 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -87,7 +87,7 @@ pub use sp_runtime::BuildStorage; /// Implementations of some helper traits passed into runtime modules as associated types. pub mod impls; -use impls::Author; +use impls::{Author, CreditToBlockAuthor}; /// Constant values used within the runtime. pub mod constants; @@ -432,6 +432,14 @@ impl pallet_transaction_payment::Config for Runtime { TargetedFeeAdjustment; } +impl pallet_asset_tx_payment::Config for Runtime { + type Fungibles = Assets; + type OnChargeAssetTransaction = pallet_asset_tx_payment::FungiblesAdapter< + pallet_assets::BalanceToAssetBalance, + CreditToBlockAuthor, + >; +} + parameter_types! { pub const MinimumPeriod: Moment = SLOT_DURATION / 2; } @@ -969,7 +977,7 @@ where frame_system::CheckEra::::from(era), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + pallet_asset_tx_payment::ChargeAssetTxPayment::::from(tip, None), ); let raw_payload = SignedPayload::new(call, extra) .map_err(|e| { @@ -1168,7 +1176,7 @@ parameter_types! { impl pallet_assets::Config for Runtime { type Event = Event; - type Balance = u64; + type Balance = u128; type AssetId = u32; type Currency = Balances; type ForceOrigin = EnsureRoot; @@ -1257,6 +1265,7 @@ construct_runtime!( Indices: pallet_indices, Balances: pallet_balances, TransactionPayment: pallet_transaction_payment, + AssetTxPayment: pallet_asset_tx_payment, ElectionProviderMultiPhase: pallet_election_provider_multi_phase, Staking: pallet_staking, Session: pallet_session, @@ -1315,7 +1324,7 @@ pub type SignedExtra = ( frame_system::CheckEra, frame_system::CheckNonce, frame_system::CheckWeight, - pallet_transaction_payment::ChargeTransactionPayment, + pallet_asset_tx_payment::ChargeAssetTxPayment, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; diff --git a/substrate/bin/node/test-runner-example/Cargo.toml b/substrate/bin/node/test-runner-example/Cargo.toml index b664cdb8e5..831a687254 100644 --- a/substrate/bin/node/test-runner-example/Cargo.toml +++ b/substrate/bin/node/test-runner-example/Cargo.toml @@ -11,6 +11,7 @@ test-runner = { path = "../../../test-utils/test-runner" } frame-system = { path = "../../../frame/system" } frame-benchmarking = { path = "../../../frame/benchmarking" } pallet-transaction-payment = { path = "../../../frame/transaction-payment" } +pallet-asset-tx-payment = { path = "../../../frame/transaction-payment/asset-tx-payment/" } node-runtime = { path = "../runtime" } node-primitives = { path = "../primitives" } diff --git a/substrate/bin/node/test-runner-example/src/lib.rs b/substrate/bin/node/test-runner-example/src/lib.rs index 0de7f5a4e2..68c14b73bf 100644 --- a/substrate/bin/node/test-runner-example/src/lib.rs +++ b/substrate/bin/node/test-runner-example/src/lib.rs @@ -77,7 +77,7 @@ impl ChainInfo for NodeTemplateChainInfo { frame_system::Pallet::::account_nonce(from), ), frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(0), + pallet_asset_tx_payment::ChargeAssetTxPayment::::from(0, None), ) } } diff --git a/substrate/bin/node/testing/Cargo.toml b/substrate/bin/node/testing/Cargo.toml index 1854029b07..0e5ed07ac2 100644 --- a/substrate/bin/node/testing/Cargo.toml +++ b/substrate/bin/node/testing/Cargo.toml @@ -38,6 +38,7 @@ sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/c frame-system = { version = "4.0.0-dev", path = "../../../frame/system" } substrate-test-client = { version = "2.0.0", path = "../../../test-utils/client" } pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" } +pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment/" } sp-api = { version = "4.0.0-dev", path = "../../../primitives/api" } sp-timestamp = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/timestamp" } sp-block-builder = { version = "4.0.0-dev", path = "../../../primitives/block-builder" } diff --git a/substrate/bin/node/testing/src/keyring.rs b/substrate/bin/node/testing/src/keyring.rs index 4e2d88b4bb..1040e90c4d 100644 --- a/substrate/bin/node/testing/src/keyring.rs +++ b/substrate/bin/node/testing/src/keyring.rs @@ -76,7 +76,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_transaction_payment::ChargeTransactionPayment::from(extra_fee), + pallet_asset_tx_payment::ChargeAssetTxPayment::from(extra_fee, None), ) } diff --git a/substrate/frame/transaction-payment/asset-tx-payment/Cargo.toml b/substrate/frame/transaction-payment/asset-tx-payment/Cargo.toml new file mode 100644 index 0000000000..a381145d66 --- /dev/null +++ b/substrate/frame/transaction-payment/asset-tx-payment/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "pallet-asset-tx-payment" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "pallet to manage transaction payments in assets" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# Substrate dependencies +sp-core = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/core" } +sp-io = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/io" } +sp-runtime = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/runtime" } +sp-std = { version = "4.0.0-dev", 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 = ".." } + +# Other dependencies +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.126", optional = true } + +[dev-dependencies] +smallvec = "1.7.0" +serde_json = "1.0.68" + +sp-storage = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/storage" } + +pallet-assets = { version = "4.0.0-dev", path = "../../assets" } +pallet-authorship = { version = "4.0.0-dev", path = "../../authorship" } +pallet-balances = { version = "4.0.0-dev", path = "../../balances" } + + +[features] +default = ["std"] +std = [ + "serde", + "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/substrate/frame/transaction-payment/asset-tx-payment/README.md b/substrate/frame/transaction-payment/asset-tx-payment/README.md new file mode 100644 index 0000000000..fc860347d8 --- /dev/null +++ b/substrate/frame/transaction-payment/asset-tx-payment/README.md @@ -0,0 +1,21 @@ +# pallet-asset-tx-payment + +## Asset 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 [`FungiblesAdapter`] (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/substrate/frame/transaction-payment/asset-tx-payment/src/lib.rs b/substrate/frame/transaction-payment/asset-tx-payment/src/lib.rs new file mode 100644 index 0000000000..1f22669857 --- /dev/null +++ b/substrate/frame/transaction-payment/asset-tx-payment/src/lib.rs @@ -0,0 +1,288 @@ +// Copyright (C) 2021 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 Transaction Payment Pallet +//! +//! This pallet allows runtimes that include it to pay for transactions in assets other than the +//! main 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 [`FungiblesAdapter`] (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`]). + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; + +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::DispatchResult, + traits::{ + tokens::{ + fungibles::{Balanced, CreditOf, Inspect}, + WithdrawConsequence, + }, + IsType, + }, + weights::{DispatchInfo, PostDispatchInfo}, + 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 tests; + +mod payment; +pub use payment::*; + +// Type aliases used for interaction with `OnChargeTransaction`. +pub(crate) type OnChargeTransactionOf = + ::OnChargeTransaction; +// Balance type alias. +pub(crate) type BalanceOf = as OnChargeTransaction>::Balance; +// Liquity info type alias. +pub(crate) type LiquidityInfoOf = + as OnChargeTransaction>::LiquidityInfo; + +// Type alias used for interaction with fungibles (assets). +// Balance type alias. +pub(crate) type AssetBalanceOf = + <::Fungibles as Inspect<::AccountId>>::Balance; +/// Asset id type alias. +pub(crate) type AssetIdOf = + <::Fungibles as Inspect<::AccountId>>::AssetId; + +// Type aliases used for interaction with `OnChargeAssetTransaction`. +// Balance type alias. +pub(crate) type ChargeAssetBalanceOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::Balance; +// Asset id type alias. +pub(crate) type ChargeAssetIdOf = + <::OnChargeAssetTransaction as OnChargeAssetTransaction>::AssetId; +// Liquity 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. + Nothing, + /// The initial fee was payed in the native currency. + Native(LiquidityInfoOf), + /// The initial fee was payed in an asset. + Asset(CreditOf), +} + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_transaction_payment::Config { + /// 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] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); +} + +/// Require the transactor pay for themselves and maybe 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 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::Call: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + Sync + FixedPointOperand + IsType>, + ChargeAssetIdOf: Send + Sync, + CreditOf: IsType>, +{ + /// 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::Call, + 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(|i| (fee, InitialPayment::Asset(i.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::Call: Dispatchable, + AssetBalanceOf: Send + Sync + FixedPointOperand, + BalanceOf: Send + Sync + From + FixedPointOperand + IsType>, + ChargeAssetIdOf: Send + Sync, + CreditOf: IsType>, +{ + const IDENTIFIER: &'static str = "ChargeAssetTxPayment"; + type AccountId = T::AccountId; + type Call = T::Call; + type AdditionalSigned = (); + type Pre = ( + // tip + BalanceOf, + // who paid the fee + Self::AccountId, + // imbalance resulting from withdrawing the fee + InitialPayment, + ); + + 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)) + } + + fn post_dispatch( + pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + let (tip, who, initial_payment) = pre; + match initial_payment { + InitialPayment::Native(already_withdrawn) => { + pallet_transaction_payment::ChargeTransactionPayment::::post_dispatch( + (tip, who, already_withdrawn), + info, + post_info, + len, + result, + )?; + }, + InitialPayment::Asset(already_withdrawn) => { + let actual_fee = pallet_transaction_payment::Pallet::::compute_actual_fee( + len as u32, info, post_info, tip, + ); + T::OnChargeAssetTransaction::correct_and_deposit_fee( + &who, + info, + post_info, + actual_fee.into(), + tip.into(), + already_withdrawn.into(), + )?; + }, + 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/substrate/frame/transaction-payment/asset-tx-payment/src/payment.rs b/substrate/frame/transaction-payment/asset-tx-payment/src/payment.rs new file mode 100644 index 0000000000..09482f9649 --- /dev/null +++ b/substrate/frame/transaction-payment/asset-tx-payment/src/payment.rs @@ -0,0 +1,168 @@ +// Copyright (C) 2021 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::{ + traits::{ + fungibles::{Balanced, CreditOf, Inspect}, + tokens::BalanceConversion, + }, + unsigned::TransactionValidityError, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{ + AtLeast32BitUnsigned, DispatchInfoOf, MaybeSerializeDeserialize, One, PostDispatchInfoOf, + }, + transaction_validity::InvalidTransaction, +}; +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: AtLeast32BitUnsigned + + FullCodec + + Copy + + MaybeSerializeDeserialize + + Debug + + Default + + TypeInfo; + /// 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; + + /// Before the transaction is executed the payment of the transaction fees needs to be secured. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + call: &T::Call, + dispatch_info: &DispatchInfoOf, + asset_id: Self::AssetId, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result; + + /// After the transaction was executed the actual fee can be calculated. + /// This function should refund any overpaid fees and optionally deposit + /// the corrected amount. + /// + /// Note: The `fee` already includes the `tip`. + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError>; +} + +/// Allows specifying what to do with the withdrawn asset fees. +pub trait HandleCredit> { + /// Implement to determine what to do with the withdrawn asset fees. + /// Default for `CreditOf` from the assets pallet is to burn and + /// decrease total issuance. + fn handle_credit(credit: CreditOf); +} + +/// Default implementation that just drops the credit according to the `OnDrop` in the underlying +/// imbalance type. +impl> HandleCredit for () { + fn handle_credit(_credit: CreditOf) {} +} + +/// Implements the asset transaction for a balance to asset converter (implementing +/// [`BalanceConversion`]) and a credit handler (implementing [`HandleCredit`]). +/// +/// The credit handler is given the complete fee in terms of the asset used for the transaction. +pub struct FungiblesAdapter(PhantomData<(CON, HC)>); + +/// Default implementation for a runtime instantiating this pallet, a balance to asset converter and +/// a credit handler. +impl OnChargeAssetTransaction for FungiblesAdapter +where + T: Config, + CON: BalanceConversion, AssetIdOf, AssetBalanceOf>, + HC: HandleCredit, + AssetIdOf: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo, +{ + type Balance = BalanceOf; + type AssetId = AssetIdOf; + type LiquidityInfo = CreditOf; + + /// Withdraw the predicted fee from the transaction origin. + /// + /// Note: The `fee` already includes the `tip`. + fn withdraw_fee( + who: &T::AccountId, + _call: &T::Call, + _info: &DispatchInfoOf, + asset_id: Self::AssetId, + fee: Self::Balance, + _tip: Self::Balance, + ) -> Result { + // We don't know the precision of the underlying asset. Because the converted fee could be + // less than one (e.g. 0.5) but gets rounded down by integer division we introduce a minimum + // fee. + let min_converted_fee = if fee.is_zero() { Zero::zero() } else { One::one() }; + let converted_fee = CON::to_asset_balance(fee, asset_id) + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))? + .max(min_converted_fee); + let can_withdraw = >::can_withdraw( + asset_id.into(), + who, + converted_fee, + ); + if !matches!(can_withdraw, WithdrawConsequence::Success) { + return Err(InvalidTransaction::Payment.into()) + } + >::withdraw(asset_id.into(), who, converted_fee) + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment)) + } + + /// Hand the fee and the tip over to the `[HandleCredit]` implementation. + /// Since the predicted fee might have been too high, parts of the fee may be refunded. + /// + /// Note: The `corrected_fee` already includes the `tip`. + fn correct_and_deposit_fee( + who: &T::AccountId, + _dispatch_info: &DispatchInfoOf, + _post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + _tip: Self::Balance, + paid: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + let min_converted_fee = if corrected_fee.is_zero() { Zero::zero() } else { One::one() }; + // Convert the corrected fee into the asset used for payment. + let converted_fee = CON::to_asset_balance(corrected_fee, paid.asset().into()) + .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })? + .max(min_converted_fee); + // Calculate how much refund we should return. + let (final_fee, refund) = paid.split(converted_fee); + // Refund to the account that paid the fees. If this fails, the account might have dropped + // below the existential balance. In that case we don't refund anything. + let _ = >::resolve(who, refund); + // Handle the final fee, e.g. by transferring to the block author or burning. + HC::handle_credit(final_fee); + Ok(()) + } +} diff --git a/substrate/frame/transaction-payment/asset-tx-payment/src/tests.rs b/substrate/frame/transaction-payment/asset-tx-payment/src/tests.rs new file mode 100644 index 0000000000..bd5dc57239 --- /dev/null +++ b/substrate/frame/transaction-payment/asset-tx-payment/src/tests.rs @@ -0,0 +1,748 @@ +// Copyright (C) 2021 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_tx_payment; + +use frame_support::{ + assert_ok, + pallet_prelude::*, + parameter_types, + traits::{fungibles::Mutate, FindAuthor}, + weights::{ + DispatchClass, DispatchInfo, PostDispatchInfo, Weight, WeightToFeeCoefficient, + WeightToFeeCoefficients, WeightToFeePolynomial, + }, + ConsensusEngineId, +}; +use frame_system as system; +use frame_system::EnsureRoot; +use pallet_balances::Call as BalancesCall; +use pallet_transaction_payment::CurrencyAdapter; +use smallvec::smallvec; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, ConvertInto, IdentityLookup, StaticLookup}, + Perbill, +}; +use std::cell::RefCell; + +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::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + TransactionPayment: pallet_transaction_payment::{Pallet, Storage}, + Assets: pallet_assets::{Pallet, Call, Storage, Event}, + Authorship: pallet_authorship::{Pallet, Call, Storage}, + AssetTxPayment: pallet_asset_tx_payment::{Pallet}, + } +); + +const CALL: &::Call = + &Call::Balances(BalancesCall::transfer { dest: 2, value: 69 }); + +thread_local! { + static EXTRINSIC_BASE_WEIGHT: RefCell = RefCell::new(0); +} + +pub struct BlockWeights; +impl Get for BlockWeights { + fn get() -> frame_system::limits::BlockWeights { + frame_system::limits::BlockWeights::builder() + .base_block(0) + .for_class(DispatchClass::all(), |weights| { + weights.base_extrinsic = EXTRINSIC_BASE_WEIGHT.with(|v| *v.borrow()).into(); + }) + .for_class(DispatchClass::non_mandatory(), |weights| { + weights.max_total = 1024.into(); + }) + .build_or_panic() + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub static TransactionByteFee: u64 = 1; + pub static WeightToFee: u64 = 1; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 10; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type MaxLocks = (); + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; +} + +impl WeightToFeePolynomial for WeightToFee { + type Balance = u64; + + fn polynomial() -> WeightToFeeCoefficients { + smallvec![WeightToFeeCoefficient { + degree: 1, + coeff_frac: Perbill::zero(), + coeff_integer: WEIGHT_TO_FEE.with(|v| *v.borrow()), + negative: false, + }] + } +} + +parameter_types! { + pub const OperationalFeeMultiplier: u8 = 5; +} + +impl pallet_transaction_payment::Config for Runtime { + type OnChargeTransaction = CurrencyAdapter; + type TransactionByteFee = TransactionByteFee; + type WeightToFee = WeightToFee; + type FeeMultiplierUpdate = (); + type OperationalFeeMultiplier = OperationalFeeMultiplier; +} + +parameter_types! { + pub const AssetDeposit: u64 = 2; + pub const MetadataDeposit: u64 = 0; + pub const StringLimit: u32 = 20; +} + +impl pallet_assets::Config for Runtime { + type Event = Event; + type Balance = Balance; + type AssetId = u32; + type Currency = Balances; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDeposit; + type MetadataDepositPerByte = MetadataDeposit; + type ApprovalDeposit = MetadataDeposit; + type StringLimit = StringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = (); +} + +pub struct HardcodedAuthor; +const BLOCK_AUTHOR: AccountId = 1234; +impl FindAuthor for HardcodedAuthor { + fn find_author<'a, I>(_: I) -> Option + where + I: 'a + IntoIterator, + { + Some(BLOCK_AUTHOR) + } +} + +impl pallet_authorship::Config for Runtime { + type FindAuthor = HardcodedAuthor; + type UncleGenerations = (); + type FilterUncle = (); + type EventHandler = (); +} + +pub struct CreditToBlockAuthor; +impl HandleCredit for CreditToBlockAuthor { + fn handle_credit(credit: CreditOf) { + let author = pallet_authorship::Pallet::::author(); + // What to do in case paying the author fails (e.g. because `fee < min_balance`) + // default: drop the result which will trigger the `OnDrop` of the imbalance. + let _ = >::resolve(&author, credit); + } +} + +impl Config for Runtime { + type Fungibles = Assets; + type OnChargeAssetTransaction = FungiblesAdapter< + pallet_assets::BalanceToAssetBalance, + CreditToBlockAuthor, + >; +} + +pub struct ExtBuilder { + balance_factor: u64, + base_weight: u64, + byte_fee: u64, + weight_to_fee: u64, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { balance_factor: 1, base_weight: 0, byte_fee: 1, weight_to_fee: 1 } + } +} + +impl ExtBuilder { + pub fn base_weight(mut self, base_weight: u64) -> 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) { + EXTRINSIC_BASE_WEIGHT.with(|v| *v.borrow_mut() = 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() } +} + +#[test] +fn transaction_payment_in_native_possible() { + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(5) + .build() + .execute_with(|| { + let len = 10; + let pre = ChargeAssetTxPayment::::from(0, None) + .pre_dispatch(&1, CALL, &info_from_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( + pre, + &info_from_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(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( + pre, + &info_from_weight(100), + &post_info_from_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(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 1; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), 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); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + // check that the block author gets rewarded + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), fee); + }); +} + +#[test] +fn transaction_payment_without_fee() { + let base_weight = 5; + let balance_factor = 100; + ExtBuilder::default() + .balance_factor(balance_factor) + .base_weight(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 1; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), 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); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &post_info_from_pays(Pays::No), + len, + &Ok(()) + )); + // caller should be refunded + assert_eq!(Assets::balance(asset_id, caller), balance); + // check that the block author did not get rewarded + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), 0); + }); +} + +#[test] +fn asset_transaction_payment_with_tip_and_refund() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 2; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 1000; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 100; + let tip = 5; + let len = 10; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee_with_tip = + (base_weight + weight + len as u64 + tip) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(tip, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), len) + .unwrap(); + assert_eq!(Assets::balance(asset_id, caller), balance - fee_with_tip); + + let final_weight = 50; + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &post_info_from_weight(final_weight), + len, + &Ok(()) + )); + let final_fee = + fee_with_tip - (weight - final_weight) * min_balance / ExistentialDeposit::get(); + assert_eq!(Assets::balance(asset_id, caller), balance - (final_fee)); + assert_eq!(Assets::balance(asset_id, BLOCK_AUTHOR), final_fee); + }); +} + +#[test] +fn payment_from_account_with_only_assets() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 2; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &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; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), len) + .unwrap(); + assert_eq!(Balances::free_balance(caller), 0); + // check that fee was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - fee); + assert_eq!(Balances::free_balance(caller), 0); + }); +} + +#[test] +fn payment_only_with_existing_sufficient_asset() { + let base_weight = 5; + ExtBuilder::default() + .balance_factor(100) + .base_weight(base_weight) + .build() + .execute_with(|| { + let asset_id = 1; + let caller = 1; + let weight = 5; + let len = 10; + // pre_dispatch fails for non-existent asset + assert!(ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), len) + .is_err()); + + // create the non-sufficient asset + let min_balance = 2; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + false, /* is_sufficient */ + min_balance + )); + // pre_dispatch fails for non-sufficient asset + assert!(ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), len) + .is_err()); + }); +} + +#[test] +fn converted_fee_is_never_zero_if_input_fee_is_not() { + let base_weight = 1; + ExtBuilder::default() + .balance_factor(100) + .base_weight(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 1; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + // naive fee calculation would round down to zero + assert_eq!(fee, 0); + { + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_pays(Pays::No), len) + .unwrap(); + // `Pays::No` still implies no fees + assert_eq!(Assets::balance(asset_id, caller), balance); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_pays(Pays::No), + &post_info_from_pays(Pays::No), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance); + } + let pre = ChargeAssetTxPayment::::from(0, Some(asset_id)) + .pre_dispatch(&caller, CALL, &info_from_weight(weight), len) + .unwrap(); + // check that at least one coin was charged in the given asset + assert_eq!(Assets::balance(asset_id, caller), balance - 1); + + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &default_post_info(), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance - 1); + }); +} + +#[test] +fn post_dispatch_fee_is_zero_if_pre_dispatch_fee_is_zero() { + let base_weight = 1; + ExtBuilder::default() + .balance_factor(100) + .base_weight(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 100; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + // we convert the from weight to fee based on the ratio between asset min balance and + // existential deposit + let fee = (base_weight + weight + len as u64) * min_balance / ExistentialDeposit::get(); + // 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) = ⪯ + 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( + 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(base_weight) + .build() + .execute_with(|| { + // create the asset + let asset_id = 1; + let min_balance = 100; + assert_ok!(Assets::force_create( + Origin::root(), + asset_id, + 42, /* owner */ + true, /* is_sufficient */ + min_balance + )); + + // mint into the caller account + let caller = 333; + let beneficiary = ::Lookup::unlookup(caller); + let balance = 100; + assert_ok!(Assets::mint_into(asset_id, &beneficiary, balance)); + assert_eq!(Assets::balance(asset_id, caller), balance); + let weight = 1; + let len = 1; + let pre = ChargeAssetTxPayment::::pre_dispatch_unsigned( + CALL, + &info_from_weight(weight), + len, + ) + .unwrap(); + + assert_eq!(Assets::balance(asset_id, caller), balance); + let (_tip, _who, initial_payment) = ⪯ + let not_paying = match initial_payment { + &InitialPayment::Nothing => true, + _ => false, + }; + assert!(not_paying, "initial payment is Nothing for unsigned extrinsics"); + + // `Pays::Yes` on post-dispatch does not mean we pay (we never charge more than the + // initial fee) + assert_ok!(ChargeAssetTxPayment::::post_dispatch( + pre, + &info_from_weight(weight), + &post_info_from_pays(Pays::Yes), + len, + &Ok(()) + )); + assert_eq!(Assets::balance(asset_id, caller), balance); + }); +}