Skip to content

Commit da2d90e

Browse files
authored
feat: replace l2 extra fees with price overriders (#460)
1 parent 6b838c5 commit da2d90e

File tree

10 files changed

+662
-285
lines changed

10 files changed

+662
-285
lines changed

src/domain/transaction/evm/price_calculator.rs

Lines changed: 249 additions & 109 deletions
Large diffs are not rendered by default.

src/domain/transaction/mod.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
services::{
2525
gas::{
2626
cache::GasPriceCache, evm_gas_price::EvmGasPriceService,
27-
network_extra_fee::NetworkExtraFeeCalculatorService,
27+
price_params_handler::PriceParamsHandler,
2828
},
2929
get_network_provider, EvmSignerFactory, StellarSignerFactory,
3030
},
@@ -415,8 +415,8 @@ impl RelayerTransactionFactory {
415415

416416
let evm_provider = get_network_provider(&network, relayer.custom_rpc_urls.clone())?;
417417
let signer_service = EvmSignerFactory::create_evm_signer(signer.into()).await?;
418-
let network_extra_fee_calculator =
419-
NetworkExtraFeeCalculatorService::new(network.clone(), evm_provider.clone());
418+
let price_params_handler =
419+
PriceParamsHandler::for_network(&network, evm_provider.clone());
420420

421421
let evm_gas_cache = GasPriceCache::global();
422422

@@ -434,10 +434,8 @@ impl RelayerTransactionFactory {
434434
let gas_price_service =
435435
EvmGasPriceService::new(evm_provider.clone(), network.clone(), cache);
436436

437-
let price_calculator = evm::PriceCalculator::new(
438-
gas_price_service,
439-
Some(network_extra_fee_calculator),
440-
);
437+
let price_calculator =
438+
evm::PriceCalculator::new(gas_price_service, price_params_handler);
441439

442440
Ok(NetworkTransaction::Evm(Box::new(
443441
DefaultEvmTransaction::new(

src/services/gas/cache.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
config::GasPriceCacheConfig,
99
constants::{GAS_PRICE_CACHE_REFRESH_TIMEOUT_SECS, HISTORICAL_BLOCKS},
1010
models::{EvmNetwork, TransactionError},
11-
services::{gas::l2_fee::L2FeeData, EvmProviderTrait},
11+
services::EvmProviderTrait,
1212
};
1313
use alloy::rpc::types::{BlockNumberOrTag, FeeHistory};
1414
use dashmap::DashMap;
@@ -33,7 +33,7 @@ pub struct GasPriceCacheEntry {
3333
pub gas_price: u128,
3434
pub base_fee_per_gas: u128,
3535
pub fee_history: FeeHistory,
36-
pub l2_fee_data: Option<L2FeeData>,
36+
3737
pub fetched_at: Instant,
3838
pub stale_after: Duration,
3939
pub expire_after: Duration,
@@ -45,15 +45,13 @@ impl GasPriceCacheEntry {
4545
gas_price: u128,
4646
base_fee_per_gas: u128,
4747
fee_history: FeeHistory,
48-
l2_fee_data: Option<L2FeeData>,
4948
stale_after: Duration,
5049
expire_after: Duration,
5150
) -> Self {
5251
Self {
5352
gas_price,
5453
base_fee_per_gas,
5554
fee_history,
56-
l2_fee_data,
5755
fetched_at: Instant::now(),
5856
stale_after,
5957
expire_after,
@@ -168,7 +166,6 @@ impl GasPriceCache {
168166
gas_price,
169167
base_fee_per_gas,
170168
fee_history,
171-
None,
172169
Duration::from_millis(cfg.stale_after_ms),
173170
Duration::from_millis(cfg.expire_after_ms),
174171
);
@@ -285,7 +282,6 @@ impl GasPriceCache {
285282
fresh_gas_price,
286283
fresh_base_fee,
287284
fee_hist,
288-
None,
289285
Duration::from_millis(cfg.stale_after_ms),
290286
Duration::from_millis(cfg.expire_after_ms),
291287
);
@@ -340,7 +336,6 @@ mod tests {
340336
gas_price,
341337
base_fee,
342338
fee_history,
343-
None,
344339
Duration::from_secs(30),
345340
Duration::from_secs(120),
346341
);
@@ -365,7 +360,6 @@ mod tests {
365360
gas_price,
366361
base_fee,
367362
fee_history,
368-
None,
369363
Duration::from_secs(30),
370364
Duration::from_secs(120),
371365
);
@@ -387,7 +381,6 @@ mod tests {
387381
gas_price,
388382
base_fee,
389383
fee_history,
390-
None,
391384
Duration::from_secs(30),
392385
Duration::from_secs(120),
393386
);
@@ -417,7 +410,6 @@ mod tests {
417410
gas_price,
418411
base_fee,
419412
fee_history,
420-
None,
421413
Duration::from_secs(30),
422414
Duration::from_secs(120),
423415
);
@@ -457,7 +449,6 @@ mod tests {
457449
gas_price,
458450
base_fee,
459451
fee_history,
460-
None,
461452
Duration::from_secs(30),
462453
Duration::from_secs(120),
463454
);

src/services/gas/handlers/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! Price parameter handlers for network-specific gas price customizations.
2+
3+
pub mod optimism;
4+
#[cfg(test)]
5+
pub mod test_mock;
6+
7+
pub use optimism::OptimismPriceHandler;
8+
#[cfg(test)]
9+
pub use test_mock::MockPriceHandler;
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
use crate::{
2+
constants::{DEFAULT_GAS_LIMIT, OPTIMISM_GAS_PRICE_ORACLE_ADDRESS},
3+
domain::evm::PriceParams,
4+
models::{evm::EvmTransactionRequest, TransactionError, U256},
5+
services::provider::evm::EvmProviderTrait,
6+
};
7+
use alloy::{
8+
primitives::{Address, Bytes, TxKind},
9+
rpc::types::{TransactionInput, TransactionRequest},
10+
};
11+
12+
#[derive(Debug, Clone)]
13+
pub 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)]
25+
pub struct OptimismPriceHandler<P> {
26+
provider: P,
27+
oracle_address: Address,
28+
}
29+
30+
impl<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: &EvmTransactionRequest) -> 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))) / 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: &EvmTransactionRequest,
111+
) -> Result<U256, TransactionError> {
112+
let tx_compressed_size = Self::calculate_compressed_tx_size(tx);
113+
114+
let weighted_gas_price = U256::from(16)
115+
.saturating_mul(U256::from(fee_data.base_fee_scalar))
116+
.saturating_mul(U256::from(fee_data.l1_base_fee))
117+
+ U256::from(fee_data.blob_base_fee_scalar)
118+
.saturating_mul(U256::from(fee_data.blob_base_fee));
119+
120+
Ok(tx_compressed_size.saturating_mul(weighted_gas_price))
121+
}
122+
123+
pub async fn handle_price_params(
124+
&self,
125+
tx: &EvmTransactionRequest,
126+
mut original_params: PriceParams,
127+
) -> Result<PriceParams, TransactionError> {
128+
// Fetch Optimism fee data and calculate L1 data cost
129+
let fee_data = self.fetch_fee_data().await?;
130+
let l1_data_cost = self.calculate_fee(&fee_data, tx)?;
131+
132+
// Add the L1 data cost as extra fee
133+
original_params.extra_fee = Some(l1_data_cost);
134+
135+
// Recalculate total cost with the extra fee
136+
let gas_limit = tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT);
137+
let value = tx.value;
138+
let is_eip1559 = original_params.max_fee_per_gas.is_some();
139+
140+
original_params.total_cost =
141+
original_params.calculate_total_cost(is_eip1559, gas_limit, value);
142+
143+
Ok(original_params)
144+
}
145+
}
146+
147+
#[cfg(test)]
148+
mod tests {
149+
use super::*;
150+
use crate::services::provider::evm::MockEvmProviderTrait;
151+
152+
#[tokio::test]
153+
async fn test_optimism_price_handler() {
154+
let mut mock_provider = MockEvmProviderTrait::new();
155+
156+
// Mock all the contract calls for Optimism oracle
157+
mock_provider.expect_call_contract().returning(|_| {
158+
// Return mock data for oracle calls
159+
Box::pin(async { Ok(vec![0u8; 32].into()) })
160+
});
161+
162+
let handler = OptimismPriceHandler::new(mock_provider);
163+
164+
let tx = EvmTransactionRequest {
165+
to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
166+
value: U256::from(1_000_000_000_000_000_000u128),
167+
data: Some("0x1234567890abcdef".to_string()),
168+
gas_limit: Some(21000),
169+
gas_price: Some(20_000_000_000),
170+
max_fee_per_gas: None,
171+
max_priority_fee_per_gas: None,
172+
speed: None,
173+
valid_until: None,
174+
};
175+
176+
let original_params = PriceParams {
177+
gas_price: Some(20_000_000_000),
178+
max_fee_per_gas: None,
179+
max_priority_fee_per_gas: None,
180+
is_min_bumped: None,
181+
extra_fee: None,
182+
total_cost: U256::ZERO,
183+
};
184+
185+
let result = handler.handle_price_params(&tx, original_params).await;
186+
187+
assert!(result.is_ok());
188+
let handled_params = result.unwrap();
189+
190+
// Gas price should remain unchanged for Optimism (only extra fee is added)
191+
assert_eq!(handled_params.gas_price, Some(20_000_000_000));
192+
193+
// Extra fee should be added
194+
assert!(handled_params.extra_fee.is_some());
195+
196+
// Total cost should be recalculated
197+
assert!(handled_params.total_cost > U256::ZERO);
198+
}
199+
200+
#[test]
201+
fn test_calculate_compressed_tx_size() {
202+
// Test with empty data
203+
let empty_tx = EvmTransactionRequest {
204+
to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
205+
value: U256::from(1_000_000_000_000_000_000u128),
206+
data: None,
207+
gas_limit: Some(21000),
208+
gas_price: Some(20_000_000_000),
209+
max_fee_per_gas: None,
210+
max_priority_fee_per_gas: None,
211+
speed: None,
212+
valid_until: None,
213+
};
214+
215+
let size =
216+
OptimismPriceHandler::<MockEvmProviderTrait>::calculate_compressed_tx_size(&empty_tx);
217+
assert_eq!(size, U256::ZERO);
218+
219+
// Test with data containing zeros and non-zeros
220+
let data_tx = EvmTransactionRequest {
221+
data: Some("0x00001234".to_string()), // 2 zero bytes, 2 non-zero bytes
222+
..empty_tx
223+
};
224+
225+
let size =
226+
OptimismPriceHandler::<MockEvmProviderTrait>::calculate_compressed_tx_size(&data_tx);
227+
// Expected: ((2 * 4) + (2 * 16)) / 16 = (8 + 32) / 16 = 40 / 16 = 2.5 -> 2 (integer division)
228+
let expected =
229+
(U256::from(2) * U256::from(4) + U256::from(2) * U256::from(16)) / U256::from(16);
230+
assert_eq!(size, expected);
231+
}
232+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::{
2+
domain::evm::PriceParams,
3+
models::{evm::EvmTransactionRequest, TransactionError, U256},
4+
};
5+
6+
#[derive(Debug, Clone, Default)]
7+
pub struct MockPriceHandler;
8+
9+
impl MockPriceHandler {
10+
pub fn new() -> Self {
11+
Self
12+
}
13+
14+
pub async fn handle_price_params(
15+
&self,
16+
_tx: &EvmTransactionRequest,
17+
mut original_params: PriceParams,
18+
) -> Result<PriceParams, TransactionError> {
19+
original_params.extra_fee = Some(U256::from(42u128));
20+
original_params.total_cost = original_params.total_cost + U256::from(42u128);
21+
Ok(original_params)
22+
}
23+
}

0 commit comments

Comments
 (0)