mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
feat: ✨ Implement dynamic fee adjustment (#251)
# ✨ Implement Dynamic Fee Adjustment Mechanism
## Overview
Implements a dynamic fee adjustment mechanism, replacing the constant
fee multiplier with an adaptive multiplier that responds to network
congestion, following Moonbeam's pattern.
## Changes
- Replaces `ConstFeeMultiplier` with `TargetedFeeAdjustment` across all
runtime configurations (mainnet, stagenet, testnet)
- Implements an EIP-1559-like slow-adjusting fee mechanism that prevents
DoS attacks by adjusting fees based on block fullness
- **Configurable Parameters**:
- Target block fullness: 35%
- Adjustment variable: 4/1000 (responds in ~1 hour at extreme
congestion)
- Two modes:
- `SlowAdjustingFeeUpdate` for mainnet and testnet.
- `FastAdjustingFeeUpdate` for stagenet.
- Adds tests coverage for different fee scenarios
## Technical Details
The fee adjustment algorithm works as follows:
```
diff = (previous_block_weight - target) / maximum_block_weight
next_multiplier = prev_multiplier * (1 + (v * diff) + ((v * diff)^2 / 2))
assert(next_multiplier > min)
```
**Where:**
- `v` = AdjustmentVariable
- `target` = TargetBlockFullness
- `min` = MinimumMultiplier
`SlowAdjustingFeeUpdate` sets a minimum multiplier of `1x` for a
conservative fee adjustment, while `FastAdjustingFeeUpdate` sets it to
`0.1x`, which is mainly used for dev networks / testing.
This commit is contained in:
parent
830d4baf8a
commit
782321e5d0
9 changed files with 794 additions and 21 deletions
|
|
@ -123,7 +123,7 @@ use pallet_evm::{
|
|||
use pallet_grandpa::AuthorityId as GrandpaId;
|
||||
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
|
||||
use pallet_transaction_payment::{
|
||||
ConstFeeMultiplier, FungibleAdapter, Multiplier, Pallet as TransactionPayment,
|
||||
FungibleAdapter, Multiplier, Pallet as TransactionPayment, TargetedFeeAdjustment,
|
||||
};
|
||||
use polkadot_primitives::Moment;
|
||||
use runtime_params::RuntimeParameters;
|
||||
|
|
@ -144,10 +144,8 @@ use sp_consensus_beefy::{
|
|||
use sp_core::{crypto::KeyTypeId, Get, H160, H256, U256};
|
||||
use sp_runtime::FixedU128;
|
||||
use sp_runtime::{
|
||||
traits::{
|
||||
Convert, ConvertInto, IdentityLookup, Keccak256, One, OpaqueKeys, UniqueSaturatedInto,
|
||||
},
|
||||
FixedPointNumber, Perbill,
|
||||
traits::{Convert, ConvertInto, IdentityLookup, Keccak256, OpaqueKeys, UniqueSaturatedInto},
|
||||
FixedPointNumber, Perbill, Perquintill,
|
||||
};
|
||||
use sp_staking::{EraIndex, SessionIndex};
|
||||
use sp_std::{
|
||||
|
|
@ -451,9 +449,41 @@ impl pallet_grandpa::Config for Runtime {
|
|||
}
|
||||
|
||||
parameter_types! {
|
||||
pub FeeMultiplier: Multiplier = Multiplier::one();
|
||||
/// The portion of the `NORMAL_DISPATCH_RATIO` that we adjust the fees with. Blocks filled less
|
||||
/// than this will decrease the weight and more will increase.
|
||||
pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(35);
|
||||
/// The adjustment variable of the runtime. Higher values will cause `TargetBlockFullness` to
|
||||
/// change the fees more rapidly. This fast multiplier responds by doubling/halving in
|
||||
/// approximately one hour at extreme block congestion levels.
|
||||
pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(4, 1_000);
|
||||
/// Minimum amount of the multiplier. This value cannot be too low. A test case should ensure
|
||||
/// that combined with `AdjustmentVariable`, we can recover from the minimum.
|
||||
/// See `multiplier_can_grow_from_zero` in integration_tests.rs.
|
||||
pub MinimumMultiplier: Multiplier = Multiplier::from(1u128);
|
||||
/// Maximum multiplier. We pick a value that is expensive but not impossibly so; it should act
|
||||
/// as a safety net.
|
||||
pub MaximumMultiplier: Multiplier = Multiplier::from(100_000u128);
|
||||
}
|
||||
|
||||
/// SlowAdjustingFeeUpdate implements a dynamic fee adjustment mechanism similar to Ethereum's EIP-1559.
|
||||
/// It adjusts transaction fees based on network congestion to prevent DoS attacks.
|
||||
/// This version uses a higher minimum multiplier for more conservative fee adjustments.
|
||||
///
|
||||
/// The algorithm works as follows:
|
||||
/// diff = (previous_block_weight - target) / maximum_block_weight
|
||||
/// next_multiplier = prev_multiplier * (1 + (v * diff) + ((v * diff)^2 / 2))
|
||||
/// assert(next_multiplier > min)
|
||||
/// where: v is AdjustmentVariable
|
||||
/// target is TargetBlockFullness
|
||||
/// min is MinimumMultiplier
|
||||
pub type SlowAdjustingFeeUpdate<R> = TargetedFeeAdjustment<
|
||||
R,
|
||||
TargetBlockFullness,
|
||||
AdjustmentVariable,
|
||||
MinimumMultiplier,
|
||||
MaximumMultiplier,
|
||||
>;
|
||||
|
||||
impl pallet_transaction_payment::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = FungibleAdapter<
|
||||
|
|
@ -472,7 +502,7 @@ impl pallet_transaction_payment::Config for Runtime {
|
|||
type LengthToFee = IdentityFee<Balance>;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type LengthToFee = benchmark_helpers::BenchmarkWeightToFee;
|
||||
type FeeMultiplierUpdate = ConstFeeMultiplier<FeeMultiplier>;
|
||||
type FeeMultiplierUpdate = SlowAdjustingFeeUpdate<Runtime>;
|
||||
type WeightInfo = mainnet_weights::pallet_transaction_payment::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
|
|
|
|||
227
operator/runtime/mainnet/tests/fee_adjustment.rs
Normal file
227
operator/runtime/mainnet/tests/fee_adjustment.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Copyright 2025 Moonbeam Foundation.
|
||||
// This file is part of DataHaven.
|
||||
|
||||
//! Fee adjustment integration tests for DataHaven mainnet runtime
|
||||
//! Based on Moonbeam's fee adjustment tests
|
||||
|
||||
use datahaven_mainnet_runtime::{
|
||||
configs::{
|
||||
MinimumMultiplier, RuntimeBlockWeights, SlowAdjustingFeeUpdate, TargetBlockFullness,
|
||||
TransactionPaymentAsGasPrice,
|
||||
},
|
||||
currency::WEIGHT_FEE,
|
||||
Runtime, System,
|
||||
};
|
||||
use datahaven_runtime_common::constants::gas::WEIGHT_PER_GAS;
|
||||
use fp_evm::FeeCalculator;
|
||||
use frame_support::pallet_prelude::DispatchClass;
|
||||
use frame_support::traits::OnFinalize;
|
||||
use sp_core::U256;
|
||||
use sp_runtime::{traits::Convert, BuildStorage, FixedPointNumber, FixedU128, Perbill};
|
||||
|
||||
/// Helper function to run tests with a specific system weight
|
||||
fn run_with_system_weight<F>(w: frame_support::weights::Weight, mut assertions: F)
|
||||
where
|
||||
F: FnMut() -> (),
|
||||
{
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
System::set_block_consumed_resources(w, 0);
|
||||
assertions()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiplier_can_grow_from_zero() {
|
||||
let minimum_multiplier = MinimumMultiplier::get();
|
||||
let target = TargetBlockFullness::get()
|
||||
* RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
// if the min is too small, then this will not change, and we are doomed forever.
|
||||
// the weight is 1/100th bigger than target.
|
||||
run_with_system_weight(target * 101 / 100, || {
|
||||
let next = SlowAdjustingFeeUpdate::<Runtime>::convert(minimum_multiplier);
|
||||
assert!(
|
||||
next > minimum_multiplier,
|
||||
"{:?} !>= {:?}",
|
||||
next,
|
||||
minimum_multiplier
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_calculation() {
|
||||
let base_extrinsic = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.base_extrinsic;
|
||||
let multiplier = FixedU128::from_float(0.999000000000000000);
|
||||
let extrinsic_len = 100u32;
|
||||
let extrinsic_weight = 5_000u64;
|
||||
let tip = 42u128;
|
||||
|
||||
// For IdentityFee, the fee is just the weight itself
|
||||
// Formula: base_extrinsic + (multiplier * call_weight) + extrinsic_len + tip
|
||||
let expected_fee = base_extrinsic.ref_time() as u128
|
||||
+ (multiplier.saturating_mul_int(extrinsic_weight as u128))
|
||||
+ extrinsic_len as u128
|
||||
+ tip;
|
||||
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual_fee = pallet_transaction_payment::Pallet::<Runtime>::compute_fee(
|
||||
extrinsic_len,
|
||||
&frame_support::dispatch::DispatchInfo {
|
||||
class: DispatchClass::Normal,
|
||||
pays_fee: frame_support::dispatch::Pays::Yes,
|
||||
call_weight: frame_support::weights::Weight::from_parts(extrinsic_weight, 1),
|
||||
extension_weight: frame_support::weights::Weight::zero(),
|
||||
},
|
||||
tip,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
expected_fee, actual_fee,
|
||||
"The actual fee did not match the expected fee, expected: {}, actual: {}",
|
||||
expected_fee, actual_fee
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_is_deterministic() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier = FixedU128::from_u32(1);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual = TransactionPaymentAsGasPrice::min_gas_price().0;
|
||||
let expected: U256 = multiplier
|
||||
.saturating_mul_int(WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128))
|
||||
.into();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_has_no_precision_loss_from_saturating_mul_int() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier_1 = FixedU128::from_float(0.999593900000000000);
|
||||
let multiplier_2 = FixedU128::from_float(0.999593200000000000);
|
||||
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_1);
|
||||
let a = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_2);
|
||||
let b = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
|
||||
assert_ne!(
|
||||
a, b,
|
||||
"both gas prices were equal, unexpected precision loss incurred"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_scenarios() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let weight_fee_per_gas = WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128);
|
||||
let sim = |start_gas_price: u128, fullness: Perbill, num_blocks: u64| -> U256 {
|
||||
let start_multiplier = FixedU128::from_rational(start_gas_price, weight_fee_per_gas);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(start_multiplier);
|
||||
|
||||
let normal_weight = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
let block_weight = normal_weight * fullness;
|
||||
|
||||
for i in 0..num_blocks {
|
||||
System::set_block_number(i as u32);
|
||||
System::set_block_consumed_resources(block_weight, 0);
|
||||
pallet_transaction_payment::Pallet::<Runtime>::on_finalize(i as u32);
|
||||
}
|
||||
|
||||
TransactionPaymentAsGasPrice::min_gas_price().0
|
||||
};
|
||||
|
||||
// The expected values are the ones observed during test execution,
|
||||
// they are expected to change when parameters that influence
|
||||
// the fee calculation are changed, and should be updated accordingly.
|
||||
// If a test fails when nothing specific to fees has changed,
|
||||
// it may indicate an unexpected collateral effect and should be investigated
|
||||
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 1),
|
||||
U256::from(31_250_000_000u128), // lower bound enforced
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 1),
|
||||
U256::from(31_250_000_000u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 1),
|
||||
U256::from(31_268_755_625u128), // slightly higher than lower bound
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 1),
|
||||
U256::from(31_331_355_625u128), // a bit higher than before
|
||||
);
|
||||
|
||||
// 1 "real" hour (at 12-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 600),
|
||||
U256::from(31_250_000_000u128), // lower bound enforced
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 600),
|
||||
U256::from(31_250_000_000u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 600),
|
||||
U256::from(44_791_543_237u128), // DataHaven specific value
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 600),
|
||||
U256::from(148_712_903_041u128), // DataHaven specific value
|
||||
);
|
||||
|
||||
// 1 "real" day (at 12-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 14400),
|
||||
U256::from(31_250_000_000u128), // lower bound enforced
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 14400),
|
||||
U256::from(31_250_000_000u128), // lower bound enforced if threshold not reached
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 14400),
|
||||
U256::from(176_666_465_470_908u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 14400),
|
||||
U256::from(3_125_000_000_000_000u128),
|
||||
// upper bound enforced (min_gas_price * MaximumMultiplier)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Integration tests for DataHaven mainnet runtime
|
||||
|
||||
pub mod common;
|
||||
mod fee_adjustment;
|
||||
pub mod governance;
|
||||
mod migrations;
|
||||
mod native_token_transfer;
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ use pallet_evm::{
|
|||
use pallet_grandpa::AuthorityId as GrandpaId;
|
||||
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
|
||||
use pallet_transaction_payment::{
|
||||
ConstFeeMultiplier, FungibleAdapter, Multiplier, Pallet as TransactionPayment,
|
||||
FungibleAdapter, Multiplier, Pallet as TransactionPayment, TargetedFeeAdjustment,
|
||||
};
|
||||
use polkadot_primitives::Moment;
|
||||
use runtime_params::RuntimeParameters;
|
||||
|
|
@ -144,10 +144,8 @@ use sp_consensus_beefy::{
|
|||
use sp_core::{crypto::KeyTypeId, Get, H160, H256, U256};
|
||||
use sp_runtime::FixedU128;
|
||||
use sp_runtime::{
|
||||
traits::{
|
||||
Convert, ConvertInto, IdentityLookup, Keccak256, One, OpaqueKeys, UniqueSaturatedInto,
|
||||
},
|
||||
FixedPointNumber, Perbill,
|
||||
traits::{Convert, ConvertInto, IdentityLookup, Keccak256, OpaqueKeys, UniqueSaturatedInto},
|
||||
FixedPointNumber, Perbill, Perquintill,
|
||||
};
|
||||
use sp_staking::{EraIndex, SessionIndex};
|
||||
use sp_std::{
|
||||
|
|
@ -450,9 +448,39 @@ impl pallet_grandpa::Config for Runtime {
|
|||
}
|
||||
|
||||
parameter_types! {
|
||||
pub FeeMultiplier: Multiplier = Multiplier::one();
|
||||
/// The portion of the `NORMAL_DISPATCH_RATIO` that we adjust the fees with. Blocks filled less
|
||||
/// than this will decrease the weight and more will increase.
|
||||
pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(35);
|
||||
/// The adjustment variable of the runtime. Higher values will cause `TargetBlockFullness` to
|
||||
/// change the fees more rapidly. This low value causes changes to occur slowly over time.
|
||||
pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(4, 1_000);
|
||||
/// Minimum amount of the multiplier. This value cannot be too low. A test case should ensure
|
||||
/// that combined with `AdjustmentVariable`, we can recover from the minimum.
|
||||
/// See `multiplier_can_grow_from_zero` in integration_tests.rs.
|
||||
pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 10);
|
||||
/// Maximum multiplier. We pick a value that is expensive but not impossibly so; it should act
|
||||
/// as a safety net.
|
||||
pub MaximumMultiplier: Multiplier = Multiplier::from(100_000u128);
|
||||
}
|
||||
|
||||
/// FastAdjustingFeeUpdate implements a dynamic fee adjustment mechanism similar to Ethereum's EIP-1559.
|
||||
/// It adjusts transaction fees based on network congestion to prevent DoS attacks.
|
||||
///
|
||||
/// The algorithm works as follows:
|
||||
/// diff = (previous_block_weight - target) / maximum_block_weight
|
||||
/// next_multiplier = prev_multiplier * (1 + (v * diff) + ((v * diff)^2 / 2))
|
||||
/// assert(next_multiplier > min)
|
||||
/// where: v is AdjustmentVariable
|
||||
/// target is TargetBlockFullness
|
||||
/// min is MinimumMultiplier
|
||||
pub type FastAdjustingFeeUpdate<R> = TargetedFeeAdjustment<
|
||||
R,
|
||||
TargetBlockFullness,
|
||||
AdjustmentVariable,
|
||||
MinimumMultiplier,
|
||||
MaximumMultiplier,
|
||||
>;
|
||||
|
||||
impl pallet_transaction_payment::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = FungibleAdapter<
|
||||
|
|
@ -471,7 +499,7 @@ impl pallet_transaction_payment::Config for Runtime {
|
|||
type LengthToFee = IdentityFee<Balance>;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type LengthToFee = benchmark_helpers::BenchmarkWeightToFee;
|
||||
type FeeMultiplierUpdate = ConstFeeMultiplier<FeeMultiplier>;
|
||||
type FeeMultiplierUpdate = FastAdjustingFeeUpdate<Runtime>;
|
||||
type WeightInfo = stagenet_weights::pallet_transaction_payment::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
|
|
|
|||
227
operator/runtime/stagenet/tests/fee_adjustment.rs
Normal file
227
operator/runtime/stagenet/tests/fee_adjustment.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Copyright 2025 Moonbeam Foundation.
|
||||
// This file is part of DataHaven.
|
||||
|
||||
//! Fee adjustment integration tests for DataHaven stagenet runtime
|
||||
//! Based on Moonbeam's fee adjustment tests
|
||||
|
||||
use datahaven_runtime_common::constants::gas::WEIGHT_PER_GAS;
|
||||
use datahaven_stagenet_runtime::{
|
||||
configs::{
|
||||
FastAdjustingFeeUpdate, MinimumMultiplier, RuntimeBlockWeights, TargetBlockFullness,
|
||||
TransactionPaymentAsGasPrice,
|
||||
},
|
||||
currency::WEIGHT_FEE,
|
||||
Runtime, System,
|
||||
};
|
||||
use fp_evm::FeeCalculator;
|
||||
use frame_support::pallet_prelude::DispatchClass;
|
||||
use frame_support::traits::OnFinalize;
|
||||
use sp_core::U256;
|
||||
use sp_runtime::{traits::Convert, BuildStorage, FixedPointNumber, FixedU128, Perbill};
|
||||
|
||||
/// Helper function to run tests with a specific system weight
|
||||
fn run_with_system_weight<F>(w: frame_support::weights::Weight, mut assertions: F)
|
||||
where
|
||||
F: FnMut() -> (),
|
||||
{
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
System::set_block_consumed_resources(w, 0);
|
||||
assertions()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiplier_can_grow_from_zero() {
|
||||
let minimum_multiplier = MinimumMultiplier::get();
|
||||
let target = TargetBlockFullness::get()
|
||||
* RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
// if the min is too small, then this will not change, and we are doomed forever.
|
||||
// the weight is 1/100th bigger than target.
|
||||
run_with_system_weight(target * 101 / 100, || {
|
||||
let next = FastAdjustingFeeUpdate::<Runtime>::convert(minimum_multiplier);
|
||||
assert!(
|
||||
next > minimum_multiplier,
|
||||
"{:?} !>= {:?}",
|
||||
next,
|
||||
minimum_multiplier
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_calculation() {
|
||||
let base_extrinsic = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.base_extrinsic;
|
||||
let multiplier = FixedU128::from_float(0.999000000000000000);
|
||||
let extrinsic_len = 100u32;
|
||||
let extrinsic_weight = 5_000u64;
|
||||
let tip = 42u128;
|
||||
|
||||
// For IdentityFee, the fee is just the weight itself
|
||||
// Formula: base_extrinsic + (multiplier * call_weight) + extrinsic_len + tip
|
||||
let expected_fee = base_extrinsic.ref_time() as u128
|
||||
+ (multiplier.saturating_mul_int(extrinsic_weight as u128))
|
||||
+ extrinsic_len as u128
|
||||
+ tip;
|
||||
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual_fee = pallet_transaction_payment::Pallet::<Runtime>::compute_fee(
|
||||
extrinsic_len,
|
||||
&frame_support::dispatch::DispatchInfo {
|
||||
class: DispatchClass::Normal,
|
||||
pays_fee: frame_support::dispatch::Pays::Yes,
|
||||
call_weight: frame_support::weights::Weight::from_parts(extrinsic_weight, 1),
|
||||
extension_weight: frame_support::weights::Weight::zero(),
|
||||
},
|
||||
tip,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
expected_fee, actual_fee,
|
||||
"The actual fee did not match the expected fee, expected: {}, actual: {}",
|
||||
expected_fee, actual_fee
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_is_deterministic() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier = FixedU128::from_u32(1);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual = TransactionPaymentAsGasPrice::min_gas_price().0;
|
||||
let expected: U256 = multiplier
|
||||
.saturating_mul_int(WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128))
|
||||
.into();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_has_no_precision_loss_from_saturating_mul_int() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier_1 = FixedU128::from_float(0.999593900000000000);
|
||||
let multiplier_2 = FixedU128::from_float(0.999593200000000000);
|
||||
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_1);
|
||||
let a = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_2);
|
||||
let b = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
|
||||
assert_ne!(
|
||||
a, b,
|
||||
"both gas prices were equal, unexpected precision loss incurred"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_scenarios() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let weight_fee_per_gas = WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128);
|
||||
let sim = |start_gas_price: u128, fullness: Perbill, num_blocks: u64| -> U256 {
|
||||
let start_multiplier = FixedU128::from_rational(start_gas_price, weight_fee_per_gas);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(start_multiplier);
|
||||
|
||||
let normal_weight = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
let block_weight = normal_weight * fullness;
|
||||
|
||||
for i in 0..num_blocks {
|
||||
System::set_block_number(i as u32);
|
||||
System::set_block_consumed_resources(block_weight, 0);
|
||||
pallet_transaction_payment::Pallet::<Runtime>::on_finalize(i as u32);
|
||||
}
|
||||
|
||||
TransactionPaymentAsGasPrice::min_gas_price().0
|
||||
};
|
||||
|
||||
// The expected values are the ones observed during test execution,
|
||||
// they are expected to change when parameters that influence
|
||||
// the fee calculation are changed, and should be updated accordingly.
|
||||
// If a test fails when nothing specific to fees has changed,
|
||||
// it may indicate an unexpected collateral effect and should be investigated
|
||||
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 1),
|
||||
U256::from(998_600_980),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 1),
|
||||
U256::from(999_600_080),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 1),
|
||||
U256::from(1_000_600_180),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 1),
|
||||
U256::from(1_002_603_380),
|
||||
);
|
||||
|
||||
// 1 "real" hour (at 6-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 600),
|
||||
U256::from(431_710_642),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 600),
|
||||
U256::from(786_627_866),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 600),
|
||||
U256::from(1_433_329_383u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 600),
|
||||
U256::from(4_758_812_897u128),
|
||||
);
|
||||
|
||||
// 1 "real" day (at 6-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 14400),
|
||||
U256::from(31_250_000), // lower bound enforced
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 14400),
|
||||
U256::from(31_250_000), // lower bound enforced if threshold not reached
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 14400),
|
||||
U256::from(5_653_326_895_069u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 14400),
|
||||
U256::from(31_250_000_000_000u128),
|
||||
// upper bound enforced (min_gas_price * MaximumMultiplier)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Integration tests for DataHaven stagenet runtime
|
||||
|
||||
pub mod common;
|
||||
mod fee_adjustment;
|
||||
pub mod governance;
|
||||
mod native_token_transfer;
|
||||
mod proxy;
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ use pallet_evm::{
|
|||
use pallet_grandpa::AuthorityId as GrandpaId;
|
||||
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
|
||||
use pallet_transaction_payment::{
|
||||
ConstFeeMultiplier, FungibleAdapter, Multiplier, Pallet as TransactionPayment,
|
||||
FungibleAdapter, Multiplier, Pallet as TransactionPayment, TargetedFeeAdjustment,
|
||||
};
|
||||
use polkadot_primitives::Moment;
|
||||
use runtime_params::RuntimeParameters;
|
||||
|
|
@ -144,10 +144,8 @@ use sp_consensus_beefy::{
|
|||
use sp_core::{crypto::KeyTypeId, Get, H160, H256, U256};
|
||||
use sp_runtime::FixedU128;
|
||||
use sp_runtime::{
|
||||
traits::{
|
||||
Convert, ConvertInto, IdentityLookup, Keccak256, One, OpaqueKeys, UniqueSaturatedInto,
|
||||
},
|
||||
FixedPointNumber, Perbill,
|
||||
traits::{Convert, ConvertInto, IdentityLookup, Keccak256, OpaqueKeys, UniqueSaturatedInto},
|
||||
FixedPointNumber, Perbill, Perquintill,
|
||||
};
|
||||
use sp_staking::{EraIndex, SessionIndex};
|
||||
use sp_std::{
|
||||
|
|
@ -450,9 +448,42 @@ impl pallet_grandpa::Config for Runtime {
|
|||
}
|
||||
|
||||
parameter_types! {
|
||||
pub FeeMultiplier: Multiplier = Multiplier::one();
|
||||
/// The portion of the `NORMAL_DISPATCH_RATIO` that we adjust the fees with. Blocks filled less
|
||||
/// than this will decrease the weight and more will increase.
|
||||
pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(35);
|
||||
/// The adjustment variable of the runtime. Higher values will cause `TargetBlockFullness` to
|
||||
/// change the fees more rapidly. This low value causes changes to occur slowly over time.
|
||||
pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(4, 1_000);
|
||||
/// Minimum amount of the multiplier. This value cannot be too low. A test case should ensure
|
||||
/// that combined with `AdjustmentVariable`, we can recover from the minimum.
|
||||
/// See `multiplier_can_grow_from_zero` in integration_tests.rs.
|
||||
/// This value is currently only used by pallet-transaction-payment as an assertion that the
|
||||
/// next multiplier is always > min value.
|
||||
pub MinimumMultiplier: Multiplier = Multiplier::from(1u128);
|
||||
/// Maximum multiplier. We pick a value that is expensive but not impossibly so; it should act
|
||||
/// as a safety net.
|
||||
pub MaximumMultiplier: Multiplier = Multiplier::from(100_000u128);
|
||||
}
|
||||
|
||||
/// SlowAdjustingFeeUpdate implements a dynamic fee adjustment mechanism similar to Ethereum's EIP-1559.
|
||||
/// It adjusts transaction fees based on network congestion to prevent DoS attacks.
|
||||
/// This version uses a higher minimum multiplier for more conservative fee adjustments.
|
||||
///
|
||||
/// The algorithm works as follows:
|
||||
/// diff = (previous_block_weight - target) / maximum_block_weight
|
||||
/// next_multiplier = prev_multiplier * (1 + (v * diff) + ((v * diff)^2 / 2))
|
||||
/// assert(next_multiplier > min)
|
||||
/// where: v is AdjustmentVariable
|
||||
/// target is TargetBlockFullness
|
||||
/// min is MinimumMultiplier
|
||||
pub type SlowAdjustingFeeUpdate<R> = TargetedFeeAdjustment<
|
||||
R,
|
||||
TargetBlockFullness,
|
||||
AdjustmentVariable,
|
||||
MinimumMultiplier,
|
||||
MaximumMultiplier,
|
||||
>;
|
||||
|
||||
impl pallet_transaction_payment::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = FungibleAdapter<
|
||||
|
|
@ -471,7 +502,7 @@ impl pallet_transaction_payment::Config for Runtime {
|
|||
type LengthToFee = IdentityFee<Balance>;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type LengthToFee = benchmark_helpers::BenchmarkWeightToFee;
|
||||
type FeeMultiplierUpdate = ConstFeeMultiplier<FeeMultiplier>;
|
||||
type FeeMultiplierUpdate = SlowAdjustingFeeUpdate<Runtime>;
|
||||
type WeightInfo = testnet_weights::pallet_transaction_payment::WeightInfo<Runtime>;
|
||||
}
|
||||
|
||||
|
|
|
|||
227
operator/runtime/testnet/tests/fee_adjustment.rs
Normal file
227
operator/runtime/testnet/tests/fee_adjustment.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Copyright 2025 Moonbeam Foundation.
|
||||
// This file is part of DataHaven.
|
||||
|
||||
//! Fee adjustment integration tests for DataHaven testnet runtime
|
||||
//! Based on Moonbeam's fee adjustment tests
|
||||
|
||||
use datahaven_runtime_common::constants::gas::WEIGHT_PER_GAS;
|
||||
use datahaven_testnet_runtime::{
|
||||
configs::{
|
||||
MinimumMultiplier, RuntimeBlockWeights, SlowAdjustingFeeUpdate, TargetBlockFullness,
|
||||
TransactionPaymentAsGasPrice,
|
||||
},
|
||||
currency::WEIGHT_FEE,
|
||||
Runtime, System,
|
||||
};
|
||||
use fp_evm::FeeCalculator;
|
||||
use frame_support::pallet_prelude::DispatchClass;
|
||||
use frame_support::traits::OnFinalize;
|
||||
use sp_core::U256;
|
||||
use sp_runtime::{traits::Convert, BuildStorage, FixedPointNumber, FixedU128, Perbill};
|
||||
|
||||
/// Helper function to run tests with a specific system weight
|
||||
fn run_with_system_weight<F>(w: frame_support::weights::Weight, mut assertions: F)
|
||||
where
|
||||
F: FnMut() -> (),
|
||||
{
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
System::set_block_consumed_resources(w, 0);
|
||||
assertions()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiplier_can_grow_from_zero() {
|
||||
let minimum_multiplier = MinimumMultiplier::get();
|
||||
let target = TargetBlockFullness::get()
|
||||
* RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
// if the min is too small, then this will not change, and we are doomed forever.
|
||||
// the weight is 1/100th bigger than target.
|
||||
run_with_system_weight(target * 101 / 100, || {
|
||||
let next = SlowAdjustingFeeUpdate::<Runtime>::convert(minimum_multiplier);
|
||||
assert!(
|
||||
next > minimum_multiplier,
|
||||
"{:?} !>= {:?}",
|
||||
next,
|
||||
minimum_multiplier
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_calculation() {
|
||||
let base_extrinsic = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.base_extrinsic;
|
||||
let multiplier = FixedU128::from_float(0.999000000000000000);
|
||||
let extrinsic_len = 100u32;
|
||||
let extrinsic_weight = 5_000u64;
|
||||
let tip = 42u128;
|
||||
|
||||
// For IdentityFee, the fee is just the weight itself
|
||||
// Formula: base_extrinsic + (multiplier * call_weight) + extrinsic_len + tip
|
||||
let expected_fee = base_extrinsic.ref_time() as u128
|
||||
+ (multiplier.saturating_mul_int(extrinsic_weight as u128))
|
||||
+ extrinsic_len as u128
|
||||
+ tip;
|
||||
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual_fee = pallet_transaction_payment::Pallet::<Runtime>::compute_fee(
|
||||
extrinsic_len,
|
||||
&frame_support::dispatch::DispatchInfo {
|
||||
class: DispatchClass::Normal,
|
||||
pays_fee: frame_support::dispatch::Pays::Yes,
|
||||
call_weight: frame_support::weights::Weight::from_parts(extrinsic_weight, 1),
|
||||
extension_weight: frame_support::weights::Weight::zero(),
|
||||
},
|
||||
tip,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
expected_fee, actual_fee,
|
||||
"The actual fee did not match the expected fee, expected: {}, actual: {}",
|
||||
expected_fee, actual_fee
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_is_deterministic() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier = FixedU128::from_u32(1);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier);
|
||||
let actual = TransactionPaymentAsGasPrice::min_gas_price().0;
|
||||
let expected: U256 = multiplier
|
||||
.saturating_mul_int(WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128))
|
||||
.into();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gas_price_has_no_precision_loss_from_saturating_mul_int() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let multiplier_1 = FixedU128::from_float(0.999593900000000000);
|
||||
let multiplier_2 = FixedU128::from_float(0.999593200000000000);
|
||||
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_1);
|
||||
let a = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(multiplier_2);
|
||||
let b = TransactionPaymentAsGasPrice::min_gas_price();
|
||||
|
||||
assert_ne!(
|
||||
a, b,
|
||||
"both gas prices were equal, unexpected precision loss incurred"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_scenarios() {
|
||||
let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
|
||||
.build_storage()
|
||||
.unwrap()
|
||||
.into();
|
||||
t.execute_with(|| {
|
||||
let weight_fee_per_gas = WEIGHT_FEE.saturating_mul(WEIGHT_PER_GAS as u128);
|
||||
let sim = |start_gas_price: u128, fullness: Perbill, num_blocks: u64| -> U256 {
|
||||
let start_multiplier = FixedU128::from_rational(start_gas_price, weight_fee_per_gas);
|
||||
pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(start_multiplier);
|
||||
|
||||
let normal_weight = RuntimeBlockWeights::get()
|
||||
.get(DispatchClass::Normal)
|
||||
.max_total
|
||||
.unwrap();
|
||||
let block_weight = normal_weight * fullness;
|
||||
|
||||
for i in 0..num_blocks {
|
||||
System::set_block_number(i as u32);
|
||||
System::set_block_consumed_resources(block_weight, 0);
|
||||
pallet_transaction_payment::Pallet::<Runtime>::on_finalize(i as u32);
|
||||
}
|
||||
|
||||
TransactionPaymentAsGasPrice::min_gas_price().0
|
||||
};
|
||||
|
||||
// The expected values are the ones observed during test execution,
|
||||
// they are expected to change when parameters that influence
|
||||
// the fee calculation are changed, and should be updated accordingly.
|
||||
// If a test fails when nothing specific to fees has changed,
|
||||
// it may indicate an unexpected collateral effect and should be investigated
|
||||
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 1),
|
||||
U256::from(998_600_980),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 1),
|
||||
U256::from(999_600_080),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 1),
|
||||
U256::from(1_000_600_180),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 1),
|
||||
U256::from(1_002_603_380),
|
||||
);
|
||||
|
||||
// 1 "real" hour (at 6-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 600),
|
||||
U256::from(431_710_642),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 600),
|
||||
U256::from(786_627_866),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 600),
|
||||
U256::from(1_433_329_383u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 600),
|
||||
U256::from(4_758_812_897u128),
|
||||
);
|
||||
|
||||
// 1 "real" day (at 6-second blocks)
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(0), 14400),
|
||||
U256::from(312_500_000), // lower bound enforced
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(25), 14400),
|
||||
U256::from(312_500_000), // lower bound enforced if threshold not reached
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(50), 14400),
|
||||
U256::from(5_653_326_895_069u128),
|
||||
);
|
||||
assert_eq!(
|
||||
sim(1_000_000_000, Perbill::from_percent(100), 14400),
|
||||
U256::from(31_250_000_000_000u128),
|
||||
// upper bound enforced (min_gas_price * MaximumMultiplier)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Integration tests for DataHaven testnet runtime
|
||||
|
||||
pub mod common;
|
||||
mod fee_adjustment;
|
||||
pub mod governance;
|
||||
mod native_token_transfer;
|
||||
mod proxy;
|
||||
|
|
|
|||
Loading…
Reference in a new issue