Skip to main content

alloy_eips/eip1559/
helpers.rs

1use crate::eip1559::{constants::GAS_LIMIT_BOUND_DIVISOR, BaseFeeParams};
2
3/// Calculates the effective gas price for a dynamic fee transaction.
4///
5/// This is a utility function for EIP-1559 and similar transactions that use dynamic fees.
6///
7/// For EIP-1559 transactions, the effective gas price is calculated as:
8/// - If no base fee: returns `max_fee_per_gas`
9/// - If base fee exists: returns `min(max_fee_per_gas, max_priority_fee_per_gas + base_fee)`
10///
11/// This ensures that the total fee doesn't exceed the maximum fee per gas, while also
12/// ensuring that the priority fee doesn't exceed the maximum priority fee per gas.
13#[inline]
14pub fn calc_effective_gas_price(
15    max_fee_per_gas: u128,
16    max_priority_fee_per_gas: u128,
17    base_fee: Option<u64>,
18) -> u128 {
19    base_fee.map_or(max_fee_per_gas, |base_fee| {
20        // if the tip is greater than the max priority fee per gas, set it to the max
21        // priority fee per gas + base fee
22        let tip = max_fee_per_gas.saturating_sub(base_fee as u128);
23        if tip > max_priority_fee_per_gas {
24            max_priority_fee_per_gas + base_fee as u128
25        } else {
26            // otherwise return the max fee per gas
27            max_fee_per_gas
28        }
29    })
30}
31
32/// Return type of EIP1155 gas fee estimator.
33///
34/// Contains EIP-1559 fields
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
38pub struct Eip1559Estimation {
39    /// The max fee per gas.
40    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
41    pub max_fee_per_gas: u128,
42    /// The max priority fee per gas.
43    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
44    pub max_priority_fee_per_gas: u128,
45}
46
47impl Eip1559Estimation {
48    /// Scales the [`Eip1559Estimation`] by the given percentage value.
49    ///
50    /// ```
51    /// use alloy_eips::eip1559::Eip1559Estimation;
52    /// let est =
53    ///     Eip1559Estimation { max_fee_per_gas: 100, max_priority_fee_per_gas: 100 }.scaled_by_pct(10);
54    /// assert_eq!(est.max_fee_per_gas, 110);
55    /// assert_eq!(est.max_priority_fee_per_gas, 110);
56    /// ```
57    pub const fn scale_by_pct(&mut self, pct: u64) {
58        self.max_fee_per_gas = self.max_fee_per_gas * (100 + pct as u128) / 100;
59        self.max_priority_fee_per_gas = self.max_priority_fee_per_gas * (100 + pct as u128) / 100;
60    }
61
62    /// Consumes the type and returns the scaled estimation.
63    pub const fn scaled_by_pct(mut self, pct: u64) -> Self {
64        self.scale_by_pct(pct);
65        self
66    }
67}
68
69/// Calculate the base fee for the next block based on the EIP-1559 specification.
70///
71/// This function calculates the base fee for the next block according to the rules defined in the
72/// EIP-1559. EIP-1559 introduces a new transaction pricing mechanism that includes a
73/// fixed-per-block network fee that is burned and dynamically adjusts block sizes to handles
74/// transient congestion.
75///
76/// For each block, the base fee per gas is determined by the gas used in the parent block and the
77/// target gas (the block gas limit divided by the elasticity multiplier). The algorithm increases
78/// the base fee when blocks are congested and decreases it when they are under the target gas
79/// usage. The base fee per gas is always burned.
80///
81/// Parameters:
82/// - `gas_used`: The gas used in the current block.
83/// - `gas_limit`: The gas limit of the current block.
84/// - `base_fee`: The current base fee per gas.
85/// - `base_fee_params`: Base fee parameters such as elasticity multiplier and max change
86///   denominator.
87///
88/// Returns:
89/// The calculated base fee for the next block as a `u64`.
90///
91/// For more information, refer to the [EIP-1559 spec](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md).
92pub fn calc_next_block_base_fee(
93    gas_used: u64,
94    gas_limit: u64,
95    base_fee: u64,
96    base_fee_params: BaseFeeParams,
97) -> u64 {
98    let elasticity = base_fee_params.elasticity_multiplier;
99    let max_change_denominator = base_fee_params.max_change_denominator;
100
101    // Without these checks, `gas_limit / elasticity` or the EIP-1559 update term can divide by
102    // zero (e.g. elasticity or denominator set to zero from misconfiguration / malformed
103    // Holocene header data, or `gas_limit < elasticity` on chains that do not enforce a minimum
104    // gas limit). Nethermind returns the parent base fee unchanged in these cases; see
105    // `DefaultBaseFeeCalculator` in Nethermind.Core.
106    if elasticity == 0 || max_change_denominator == 0 {
107        return base_fee;
108    }
109
110    // Calculate the target gas by dividing the gas limit by the elasticity multiplier.
111    let gas_target = (gas_limit as u128 / elasticity) as u64;
112
113    if gas_target == 0 {
114        return base_fee;
115    }
116
117    match gas_used.cmp(&gas_target) {
118        // If the gas used in the current block is equal to the gas target, the base fee remains the
119        // same (no increase).
120        core::cmp::Ordering::Equal => base_fee,
121        // If the gas used in the current block is greater than the gas target, calculate a new
122        // increased base fee.
123        core::cmp::Ordering::Greater => {
124            // Calculate the increase in base fee based on the formula defined by EIP-1559.
125            base_fee
126                + (core::cmp::max(
127                    // Ensure a minimum increase of 1.
128                    1,
129                    base_fee as u128 * (gas_used - gas_target) as u128
130                        / (gas_target as u128 * base_fee_params.max_change_denominator),
131                ) as u64)
132        }
133        // If the gas used in the current block is less than the gas target, calculate a new
134        // decreased base fee.
135        core::cmp::Ordering::Less => {
136            // Calculate the decrease in base fee based on the formula defined by EIP-1559.
137            base_fee.saturating_sub(
138                (base_fee as u128 * (gas_target - gas_used) as u128
139                    / (gas_target as u128 * base_fee_params.max_change_denominator))
140                    as u64,
141            )
142        }
143    }
144}
145
146/// Calculate the gas limit for the next block based on parent and desired gas limits.
147/// Ref: <https://github.com/ethereum/go-ethereum/blob/88cbfab332c96edfbe99d161d9df6a40721bd786/core/block_validator.go#L166>
148pub fn calculate_block_gas_limit(parent_gas_limit: u64, desired_gas_limit: u64) -> u64 {
149    calculate_block_gas_limit_with_bound_divisor(
150        parent_gas_limit,
151        desired_gas_limit,
152        GAS_LIMIT_BOUND_DIVISOR,
153    )
154}
155
156/// Calculate the gas limit for the next block based on parent and desired gas limits and a custom
157/// gas limit bound divisor.
158///
159/// # Panics
160///
161/// Panics if `gas_limit_bound_divisor` is zero.
162pub fn calculate_block_gas_limit_with_bound_divisor(
163    parent_gas_limit: u64,
164    desired_gas_limit: u64,
165    gas_limit_bound_divisor: u64,
166) -> u64 {
167    assert!(gas_limit_bound_divisor != 0, "gas limit bound divisor must be non-zero");
168
169    let delta = (parent_gas_limit / gas_limit_bound_divisor).saturating_sub(1);
170    let min_gas_limit = parent_gas_limit.saturating_sub(delta);
171    let max_gas_limit = parent_gas_limit.saturating_add(delta);
172    desired_gas_limit.clamp(min_gas_limit, max_gas_limit)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::eip1559::constants::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256};
179
180    #[test]
181    fn min_protocol_sanity() {
182        assert_eq!(MIN_PROTOCOL_BASE_FEE_U256.to::<u64>(), MIN_PROTOCOL_BASE_FEE);
183    }
184
185    #[test]
186    fn calculate_block_gas_limit_bounds_desired_limit() {
187        let parent_gas_limit = 30_000_000;
188        let min_gas_limit = 29_970_705;
189        let max_gas_limit = 30_029_295;
190
191        assert_eq!(calculate_block_gas_limit(parent_gas_limit, 20_000_000), min_gas_limit);
192        assert_eq!(calculate_block_gas_limit(parent_gas_limit, 30_000_000), 30_000_000);
193        assert_eq!(calculate_block_gas_limit(parent_gas_limit, 40_000_000), max_gas_limit);
194    }
195
196    #[test]
197    fn calculate_block_gas_limit_uses_custom_bound_divisor() {
198        assert_eq!(calculate_block_gas_limit_with_bound_divisor(1_000, 2_000, 10), 1_099);
199    }
200
201    #[test]
202    #[should_panic(expected = "gas limit bound divisor must be non-zero")]
203    fn calculate_block_gas_limit_rejects_zero_bound_divisor() {
204        calculate_block_gas_limit_with_bound_divisor(1_000, 2_000, 0);
205    }
206
207    #[test]
208    fn calculate_base_fee_success() {
209        let base_fee = [
210            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
211            1, 2,
212        ];
213        let gas_used = [
214            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
215            10000000,
216        ];
217        let gas_limit = [
218            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
219            18000000, 18000000,
220        ];
221        let next_base_fee = [
222            1125000000, 1083333333, 1053571428, 1179939062, 1116028649, 918084097, 1063811730, 1,
223            2, 3,
224        ];
225
226        for i in 0..base_fee.len() {
227            assert_eq!(
228                next_base_fee[i],
229                calc_next_block_base_fee(
230                    gas_used[i],
231                    gas_limit[i],
232                    base_fee[i],
233                    BaseFeeParams::ethereum(),
234                )
235            );
236        }
237    }
238
239    #[test]
240    fn calculate_optimism_sepolia_base_fee_success() {
241        let base_fee = [
242            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
243            1, 2,
244        ];
245        let gas_used = [
246            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
247            10000000,
248        ];
249        let gas_limit = [
250            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
251            18000000, 18000000,
252        ];
253        let next_base_fee = [
254            1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1,
255            2, 3,
256        ];
257
258        for i in 0..base_fee.len() {
259            assert_eq!(
260                next_base_fee[i],
261                calc_next_block_base_fee(
262                    gas_used[i],
263                    gas_limit[i],
264                    base_fee[i],
265                    BaseFeeParams::optimism_sepolia(),
266                )
267            );
268        }
269    }
270
271    #[test]
272    fn calculate_optimism_base_fee_success() {
273        let base_fee = [
274            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
275            1, 2,
276        ];
277        let gas_used = [
278            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
279            10000000,
280        ];
281        let gas_limit = [
282            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
283            18000000, 18000000,
284        ];
285        let next_base_fee = [
286            1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1,
287            2, 3,
288        ];
289
290        for i in 0..base_fee.len() {
291            assert_eq!(
292                next_base_fee[i],
293                calc_next_block_base_fee(
294                    gas_used[i],
295                    gas_limit[i],
296                    base_fee[i],
297                    BaseFeeParams::optimism(),
298                )
299            );
300        }
301    }
302
303    #[test]
304    fn calculate_optimism_canyon_base_fee_success() {
305        let base_fee = [
306            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
307            1, 2,
308        ];
309        let gas_used = [
310            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
311            10000000,
312        ];
313        let gas_limit = [
314            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
315            18000000, 18000000,
316        ];
317        let next_base_fee = [
318            1020000009, 1016000000, 1013142859, 1091550909, 1073187043, 1045042012, 1059031864, 1,
319            2, 3,
320        ];
321
322        for i in 0..base_fee.len() {
323            assert_eq!(
324                next_base_fee[i],
325                calc_next_block_base_fee(
326                    gas_used[i],
327                    gas_limit[i],
328                    base_fee[i],
329                    BaseFeeParams::optimism_canyon(),
330                )
331            );
332        }
333    }
334
335    #[test]
336    fn calculate_base_sepolia_base_fee_success() {
337        let base_fee = [
338            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
339            1, 2,
340        ];
341        let gas_used = [
342            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
343            10000000,
344        ];
345        let gas_limit = [
346            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
347            18000000, 18000000,
348        ];
349        let next_base_fee = [
350            1180000000, 1146666666, 1122857142, 1244299375, 1189416692, 1028254188, 1144836295, 1,
351            2, 3,
352        ];
353
354        for i in 0..base_fee.len() {
355            assert_eq!(
356                next_base_fee[i],
357                calc_next_block_base_fee(
358                    gas_used[i],
359                    gas_limit[i],
360                    base_fee[i],
361                    BaseFeeParams::base_sepolia(),
362                )
363            );
364        }
365    }
366
367    #[test]
368    fn next_base_fee_no_panic_zero_elasticity() {
369        let p = BaseFeeParams::new(8, 0);
370        assert_eq!(calc_next_block_base_fee(1, 30_000_000, 1_000_000_000, p), 1_000_000_000);
371    }
372
373    #[test]
374    fn next_base_fee_no_panic_zero_denominator() {
375        let p = BaseFeeParams::new(0, 2);
376        assert_eq!(
377            calc_next_block_base_fee(15_000_000, 30_000_000, 1_000_000_000, p),
378            1_000_000_000
379        );
380    }
381
382    #[test]
383    fn next_base_fee_no_panic_gas_limit_below_elasticity() {
384        let p = BaseFeeParams::ethereum();
385        // gas_target = 1 / 2 = 0; gas_used > 0 used to hit a divide-by-zero in the increase path.
386        assert_eq!(calc_next_block_base_fee(1, 1, 1_000_000_000, p), 1_000_000_000);
387    }
388}