openzeppelin_relayer/services/gas/
optimism_extra_fee.rs

1use alloy::{
2    consensus::{TxEip1559, TxLegacy},
3    hex::FromHex,
4    primitives::{Address, Bytes},
5    rpc::types::{TransactionInput, TransactionRequest},
6};
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use solana_sdk::packet::Encode;
10
11use crate::{
12    constants::OPTIMISM_GAS_PRICE_ORACLE_ADDRESS,
13    models::{EvmTransactionData, EvmTransactionDataTrait, TransactionError, U256},
14    services::EvmProviderTrait,
15};
16
17use super::NetworkExtraFeeCalculatorServiceTrait;
18
19// Function selectors as constants
20const FN_SELECTOR_L1_BASE_FEE: [u8; 4] = [81, 155, 75, 211]; // bytes4(keccak256("l1BaseFee()"))
21const FN_SELECTOR_BASE_FEE: [u8; 4] = [110, 242, 92, 58]; // bytes4(keccak256("baseFee()"))
22const FN_SELECTOR_DECIMALS: [u8; 4] = [49, 60, 229, 103]; // bytes4(keccak256("decimals()"))
23const FN_SELECTOR_BLOB_BASE_FEE: [u8; 4] = [248, 32, 97, 64]; // bytes4(keccak256("blobBaseFee()"))
24const FN_SELECTOR_BASE_FEE_SCALAR: [u8; 4] = [197, 152, 89, 24]; // bytes4(keccak256("baseFeeScalar()"))
25const FN_SELECTOR_BLOB_BASE_FEE_SCALAR: [u8; 4] = [104, 213, 220, 166]; // bytes4(keccak256("blobBaseFeeScalar()"))
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OptimismModifiers {
29    pub l1_base_fee: U256,
30    pub base_fee: U256,
31    pub decimals: U256,
32    pub blob_base_fee: U256,
33    pub base_fee_scalar: u32,
34    pub blob_base_fee_scalar: u32,
35}
36
37#[cfg(test)]
38impl Default for OptimismModifiers {
39    fn default() -> Self {
40        Self {
41            l1_base_fee: U256::ZERO,
42            base_fee: U256::ZERO,
43            decimals: U256::ZERO,
44            blob_base_fee: U256::ZERO,
45            base_fee_scalar: 0,
46            blob_base_fee_scalar: 0,
47        }
48    }
49}
50
51pub struct OptimismExtraFeeService<P> {
52    provider: P,
53}
54
55impl<P> OptimismExtraFeeService<P> {
56    /// Create a new Optimism extra fee service
57    ///
58    /// # Arguments
59    ///
60    /// * `provider` - The provider to get the extra fee for
61    ///
62    pub fn new(provider: P) -> Self {
63        Self { provider }
64    }
65}
66
67impl<P: EvmProviderTrait> OptimismExtraFeeService<P> {
68    /// Create a contract call for the given function selector
69    ///
70    /// # Arguments
71    ///
72    /// * `bytes_fn_selector` - The function selector to create a contract call for
73    ///
74    /// # Returns
75    fn create_contract_call(
76        &self,
77        bytes_fn_selector: Vec<u8>,
78    ) -> Result<TransactionRequest, TransactionError> {
79        let oracle_address = Address::from_hex(OPTIMISM_GAS_PRICE_ORACLE_ADDRESS)
80            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
81
82        let fn_selector = Bytes::from(bytes_fn_selector);
83        let tx = TransactionRequest::default()
84            .to(oracle_address)
85            .input(TransactionInput::new(fn_selector));
86
87        Ok(tx)
88    }
89
90    /// Get the fee for the given function selector
91    ///
92    /// # Arguments
93    ///
94    /// * `fn_selector` - The function selector to get the fee for
95    ///
96    /// # Returns
97    async fn get_fee(&self, fn_selector: Vec<u8>) -> Result<U256, TransactionError> {
98        let tx = self.create_contract_call(fn_selector)?;
99        let result = self.provider.call_contract(&tx).await?;
100        Ok(U256::from_be_slice(result.as_ref()))
101    }
102
103    /// Get the price modifiers for optimism
104    ///
105    /// # Returns
106    ///
107    /// A `Result` containing the price modifiers or a `TransactionError`.
108    pub async fn get_modifiers(&self) -> Result<OptimismModifiers, TransactionError> {
109        let (l1_base_fee, base_fee, decimals, blob_base_fee, base_fee_scalar, blob_base_fee_scalar) =
110            tokio::try_join!(
111                self.get_fee(FN_SELECTOR_L1_BASE_FEE.to_vec()),
112                self.get_fee(FN_SELECTOR_BASE_FEE.to_vec()),
113                self.get_fee(FN_SELECTOR_DECIMALS.to_vec()),
114                self.get_fee(FN_SELECTOR_BLOB_BASE_FEE.to_vec()),
115                self.get_fee(FN_SELECTOR_BASE_FEE_SCALAR.to_vec()),
116                self.get_fee(FN_SELECTOR_BLOB_BASE_FEE_SCALAR.to_vec()),
117            )
118            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
119
120        let base_fee_scalar: u32 = base_fee_scalar.try_into().map_err(|e| {
121            TransactionError::UnexpectedError(format!("Failed to convert base fee scalar: {}", e))
122        })?;
123
124        let blob_base_fee_scalar: u32 = blob_base_fee_scalar.try_into().map_err(|e| {
125            TransactionError::UnexpectedError(format!(
126                "Failed to convert blob base fee scalar: {}",
127                e
128            ))
129        })?;
130
131        Ok(OptimismModifiers {
132            l1_base_fee,
133            base_fee,
134            decimals,
135            blob_base_fee,
136            base_fee_scalar,
137            blob_base_fee_scalar,
138        })
139    }
140}
141
142#[async_trait]
143impl<P: EvmProviderTrait> NetworkExtraFeeCalculatorServiceTrait for OptimismExtraFeeService<P> {
144    /// Get the extra fee for the given transaction data
145    ///
146    /// # Arguments
147    ///
148    /// * `tx_data` - The transaction data to get the extra fee for
149    ///
150    async fn get_extra_fee(&self, tx_data: &EvmTransactionData) -> Result<U256, TransactionError> {
151        let bytes = if tx_data.is_eip1559() {
152            let tx_eip1559 = TxEip1559::try_from(tx_data)?;
153            let mut bytes = Vec::new();
154            tx_eip1559.encode(&mut bytes).map_err(|e| {
155                TransactionError::InvalidType(format!("Failed to encode transaction: {}", e))
156            })?;
157            bytes
158        } else {
159            let tx_legacy = TxLegacy::try_from(tx_data)?;
160            let mut bytes = Vec::new();
161            tx_legacy.encode(&mut bytes).map_err(|e| {
162                TransactionError::InvalidType(format!("Failed to encode transaction: {}", e))
163            })?;
164            bytes
165        };
166
167        // Ecotone L1 Data Fee Calculation
168        // https://docs.optimism.io/stack/transactions/fees#ecotone
169        let zero_bytes = U256::from(bytes.iter().filter(|&b| *b == 0).count());
170        let non_zero_bytes = U256::from(bytes.len()) - zero_bytes;
171
172        let tx_compressed_size =
173            ((zero_bytes * U256::from(4)) + (non_zero_bytes * U256::from(16))) / U256::from(16);
174
175        let gas_modifiers = self.get_modifiers().await?;
176
177        let weighted_gas_price =
178            U256::from(16) * U256::from(gas_modifiers.base_fee_scalar) * gas_modifiers.base_fee
179                + U256::from(gas_modifiers.blob_base_fee_scalar) * gas_modifiers.blob_base_fee;
180
181        let l1_data_fee = tx_compressed_size * weighted_gas_price;
182
183        Ok(l1_data_fee)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::services::{MockEvmProviderTrait, ProviderError};
191    use alloy::primitives::TxKind;
192
193    fn setup_mock_provider_for_modifiers() -> MockEvmProviderTrait {
194        let mut mock_provider = MockEvmProviderTrait::new();
195
196        let l1_base_fee_bytes = U256::from(10_000_000_000u64).to_be_bytes::<32>();
197        mock_provider
198            .expect_call_contract()
199            .times(1)
200            .returning(move |_| {
201                Box::pin(async move { Ok(Bytes::from(l1_base_fee_bytes.to_vec())) })
202            });
203
204        let base_fee_bytes = U256::from(1_000_000_000u64).to_be_bytes::<32>();
205        mock_provider
206            .expect_call_contract()
207            .times(1)
208            .returning(move |_| Box::pin(async move { Ok(Bytes::from(base_fee_bytes.to_vec())) }));
209
210        let decimals_bytes = U256::from(9u64).to_be_bytes::<32>();
211        mock_provider
212            .expect_call_contract()
213            .times(1)
214            .returning(move |_| Box::pin(async move { Ok(Bytes::from(decimals_bytes.to_vec())) }));
215
216        let blob_base_fee_bytes = U256::from(100u64).to_be_bytes::<32>();
217        mock_provider
218            .expect_call_contract()
219            .times(1)
220            .returning(move |_| {
221                Box::pin(async move { Ok(Bytes::from(blob_base_fee_bytes.to_vec())) })
222            });
223
224        let base_fee_scalar_bytes = U256::from(684000u64).to_be_bytes::<32>();
225        mock_provider
226            .expect_call_contract()
227            .times(1)
228            .returning(move |_| {
229                Box::pin(async move { Ok(Bytes::from(base_fee_scalar_bytes.to_vec())) })
230            });
231
232        let blob_base_fee_scalar_bytes = U256::from(50000u64).to_be_bytes::<32>();
233        mock_provider
234            .expect_call_contract()
235            .times(1)
236            .returning(move |_| {
237                Box::pin(async move { Ok(Bytes::from(blob_base_fee_scalar_bytes.to_vec())) })
238            });
239
240        mock_provider
241    }
242
243    fn create_test_evm_transaction_data(is_eip1559: bool) -> EvmTransactionData {
244        let mut tx_data = EvmTransactionData {
245            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
246            to: Some("0xa24Cea55A6171FbA0935c9e171c4Efe5Ba28DF91".to_string()),
247            value: U256::from(1000000000),
248            data: Some("0x0123".to_string()),
249            nonce: Some(1),
250            chain_id: 10,
251            gas_limit: Some(21000),
252            hash: None,
253            signature: None,
254            speed: None,
255            raw: None,
256            gas_price: None,
257            max_fee_per_gas: None,
258            max_priority_fee_per_gas: None,
259        };
260
261        if is_eip1559 {
262            tx_data.max_fee_per_gas = Some(30000000000);
263            tx_data.max_priority_fee_per_gas = Some(2000000000);
264        } else {
265            tx_data.gas_price = Some(20000000000);
266        }
267
268        tx_data
269    }
270
271    #[test]
272    fn test_create_contract_call() {
273        let mock_provider = MockEvmProviderTrait::new();
274        let service = OptimismExtraFeeService::new(mock_provider);
275
276        let result = service.create_contract_call(FN_SELECTOR_L1_BASE_FEE.to_vec());
277        assert!(result.is_ok());
278
279        let tx_request = result.unwrap();
280
281        let expected_address = Address::from_hex(OPTIMISM_GAS_PRICE_ORACLE_ADDRESS).unwrap();
282        assert_eq!(tx_request.to, Some(TxKind::Call(expected_address)));
283
284        assert!(matches!(tx_request.input, TransactionInput { .. }));
285    }
286
287    #[tokio::test]
288    async fn test_get_modifiers() {
289        let mock_provider = setup_mock_provider_for_modifiers();
290        let service = OptimismExtraFeeService::new(mock_provider);
291
292        let modifiers = service.get_modifiers().await;
293        assert!(
294            modifiers.is_ok(),
295            "Failed to get modifiers: {:?}",
296            modifiers.err()
297        );
298
299        let modifiers = modifiers.unwrap();
300
301        assert_eq!(
302            modifiers.l1_base_fee,
303            U256::from(10_000_000_000u64),
304            "L1 base fee mismatch"
305        );
306        assert_eq!(
307            modifiers.base_fee,
308            U256::from(1_000_000_000u64),
309            "Base fee mismatch"
310        );
311        assert_eq!(modifiers.decimals, U256::from(9u64), "Decimals mismatch");
312        assert_eq!(
313            modifiers.blob_base_fee,
314            U256::from(100u64),
315            "Blob base fee mismatch"
316        );
317        assert_eq!(
318            modifiers.base_fee_scalar, 684000,
319            "Base fee scalar mismatch"
320        );
321        assert_eq!(
322            modifiers.blob_base_fee_scalar, 50000,
323            "Blob base fee scalar mismatch"
324        );
325    }
326
327    #[tokio::test]
328    async fn test_get_extra_fee_eip1559_transaction() {
329        let mock_provider = setup_mock_provider_for_modifiers();
330        let service = OptimismExtraFeeService::new(mock_provider);
331
332        let tx_data = create_test_evm_transaction_data(true);
333        let extra_fee = service.get_extra_fee(&tx_data).await;
334
335        assert!(
336            extra_fee.is_ok(),
337            "Failed to get extra fee: {:?}",
338            extra_fee.err()
339        );
340
341        let extra_fee = extra_fee.unwrap();
342        assert!(
343            extra_fee > U256::ZERO,
344            "Extra fee should be greater than zero"
345        );
346    }
347
348    #[tokio::test]
349    async fn test_get_extra_fee_legacy_transaction() {
350        let mock_provider = setup_mock_provider_for_modifiers();
351        let service = OptimismExtraFeeService::new(mock_provider);
352
353        let tx_data = create_test_evm_transaction_data(false);
354        let extra_fee = service.get_extra_fee(&tx_data).await;
355
356        assert!(
357            extra_fee.is_ok(),
358            "Failed to get extra fee: {:?}",
359            extra_fee.err()
360        );
361
362        let extra_fee = extra_fee.unwrap();
363        assert!(
364            extra_fee > U256::ZERO,
365            "Extra fee should be greater than zero"
366        );
367    }
368
369    #[tokio::test]
370    async fn test_get_modifiers_error_handling() {
371        let mut mock_provider = MockEvmProviderTrait::new();
372
373        mock_provider.expect_call_contract().returning(|_| {
374            Box::pin(async { Err(ProviderError::Other("Simulated RPC error".to_string())) })
375        });
376
377        let service = OptimismExtraFeeService::new(mock_provider);
378        let result = service.get_modifiers().await;
379
380        assert!(result.is_err());
381        if let Err(e) = result {
382            match e {
383                TransactionError::UnexpectedError(msg) => {
384                    assert!(msg.contains("Simulated RPC error"));
385                }
386                _ => panic!("Expected UnexpectedError but got {:?}", e),
387            }
388        }
389    }
390}