Skip to content

Commit

Permalink
Adds syntax for marking calls feeless (#1926)
Browse files Browse the repository at this point in the history
Fixes #1725

This PR adds the following changes:
1. An attribute `pallet::feeless_if` that can be optionally attached to
a call like so:
```rust
#[pallet::feeless_if(|_origin: &OriginFor<T>, something: &u32| -> bool {
	*something == 0
})]
pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
     ....
}
```
The closure passed accepts references to arguments as specified in the
call fn. It returns a boolean that denotes the conditions required for
this call to be "feeless".

2. A signed extension `SkipCheckIfFeeless<T: SignedExtension>` that
wraps a transaction payment processor such as
`pallet_transaction_payment::ChargeTransactionPayment`. It checks for
all calls annotated with `pallet::feeless_if` to see if the conditions
are met. If so, the wrapped signed extension is not called, essentially
making the call feeless.

In order to use this, you can simply replace your existing signed
extension that manages transaction payment like so:
```diff
- pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
+ pallet_skip_feeless_payment::SkipCheckIfFeeless<
+	Runtime,
+	pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
+ >,
```

### Todo
- [x] Tests
- [x] Docs
- [x] Prdoc

---------

Co-authored-by: Nikhil Gupta <>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
  • Loading branch information
4 people authored Nov 13, 2023
1 parent ebcf0a0 commit 60c77a2
Show file tree
Hide file tree
Showing 33 changed files with 874 additions and 54 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ members = [
"substrate/frame/transaction-payment/asset-tx-payment",
"substrate/frame/transaction-payment/rpc",
"substrate/frame/transaction-payment/rpc/runtime-api",
"substrate/frame/transaction-payment/skip-feeless-payment",
"substrate/frame/transaction-storage",
"substrate/frame/treasury",
"substrate/frame/try-runtime",
Expand Down
30 changes: 30 additions & 0 deletions prdoc/pr_1926.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
title: Adds syntax for marking calls feeless

doc:
- audience: Core Dev
description: |
1. Adds an attribute `#[pallet::feeless_if]` that can be optionally attached to a `pallet::call`.
2. Adds a signed extension SkipCheckIfFeeless<T: SignedExtension> that wraps a transaction
payment processor to potentially skip payment fees for such calls.
Note that both the attribute and the signed extension are needed to make the call feeless.

migrations:
db: []

runtime: []

crates:
- name: "frame-support-procedural"
semver: minor
- name: "pallet-skip-feeless-payment"
semver: major
- pallet-example-kitchensink
semver: patch
- kitchensink-runtime
semver: major
- node-testing
semver: patch
- node-cli
semver: patch

host_functions: []
3 changes: 3 additions & 0 deletions substrate/bin/node/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pallet-assets = { path = "../../../frame/assets" }
pallet-asset-conversion-tx-payment = { path = "../../../frame/transaction-payment/asset-conversion-tx-payment" }
pallet-asset-tx-payment = { path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-im-online = { path = "../../../frame/im-online", default-features = false}
pallet-skip-feeless-payment = { path = "../../../frame/transaction-payment/skip-feeless-payment", default-features = false}

# node-specific dependencies
kitchensink-runtime = { path = "../runtime" }
Expand Down Expand Up @@ -168,6 +169,7 @@ runtime-benchmarks = [
"pallet-assets/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-im-online/runtime-benchmarks",
"pallet-skip-feeless-payment/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"sc-client-db/runtime-benchmarks",
"sc-service/runtime-benchmarks",
Expand All @@ -183,6 +185,7 @@ try-runtime = [
"pallet-assets/try-runtime",
"pallet-balances/try-runtime",
"pallet-im-online/try-runtime",
"pallet-skip-feeless-payment/try-runtime",
"pallet-timestamp/try-runtime",
"sp-runtime/try-runtime",
"substrate-cli-test-utils/try-runtime",
Expand Down
38 changes: 21 additions & 17 deletions substrate/bin/node/cli/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,24 @@ pub fn create_extrinsic(
.map(|c| c / 2)
.unwrap_or(2) as u64;
let tip = 0;
let extra: kitchensink_runtime::SignedExtra = (
frame_system::CheckNonZeroSender::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckSpecVersion::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckTxVersion::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckGenesis::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckEra::<kitchensink_runtime::Runtime>::from(generic::Era::mortal(
period,
best_block.saturated_into(),
)),
frame_system::CheckNonce::<kitchensink_runtime::Runtime>::from(nonce),
frame_system::CheckWeight::<kitchensink_runtime::Runtime>::new(),
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<kitchensink_runtime::Runtime>::from(
tip, None,
),
);
let extra: kitchensink_runtime::SignedExtra =
(
frame_system::CheckNonZeroSender::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckSpecVersion::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckTxVersion::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckGenesis::<kitchensink_runtime::Runtime>::new(),
frame_system::CheckEra::<kitchensink_runtime::Runtime>::from(generic::Era::mortal(
period,
best_block.saturated_into(),
)),
frame_system::CheckNonce::<kitchensink_runtime::Runtime>::from(nonce),
frame_system::CheckWeight::<kitchensink_runtime::Runtime>::new(),
pallet_skip_feeless_payment::SkipCheckIfFeeless::from(
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<
kitchensink_runtime::Runtime,
>::from(tip, None),
),
);

let raw_payload = kitchensink_runtime::SignedPayload::from_raw(
function.clone(),
Expand Down Expand Up @@ -879,8 +882,9 @@ 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_conversion_tx_payment::ChargeAssetTxPayment::from(0, None);
let tx_payment = pallet_skip_feeless_payment::SkipCheckIfFeeless::from(
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::from(0, None),
);
let extra = (
check_non_zero_sender,
check_spec_version,
Expand Down
4 changes: 4 additions & 0 deletions substrate/bin/node/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pallet-transaction-payment = { path = "../../../frame/transaction-payment", defa
pallet-transaction-payment-rpc-runtime-api = { path = "../../../frame/transaction-payment/rpc/runtime-api", default-features = false}
pallet-asset-conversion-tx-payment = { path = "../../../frame/transaction-payment/asset-conversion-tx-payment", default-features = false}
pallet-asset-tx-payment = { path = "../../../frame/transaction-payment/asset-tx-payment", default-features = false}
pallet-skip-feeless-payment = { path = "../../../frame/transaction-payment/skip-feeless-payment", default-features = false}
pallet-transaction-storage = { path = "../../../frame/transaction-storage", default-features = false}
pallet-uniques = { path = "../../../frame/uniques", default-features = false}
pallet-vesting = { path = "../../../frame/vesting", default-features = false}
Expand Down Expand Up @@ -212,6 +213,7 @@ std = [
"pallet-scheduler/std",
"pallet-session-benchmarking?/std",
"pallet-session/std",
"pallet-skip-feeless-payment/std",
"pallet-society/std",
"pallet-staking-runtime-api/std",
"pallet-staking/std",
Expand Down Expand Up @@ -308,6 +310,7 @@ runtime-benchmarks = [
"pallet-salary/runtime-benchmarks",
"pallet-scheduler/runtime-benchmarks",
"pallet-session-benchmarking/runtime-benchmarks",
"pallet-skip-feeless-payment/runtime-benchmarks",
"pallet-society/runtime-benchmarks",
"pallet-staking/runtime-benchmarks",
"pallet-state-trie-migration/runtime-benchmarks",
Expand Down Expand Up @@ -381,6 +384,7 @@ try-runtime = [
"pallet-salary/try-runtime",
"pallet-scheduler/try-runtime",
"pallet-session/try-runtime",
"pallet-skip-feeless-payment/try-runtime",
"pallet-society/try-runtime",
"pallet-staking/try-runtime",
"pallet-state-trie-migration/try-runtime",
Expand Down
16 changes: 14 additions & 2 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,10 @@ impl pallet_asset_conversion_tx_payment::Config for Runtime {
pallet_asset_conversion_tx_payment::AssetConversionAdapter<Balances, AssetConversion>;
}

impl pallet_skip_feeless_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}

parameter_types! {
pub const MinimumPeriod: Moment = SLOT_DURATION / 2;
}
Expand Down Expand Up @@ -1394,7 +1398,11 @@ where
frame_system::CheckEra::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<Runtime>::from(tip, None),
pallet_skip_feeless_payment::SkipCheckIfFeeless::from(
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::<Runtime>::from(
tip, None,
),
),
);
let raw_payload = SignedPayload::new(call, extra)
.map_err(|e| {
Expand Down Expand Up @@ -2134,6 +2142,7 @@ construct_runtime!(
Statement: pallet_statement,
Broker: pallet_broker,
Mixnet: pallet_mixnet,
SkipFeelessPayment: pallet_skip_feeless_payment,
}
);

Expand All @@ -2160,7 +2169,10 @@ pub type SignedExtra = (
frame_system::CheckEra<Runtime>,
frame_system::CheckNonce<Runtime>,
frame_system::CheckWeight<Runtime>,
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment<Runtime>,
pallet_skip_feeless_payment::SkipCheckIfFeeless<
Runtime,
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment<Runtime>,
>,
);

/// Unchecked extrinsic type as expected by this runtime.
Expand Down
1 change: 1 addition & 0 deletions substrate/bin/node/testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pallet-asset-conversion = { path = "../../../frame/asset-conversion" }
pallet-assets = { path = "../../../frame/assets" }
pallet-asset-conversion-tx-payment = { path = "../../../frame/transaction-payment/asset-conversion-tx-payment" }
pallet-asset-tx-payment = { path = "../../../frame/transaction-payment/asset-tx-payment" }
pallet-skip-feeless-payment = { path = "../../../frame/transaction-payment/skip-feeless-payment" }
sc-block-builder = { path = "../../../client/block-builder" }
sc-client-api = { path = "../../../client/api" }
sc-client-db = { path = "../../../client/db", features = ["rocksdb"]}
Expand Down
4 changes: 3 additions & 1 deletion substrate/bin/node/testing/src/keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ pub fn signed_extra(nonce: Nonce, extra_fee: Balance) -> SignedExtra {
frame_system::CheckEra::from(Era::mortal(256, 0)),
frame_system::CheckNonce::from(nonce),
frame_system::CheckWeight::new(),
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::from(extra_fee, None),
pallet_skip_feeless_payment::SkipCheckIfFeeless::from(
pallet_asset_conversion_tx_payment::ChargeAssetTxPayment::from(extra_fee, None),
),
)
}

Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/examples/kitchensink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::set_foo_benchmark())]
/// Marks this call as feeless if `new_foo` is zero.
#[pallet::feeless_if(|_origin: &OriginFor<T>, new_foo: &u32, _other_compact: &u128| -> bool {
*new_foo == 0
})]
pub fn set_foo(
_: OriginFor<T>,
new_foo: u32,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ pub fn expand_outer_dispatch(
}
}

impl #scrate::dispatch::CheckIfFeeless for RuntimeCall {
type Origin = #system_path::pallet_prelude::OriginFor<#runtime>;
fn is_feeless(&self, origin: &Self::Origin) -> bool {
match self {
#(
#pallet_attrs
#variant_patterns => call.is_feeless(origin),
)*
}
}
}

impl #scrate::traits::GetCallMetadata for RuntimeCall {
fn get_call_metadata(&self) -> #scrate::traits::CallMetadata {
use #scrate::traits::GetCallName;
Expand Down
30 changes: 30 additions & 0 deletions substrate/frame/support/procedural/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,36 @@ pub fn call_index(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}

/// Each dispatchable may be annotated with the `#[pallet::feeless_if($closure)]` attribute,
/// which explicitly defines the condition for the dispatchable to be feeless.
///
/// The arguments for the closure must be the referenced arguments of the dispatchable function.
///
/// The closure must return `bool`.
///
/// ### Example
/// ```ignore
/// #[pallet::feeless_if(|_origin: &OriginFor<T>, something: &u32| -> bool {
/// *something == 0
/// })]
/// pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
/// ....
/// }
/// ```
///
/// Please note that this only works for signed dispatchables and requires a signed extension
/// such as `SkipCheckIfFeeless` as defined in `pallet-skip-feeless-payment` to wrap the existing
/// payment extension. Else, this is completely ignored and the dispatchable is still charged.
///
/// ### Macro expansion
///
/// The macro implements the `CheckIfFeeless` trait on the dispatchable and calls the corresponding
/// closure in the implementation.
#[proc_macro_attribute]
pub fn feeless_if(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}

/// Allows you to define some extra constants to be added into constant metadata.
///
/// Item must be defined as:
Expand Down
27 changes: 27 additions & 0 deletions substrate/frame/support/procedural/src/pallet/expand/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream {
})
.collect::<Vec<_>>();

let feeless_check = methods.iter().map(|method| &method.feeless_check).collect::<Vec<_>>();
let feeless_check_result =
feeless_check.iter().zip(args_name.iter()).map(|(feeless_check, arg_name)| {
if let Some(feeless_check) = feeless_check {
quote::quote!(#feeless_check(origin, #( #arg_name, )*))
} else {
quote::quote!(false)
}
});

quote::quote_spanned!(span =>
mod warnings {
#(
Expand Down Expand Up @@ -347,6 +357,23 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream {
}
}

impl<#type_impl_gen> #frame_support::dispatch::CheckIfFeeless for #call_ident<#type_use_gen>
#where_clause
{
type Origin = #frame_system::pallet_prelude::OriginFor<T>;
#[allow(unused_variables)]
fn is_feeless(&self, origin: &Self::Origin) -> bool {
match *self {
#(
Self::#fn_name { #( #args_name_pattern_ref, )* } => {
#feeless_check_result
},
)*
Self::__Ignore(_, _) => unreachable!("__Ignore cannot be used"),
}
}
}

impl<#type_impl_gen> #frame_support::traits::GetCallName for #call_ident<#type_use_gen>
#where_clause
{
Expand Down
Loading

0 comments on commit 60c77a2

Please sign in to comment.