use alloy::primitives::U256;
use std::cmp::Ordering;
pub const DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u128 = 8;
pub const DEFAULT_ELASTICITY_MULTIPLIER: u128 = 2;
#[must_use]
pub fn next_base_fee(
parent_base_fee: u128,
parent_gas_used: u128,
parent_gas_limit: u128,
min_base_fee: Option<u128>,
base_fee_max_change_denominator: u128,
elasticity_multiplier: u128,
) -> u128 {
let last_gas_target = parent_gas_limit / elasticity_multiplier;
let working_base_fee = match parent_gas_used.cmp(&last_gas_target) {
Ordering::Equal => parent_base_fee,
Ordering::Greater => {
let gas_used_delta = parent_gas_used - last_gas_target;
let delta = U256::from(parent_base_fee) * U256::from(gas_used_delta)
/ U256::from(last_gas_target)
/ U256::from(base_fee_max_change_denominator);
let base_fee_delta = std::cmp::max(delta.to::<u128>(), 1);
parent_base_fee + base_fee_delta
}
Ordering::Less => {
let gas_used_delta = last_gas_target - parent_gas_used;
let delta = U256::from(parent_base_fee) * U256::from(gas_used_delta)
/ U256::from(last_gas_target)
/ U256::from(base_fee_max_change_denominator);
let base_fee_delta = delta.to::<u128>();
parent_base_fee - base_fee_delta
}
};
match min_base_fee {
Some(min) => working_base_fee.max(min),
None => working_base_fee,
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::too_many_lines)]
use super::*;
type Case = (u128, u128, u128, Option<u128>, u128, u128, u128);
fn nbf(bf: u128, gu: u128, gl: u128) -> u128 {
next_base_fee(bf, gu, gl, None, 8, 2)
}
#[test]
fn parity_vs_python_oracle() {
let cases: &[Case] = &[
(
1_000_000_000,
15_000_000,
30_000_000,
None,
8,
2,
1_000_000_000,
),
(7, 5, 10, None, 8, 2, 7),
(
1_000_000_000,
30_000_000,
30_000_000,
None,
8,
2,
1_125_000_000,
),
(
1_000_000_000,
20_000_000,
30_000_000,
None,
8,
2,
1_041_666_666,
),
(875_000_000, 29_999_999, 30_000_000, None, 8, 2, 984_374_992),
(1_000_000, 1, 2, None, 8, 2, 1_000_000),
(1, 1, 2, None, 8, 2, 1),
(0, 1, 2, None, 8, 2, 0),
(
1_000_000_000,
10_000_000,
30_000_000,
None,
8,
2,
958_333_334,
),
(1_000_000_000, 0, 30_000_000, None, 8, 2, 875_000_000),
(100, 1, 10, None, 8, 2, 90),
(1_000_000, 0, 2, None, 8, 2, 875_000),
(
1_000_000_000,
0,
30_000_000,
Some(900_000_000),
8,
2,
900_000_000,
),
(1_000_000_000, 0, 30_000_000, Some(0), 8, 2, 875_000_000),
(
1_000_000_000,
30_000_000,
30_000_000,
Some(2_000_000_000),
8,
2,
2_000_000_000,
),
(
1_000_000_000,
10_000_000,
30_000_000,
None,
17,
2,
980_392_157,
),
(
1_000_000_000,
10_000_000,
20_000_000,
None,
8,
2,
1_000_000_000,
),
(
1_000_000_000,
15_000_000,
30_000_000,
None,
8,
3,
1_062_500_000,
),
(500_000_000, 9_000_000, 30_000_000, None, 8, 2, 475_000_000),
(
100_000_000_000_000_000_000,
30_000_000,
30_000_000,
None,
8,
2,
112_500_000_000_000_000_000,
),
(
1_000_000_000_000_000_000_000_000_000_000,
30_000_000,
30_000_000,
None,
8,
2,
1_125_000_000_000_000_000_000_000_000_000,
),
(
1_329_227_995_784_915_872_903_807_060_280_344_576,
15_000_000,
30_000_000,
None,
8,
2,
1_329_227_995_784_915_872_903_807_060_280_344_576,
),
(
1_329_227_995_784_915_872_903_807_060_280_344_576,
30_000_000,
30_000_000,
None,
8,
2,
1_495_381_495_258_030_357_016_782_942_815_387_648,
),
];
for &(bf, gu, gl, min, d, e, want) in cases {
let got = next_base_fee(bf, gu, gl, min, d, e);
assert_eq!(
got, want,
"parity mismatch: next_base_fee(bf={bf}, gu={gu}, gl={gl}, min={min:?}, d={d}, e={e})"
);
}
}
#[test]
fn property_target_is_flat() {
use proptest::prelude::*;
proptest!(|(bf in 0u128..u128::MAX / 2, gl in 2u128..u128::MAX / 2, e in 2u128..=4)| {
let target = gl / e;
if target > 0 {
prop_assert_eq!(next_base_fee(bf, target, gl, None, 8, e), bf);
}
});
}
#[test]
fn property_over_target_increases() {
use proptest::prelude::*;
proptest!(|(bf in 1u128..u128::MAX / 2, gl in 4u128..u128::MAX / 2, e in 2u128..=4, over in 1u128..1000)| {
let target = gl / e;
if target > 0 && target + over <= gl && bf < u128::MAX / 4 {
let next = next_base_fee(bf, target + over, gl, None, 8, e);
prop_assert!(next > bf, "next={next} bf={bf}");
}
});
}
#[test]
fn property_under_target_decreases() {
use proptest::prelude::*;
proptest!(|(bf in 1u128..u128::MAX / 2, gl in 4u128..u128::MAX / 2, e in 2u128..=4, under in 0u128..1000)| {
let target = gl / e;
if target > 0 && under < target {
let next = next_base_fee(bf, target - under, gl, None, 8, e);
prop_assert!(next <= bf, "next={next} bf={bf}");
}
});
}
#[test]
fn property_min_clamp_floor() {
use proptest::prelude::*;
proptest!(|(bf in 100u128..u128::MAX / 4, gl in 4u128..u128::MAX / 2, min in 0u128..u128::MAX / 4)| {
let target = gl / 2;
if target > 0 {
let next = next_base_fee(bf, 0, gl, Some(min), 8, 2);
prop_assert!(next >= min, "next={next} < min={min}");
}
});
}
#[expect(clippy::manual_checked_ops)] #[test]
fn property_max_change_bounded() {
use proptest::prelude::*;
proptest!(|(bf in 1u128..10u128.pow(30), gl in 4u128..100_000_000, denom in 2u128..=64)| {
let target = gl / 2;
if target > 0 {
let delta_max_up = bf * (gl - target) / target / denom;
let up = next_base_fee(bf, gl, gl, None, denom, 2); prop_assert!(up <= bf + delta_max_up + 1, "up={up} > bf+{delta_max_up}+1 (bf={bf},gl={gl},d={denom})");
let delta_max_down = bf / denom;
let down = next_base_fee(bf, 0, gl, None, denom, 2); prop_assert!(down >= bf - delta_max_down, "down={down} < bf-{delta_max_down} (bf={bf},gl={gl},d={denom})");
}
});
}
#[test]
fn min_fee_zero_is_noop() {
let none = next_base_fee(1_000_000_000, 0, 30_000_000, None, 8, 2);
let zero = next_base_fee(1_000_000_000, 0, 30_000_000, Some(0), 8, 2);
assert_eq!(none, zero);
assert_eq!(zero, 875_000_000);
}
#[test]
fn increase_floor_bumps_by_one() {
assert_eq!(next_base_fee(1, 2, 2, None, 8, 2), 2);
assert_eq!(next_base_fee(0, 2, 2, None, 8, 2), 1);
}
#[test]
fn eip1559_defaults() {
assert_eq!(DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, 8);
assert_eq!(DEFAULT_ELASTICITY_MULTIPLIER, 2);
}
#[test]
fn convenience_matches_explicit_defaults() {
assert_eq!(nbf(1_000_000_000, 0, 30_000_000), 875_000_000);
}
}