openzeppelin_relayer/services/gas/handlers/
optimism.rs

1use crate::{
2    constants::{DEFAULT_GAS_LIMIT, OPTIMISM_GAS_PRICE_ORACLE_ADDRESS},
3    domain::evm::PriceParams,
4    models::{EvmTransactionData, TransactionError, U256},
5    services::provider::evm::EvmProviderTrait,
6};
7use alloy::{
8    primitives::{Address, Bytes, TxKind},
9    rpc::types::{TransactionInput, TransactionRequest},
10};
11
12#[derive(Debug, Clone)]
13pub struct OptimismFeeData {
14    pub l1_base_fee: U256,
15    pub base_fee: U256,
16    pub decimals: U256,
17    pub blob_base_fee: U256,
18    pub base_fee_scalar: U256,
19    pub blob_base_fee_scalar: U256,
20}
21
22/// Price parameter handler for Optimism-based networks
23/// This calculates L1 data availability costs and adds them as extra fees
24#[derive(Debug, Clone)]
25pub struct OptimismPriceHandler<P> {
26    provider: P,
27    oracle_address: Address,
28}
29
30impl<P: EvmProviderTrait> OptimismPriceHandler<P> {
31    pub fn new(provider: P) -> Self {
32        Self {
33            provider,
34            oracle_address: OPTIMISM_GAS_PRICE_ORACLE_ADDRESS.parse().unwrap(),
35        }
36    }
37
38    // Function selectors for Optimism GasPriceOracle
39    // bytes4(keccak256("l1BaseFee()"))
40    const FN_SELECTOR_L1_BASE_FEE: [u8; 4] = [81, 155, 75, 211];
41    // bytes4(keccak256("baseFee()"))
42    const FN_SELECTOR_BASE_FEE: [u8; 4] = [110, 242, 92, 58];
43    // bytes4(keccak256("decimals()"))
44    const FN_SELECTOR_DECIMALS: [u8; 4] = [49, 60, 229, 103];
45    // bytes4(keccak256("blobBaseFee()"))
46    const FN_SELECTOR_BLOB_BASE_FEE: [u8; 4] = [248, 32, 97, 64];
47    // bytes4(keccak256("baseFeeScalar()"))
48    const FN_SELECTOR_BASE_FEE_SCALAR: [u8; 4] = [197, 152, 89, 24];
49    // bytes4(keccak256("blobBaseFeeScalar()"))
50    const FN_SELECTOR_BLOB_BASE_FEE_SCALAR: [u8; 4] = [104, 213, 220, 166];
51
52    fn create_contract_call(&self, selector: [u8; 4]) -> TransactionRequest {
53        let mut data = Vec::with_capacity(4);
54        data.extend_from_slice(&selector);
55        TransactionRequest {
56            to: Some(TxKind::Call(self.oracle_address)),
57            input: TransactionInput::from(Bytes::from(data)),
58            ..Default::default()
59        }
60    }
61
62    async fn read_u256(&self, selector: [u8; 4]) -> Result<U256, TransactionError> {
63        let call = self.create_contract_call(selector);
64        let bytes = self
65            .provider
66            .call_contract(&call)
67            .await
68            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
69        Ok(U256::from_be_slice(bytes.as_ref()))
70    }
71
72    fn calculate_compressed_tx_size(tx: &EvmTransactionData) -> U256 {
73        let data_bytes: Vec<u8> = tx
74            .data
75            .as_ref()
76            .and_then(|hex_str| hex::decode(hex_str.trim_start_matches("0x")).ok())
77            .unwrap_or_default();
78
79        let zero_bytes = U256::from(data_bytes.iter().filter(|&b| *b == 0).count());
80        let non_zero_bytes = U256::from(data_bytes.len()) - zero_bytes;
81
82        (zero_bytes * U256::from(4)) + (non_zero_bytes * U256::from(16))
83    }
84
85    pub async fn fetch_fee_data(&self) -> Result<OptimismFeeData, TransactionError> {
86        let (l1_base_fee, base_fee, decimals, blob_base_fee, base_fee_scalar, blob_base_fee_scalar) =
87            tokio::try_join!(
88                self.read_u256(Self::FN_SELECTOR_L1_BASE_FEE),
89                self.read_u256(Self::FN_SELECTOR_BASE_FEE),
90                self.read_u256(Self::FN_SELECTOR_DECIMALS),
91                self.read_u256(Self::FN_SELECTOR_BLOB_BASE_FEE),
92                self.read_u256(Self::FN_SELECTOR_BASE_FEE_SCALAR),
93                self.read_u256(Self::FN_SELECTOR_BLOB_BASE_FEE_SCALAR)
94            )
95            .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?;
96
97        Ok(OptimismFeeData {
98            l1_base_fee,
99            base_fee,
100            decimals,
101            blob_base_fee,
102            base_fee_scalar,
103            blob_base_fee_scalar,
104        })
105    }
106
107    pub fn calculate_fee(
108        &self,
109        fee_data: &OptimismFeeData,
110        tx: &EvmTransactionData,
111    ) -> Result<U256, TransactionError> {
112        // Ecotone cost formula from code:
113        // https://github.com/ethereum-optimism/op-geth/blob/0402d543c3d0cff3a3d344c0f4f83809edb44f10/core/types/rollup_cost.go#L188-L219
114        //
115        // Ecotone L1 cost function:
116        //
117        //   (calldataGas/16)*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/1e6
118        //
119        // We divide "calldataGas" by 16 to change from units of calldata gas to "estimated # of bytes when
120        // compressed". Known as "compressedTxSize" in the spec.
121        //
122        // Function is actually computed as follows for better precision under integer arithmetic:
123        //
124        //   calldataGas*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/16e6
125
126        let calldata_gas_used = Self::calculate_compressed_tx_size(tx);
127
128        let ecotone_divisor = U256::from(1_000_000 * 16);
129        let calldata_cost_per_byte = U256::from(fee_data.l1_base_fee)
130            .saturating_mul(U256::from(16))
131            .saturating_mul(U256::from(fee_data.base_fee_scalar));
132        let blob_cost_per_byte = U256::from(fee_data.blob_base_fee)
133            .saturating_mul(U256::from(fee_data.blob_base_fee_scalar));
134        let fee = calldata_cost_per_byte
135            .saturating_add(blob_cost_per_byte)
136            .saturating_mul(U256::from(calldata_gas_used))
137            .wrapping_div(ecotone_divisor);
138        Ok(fee)
139    }
140
141    pub async fn handle_price_params(
142        &self,
143        tx: &EvmTransactionData,
144        mut original_params: PriceParams,
145    ) -> Result<PriceParams, TransactionError> {
146        // Fetch Optimism fee data and calculate L1 data cost
147        let fee_data = self.fetch_fee_data().await?;
148        let l1_data_cost = self.calculate_fee(&fee_data, tx)?;
149
150        // Add the L1 data cost as extra fee
151        original_params.extra_fee = Some(l1_data_cost);
152
153        // Recalculate total cost with the extra fee
154        let gas_limit = tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT);
155        let value = tx.value;
156        let is_eip1559 = original_params.max_fee_per_gas.is_some();
157
158        original_params.total_cost =
159            original_params.calculate_total_cost(is_eip1559, gas_limit, value);
160
161        Ok(original_params)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::services::provider::evm::MockEvmProviderTrait;
169
170    #[tokio::test]
171    async fn test_optimism_price_handler() {
172        let mut mock_provider = MockEvmProviderTrait::new();
173
174        // Mock all the contract calls for Optimism oracle
175        mock_provider.expect_call_contract().returning(|_| {
176            // Return mock data for oracle calls
177            Box::pin(async { Ok(vec![0u8; 32].into()) })
178        });
179
180        let handler = OptimismPriceHandler::new(mock_provider);
181
182        let tx = EvmTransactionData {
183            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
184            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
185            value: U256::from(1_000_000_000_000_000_000u128),
186            data: Some("0x1234567890abcdef".to_string()),
187            gas_limit: Some(21000),
188            gas_price: Some(20_000_000_000),
189            max_fee_per_gas: None,
190            max_priority_fee_per_gas: None,
191            speed: None,
192            nonce: None,
193            chain_id: 10, // Optimism chain ID
194            hash: None,
195            signature: None,
196            raw: None,
197        };
198
199        let original_params = PriceParams {
200            gas_price: Some(20_000_000_000),
201            max_fee_per_gas: None,
202            max_priority_fee_per_gas: None,
203            is_min_bumped: None,
204            extra_fee: None,
205            total_cost: U256::ZERO,
206        };
207
208        let result = handler.handle_price_params(&tx, original_params).await;
209
210        assert!(result.is_ok());
211        let handled_params = result.unwrap();
212
213        // Gas price should remain unchanged for Optimism (only extra fee is added)
214        assert_eq!(handled_params.gas_price, Some(20_000_000_000));
215
216        // Extra fee should be added
217        assert!(handled_params.extra_fee.is_some());
218
219        // Total cost should be recalculated
220        assert!(handled_params.total_cost > U256::ZERO);
221    }
222
223    #[test]
224    fn test_calculate_compressed_tx_size() {
225        // Test with empty data
226        let empty_tx = EvmTransactionData {
227            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
228            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
229            value: U256::from(1_000_000_000_000_000_000u128),
230            data: None,
231            gas_limit: Some(21000),
232            gas_price: Some(20_000_000_000),
233            max_fee_per_gas: None,
234            max_priority_fee_per_gas: None,
235            speed: None,
236            nonce: None,
237            chain_id: 10,
238            hash: None,
239            signature: None,
240            raw: None,
241        };
242
243        let size =
244            OptimismPriceHandler::<MockEvmProviderTrait>::calculate_compressed_tx_size(&empty_tx);
245        assert_eq!(size, U256::ZERO);
246
247        // Test with data containing zeros and non-zeros
248        let data_tx = EvmTransactionData {
249            data: Some("0x00001234".to_string()), // 2 zero bytes, 2 non-zero bytes
250            ..empty_tx
251        };
252
253        let size =
254            OptimismPriceHandler::<MockEvmProviderTrait>::calculate_compressed_tx_size(&data_tx);
255        // Expected: ((2 * 4) + (2 * 16)) / 16 = (8 + 32) / 16 = 40 / 16 = 2.5 -> 2 (integer division)
256        let expected = U256::from(2) * U256::from(4) + U256::from(2) * U256::from(16);
257        assert_eq!(size, expected);
258    }
259
260    #[test]
261    fn test_calculate_fee_with_specific_data_and_fee_data() {
262        let mock_provider = MockEvmProviderTrait::new();
263        let handler = OptimismPriceHandler::new(mock_provider);
264
265        let fee_data = OptimismFeeData {
266            l1_base_fee: U256::from(422079632u64),
267            base_fee: U256::from(138u64),
268            decimals: U256::from(6u64),
269            blob_base_fee: U256::from(2u64),
270            base_fee_scalar: U256::from(5227u64),
271            blob_base_fee_scalar: U256::from(1014213u64),
272        };
273
274        let tx = EvmTransactionData {
275            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
276            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
277            value: U256::ZERO,
278            data: Some("0xaf524e5852ba824bfabc2bcfcdf7f0edbb486ebb05e1836c90e78047efeb949990f72e5f00000000000000000000000000000000000000000000000000000000000000600f0b4ec422bb6297b5ded2971c583488bc1a714a2b11201bb32988080dec689b0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000653636313431353161306633613839373337393633303932650000000000000000363831306565633032393766616339373337303865316531000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000014cab1a2f55e1c3f8905f46c0f9f73746b7fc160c5000000000000000000000000".to_string()),
279            gas_limit: Some(21000),
280            gas_price: Some(20_000_000_000),
281            max_fee_per_gas: None,
282            max_priority_fee_per_gas: None,
283            speed: None,
284            nonce: None,
285            chain_id: 10,
286            hash: None,
287            signature: None,
288            raw: None,
289        };
290
291        let result = handler.calculate_fee(&fee_data, &tx);
292        assert!(result.is_ok());
293        let fee = result.unwrap();
294        assert_eq!(fee, U256::from(7342268088u64));
295    }
296}