From b45ad6d55588fcae8ad8d40338553adf2a69923b Mon Sep 17 00:00:00 2001 From: Jay White Date: Thu, 1 Oct 2026 14:42:35 -0400 Subject: [PATCH 1/5] feat: add pallet-parameters with root-settable per-diem reward rate --- runtime/Cargo.toml | 1 + runtime/src/lib.rs | 39 +++++++++++++++++++++++++++ runtime/src/tests.rs | 64 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9da65324..4ebb5d52 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -49,6 +49,7 @@ polkadot-sdk = { workspace = true, features = [ "pallet-authorship", "pallet-im-online", "pallet-offences", + "pallet-parameters", "pallet-authority-discovery", "pallet-election-provider-multi-phase", "pallet-babe", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 08a0e919..a50f9fd2 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -20,9 +20,11 @@ use polkadot_sdk::frame_election_provider_support::{ SequentialPhragmen, }; use polkadot_sdk::frame_support::dispatch::DispatchClass; +use polkadot_sdk::frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; use polkadot_sdk::frame_support::genesis_builder_helper::{build_state, get_preset}; use polkadot_sdk::frame_support::traits::VariantCountOf; pub use polkadot_sdk::frame_support::traits::{ + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, @@ -109,6 +111,7 @@ use polkadot_sdk::{ pallet_migrations, pallet_multisig, pallet_offences, + pallet_parameters, pallet_session, pallet_staking, pallet_staking_runtime_api, @@ -885,6 +888,38 @@ impl pallet_smartcontracts::Config for Runtime { type WeightInfo = pallet_smartcontracts::weights::SubstrateWeight; } +/// Root-settable runtime parameters. +#[dynamic_params(RuntimeParameters, pallet_parameters::Parameters::)] +pub mod dynamic_params { + use super::*; + + /// Validator reward parameters. + #[dynamic_pallet_params] + #[codec(index = 0)] + pub mod rewards { + /// The daily validator reward rate, applied to total stake; defaults to 9.7% per year. + #[codec(index = 0)] + pub static PerDiemRate: Perbill = Perbill::from_rational(97u64, 365_250u64); + } +} + +#[cfg(feature = "runtime-benchmarks")] +impl Default for RuntimeParameters { + fn default() -> Self { + RuntimeParameters::Rewards(dynamic_params::rewards::Parameters::PerDiemRate( + dynamic_params::rewards::PerDiemRate, + Some(Perbill::from_percent(1)), + )) + } +} + +impl pallet_parameters::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeParameters = RuntimeParameters; + type AdminOrigin = AsEnsureOriginWithArg>; + type WeightInfo = (); +} + impl pallet_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; // Payout up to 3 pages per block @@ -976,6 +1011,9 @@ mod runtime { #[runtime::pallet_index(72)] pub type MultiBlockMigrations = pallet_migrations::Pallet; + #[runtime::pallet_index(73)] + pub type Parameters = pallet_parameters::Pallet; + // Custom pallets start at index 100 to ensure room for future consensus work #[runtime::pallet_index(100)] pub type Permissions = pallet_permissions::Pallet; @@ -1063,6 +1101,7 @@ mod benches { [pallet_staking, Staking] [pallet_sudo, Sudo] [pallet_multisig, Multisig] + [pallet_parameters, Parameters] [pallet_migrations, MultiBlockMigrations] [frame_system, SystemBench::] [pallet_timestamp, Timestamp] diff --git a/runtime/src/tests.rs b/runtime/src/tests.rs index c7e6d9c8..5404e72b 100644 --- a/runtime/src/tests.rs +++ b/runtime/src/tests.rs @@ -1,7 +1,34 @@ +use polkadot_sdk::frame_support::traits::Get; +use polkadot_sdk::frame_support::{assert_noop, assert_ok}; use polkadot_sdk::pallet_staking::EraPayout; +use polkadot_sdk::sp_io::TestExternalities; use polkadot_sdk::sp_runtime::traits::Zero; +use polkadot_sdk::sp_runtime::{DispatchError, DispatchResult, Perbill}; -use crate::{Balance, EraPayout as SXTPayout, DOLLARS}; +use crate::{ + dynamic_params, + AccountId, + Balance, + EraPayout as SXTPayout, + Parameters, + RuntimeOrigin, + RuntimeParameters, + DOLLARS, +}; + +fn set_per_diem_rate(origin: RuntimeOrigin, rate: Perbill) -> DispatchResult { + Parameters::set_parameter( + origin, + RuntimeParameters::Rewards(dynamic_params::rewards::Parameters::PerDiemRate( + dynamic_params::rewards::PerDiemRate, + Some(rate), + )), + ) +} + +fn set_per_diem_rate_as_root(rate: Perbill) { + assert_ok!(set_per_diem_rate(RuntimeOrigin::root(), rate)); +} #[test] fn era_payout_calculation_works() { @@ -18,3 +45,38 @@ fn era_payout_calculation_works() { let single_era_payout = Balance::from(26557152635181379u128); assert_eq!(to_stakers, single_era_payout); } + +#[test] +fn per_diem_rate_defaults_to_9_7_percent_per_year() { + TestExternalities::default().execute_with(|| { + assert_eq!( + dynamic_params::rewards::PerDiemRate::get(), + Perbill::from_rational(97u64, 365_250u64) + ); + }); +} + +#[test] +fn root_can_set_per_diem_rate() { + TestExternalities::default().execute_with(|| { + set_per_diem_rate_as_root(Perbill::from_percent(1)); + + assert_eq!( + dynamic_params::rewards::PerDiemRate::get(), + Perbill::from_percent(1) + ); + }); +} + +#[test] +fn non_root_cannot_set_per_diem_rate() { + TestExternalities::default().execute_with(|| { + assert_noop!( + set_per_diem_rate( + RuntimeOrigin::signed(AccountId::from([1; 32])), + Perbill::from_percent(1) + ), + DispatchError::BadOrigin + ); + }); +} From 262a6257f59a34bac651833320db60ee7937a077 Mon Sep 17 00:00:00 2001 From: Jay White Date: Mon, 5 Oct 2026 17:41:34 -0400 Subject: [PATCH 2/5] feat: collect transaction fees in pallet-rewards --- pallets/rewards/src/lib.rs | 26 ++++++++++++++ pallets/rewards/src/mock.rs | 1 + pallets/rewards/src/tests.rs | 28 ++++++++++++++- runtime/src/lib.rs | 3 +- runtime/src/tests.rs | 69 ++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) diff --git a/pallets/rewards/src/lib.rs b/pallets/rewards/src/lib.rs index 90ddfaf9..06ebbcc2 100644 --- a/pallets/rewards/src/lib.rs +++ b/pallets/rewards/src/lib.rs @@ -24,13 +24,24 @@ pub use pallet::*; pub mod pallet { // Import various useful types required by all FRAME pallets. use polkadot_sdk::frame_support::pallet_prelude::*; + use polkadot_sdk::frame_support::traits::{Currency, Imbalance, OnUnbalanced}; use polkadot_sdk::frame_support::weights::Weight; use polkadot_sdk::frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; use polkadot_sdk::pallet_staking::WeightInfo; + use polkadot_sdk::sp_runtime::Saturating; use polkadot_sdk::{frame_system, pallet_staking, sp_staking}; use super::*; + /// The balance type of [`Config::Currency`]. + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + /// The negative imbalance type of [`Config::Currency`]. + type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId, + >>::NegativeImbalance; + /// Rewards pallet, providing automated reward payouts for validator block rewards #[pallet::pallet] pub struct Pallet(_); @@ -44,6 +55,8 @@ pub mod pallet { + IsType<::RuntimeEvent>; /// How many payout calls may be made per block (prevents overweight). type MaxPayoutsPerBlock: Get; + /// The currency transaction fees are collected in. + type Currency: Currency; } /// The next era that we expect to pay out. @@ -56,6 +69,19 @@ pub mod pallet { #[pallet::getter(fn payer_account)] pub type PayerAccount = StorageValue<_, T::AccountId, OptionQuery>; + /// Transaction fees collected for inclusion in the next era's validator payout. + #[pallet::storage] + pub type CollectedFees = StorageValue<_, BalanceOf, ValueQuery>; + + /// Adds transaction fees to [`CollectedFees`]. + pub struct DealWithFees(PhantomData); + + impl OnUnbalanced> for DealWithFees { + fn on_nonzero_unbalanced(credit: NegativeImbalanceOf) { + CollectedFees::::mutate(|total| *total = total.saturating_add(credit.peek())); + } + } + /// Errors that could occur while processing validator rewards for payout #[pallet::error] pub enum Error { diff --git a/pallets/rewards/src/mock.rs b/pallets/rewards/src/mock.rs index 2dea7d2e..60338fb8 100644 --- a/pallets/rewards/src/mock.rs +++ b/pallets/rewards/src/mock.rs @@ -74,6 +74,7 @@ pub const MAX_AUTHORITIES: u32 = 100_000u32; impl pallet_rewards::Config for Test { type RuntimeEvent = RuntimeEvent; type MaxPayoutsPerBlock = ConstU32<3>; + type Currency = Balances; } parameter_types! { diff --git a/pallets/rewards/src/tests.rs b/pallets/rewards/src/tests.rs index 48c5d987..b47db31d 100644 --- a/pallets/rewards/src/tests.rs +++ b/pallets/rewards/src/tests.rs @@ -1,6 +1,32 @@ -use crate::mock::new_test_ext; +use polkadot_sdk::frame_support::traits::{Currency, OnUnbalanced}; + +use crate::mock::{new_test_ext, Balances, Test}; +use crate::{CollectedFees, DealWithFees}; #[test] fn staking_rewards_pay_out() { new_test_ext().execute_with(|| {}); } + +#[test] +fn unbalanced_fees_accumulate_in_collected_fees() { + new_test_ext().execute_with(|| { + assert_eq!(CollectedFees::::get(), 0); + + DealWithFees::::on_unbalanced(Balances::issue(100)); + DealWithFees::::on_unbalanceds([Balances::issue(20), Balances::issue(3)].into_iter()); + + assert_eq!(CollectedFees::::get(), 123); + }); +} + +#[test] +fn collected_fees_saturate() { + new_test_ext().execute_with(|| { + CollectedFees::::put(u128::MAX - 1); + + DealWithFees::::on_unbalanced(Balances::issue(5)); + + assert_eq!(CollectedFees::::get(), u128::MAX); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a50f9fd2..212ddda1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -434,7 +434,7 @@ parameter_types! { impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[allow(deprecated)] - type OnChargeTransaction = CurrencyAdapter; + type OnChargeTransaction = CurrencyAdapter>; type WeightToFee = ConstantMultiplier; type LengthToFee = ConstantMultiplier; type FeeMultiplierUpdate = (); @@ -924,6 +924,7 @@ impl pallet_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; // Payout up to 3 pages per block type MaxPayoutsPerBlock = ConstU32<3>; + type Currency = Balances; } #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/src/tests.rs b/runtime/src/tests.rs index 5404e72b..ccb35630 100644 --- a/runtime/src/tests.rs +++ b/runtime/src/tests.rs @@ -1,18 +1,32 @@ +use codec::Encode; +use pallet_rewards::CollectedFees; +use polkadot_sdk::frame_support::traits::fungible::Mutate; use polkadot_sdk::frame_support::traits::Get; use polkadot_sdk::frame_support::{assert_noop, assert_ok}; use polkadot_sdk::pallet_staking::EraPayout; +use polkadot_sdk::pallet_transaction_payment::{Config, OnChargeTransaction}; +use polkadot_sdk::sp_core::{sr25519, Pair}; use polkadot_sdk::sp_io::TestExternalities; +use polkadot_sdk::sp_runtime::generic::Era; use polkadot_sdk::sp_runtime::traits::Zero; use polkadot_sdk::sp_runtime::{DispatchError, DispatchResult, Perbill}; +use polkadot_sdk::{frame_system, pallet_transaction_payment}; use crate::{ dynamic_params, AccountId, Balance, + Balances, + BuildStorage, EraPayout as SXTPayout, + Executive, Parameters, + Runtime, + RuntimeCall, RuntimeOrigin, RuntimeParameters, + SignedPayload, + UncheckedExtrinsic, DOLLARS, }; @@ -46,6 +60,61 @@ fn era_payout_calculation_works() { assert_eq!(to_stakers, single_era_payout); } +#[test] +fn transaction_fees_increase_collected_fees() { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + TestExternalities::new(storage).execute_with(|| { + let pair = sr25519::Pair::from_seed(&[1; 32]); + let who = AccountId::from(pair.public()); + Balances::mint_into(&who, DOLLARS).unwrap(); + + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + let extra = ( + frame_system::CheckNonZeroSender::new(), + frame_system::CheckSpecVersion::new(), + frame_system::CheckTxVersion::new(), + frame_system::CheckGenesis::new(), + frame_system::CheckEra::from(Era::Immortal), + frame_system::CheckNonce::from(0), + frame_system::CheckWeight::new(), + pallet_transaction_payment::ChargeTransactionPayment::from(0), + ); + let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); + let signature = payload.using_encoded(|m| pair.sign(m)); + let extrinsic = UncheckedExtrinsic::new_signed(call, who.into(), signature.into(), extra); + Executive::apply_extrinsic(extrinsic).unwrap().unwrap(); + assert!(CollectedFees::::get() > 0); + }); +} + +#[test] +fn fee_handler_deposits_into_collected_fees() { + TestExternalities::default().execute_with(|| { + let who = AccountId::from([1; 32]); + Balances::mint_into(&who, 10 * DOLLARS).unwrap(); + let fee = <::OnChargeTransaction as OnChargeTransaction>::withdraw_fee( + &who, + &RuntimeCall::System(frame_system::Call::remark { remark: vec![] }), + &Default::default(), + 2 * DOLLARS, + 0, + ) + .unwrap(); + <::OnChargeTransaction as OnChargeTransaction>::correct_and_deposit_fee( + &who, + &Default::default(), + &Default::default(), + 2 * DOLLARS, + 0, + fee, + ) + .unwrap(); + assert_eq!(CollectedFees::::get(), 2 * DOLLARS); + }); +} + #[test] fn per_diem_rate_defaults_to_9_7_percent_per_year() { TestExternalities::default().execute_with(|| { From 6aa6496262def149639f9808b10a80b30e9a14e3 Mon Sep 17 00:00:00 2001 From: Jay White Date: Thu, 1 Oct 2026 14:42:35 -0400 Subject: [PATCH 3/5] feat: pay validators a per-diem rate of total stake plus collected fees --- runtime/src/lib.rs | 22 +++++++++--------- runtime/src/tests.rs | 53 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 212ddda1..9ece706c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -31,6 +31,7 @@ pub use polkadot_sdk::frame_support::traits::{ ConstU64, ConstU8, Currency, + Get, KeyOwnerProofSystem, Randomness, StorageInfo, @@ -59,7 +60,6 @@ pub use polkadot_sdk::pallet_timestamp::Call as TimestampCall; #[allow(deprecated)] use polkadot_sdk::pallet_transaction_payment::{CurrencyAdapter, Multiplier}; use polkadot_sdk::sp_api::impl_runtime_apis; -use polkadot_sdk::sp_arithmetic::traits::UniqueSaturatedInto; use polkadot_sdk::sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use polkadot_sdk::sp_consensus_babe::AuthorityId as BabeId; use polkadot_sdk::sp_core::crypto::KeyTypeId; @@ -247,6 +247,7 @@ macro_rules! prod_or_dev { /// pub const MILLISECS_PER_BLOCK: u64 = 3000; pub const SECS_PER_BLOCK: u64 = MILLISECS_PER_BLOCK / 1000; +pub const MILLISECONDS_PER_DAY: u64 = 1000 * 3600 * 24; // NOTE: Currently it is not possible to change the slot duration after the chain has started. // Attempting to do so will brick block production. @@ -541,17 +542,14 @@ impl pallet_staking::EraPayout for EraPayout { _total_issuance: Balance, era_duration_millis: u64, ) -> (Balance, Balance) { - const MILLISECONDS_PER_YEAR: u64 = (1000 * 3600 * 24 * 36525) / 100; - // A normal-sized era will have 1 / 365.25 here: - let relative_era_len = - FixedU128::from_rational(era_duration_millis.into(), MILLISECONDS_PER_YEAR.into()); - - let base_rate = FixedU128::from_rational(97, 1000); - let yearly_emission = base_rate.saturating_mul_int(total_staked); - - let era_emission = relative_era_len.saturating_mul_int(yearly_emission); - - (era_emission.unique_saturated_into(), Balance::zero()) + ( + FixedU128::from_rational(era_duration_millis.into(), MILLISECONDS_PER_DAY.into()) + .saturating_mul_int( + dynamic_params::rewards::PerDiemRate::get().mul_floor(total_staked), + ) + .saturating_add(pallet_rewards::CollectedFees::::take()), + Balance::zero(), + ) } } diff --git a/runtime/src/tests.rs b/runtime/src/tests.rs index ccb35630..b52e5016 100644 --- a/runtime/src/tests.rs +++ b/runtime/src/tests.rs @@ -28,6 +28,7 @@ use crate::{ SignedPayload, UncheckedExtrinsic, DOLLARS, + MILLISECONDS_PER_DAY, }; fn set_per_diem_rate(origin: RuntimeOrigin, rate: Perbill) -> DispatchResult { @@ -45,19 +46,51 @@ fn set_per_diem_rate_as_root(rate: Perbill) { } #[test] -fn era_payout_calculation_works() { - let test_staked: Balance = Balance::from(100 * DOLLARS); - let test_issued: Balance = Balance::from(1000 * DOLLARS); +fn era_payout_defaults_to_yearly_rate_of_9_7_percent() { + TestExternalities::default().execute_with(|| { + let (payout, rest) = + SXTPayout::era_payout(365_250 * DOLLARS, 1000 * DOLLARS, MILLISECONDS_PER_DAY); + assert_eq!(rest, Balance::zero()); + assert!(payout.abs_diff(97 * DOLLARS) <= 97 * DOLLARS / 100_000); + }); +} - // One day of Milliseconds - let test_ms_per_era = 1000 * 3600 * 24; +#[test] +fn era_payout_pays_per_diem_rate_of_total_stake() { + TestExternalities::default().execute_with(|| { + set_per_diem_rate_as_root(Perbill::from_percent(1)); - let (to_stakers, to_treasury) = - SXTPayout::era_payout(test_staked, test_issued, test_ms_per_era); - assert_eq!(to_treasury, Balance::zero()); + assert_eq!( + SXTPayout::era_payout(100 * DOLLARS, 1000 * DOLLARS, MILLISECONDS_PER_DAY), + (DOLLARS, Balance::zero()) + ); + assert_eq!( + SXTPayout::era_payout(100 * DOLLARS, 1000 * DOLLARS, MILLISECONDS_PER_DAY / 2), + (DOLLARS / 2, Balance::zero()) + ); + assert_eq!( + SXTPayout::era_payout(100 * DOLLARS, 1000 * DOLLARS, 7 * MILLISECONDS_PER_DAY), + (7 * DOLLARS, Balance::zero()) + ); + }); +} + +#[test] +fn era_payout_includes_and_drains_collected_fees() { + TestExternalities::default().execute_with(|| { + set_per_diem_rate_as_root(Perbill::from_percent(1)); + CollectedFees::::put(5 * DOLLARS); - let single_era_payout = Balance::from(26557152635181379u128); - assert_eq!(to_stakers, single_era_payout); + assert_eq!( + SXTPayout::era_payout(100 * DOLLARS, 1000 * DOLLARS, MILLISECONDS_PER_DAY), + (6 * DOLLARS, Balance::zero()) + ); + assert_eq!(CollectedFees::::get(), Balance::zero()); + assert_eq!( + SXTPayout::era_payout(100 * DOLLARS, 1000 * DOLLARS, MILLISECONDS_PER_DAY), + (DOLLARS, Balance::zero()) + ); + }); } #[test] From a72a7a1ab5e9a026a22b14dd450157c1a298e684 Mon Sep 17 00:00:00 2001 From: Jay White Date: Thu, 1 Oct 2026 14:42:35 -0400 Subject: [PATCH 4/5] refactor: make the zero transaction fee multiplier explicit --- runtime/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 9ece706c..114af03b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -58,7 +58,7 @@ use polkadot_sdk::pallet_grandpa::AuthorityId as GrandpaId; pub use polkadot_sdk::pallet_im_online::sr25519::AuthorityId as ImOnlineId; pub use polkadot_sdk::pallet_timestamp::Call as TimestampCall; #[allow(deprecated)] -use polkadot_sdk::pallet_transaction_payment::{CurrencyAdapter, Multiplier}; +use polkadot_sdk::pallet_transaction_payment::{ConstFeeMultiplier, CurrencyAdapter, Multiplier}; use polkadot_sdk::sp_api::impl_runtime_apis; use polkadot_sdk::sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use polkadot_sdk::sp_consensus_babe::AuthorityId as BabeId; @@ -426,6 +426,7 @@ parameter_types! { pub const TransactionByteFee: Balance = TARGET_BYTE_FEE; pub const WeightFeePerRefTime: Balance = WEIGHT_FEE; pub const OperationalFeeMultiplier: u8 = 5; + pub const FeeMultiplier: Multiplier = Multiplier::from_u32(0); pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(80); pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(1, 100_000); pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 1_000_000_000u128); @@ -438,7 +439,7 @@ impl pallet_transaction_payment::Config for Runtime { type OnChargeTransaction = CurrencyAdapter>; type WeightToFee = ConstantMultiplier; type LengthToFee = ConstantMultiplier; - type FeeMultiplierUpdate = (); + type FeeMultiplierUpdate = ConstFeeMultiplier; type OperationalFeeMultiplier = OperationalFeeMultiplier; } From af95e40d30950ced96a65cd7c4a47405c23166f9 Mon Sep 17 00:00:00 2001 From: Jay White Date: Thu, 1 Oct 2026 14:42:35 -0400 Subject: [PATCH 5/5] feat: make transaction fees root-settable --- runtime/src/lib.rs | 29 +++++++++--- runtime/src/tests.rs | 104 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 114af03b..15cda533 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -423,10 +423,7 @@ pub const WEIGHT_FEE: u128 = AVERAGE_INSERT_TARGET_COST_PER_ROW.saturating_div(INSERT_FEE_TARGET_CALL_WEIGHT); parameter_types! { - pub const TransactionByteFee: Balance = TARGET_BYTE_FEE; - pub const WeightFeePerRefTime: Balance = WEIGHT_FEE; pub const OperationalFeeMultiplier: u8 = 5; - pub const FeeMultiplier: Multiplier = Multiplier::from_u32(0); pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(80); pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(1, 100_000); pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 1_000_000_000u128); @@ -437,9 +434,12 @@ impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[allow(deprecated)] type OnChargeTransaction = CurrencyAdapter>; - type WeightToFee = ConstantMultiplier; - type LengthToFee = ConstantMultiplier; - type FeeMultiplierUpdate = ConstFeeMultiplier; + type WeightToFee = + ConstantMultiplier; + type LengthToFee = + ConstantMultiplier; + type FeeMultiplierUpdate = + ConstFeeMultiplier; type OperationalFeeMultiplier = OperationalFeeMultiplier; } @@ -900,6 +900,23 @@ pub mod dynamic_params { #[codec(index = 0)] pub static PerDiemRate: Perbill = Perbill::from_rational(97u64, 365_250u64); } + + /// Transaction fee parameters. + #[dynamic_pallet_params] + #[codec(index = 1)] + pub mod transaction_payment { + /// Fee per unit of `ref_time`, applied to the base fee and the multiplied weight fee. + #[codec(index = 0)] + pub static WeightFeePerRefTime: Balance = WEIGHT_FEE; + + /// Fee per byte of extrinsic length. + #[codec(index = 1)] + pub static TransactionByteFee: Balance = TARGET_BYTE_FEE; + + /// Multiplier applied to the weight fee, excluding the base fee. + #[codec(index = 2)] + pub static FeeMultiplier: Multiplier = Multiplier::from_u32(0); + } } #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/src/tests.rs b/runtime/src/tests.rs index b52e5016..0551ecee 100644 --- a/runtime/src/tests.rs +++ b/runtime/src/tests.rs @@ -1,7 +1,7 @@ use codec::Encode; use pallet_rewards::CollectedFees; use polkadot_sdk::frame_support::traits::fungible::Mutate; -use polkadot_sdk::frame_support::traits::Get; +use polkadot_sdk::frame_support::traits::{Get, Hooks}; use polkadot_sdk::frame_support::{assert_noop, assert_ok}; use polkadot_sdk::pallet_staking::EraPayout; use polkadot_sdk::pallet_transaction_payment::{Config, OnChargeTransaction}; @@ -20,15 +20,20 @@ use crate::{ BuildStorage, EraPayout as SXTPayout, Executive, + Multiplier, Parameters, Runtime, RuntimeCall, RuntimeOrigin, RuntimeParameters, SignedPayload, + TransactionPayment, UncheckedExtrinsic, + Weight, DOLLARS, MILLISECONDS_PER_DAY, + TARGET_BYTE_FEE, + WEIGHT_FEE, }; fn set_per_diem_rate(origin: RuntimeOrigin, rate: Perbill) -> DispatchResult { @@ -45,6 +50,13 @@ fn set_per_diem_rate_as_root(rate: Perbill) { assert_ok!(set_per_diem_rate(RuntimeOrigin::root(), rate)); } +fn set_transaction_payment_parameter( + origin: RuntimeOrigin, + parameter: dynamic_params::transaction_payment::Parameters, +) -> DispatchResult { + Parameters::set_parameter(origin, RuntimeParameters::TransactionPayment(parameter)) +} + #[test] fn era_payout_defaults_to_yearly_rate_of_9_7_percent() { TestExternalities::default().execute_with(|| { @@ -182,3 +194,93 @@ fn non_root_cannot_set_per_diem_rate() { ); }); } + +#[test] +fn transaction_fee_parameters_default_to_current_fees() { + TestExternalities::default().execute_with(|| { + assert_eq!( + TransactionPayment::weight_to_fee(Weight::from_parts(1, 0)), + WEIGHT_FEE + ); + assert_eq!(TransactionPayment::length_to_fee(1), TARGET_BYTE_FEE); + + TransactionPayment::on_finalize(1); + + assert_eq!( + TransactionPayment::next_fee_multiplier(), + Multiplier::zero() + ); + }); +} + +#[test] +fn root_can_set_weight_and_length_fees() { + TestExternalities::default().execute_with(|| { + assert_ok!(set_transaction_payment_parameter( + RuntimeOrigin::root(), + dynamic_params::transaction_payment::Parameters::WeightFeePerRefTime( + dynamic_params::transaction_payment::WeightFeePerRefTime, + Some(7), + ), + )); + assert_ok!(set_transaction_payment_parameter( + RuntimeOrigin::root(), + dynamic_params::transaction_payment::Parameters::TransactionByteFee( + dynamic_params::transaction_payment::TransactionByteFee, + Some(11), + ), + )); + + assert_eq!( + TransactionPayment::weight_to_fee(Weight::from_parts(10, 0)), + 70 + ); + assert_eq!(TransactionPayment::length_to_fee(10), 110); + }); +} + +#[test] +fn non_root_cannot_set_transaction_fees() { + TestExternalities::default().execute_with(|| { + let origin = RuntimeOrigin::signed(AccountId::from([1; 32])); + for parameter in [ + dynamic_params::transaction_payment::Parameters::WeightFeePerRefTime( + dynamic_params::transaction_payment::WeightFeePerRefTime, + Some(7), + ), + dynamic_params::transaction_payment::Parameters::TransactionByteFee( + dynamic_params::transaction_payment::TransactionByteFee, + Some(11), + ), + dynamic_params::transaction_payment::Parameters::FeeMultiplier( + dynamic_params::transaction_payment::FeeMultiplier, + Some(Multiplier::from_u32(2)), + ), + ] { + assert_noop!( + set_transaction_payment_parameter(origin.clone(), parameter), + DispatchError::BadOrigin + ); + } + }); +} + +#[test] +fn fee_multiplier_parameter_sets_next_fee_multiplier() { + TestExternalities::default().execute_with(|| { + assert_ok!(set_transaction_payment_parameter( + RuntimeOrigin::root(), + dynamic_params::transaction_payment::Parameters::FeeMultiplier( + dynamic_params::transaction_payment::FeeMultiplier, + Some(Multiplier::from_u32(2)), + ), + )); + + TransactionPayment::on_finalize(1); + + assert_eq!( + TransactionPayment::next_fee_multiplier(), + Multiplier::from_u32(2) + ); + }); +}