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