Skip to main content

anvil/eth/fees/
optimism.rs

1//! Optimism-specific dynamic EIP-1559 fee rules.
2
3use alloy_consensus::BlockHeader;
4use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams};
5use alloy_primitives::Bytes;
6use foundry_evm::hardfork::{FoundryHardfork, OpHardfork};
7use op_alloy_consensus::{
8    decode_holocene_extra_data, decode_jovian_extra_data, encode_holocene_extra_data,
9    encode_jovian_extra_data,
10};
11
12/// Header-derived EIP-1559 rules introduced by the Optimism Holocene and Jovian upgrades.
13#[derive(Clone, Copy, Debug)]
14pub(super) enum OptimismBaseFeeRules {
15    Holocene { params: BaseFeeParams },
16    Jovian { params: BaseFeeParams, min_base_fee: u64 },
17}
18
19impl OptimismBaseFeeRules {
20    pub(super) fn for_hardfork(hardfork: FoundryHardfork, params: BaseFeeParams) -> Option<Self> {
21        let hardfork = OpHardfork::from(hardfork);
22        if hardfork >= OpHardfork::Jovian {
23            Some(Self::Jovian { params, min_base_fee: 0 })
24        } else if hardfork >= OpHardfork::Holocene {
25            Some(Self::Holocene { params })
26        } else {
27            None
28        }
29    }
30
31    pub(super) fn decode(extra_data: &[u8]) -> Option<Self> {
32        if let Ok((elasticity, denominator, min_base_fee)) = decode_jovian_extra_data(extra_data) {
33            return Some(Self::Jovian {
34                params: BaseFeeParams::new(denominator as u128, elasticity as u128),
35                min_base_fee,
36            });
37        }
38        if let Ok((elasticity, denominator)) = decode_holocene_extra_data(extra_data) {
39            return Some(Self::Holocene {
40                params: BaseFeeParams::new(denominator as u128, elasticity as u128),
41            });
42        }
43        None
44    }
45
46    pub(super) const fn params(self) -> BaseFeeParams {
47        match self {
48            Self::Holocene { params, .. } | Self::Jovian { params, .. } => params,
49        }
50    }
51
52    pub(super) fn extra_data(self) -> Bytes {
53        match self {
54            Self::Holocene { params } => encode_holocene_extra_data([0; 8].into(), params),
55            Self::Jovian { params, min_base_fee } => {
56                encode_jovian_extra_data([0; 8].into(), params, min_base_fee)
57            }
58        }
59        .expect("optimism fee parameters fit canonical extra data")
60    }
61
62    pub(super) const fn is_jovian(self) -> bool {
63        matches!(self, Self::Jovian { .. })
64    }
65
66    pub(super) fn next_block_base_fee<H: BlockHeader>(self, header: &H) -> u64 {
67        let gas_used = match self {
68            Self::Holocene { .. } => header.gas_used(),
69            Self::Jovian { .. } => {
70                header.gas_used().max(header.blob_gas_used().unwrap_or_default())
71            }
72        };
73        let next_base_fee = calc_next_block_base_fee(
74            gas_used,
75            header.gas_limit(),
76            header.base_fee_per_gas().unwrap_or_default(),
77            self.params(),
78        );
79        match self {
80            Self::Jovian { min_base_fee, .. } => next_base_fee.max(min_base_fee),
81            Self::Holocene { .. } => next_base_fee,
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use alloy_consensus::Header;
89
90    use super::*;
91
92    #[test]
93    fn header_base_fee_rules_match_upstream_blocks() {
94        struct Case {
95            extra_data: &'static [u8],
96            gas_limit: u64,
97            gas_used: u64,
98            blob_gas_used: u64,
99            base_fee: u64,
100            expected: u64,
101        }
102
103        // Captured parent/child pairs from Base and OP Mainnet across Holocene and Jovian.
104        let cases = [
105            Case {
106                extra_data: &[1, 0, 0, 0, 100, 0, 0, 0, 5, 0, 0, 0, 0, 0, 76, 75, 64],
107                gas_limit: 0x17d7_8400,
108                gas_used: 0x132_0096,
109                blob_gas_used: 0x37_5b00,
110                base_fee: 0x4c_4b40,
111                expected: 0x4c_4b40,
112            },
113            Case {
114                extra_data: &[1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0],
115                gas_limit: 0x262_5a00,
116                gas_used: 0xa5_646d,
117                blob_gas_used: 0x22_cd60,
118                base_fee: 0x196,
119                expected: 0x196,
120            },
121            Case {
122                extra_data: &[0, 0, 0, 0, 50, 0, 0, 0, 2],
123                gas_limit: 0x858_3b00,
124                gas_used: 0x311_2ab9,
125                blob_gas_used: 0,
126                base_fee: 0xc_bdf6,
127                expected: 0xc_acae,
128            },
129            Case {
130                extra_data: &[0, 0, 0, 0, 250, 0, 0, 0, 2],
131                gas_limit: 0x262_5a00,
132                gas_used: 0xe6_2170,
133                blob_gas_used: 0,
134                base_fee: 0xd87,
135                expected: 0xd84,
136            },
137        ];
138
139        for case in cases {
140            let rules = OptimismBaseFeeRules::decode(case.extra_data).unwrap();
141            let header = Header {
142                gas_limit: case.gas_limit,
143                gas_used: case.gas_used,
144                blob_gas_used: Some(case.blob_gas_used),
145                base_fee_per_gas: Some(case.base_fee),
146                ..Default::default()
147            };
148
149            assert_eq!(rules.next_block_base_fee(&header), case.expected);
150            assert_eq!(rules.extra_data().as_ref(), case.extra_data);
151        }
152    }
153
154    #[test]
155    fn jovian_base_fee_uses_blob_gas_when_greater() {
156        let extra_data = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0];
157        let rules = OptimismBaseFeeRules::decode(&extra_data).unwrap();
158        let header = Header {
159            gas_limit: 10_000_000_000,
160            gas_used: 1_000_000_000,
161            blob_gas_used: Some(5_000_000_000),
162            base_fee_per_gas: Some(super::super::INITIAL_BASE_FEE),
163            ..Default::default()
164        };
165
166        let expected = calc_next_block_base_fee(
167            header.blob_gas_used.unwrap(),
168            header.gas_limit,
169            header.base_fee_per_gas.unwrap(),
170            BaseFeeParams::new(250, 2),
171        );
172        assert_eq!(rules.next_block_base_fee(&header), expected);
173    }
174
175    #[test]
176    fn supplied_header_overrides_cached_optimism_rules() {
177        let holocene = [0, 0, 0, 0, 50, 0, 0, 0, 2];
178        let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
179        let fallback = BaseFeeParams::ethereum();
180
181        for (cached, supplied) in
182            [(jovian.as_slice(), holocene.as_slice()), (holocene.as_slice(), jovian.as_slice())]
183        {
184            let supplied_rules = OptimismBaseFeeRules::decode(supplied).unwrap();
185            let header = Header {
186                gas_limit: 10_000_000_000,
187                gas_used: 1_000_000_000,
188                blob_gas_used: Some(5_000_000_000),
189                base_fee_per_gas: Some(super::super::INITIAL_BASE_FEE),
190                extra_data: Bytes::copy_from_slice(supplied),
191                ..Default::default()
192            };
193            let rules = super::super::BaseFeeRules::Optimism {
194                inherited: Some(OptimismBaseFeeRules::decode(cached).unwrap()),
195                fallback,
196            };
197            let parent_fees = rules.parent_header_fees(&header);
198
199            assert_eq!(parent_fees.base_fee, supplied_rules.next_block_base_fee(&header));
200            assert_eq!(parent_fees.extra_data.as_ref(), supplied);
201            assert_eq!(parent_fees.optimism_jovian, Some(supplied_rules.is_jovian()));
202        }
203    }
204}