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    let delta = (parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR).saturating_sub(1);
150    let min_gas_limit = parent_gas_limit - delta;
151    let max_gas_limit = parent_gas_limit + delta;
152    desired_gas_limit.clamp(min_gas_limit, max_gas_limit)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::eip1559::constants::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256};
159
160    #[test]
161    fn min_protocol_sanity() {
162        assert_eq!(MIN_PROTOCOL_BASE_FEE_U256.to::<u64>(), MIN_PROTOCOL_BASE_FEE);
163    }
164
165    #[test]
166    fn calculate_base_fee_success() {
167        let base_fee = [
168            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
169            1, 2,
170        ];
171        let gas_used = [
172            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
173            10000000,
174        ];
175        let gas_limit = [
176            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
177            18000000, 18000000,
178        ];
179        let next_base_fee = [
180            1125000000, 1083333333, 1053571428, 1179939062, 1116028649, 918084097, 1063811730, 1,
181            2, 3,
182        ];
183
184        for i in 0..base_fee.len() {
185            assert_eq!(
186                next_base_fee[i],
187                calc_next_block_base_fee(
188                    gas_used[i],
189                    gas_limit[i],
190                    base_fee[i],
191                    BaseFeeParams::ethereum(),
192                )
193            );
194        }
195    }
196
197    #[test]
198    fn calculate_optimism_sepolia_base_fee_success() {
199        let base_fee = [
200            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
201            1, 2,
202        ];
203        let gas_used = [
204            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
205            10000000,
206        ];
207        let gas_limit = [
208            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
209            18000000, 18000000,
210        ];
211        let next_base_fee = [
212            1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1,
213            2, 3,
214        ];
215
216        for i in 0..base_fee.len() {
217            assert_eq!(
218                next_base_fee[i],
219                calc_next_block_base_fee(
220                    gas_used[i],
221                    gas_limit[i],
222                    base_fee[i],
223                    BaseFeeParams::optimism_sepolia(),
224                )
225            );
226        }
227    }
228
229    #[test]
230    fn calculate_optimism_base_fee_success() {
231        let base_fee = [
232            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
233            1, 2,
234        ];
235        let gas_used = [
236            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
237            10000000,
238        ];
239        let gas_limit = [
240            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
241            18000000, 18000000,
242        ];
243        let next_base_fee = [
244            1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1,
245            2, 3,
246        ];
247
248        for i in 0..base_fee.len() {
249            assert_eq!(
250                next_base_fee[i],
251                calc_next_block_base_fee(
252                    gas_used[i],
253                    gas_limit[i],
254                    base_fee[i],
255                    BaseFeeParams::optimism(),
256                )
257            );
258        }
259    }
260
261    #[test]
262    fn calculate_optimism_canyon_base_fee_success() {
263        let base_fee = [
264            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
265            1, 2,
266        ];
267        let gas_used = [
268            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
269            10000000,
270        ];
271        let gas_limit = [
272            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
273            18000000, 18000000,
274        ];
275        let next_base_fee = [
276            1020000009, 1016000000, 1013142859, 1091550909, 1073187043, 1045042012, 1059031864, 1,
277            2, 3,
278        ];
279
280        for i in 0..base_fee.len() {
281            assert_eq!(
282                next_base_fee[i],
283                calc_next_block_base_fee(
284                    gas_used[i],
285                    gas_limit[i],
286                    base_fee[i],
287                    BaseFeeParams::optimism_canyon(),
288                )
289            );
290        }
291    }
292
293    #[test]
294    fn calculate_base_sepolia_base_fee_success() {
295        let base_fee = [
296            1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0,
297            1, 2,
298        ];
299        let gas_used = [
300            10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000,
301            10000000,
302        ];
303        let gas_limit = [
304            10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000,
305            18000000, 18000000,
306        ];
307        let next_base_fee = [
308            1180000000, 1146666666, 1122857142, 1244299375, 1189416692, 1028254188, 1144836295, 1,
309            2, 3,
310        ];
311
312        for i in 0..base_fee.len() {
313            assert_eq!(
314                next_base_fee[i],
315                calc_next_block_base_fee(
316                    gas_used[i],
317                    gas_limit[i],
318                    base_fee[i],
319                    BaseFeeParams::base_sepolia(),
320                )
321            );
322        }
323    }
324
325    #[test]
326    fn next_base_fee_no_panic_zero_elasticity() {
327        let p = BaseFeeParams::new(8, 0);
328        assert_eq!(calc_next_block_base_fee(1, 30_000_000, 1_000_000_000, p), 1_000_000_000);
329    }
330
331    #[test]
332    fn next_base_fee_no_panic_zero_denominator() {
333        let p = BaseFeeParams::new(0, 2);
334        assert_eq!(
335            calc_next_block_base_fee(15_000_000, 30_000_000, 1_000_000_000, p),
336            1_000_000_000
337        );
338    }
339
340    #[test]
341    fn next_base_fee_no_panic_gas_limit_below_elasticity() {
342        let p = BaseFeeParams::ethereum();
343        // gas_target = 1 / 2 = 0; gas_used > 0 used to hit a divide-by-zero in the increase path.
344        assert_eq!(calc_next_block_base_fee(1, 1, 1_000_000_000, p), 1_000_000_000);
345    }
346}