From 2b6bf2350485a524544a0f52f4fed1ad010014bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Mon, 12 Jan 2026 15:13:21 +0000 Subject: [PATCH 01/39] Migrate shared off of ethcontract --- Cargo.lock | 1 - crates/ethrpc/src/mock.rs | 12 ++ crates/shared/Cargo.toml | 1 - .../trade_verifier/balance_overrides/mod.rs | 22 +-- crates/shared/src/recent_block_cache.rs | 11 -- crates/shared/src/sources/swapr.rs | 6 +- .../src/sources/uniswap_v2/pool_fetching.rs | 132 ++++++++++-------- 7 files changed, 88 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1b4cf6332..cb9e46497d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6058,7 +6058,6 @@ dependencies = [ "database", "derivative", "derive_more 1.0.0", - "ethcontract", "ethrpc", "futures", "gas-estimation", diff --git a/crates/ethrpc/src/mock.rs b/crates/ethrpc/src/mock.rs index 6ba9d3ac7a..ef5dd5b5b8 100644 --- a/crates/ethrpc/src/mock.rs +++ b/crates/ethrpc/src/mock.rs @@ -4,6 +4,7 @@ use { crate::{Web3, alloy::MutWallet}, alloy::providers::{Provider, ProviderBuilder, mock::Asserter}, ethcontract::{ + dyns::DynTransport, futures::future::{self, Ready}, jsonrpc::{Call, Id, MethodCall, Params}, web3::{self, BatchTransport, RequestId, Transport}, @@ -33,6 +34,17 @@ impl Web3 { wallet: MutWallet::default(), } } + + // HACK(jmg-duarte): used to convert a MockTransport -> DynTransport so we can + // remove ethcontract imports from shared should be fixed in a follow up PR, + // removing web3 from ethrpc + pub fn erased(self) -> Web3 { + Web3 { + legacy: web3::Web3::new(DynTransport::new(self.legacy.transport().clone())), + alloy: self.alloy, + wallet: self.wallet, + } + } } pub fn web3() -> Web3 { diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index c0e2f04a5c..64d958b8bc 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -24,7 +24,6 @@ dashmap = { workspace = true } database = { workspace = true } derive_more = { workspace = true } derivative = { workspace = true } -ethcontract = { workspace = true } ethrpc = { workspace = true } futures = { workspace = true } gas-estimation = { workspace = true } diff --git a/crates/shared/src/price_estimation/trade_verifier/balance_overrides/mod.rs b/crates/shared/src/price_estimation/trade_verifier/balance_overrides/mod.rs index ded67ae028..44668f70b9 100644 --- a/crates/shared/src/price_estimation/trade_verifier/balance_overrides/mod.rs +++ b/crates/shared/src/price_estimation/trade_verifier/balance_overrides/mod.rs @@ -542,16 +542,7 @@ mod tests { let balance_overrides = BalanceOverrides { hardcoded: Default::default(), detector: Some(( - Detector::new( - ethrpc::Web3 { - legacy: web3::Web3::new(ethcontract::transport::DynTransport::new( - mock::MockTransport::new(), - )), - alloy: mock_web3.alloy, - wallet: mock_web3.wallet, - }, - 60, - ), + Detector::new(mock_web3.erased(), 60), Mutex::new(SizedCache::with_size(100)), )), }; @@ -599,16 +590,7 @@ mod tests { let balance_overrides = BalanceOverrides { hardcoded: Default::default(), detector: Some(( - Detector::new( - ethrpc::Web3 { - legacy: web3::Web3::new(ethcontract::transport::DynTransport::new( - mock::MockTransport::new(), - )), - alloy: mock_web3.alloy, - wallet: mock_web3.wallet, - }, - 60, - ), + Detector::new(mock_web3.erased(), 60), Mutex::new(SizedCache::with_size(100)), )), }; diff --git a/crates/shared/src/recent_block_cache.rs b/crates/shared/src/recent_block_cache.rs index f12555c53d..5c771837f4 100644 --- a/crates/shared/src/recent_block_cache.rs +++ b/crates/shared/src/recent_block_cache.rs @@ -29,7 +29,6 @@ use { alloy::eips::BlockId, anyhow::{Context, Result}, cached::{Cached, SizedCache}, - ethcontract::BlockNumber, ethrpc::block_stream::CurrentBlockWatcher, futures::{FutureExt, StreamExt}, itertools::Itertools, @@ -75,16 +74,6 @@ pub enum Block { Finalized, } -impl From for BlockNumber { - fn from(val: Block) -> Self { - match val { - Block::Recent => BlockNumber::Latest, - Block::Number(number) => BlockNumber::Number(number.into()), - Block::Finalized => BlockNumber::Finalized, - } - } -} - impl From for BlockId { fn from(value: Block) -> Self { match value { diff --git a/crates/shared/src/sources/swapr.rs b/crates/shared/src/sources/swapr.rs index 4ec0d57eca..045c890aba 100644 --- a/crates/shared/src/sources/swapr.rs +++ b/crates/shared/src/sources/swapr.rs @@ -2,10 +2,10 @@ use { crate::sources::uniswap_v2::pool_fetching::{DefaultPoolReader, Pool, PoolReading}, + alloy::eips::BlockId, anyhow::Result, contracts::alloy::ISwaprPair, - ethcontract::BlockId, - ethrpc::alloy::{conversions::IntoAlloy, errors::ignore_non_node_error}, + ethrpc::alloy::errors::ignore_non_node_error, futures::{FutureExt as _, future::BoxFuture}, model::TokenPair, num::rational::Ratio, @@ -27,7 +27,7 @@ impl PoolReading for SwaprPoolReader { async move { let pair_contract = ISwaprPair::Instance::new(pair_address, self.0.web3.alloy.clone()); - let fetch_fee = pair_contract.swapFee().block(block.into_alloy()); + let fetch_fee = pair_contract.swapFee().block(block); let (pool, fee) = futures::join!(fetch_pool, fetch_fee.call().into_future()); handle_results(pool, fee) diff --git a/crates/shared/src/sources/uniswap_v2/pool_fetching.rs b/crates/shared/src/sources/uniswap_v2/pool_fetching.rs index ce4154b1fb..85b69d592c 100644 --- a/crates/shared/src/sources/uniswap_v2/pool_fetching.rs +++ b/crates/shared/src/sources/uniswap_v2/pool_fetching.rs @@ -1,18 +1,17 @@ use { super::pair_provider::PairProvider, crate::{baseline_solver::BaselineSolvable, ethrpc::Web3, recent_block_cache::Block}, - alloy::primitives::Address, + alloy::{ + eips::BlockId, + primitives::{Address, U256}, + }, anyhow::Result, cached::{Cached, TimedCache}, contracts::alloy::{ ERC20, IUniswapLikePair::{self, IUniswapLikePair::getReservesReturn}, }, - ethcontract::{BlockId, U256}, - ethrpc::alloy::{ - conversions::{IntoAlloy, IntoLegacy}, - errors::ignore_non_node_error, - }, + ethrpc::alloy::errors::ignore_non_node_error, futures::{ FutureExt as _, future::{self, BoxFuture}, @@ -145,7 +144,7 @@ impl Pool { let denominator = reserve_out .checked_sub(amount_out)? .checked_mul(U256::from(self.fee.denom().checked_sub(*self.fee.numer())?))?; - let amount_in = numerator.checked_div(denominator)?.checked_add(1.into())?; + let amount_in = numerator.checked_div(denominator)?.checked_add(U256::ONE)?; check_final_reserves(amount_in, amount_out, reserve_in, reserve_out)?; Some(amount_in) @@ -172,24 +171,24 @@ impl BaselineSolvable for Pool { async fn get_amount_out( &self, out_token: Address, - (in_amount, in_token): (alloy::primitives::U256, Address), - ) -> Option { - self.get_amount_out(in_token, in_amount.into_legacy()) + (in_amount, in_token): (U256, Address), + ) -> Option { + self.get_amount_out(in_token, in_amount) .map(|(out_amount, token)| { assert_eq!(token, out_token); - out_amount.into_alloy() + out_amount }) } async fn get_amount_in( &self, in_token: Address, - (out_amount, out_token): (alloy::primitives::U256, Address), - ) -> Option { - self.get_amount_in(out_token, out_amount.into_legacy()) + (out_amount, out_token): (U256, Address), + ) -> Option { + self.get_amount_in(out_token, out_amount) .map(|(in_amount, token)| { assert_eq!(token, in_token); - in_amount.into_alloy() + in_amount }) } @@ -226,10 +225,9 @@ where let mut non_existent_pools = self.non_existent_pools.write().unwrap(); token_pairs.retain(|pair| non_existent_pools.cache_get(pair).is_none()); } - let block = BlockId::Number(at_block.into()); let futures = token_pairs .iter() - .map(|pair| self.pool_reader.read_state(*pair, block)) + .map(|pair| self.pool_reader.read_state(*pair, at_block.into())) .collect::>(); let results = future::try_join_all(futures).await?; @@ -280,12 +278,12 @@ impl PoolReading for DefaultPoolReader { let token1 = ERC20::Instance::new(pair.get().1, self.web3.alloy.clone()); async move { - let fetch_token0_balance = token0.balanceOf(pair_address).block(block.into_alloy()); - let fetch_token1_balance = token1.balanceOf(pair_address).block(block.into_alloy()); + let fetch_token0_balance = token0.balanceOf(pair_address).block(block); + let fetch_token1_balance = token1.balanceOf(pair_address).block(block); let pair_contract = IUniswapLikePair::Instance::new(pair_address, self.web3.alloy.clone()); - let fetch_reserves = pair_contract.getReserves().block(block.into_alloy()); + let fetch_reserves = pair_contract.getReserves().block(block); let (reserves, token0_balance, token1_balance) = futures::join!( fetch_reserves.call().into_future(), @@ -310,8 +308,8 @@ impl PoolReading for DefaultPoolReader { struct FetchedPool { pair: TokenPair, reserves: Result, - token0_balance: Result, - token1_balance: Result, + token0_balance: Result, + token1_balance: Result, } fn handle_results(fetched_pool: FetchedPool, address: Address) -> Result> { @@ -335,9 +333,7 @@ fn handle_results(fetched_pool: FetchedPool, address: Address) -> Result token0_balance? - || alloy::primitives::U256::from(r1) > token1_balance? - { + if U256::from(r0) > token0_balance? || U256::from(r1) > token1_balance? { return None; } // Errors here should never happen because reserves are uint<112, 2> @@ -391,16 +387,16 @@ mod tests { (100, 100), ); assert_eq!( - pool.get_amount_out(sell_token, 10.into()), - Some((9.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(10)), + Some((U256::from(9), buy_token)) ); assert_eq!( - pool.get_amount_out(sell_token, 100.into()), - Some((49.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(100)), + Some((U256::from(49), buy_token)) ); assert_eq!( - pool.get_amount_out(sell_token, 1000.into()), - Some((90.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(1000)), + Some((U256::from(90), buy_token)) ); //Uneven Pool @@ -410,16 +406,16 @@ mod tests { (200, 50), ); assert_eq!( - pool.get_amount_out(sell_token, 10.into()), - Some((2.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(10)), + Some((U256::from(2), buy_token)) ); assert_eq!( - pool.get_amount_out(sell_token, 100.into()), - Some((16.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(100)), + Some((U256::from(16), buy_token)) ); assert_eq!( - pool.get_amount_out(sell_token, 1000.into()), - Some((41.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(1000)), + Some((U256::from(41), buy_token)) ); // Large Numbers @@ -429,12 +425,12 @@ mod tests { (1u128 << 90, 1u128 << 90), ); assert_eq!( - pool.get_amount_out(sell_token, 10u128.pow(20).into()), - Some((99_699_991_970_459_889_807u128.into(), buy_token)) + pool.get_amount_out(sell_token, U256::from(10u128.pow(20))), + Some((U256::from(99_699_991_970_459_889_807u128), buy_token)) ); // Overflow - assert_eq!(pool.get_amount_out(sell_token, U256::max_value()), None); + assert_eq!(pool.get_amount_out(sell_token, U256::MAX), None); } #[test] @@ -449,17 +445,17 @@ mod tests { (100, 100), ); assert_eq!( - pool.get_amount_in(buy_token, 10.into()), - Some((12.into(), sell_token)) + pool.get_amount_in(buy_token, U256::from(10)), + Some((U256::from(12), sell_token)) ); assert_eq!( - pool.get_amount_in(buy_token, 99.into()), - Some((9930.into(), sell_token)) + pool.get_amount_in(buy_token, U256::from(99)), + Some((U256::from(9930), sell_token)) ); // Buying more than possible - assert_eq!(pool.get_amount_in(buy_token, 100.into()), None); - assert_eq!(pool.get_amount_in(buy_token, 1000.into()), None); + assert_eq!(pool.get_amount_in(buy_token, U256::from(100)), None); + assert_eq!(pool.get_amount_in(buy_token, U256::from(1000)), None); //Uneven Pool let pool = Pool::uniswap( @@ -468,12 +464,12 @@ mod tests { (200, 50), ); assert_eq!( - pool.get_amount_in(buy_token, 10.into()), - Some((51.into(), sell_token)) + pool.get_amount_in(buy_token, U256::from(10)), + Some((U256::from(51), sell_token)) ); assert_eq!( - pool.get_amount_in(buy_token, 49.into()), - Some((9830.into(), sell_token)) + pool.get_amount_in(buy_token, U256::from(49)), + Some((U256::from(9830), sell_token)) ); // Large Numbers @@ -483,28 +479,42 @@ mod tests { (1u128 << 90, 1u128 << 90), ); assert_eq!( - pool.get_amount_in(buy_token, 10u128.pow(20).into()), - Some((100_300_910_810_367_424_267u128.into(), sell_token)), + pool.get_amount_in(buy_token, U256::from(10u128.pow(20))), + Some((U256::from(100_300_910_810_367_424_267u128), sell_token)), ); } #[test] fn computes_final_reserves() { assert_eq!( - check_final_reserves(1.into(), 2.into(), 1_000_000.into(), 2_000_000.into(),).unwrap(), - (1_000_001.into(), 1_999_998.into()), + check_final_reserves( + U256::ONE, + U256::from(2), + U256::from(1_000_000), + U256::from(2_000_000), + ) + .unwrap(), + (U256::from(1_000_001), U256::from(1_999_998)), ); } #[test] fn check_final_reserve_limits() { // final out reserve too low - assert!(check_final_reserves(0.into(), 1.into(), 1_000_000.into(), 0.into()).is_none()); - // final in reserve too high assert!( - check_final_reserves(1.into(), 0.into(), *POOL_MAX_RESERVES, 1_000_000.into()) + check_final_reserves(U256::ZERO, U256::ONE, U256::from(1_000_000), U256::ZERO) .is_none() ); + // final in reserve too high + assert!( + check_final_reserves( + U256::ONE, + U256::ZERO, + *POOL_MAX_RESERVES, + U256::from(1_000_000) + ) + .is_none() + ); } #[test] @@ -512,8 +522,8 @@ mod tests { let fetched_pool = FetchedPool { reserves: Err(testing_alloy_node_error()), pair: Default::default(), - token0_balance: Ok(alloy::primitives::U256::from(1)), - token1_balance: Ok(alloy::primitives::U256::from(1)), + token0_balance: Ok(U256::ONE), + token1_balance: Ok(U256::ONE), }; let pool_address = Default::default(); assert!(handle_results(fetched_pool, pool_address).is_err()); @@ -524,8 +534,8 @@ mod tests { let fetched_pool = FetchedPool { reserves: Err(testing_alloy_contract_error()), pair: Default::default(), - token0_balance: Ok(alloy::primitives::U256::from(1)), - token1_balance: Ok(alloy::primitives::U256::from(1)), + token0_balance: Ok(U256::ONE), + token1_balance: Ok(U256::ONE), }; let pool_address = Default::default(); assert!( From f439af115f2c04d4968b61c7969742332bc8b25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Mon, 12 Jan 2026 16:26:20 +0000 Subject: [PATCH 02/39] Vendor gas-estimation crate into shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed external gas-estimation dependency by vendoring its code into shared/gas_price_estimation module: - Inlined GasPriceEstimating and PriorityGasPriceEstimating traits - Vendored GasPrice1559 type into price module - Created NodeGasPriceEstimator to replace web3 legacy gas estimation - Removed NativeGasEstimator and Native gas estimator config option - Updated driver and refunder crates to use vendored implementations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.lock | 22 -- Cargo.toml | 1 - crates/driver/Cargo.toml | 1 - crates/driver/src/infra/blockchain/gas.rs | 26 +-- crates/driver/src/infra/config/file/mod.rs | 40 +--- crates/refunder/Cargo.toml | 1 - crates/refunder/src/refund_service.rs | 7 +- crates/refunder/src/submitter.rs | 6 +- crates/shared/Cargo.toml | 1 - crates/shared/src/gas_price.rs | 2 +- .../shared/src/gas_price_estimation/alloy.rs | 10 +- .../shared/src/gas_price_estimation/driver.rs | 3 +- .../src/gas_price_estimation/eth_node.rs | 40 ++++ .../shared/src/gas_price_estimation/fake.rs | 3 +- crates/shared/src/gas_price_estimation/mod.rs | 55 +++-- .../shared/src/gas_price_estimation/price.rs | 211 ++++++++++++++++++ .../src/gas_price_estimation/priority.rs | 170 ++++++++++++++ crates/shared/src/order_quoting.rs | 5 +- .../src/price_estimation/competition/mod.rs | 3 +- .../src/price_estimation/competition/quote.rs | 2 +- crates/shared/src/price_estimation/factory.rs | 2 +- 21 files changed, 487 insertions(+), 124 deletions(-) create mode 100644 crates/shared/src/gas_price_estimation/eth_node.rs create mode 100644 crates/shared/src/gas_price_estimation/price.rs create mode 100644 crates/shared/src/gas_price_estimation/priority.rs diff --git a/Cargo.lock b/Cargo.lock index cb9e46497d..b73e48e595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2591,7 +2591,6 @@ dependencies = [ "ethcontract", "ethrpc", "futures", - "gas-estimation", "hex-literal", "humantime", "humantime-serde", @@ -3175,25 +3174,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" -[[package]] -name = "gas-estimation" -version = "0.1.0" -source = "git+https://github.com/cowprotocol/gas-estimation?tag=v0.7.3#5384e9d013bf33fed32b3e5366ed01b2d8cacbe4" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "http 0.2.12", - "primitive-types", - "serde", - "serde_json", - "serde_with", - "tokio", - "tracing", - "url", - "web3", -] - [[package]] name = "generator" version = "0.8.4" @@ -5267,7 +5247,6 @@ dependencies = [ "database", "ethrpc", "futures", - "gas-estimation", "humantime", "mimalloc", "number", @@ -6060,7 +6039,6 @@ dependencies = [ "derive_more 1.0.0", "ethrpc", "futures", - "gas-estimation", "hex-literal", "humantime", "indexmap 2.10.0", diff --git a/Cargo.toml b/Cargo.toml index 58b5d06988..b2b4be4887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ tikv-jemallocator = { version = "0.6", features = ["unprefixed_malloc_on_support jemalloc_pprof = { version = "0.8", features = ["symbolize"] } flate2 = "1.0.30" futures = "0.3.30" -gas-estimation = { git = "https://github.com/cowprotocol/gas-estimation", tag = "v0.7.3", features = ["web3_", "tokio_"] } const-hex = "1.17.0" hex-literal = "0.4.1" humantime = "2.1.0" diff --git a/crates/driver/Cargo.toml b/crates/driver/Cargo.toml index c4ce469e8c..ec336c1382 100644 --- a/crates/driver/Cargo.toml +++ b/crates/driver/Cargo.toml @@ -66,7 +66,6 @@ anyhow = { workspace = true } clap = { workspace = true } contracts = { workspace = true } ethcontract = { workspace = true } -gas-estimation = { workspace = true } model = { workspace = true } observe = { workspace = true, features = ["axum-tracing"] } shared = { workspace = true } diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index 9cc27680cb..cc933effe3 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -9,11 +9,12 @@ use { infra::{config::file::GasEstimatorType, mempool}, }, ethrpc::Web3, - gas_estimation::{ + + shared::gas_price_estimation::{ GasPriceEstimating, - nativegasestimator::{NativeGasEstimator, Params}, + alloy::AlloyGasPriceEstimator, + eth_node::NodeGasPriceEstimator, }, - shared::gas_price_estimation::alloy::AlloyGasPriceEstimator, std::sync::Arc, }; @@ -35,24 +36,7 @@ impl GasPriceEstimator { mempools: &[mempool::Config], ) -> Result { let gas: Arc = match gas_estimator_type { - GasEstimatorType::Native { - max_reward_percentile, - max_block_percentile, - min_block_percentile, - } => Arc::new( - NativeGasEstimator::new( - web3.transport().clone(), - Some(Params { - max_reward_percentile: *max_reward_percentile, - max_block_percentile: *max_block_percentile, - min_block_percentile: *min_block_percentile, - ..Default::default() - }), - ) - .await - .map_err(Error::GasPrice)?, - ), - GasEstimatorType::Web3 => Arc::new(web3.legacy.clone()), + GasEstimatorType::Web3 => Arc::new(NodeGasPriceEstimator::new(web3.alloy.clone())), GasEstimatorType::Alloy => Arc::new(AlloyGasPriceEstimator::new(web3.alloy.clone())), }; // TODO: simplify logic by moving gas price adjustments out of the individual diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 7dfc6894f5..74b2822292 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -728,51 +728,15 @@ pub struct LiquoriceConfig { pub http_timeout: Duration, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Default)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(tag = "estimator")] pub enum GasEstimatorType { - #[serde(rename_all = "kebab-case")] - Native { - // Effective reward value to be selected from each individual block - // Example: 20 means 20% of the transactions with the lowest gas price will be analyzed - #[serde(default = "default_max_reward_percentile")] - max_reward_percentile: usize, - // Economical priority fee to be selected from sorted individual block reward percentiles - // This constitutes the part of priority fee that doesn't depend on the time_limit - #[serde(default = "default_min_block_percentile")] - min_block_percentile: f64, - // Urgent priority fee to be selected from sorted individual block reward percentiles - // This constitutes the part of priority fee that depends on the time_limit - #[serde(default = "default_max_block_percentile")] - max_block_percentile: f64, - }, Web3, + #[default] Alloy, } -impl Default for GasEstimatorType { - fn default() -> Self { - GasEstimatorType::Native { - max_reward_percentile: default_max_reward_percentile(), - min_block_percentile: default_min_block_percentile(), - max_block_percentile: default_max_block_percentile(), - } - } -} - -fn default_max_reward_percentile() -> usize { - 20 -} - -fn default_min_block_percentile() -> f64 { - 30. -} - -fn default_max_block_percentile() -> f64 { - 60. -} - /// Defines various strategies to prioritize orders. #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case", tag = "strategy")] diff --git a/crates/refunder/Cargo.toml b/crates/refunder/Cargo.toml index 1efb71a729..e81abd9417 100644 --- a/crates/refunder/Cargo.toml +++ b/crates/refunder/Cargo.toml @@ -14,7 +14,6 @@ contracts = { workspace = true } database = { workspace = true } ethrpc = { workspace = true } futures = { workspace = true } -gas-estimation = { workspace = true } const-hex = { workspace = true } humantime = { workspace = true } mimalloc = { workspace = true, optional = true } diff --git a/crates/refunder/src/refund_service.rs b/crates/refunder/src/refund_service.rs index b756d7fc80..ed5c4e94b6 100644 --- a/crates/refunder/src/refund_service.rs +++ b/crates/refunder/src/refund_service.rs @@ -16,6 +16,7 @@ use { ethrpc::{Web3, block_stream::timestamp_of_current_block_in_seconds}, futures::{StreamExt, stream}, number::conversions::big_decimal_to_u256, + shared::gas_price_estimation::eth_node::NodeGasPriceEstimator, sqlx::PgPool, std::collections::HashMap, }; @@ -57,7 +58,7 @@ impl RefundService { start_priority_fee_tip: u64, ) -> Self { let signer_address = signer.address(); - let gas_estimator = Box::new(web3.legacy.clone()); + let gas_estimator = Box::new(NodeGasPriceEstimator::new(web3.alloy.clone())); web3.wallet.register_signer(signer); RefundService { db, @@ -292,7 +293,7 @@ impl RefundService { #[cfg(test)] mod tests { - use super::*; + use {super::*, shared::gas_price_estimation::eth_node::NodeGasPriceEstimator}; /// Creates a minimal RefundService for testing purposes. fn new_test_service(web3: Web3) -> RefundService { @@ -307,7 +308,7 @@ mod tests { submitter: Submitter { web3: web3.clone(), signer_address: Address::ZERO, - gas_estimator: Box::new(web3.legacy.clone()), + gas_estimator: Box::new(NodeGasPriceEstimator::new(web3.alloy.clone())), gas_parameters_of_last_tx: None, nonce_of_last_submission: None, max_gas_price: 0, diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index 742178edff..4872f1ab55 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -13,8 +13,10 @@ use { anyhow::{Context, Result}, contracts::alloy::CoWSwapEthFlow::{self, EthFlowOrder}, database::OrderUid, - gas_estimation::{GasPrice1559, GasPriceEstimating}, - shared::ethrpc::Web3, + shared::{ + ethrpc::Web3, + gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, + }, std::time::Duration, }; diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 64d958b8bc..ccd6ef8a79 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -26,7 +26,6 @@ derive_more = { workspace = true } derivative = { workspace = true } ethrpc = { workspace = true } futures = { workspace = true } -gas-estimation = { workspace = true } observe = { workspace = true } const-hex = { workspace = true } hex-literal = { workspace = true } diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index c67b495092..672b1fd30c 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -5,8 +5,8 @@ //! anomalies. use { + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, anyhow::Result, - gas_estimation::{GasPrice1559, GasPriceEstimating}, std::time::Duration, tracing::instrument, }; diff --git a/crates/shared/src/gas_price_estimation/alloy.rs b/crates/shared/src/gas_price_estimation/alloy.rs index dff89fa142..47b281858f 100644 --- a/crates/shared/src/gas_price_estimation/alloy.rs +++ b/crates/shared/src/gas_price_estimation/alloy.rs @@ -6,11 +6,12 @@ //! for the implementation details. use { + crate::gas_price_estimation::{GasPriceEstimating, u128_to_f64}, alloy::providers::Provider, anyhow::{Context, Result}, ethrpc::AlloyProvider, futures::TryFutureExt, - gas_estimation::{GasPrice1559, GasPriceEstimating}, + crate::gas_price_estimation::price::GasPrice1559, std::time::Duration, tracing::instrument, }; @@ -51,10 +52,3 @@ impl GasPriceEstimating for AlloyGasPriceEstimator { }) } } - -fn u128_to_f64(val: u128) -> Result { - if val > 2u128.pow(f64::MANTISSA_DIGITS) { - anyhow::bail!(format!("could not convert u128 to f64: {val}")); - } - Ok(val as f64) -} diff --git a/crates/shared/src/gas_price_estimation/driver.rs b/crates/shared/src/gas_price_estimation/driver.rs index 6d38349dfd..848fea2d6e 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -1,7 +1,8 @@ use { + crate::gas_price_estimation::GasPriceEstimating, alloy::primitives::U256, anyhow::{Context, Result}, - gas_estimation::{GasPrice1559, GasPriceEstimating}, + crate::gas_price_estimation::price::GasPrice1559, number::serialization::HexOrDecimalU256, reqwest::Url, serde::Deserialize, diff --git a/crates/shared/src/gas_price_estimation/eth_node.rs b/crates/shared/src/gas_price_estimation/eth_node.rs new file mode 100644 index 0000000000..eb41666341 --- /dev/null +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -0,0 +1,40 @@ +//! Ethereum node `GasPriceEstimating` implementation. + +use { + crate::gas_price_estimation::{GasPriceEstimating, u128_to_f64}, + alloy::providers::Provider, + anyhow::{Context, Result}, + ethrpc::AlloyProvider, + crate::gas_price_estimation::price::GasPrice1559, + std::time::Duration, +}; + +pub struct NodeGasPriceEstimator(AlloyProvider); + +impl NodeGasPriceEstimator { + pub fn new(provider: AlloyProvider) -> Self { + Self(provider) + } +} + +#[async_trait::async_trait] +impl GasPriceEstimating for NodeGasPriceEstimator { + async fn estimate_with_limits( + &self, + _gas_limit: f64, + _time_limit: Duration, + ) -> Result { + let legacy = self + .0 + .get_gas_price() + .await + .context("failed to get web3 gas price") + .map(u128_to_f64)??; + + Ok(GasPrice1559 { + base_fee_per_gas: 0.0, + max_fee_per_gas: legacy, + max_priority_fee_per_gas: legacy, + }) + } +} diff --git a/crates/shared/src/gas_price_estimation/fake.rs b/crates/shared/src/gas_price_estimation/fake.rs index 4ecd612dfe..c874fc08da 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -1,6 +1,7 @@ use { + crate::gas_price_estimation::GasPriceEstimating, anyhow::Result, - gas_estimation::{GasPrice1559, GasPriceEstimating}, + crate::gas_price_estimation::price::GasPrice1559, }; #[derive(Default)] diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index b23955be94..9c42789798 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -1,30 +1,51 @@ pub mod alloy; pub mod driver; +pub mod eth_node; pub mod fake; +pub mod price; +pub mod priority; use { crate::{ ethrpc::Web3, - gas_price_estimation::alloy::AlloyGasPriceEstimator, + gas_price_estimation::{ + alloy::AlloyGasPriceEstimator, + eth_node::NodeGasPriceEstimator, + priority::PriorityGasPriceEstimating, + }, http_client::HttpClientFactory, }, ::alloy::providers::Provider, anyhow::Result, - gas_estimation::{ - GasPriceEstimating, - PriorityGasPriceEstimating, - nativegasestimator::NativeGasEstimator, - }, - std::str::FromStr, + std::{str::FromStr, time::Duration}, tracing::instrument, url::Url, }; pub use {driver::DriverGasEstimator, fake::FakeGasPriceEstimator}; +pub const DEFAULT_GAS_LIMIT: f64 = 21000.0; +pub const DEFAULT_TIME_LIMIT: Duration = Duration::from_secs(30); + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait GasPriceEstimating: Send + Sync { + /// Estimate the gas price for a transaction to be mined "quickly". + async fn estimate(&self) -> Result { + self.estimate_with_limits(DEFAULT_GAS_LIMIT, DEFAULT_TIME_LIMIT) + .await + } + /// Estimate the gas price for a transaction that uses to be mined + /// within . + async fn estimate_with_limits( + &self, + gas_limit: f64, + time_limit: std::time::Duration, + ) -> Result; +} + #[derive(Clone, Debug)] pub enum GasEstimatorType { Web3, - Native, Driver(Url), Alloy, } @@ -36,7 +57,6 @@ impl FromStr for GasEstimatorType { match s.to_ascii_lowercase().as_str() { "web3" => Ok(GasEstimatorType::Web3), "alloy" => Ok(GasEstimatorType::Alloy), - "native" => Ok(GasEstimatorType::Native), _ => Url::parse(s).map(GasEstimatorType::Driver).map_err(|e| { format!("expected 'web3', 'native', or a valid driver URL; got {s:?}: {e}") }), @@ -62,16 +82,12 @@ pub async fn create_priority_estimator( url.clone(), ))); } - GasEstimatorType::Web3 => estimators.push(Box::new(web3.legacy.clone())), + GasEstimatorType::Web3 => { + estimators.push(Box::new(NodeGasPriceEstimator::new(web3.alloy.clone()))) + } GasEstimatorType::Alloy => { estimators.push(Box::new(AlloyGasPriceEstimator::new(web3.alloy.clone()))) } - GasEstimatorType::Native => { - match NativeGasEstimator::new(web3.transport().clone(), None).await { - Ok(estimator) => estimators.push(Box::new(estimator)), - Err(err) => tracing::error!("nativegasestimator failed: {}", err), - } - } } } anyhow::ensure!( @@ -80,3 +96,10 @@ pub async fn create_priority_estimator( ); Ok(PriorityGasPriceEstimating::new(estimators)) } + +fn u128_to_f64(val: u128) -> Result { + if val > 2u128.pow(f64::MANTISSA_DIGITS) { + anyhow::bail!(format!("could not convert u128 to f64: {val}")); + } + Ok(val as f64) +} diff --git a/crates/shared/src/gas_price_estimation/price.rs b/crates/shared/src/gas_price_estimation/price.rs new file mode 100644 index 0000000000..cb410bdcb7 --- /dev/null +++ b/crates/shared/src/gas_price_estimation/price.rs @@ -0,0 +1,211 @@ +// Vendored implementation of GasPrice1559 to start removing the dependency on +// the gas_estimation crate +use { + anyhow::{Result, anyhow}, + serde::Serialize, +}; + +/// EIP1559 gas price +#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Serialize)] +pub struct GasPrice1559 { + // Estimated base fee for the pending block (block currently being mined) + pub base_fee_per_gas: f64, + // Maximum gas price willing to pay for the transaction. + pub max_fee_per_gas: f64, + // Priority fee used to incentivize miners to include the tx in case of network congestion. + pub max_priority_fee_per_gas: f64, +} + +impl GasPrice1559 { + // Estimate the effective gas price based on the current network conditions + // (base_fee_per_gas) Beware that gas price for mined transaction could be + // different from estimated value in case of 1559 tx + // (because base_fee_per_gas can change between estimation and mining the tx). + pub fn effective_gas_price(&self) -> f64 { + std::cmp::min_by( + self.max_fee_per_gas, + self.max_priority_fee_per_gas + self.base_fee_per_gas, + |a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal), + ) + } + + // Validate against rules defined in https://eips.ethereum.org/EIPS/eip-1559 + // max_fee_per_gas >= max_priority_fee_per_gas + // max_fee_per_gas >= base_fee_per_gas + pub fn is_valid(&self) -> bool { + self.max_fee_per_gas >= self.max_priority_fee_per_gas + && self.max_fee_per_gas >= self.base_fee_per_gas + } + + // Validate and build Result based on the validation result + pub fn validate(self) -> Result { + match self.is_valid() { + true => Ok(self), + false => Err(anyhow!("invalid gas price values: {:?}", self)), + } + } + + // Bump gas price by factor. + pub fn bump(self, factor: f64) -> Self { + Self { + max_fee_per_gas: self.max_fee_per_gas * factor, + max_priority_fee_per_gas: self.max_priority_fee_per_gas * factor, + ..self + } + } + + // Ceil gas price (since its defined as float). + pub fn ceil(self) -> Self { + Self { + max_fee_per_gas: self.max_fee_per_gas.ceil(), + max_priority_fee_per_gas: self.max_priority_fee_per_gas.ceil(), + ..self + } + } + + // If current cap if higher then the input, set to input. + pub fn limit_cap(self, cap: f64) -> Self { + Self { + max_fee_per_gas: self.max_fee_per_gas.min(cap), + max_priority_fee_per_gas: self + .max_priority_fee_per_gas + .min(self.max_fee_per_gas.min(cap)), /* enforce max_priority_fee_per_gas <= + * max_fee_per_gas */ + ..self + } + } +} + +impl std::fmt::Display for GasPrice1559 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let format_unit = |wei| { + let gwei: f64 = wei / 1e9; + if gwei >= 1.0 { + format!("{:.2} Gwei", gwei) + } else { + format!("{wei} wei") + } + }; + write!( + f, + "{{ max_fee: {}, max_priority_fee: {}, base_fee: {} }}", + format_unit(self.max_fee_per_gas), + format_unit(self.max_priority_fee_per_gas), + format_unit(self.base_fee_per_gas), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::gas_price_estimation::price::GasPrice1559; + + // Copied from the source: https://github.com/ashleygwilliams/assert_approx_eq/blob/master/src/lib.rs + // should be removed as we move away from expressing gas in f64 + macro_rules! assert_approx_eq { + ($a:expr, $b:expr) => {{ + let eps = 1.0e-6; + let (a, b) = (&$a, &$b); + assert!( + (*a - *b).abs() < eps, + "assertion failed: `(left !== right)` (left: `{:?}`, right: `{:?}`, expect diff: \ + `{:?}`, real diff: `{:?}`)", + *a, + *b, + eps, + (*a - *b).abs() + ); + }}; + } + + #[test] + fn bump_and_ceil() { + let gas_price = GasPrice1559 { + max_fee_per_gas: 2.0, + max_priority_fee_per_gas: 3.0, + ..Default::default() + }; + + let gas_price_bumped = GasPrice1559 { + max_fee_per_gas: 2.25, + max_priority_fee_per_gas: 3.375, + ..Default::default() + }; + + let gas_price_bumped_and_ceiled = GasPrice1559 { + max_fee_per_gas: 3.0, + max_priority_fee_per_gas: 4.0, + ..Default::default() + }; + + assert_eq!(gas_price.bump(1.125), gas_price_bumped); + assert_eq!(gas_price.bump(1.125).ceil(), gas_price_bumped_and_ceiled); + } + + #[test] + fn limit_cap_only_max_fee_capped() { + let gas_price = GasPrice1559 { + max_fee_per_gas: 5.0, + max_priority_fee_per_gas: 3.0, + ..Default::default() + }; + + let gas_price_capped = GasPrice1559 { + max_fee_per_gas: 4.0, + max_priority_fee_per_gas: 3.0, + ..Default::default() + }; + + assert_eq!(gas_price.limit_cap(4.0), gas_price_capped); + } + + #[test] + fn limit_cap_max_fee_and_max_priority_capped() { + let gas_price = GasPrice1559 { + max_fee_per_gas: 5.0, + max_priority_fee_per_gas: 3.0, + ..Default::default() + }; + + let gas_price_capped = GasPrice1559 { + max_fee_per_gas: 2.0, + max_priority_fee_per_gas: 2.0, + ..Default::default() + }; + + assert_eq!(gas_price.limit_cap(2.0), gas_price_capped); + } + + #[test] + fn estimate_eip1559() { + assert_approx_eq!( + GasPrice1559 { + max_fee_per_gas: 10.0, + max_priority_fee_per_gas: 5.0, + base_fee_per_gas: 2.0 + } + .effective_gas_price(), + 7.0 + ); + + assert_approx_eq!( + GasPrice1559 { + max_fee_per_gas: 10.0, + max_priority_fee_per_gas: 8.0, + base_fee_per_gas: 2.0 + } + .effective_gas_price(), + 10.0 + ); + + assert_approx_eq!( + GasPrice1559 { + max_fee_per_gas: 10.0, + max_priority_fee_per_gas: 10.0, + base_fee_per_gas: 2.0 + } + .effective_gas_price(), + 10.0 + ); + } +} diff --git a/crates/shared/src/gas_price_estimation/priority.rs b/crates/shared/src/gas_price_estimation/priority.rs new file mode 100644 index 0000000000..507fe7a192 --- /dev/null +++ b/crates/shared/src/gas_price_estimation/priority.rs @@ -0,0 +1,170 @@ +use { + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, + anyhow::{Result, anyhow}, + std::{ + future::Future, + sync::atomic::{AtomicUsize, Ordering}, + time::Duration, + }, +}; + +// Errors of an individual estimator are logged as warnings until it has failed +// this many times in a row at which point they are logged as errors. +// This is useful to reduce alerts for estimators that sometimes fail individual +// requests while still getting them when the estimator really goes down. +const LOG_ERROR_AFTER_N_ERRORS: usize = 10; + +// Uses the first successful estimator. +pub struct PriorityGasPriceEstimating { + estimators: Vec, +} + +struct Estimator { + estimator: Box, + errors_in_a_row: AtomicUsize, +} + +impl PriorityGasPriceEstimating { + pub fn new(estimators: Vec>) -> Self { + let estimators = estimators + .into_iter() + .map(|estimator| Estimator { + estimator, + errors_in_a_row: AtomicUsize::new(0), + }) + .collect(); + Self { estimators } + } + + async fn prioritize<'a, T, F>(&'a self, operation: T) -> Result + where + T: Fn(&'a dyn GasPriceEstimating) -> F, + F: Future>, + { + for (i, estimator) in self.estimators.iter().enumerate() { + match operation(estimator.estimator.as_ref()).await { + Ok(result) => { + estimator.errors_in_a_row.store(0, Ordering::SeqCst); + return Ok(result); + } + Err(err) => { + let num_errors = estimator.errors_in_a_row.fetch_add(1, Ordering::SeqCst) + 1; + if num_errors < LOG_ERROR_AFTER_N_ERRORS { + tracing::warn!("gas estimator {} failed: {:?}", i, err); + } else { + tracing::error!("gas estimator {} failed: {:?}", i, err); + } + } + } + } + Err(anyhow!("all gas estimators failed")) + } +} + +#[async_trait::async_trait] +impl GasPriceEstimating for PriorityGasPriceEstimating { + async fn estimate_with_limits( + &self, + gas_limit: f64, + time_limit: Duration, + ) -> Result { + self.prioritize(|estimator| estimator.estimate_with_limits(gas_limit, time_limit)) + .await + } + + async fn estimate(&self) -> Result { + self.prioritize(|estimator| estimator.estimate()).await + } +} + +#[cfg(test)] +mod tests { + use { + crate::gas_price_estimation::{ + GasPriceEstimating, + MockGasPriceEstimating, + price::GasPrice1559, + priority::PriorityGasPriceEstimating, + }, + anyhow::anyhow, + futures::future::FutureExt, + }; + + // Copied from the source: https://github.com/ashleygwilliams/assert_approx_eq/blob/master/src/lib.rs + // should be removed as we move away from expressing gas in f64 + macro_rules! assert_approx_eq { + ($a:expr, $b:expr) => {{ + let eps = 1.0e-6; + let (a, b) = (&$a, &$b); + assert!( + (*a - *b).abs() < eps, + "assertion failed: `(left !== right)` (left: `{:?}`, right: `{:?}`, expect diff: \ + `{:?}`, real diff: `{:?}`)", + *a, + *b, + eps, + (*a - *b).abs() + ); + }}; + } + + #[test] + fn prioritize_picks_first_if_first_succeeds() { + let mut estimator_0 = MockGasPriceEstimating::new(); + let estimator_1 = MockGasPriceEstimating::new(); + + estimator_0.expect_estimate().times(1).returning(|| { + Ok(GasPrice1559 { + base_fee_per_gas: 1.0, + ..Default::default() + }) + }); + + let priority = + PriorityGasPriceEstimating::new(vec![Box::new(estimator_0), Box::new(estimator_1)]); + // estimator_1 has no expectation so would panic if called + priority.estimate().now_or_never().unwrap().unwrap(); + } + + #[test] + fn prioritize_picks_second_if_first_fails() { + let mut estimator_0 = MockGasPriceEstimating::new(); + let mut estimator_1 = MockGasPriceEstimating::new(); + + estimator_0 + .expect_estimate() + .times(1) + .returning(|| Err(anyhow!(""))); + estimator_1.expect_estimate().times(1).returning(|| { + Ok(GasPrice1559 { + base_fee_per_gas: 2.0, + ..Default::default() + }) + }); + + let priority = + PriorityGasPriceEstimating::new(vec![Box::new(estimator_0), Box::new(estimator_1)]); + let result = priority.estimate().now_or_never().unwrap().unwrap(); + assert_approx_eq!(result.base_fee_per_gas, 2.0); + } + + #[test] + fn prioritize_fails_if_all_fail() { + let mut estimator_0 = MockGasPriceEstimating::new(); + let mut estimator_1 = MockGasPriceEstimating::new(); + + estimator_0 + .expect_estimate() + .times(1) + .returning(|| Err(anyhow!(""))); + estimator_1 + .expect_estimate() + .times(1) + .returning(|| Err(anyhow!(""))); + + let priority = + PriorityGasPriceEstimating::new(vec![Box::new(estimator_0), Box::new(estimator_1)]); + let result = priority.estimate().now_or_never().unwrap(); + assert!(result.is_err()); + } +} diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 44e9a3020f..0500701f5a 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -9,6 +9,7 @@ use { account_balances::{BalanceFetching, Query}, db_order_conversions::order_kind_from, fee::FeeParameters, + gas_price_estimation::GasPriceEstimating, order_validation::PreOrderData, price_estimation::{Estimate, QuoteVerificationMode, Verification}, trade_finding::external::dto, @@ -18,7 +19,6 @@ use { chrono::{DateTime, Duration, Utc}, database::quotes::{Quote as QuoteRow, QuoteKind}, futures::TryFutureExt, - gas_estimation::GasPriceEstimating, model::{ interaction::InteractionData, order::{OrderClass, OrderKind}, @@ -785,7 +785,7 @@ mod tests { super::*, crate::{ account_balances::MockBalanceFetching, - gas_price_estimation::FakeGasPriceEstimator, + gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, price_estimation::{ HEALTHY_PRICE_ESTIMATION_TIME, MockPriceEstimating, @@ -796,7 +796,6 @@ mod tests { U256 as AlloyU256, chrono::Utc, futures::FutureExt, - gas_estimation::GasPrice1559, mockall::{Sequence, predicate::eq}, model::time, number::nonzero::NonZeroU256, diff --git a/crates/shared/src/price_estimation/competition/mod.rs b/crates/shared/src/price_estimation/competition/mod.rs index 8a692bff13..90361dd153 100644 --- a/crates/shared/src/price_estimation/competition/mod.rs +++ b/crates/shared/src/price_estimation/competition/mod.rs @@ -1,11 +1,10 @@ use { super::{QuoteVerificationMode, native::NativePriceEstimating}, - crate::price_estimation::PriceEstimationError, + crate::{gas_price_estimation::GasPriceEstimating, price_estimation::PriceEstimationError}, futures::{ future::{BoxFuture, FutureExt}, stream::{FuturesUnordered, StreamExt}, }, - gas_estimation::GasPriceEstimating, model::order::OrderKind, std::{ cmp::Ordering, diff --git a/crates/shared/src/price_estimation/competition/quote.rs b/crates/shared/src/price_estimation/competition/quote.rs index 30a168950d..6f097c347f 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -164,7 +164,7 @@ mod tests { }, }, alloy::primitives::U256, - gas_estimation::GasPrice1559, + crate::gas_price_estimation::price::GasPrice1559, model::order::OrderKind, }; diff --git a/crates/shared/src/price_estimation/factory.rs b/crates/shared/src/price_estimation/factory.rs index 37efcaf17e..0893f4cd68 100644 --- a/crates/shared/src/price_estimation/factory.rs +++ b/crates/shared/src/price_estimation/factory.rs @@ -17,6 +17,7 @@ use { baseline_solver::BaseTokens, code_fetching::CachedCodeFetcher, ethrpc::Web3, + gas_price_estimation::GasPriceEstimating, http_client::HttpClientFactory, price_estimation::{ ExternalSolver, @@ -31,7 +32,6 @@ use { anyhow::{Context as _, Result}, contracts::alloy::WETH9, ethrpc::block_stream::CurrentBlockWatcher, - gas_estimation::GasPriceEstimating, number::nonzero::NonZeroU256, rate_limit::RateLimiter, reqwest::Url, From 5e7e7a208b3fefb5962768dfa2ffb277db723eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 13 Jan 2026 14:28:54 +0000 Subject: [PATCH 03/39] remove unused fn from trait --- crates/shared/src/gas_price.rs | 12 ------------ .../shared/src/gas_price_estimation/alloy.rs | 10 ++-------- .../shared/src/gas_price_estimation/driver.rs | 19 +++++-------------- .../src/gas_price_estimation/eth_node.rs | 10 ++-------- .../shared/src/gas_price_estimation/fake.rs | 5 ++--- crates/shared/src/gas_price_estimation/mod.rs | 12 +----------- .../src/gas_price_estimation/priority.rs | 10 ---------- .../src/price_estimation/competition/quote.rs | 3 +-- 8 files changed, 13 insertions(+), 68 deletions(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 672b1fd30c..a3c8fa1d9b 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -7,7 +7,6 @@ use { crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, anyhow::Result, - std::time::Duration, tracing::instrument, }; @@ -34,17 +33,6 @@ impl GasPriceEstimating for InstrumentedGasEstimator where T: GasPriceEstimating, { - #[instrument(skip_all)] - async fn estimate_with_limits( - &self, - gas_limit: f64, - time_limit: Duration, - ) -> Result { - // Instrumenting gas estimates with limits is hard. Since we don't use - // it in the orderbook, lets leave this out for now. - self.inner.estimate_with_limits(gas_limit, time_limit).await - } - #[instrument(skip_all)] async fn estimate(&self) -> Result { let estimate = self.inner.estimate().await?; diff --git a/crates/shared/src/gas_price_estimation/alloy.rs b/crates/shared/src/gas_price_estimation/alloy.rs index 47b281858f..a1215bd0a1 100644 --- a/crates/shared/src/gas_price_estimation/alloy.rs +++ b/crates/shared/src/gas_price_estimation/alloy.rs @@ -6,13 +6,11 @@ //! for the implementation details. use { - crate::gas_price_estimation::{GasPriceEstimating, u128_to_f64}, + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, alloy::providers::Provider, anyhow::{Context, Result}, ethrpc::AlloyProvider, futures::TryFutureExt, - crate::gas_price_estimation::price::GasPrice1559, - std::time::Duration, tracing::instrument, }; @@ -27,11 +25,7 @@ impl AlloyGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for AlloyGasPriceEstimator { #[instrument(skip(self))] - async fn estimate_with_limits( - &self, - _gas_limit: f64, - _time_limit: Duration, - ) -> Result { + async fn estimate(&self) -> Result { let fees = self .0 .estimate_eip1559_fees() diff --git a/crates/shared/src/gas_price_estimation/driver.rs b/crates/shared/src/gas_price_estimation/driver.rs index 848fea2d6e..8c3b98f85b 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -1,8 +1,7 @@ use { - crate::gas_price_estimation::GasPriceEstimating, + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, alloy::primitives::U256, anyhow::{Context, Result}, - crate::gas_price_estimation::price::GasPrice1559, number::serialization::HexOrDecimalU256, reqwest::Url, serde::Deserialize, @@ -74,7 +73,11 @@ impl DriverGasEstimator { max_priority_fee_per_gas: f64::from(response.max_priority_fee_per_gas), }) } +} +#[async_trait::async_trait] +impl GasPriceEstimating for DriverGasEstimator { + #[instrument(skip(self))] async fn estimate(&self) -> Result { // Lock cache for entire duration of this method to prevent concurrent network // requests @@ -96,15 +99,3 @@ impl DriverGasEstimator { Ok(price) } } - -#[async_trait::async_trait] -impl GasPriceEstimating for DriverGasEstimator { - #[instrument(skip(self))] - async fn estimate_with_limits( - &self, - _gas_limit: f64, - _time_limit: Duration, - ) -> Result { - self.estimate().await - } -} diff --git a/crates/shared/src/gas_price_estimation/eth_node.rs b/crates/shared/src/gas_price_estimation/eth_node.rs index eb41666341..e202a79a1d 100644 --- a/crates/shared/src/gas_price_estimation/eth_node.rs +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -1,12 +1,10 @@ //! Ethereum node `GasPriceEstimating` implementation. use { - crate::gas_price_estimation::{GasPriceEstimating, u128_to_f64}, + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, alloy::providers::Provider, anyhow::{Context, Result}, ethrpc::AlloyProvider, - crate::gas_price_estimation::price::GasPrice1559, - std::time::Duration, }; pub struct NodeGasPriceEstimator(AlloyProvider); @@ -19,11 +17,7 @@ impl NodeGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for NodeGasPriceEstimator { - async fn estimate_with_limits( - &self, - _gas_limit: f64, - _time_limit: Duration, - ) -> Result { + async fn estimate(&self) -> Result { let legacy = self .0 .get_gas_price() diff --git a/crates/shared/src/gas_price_estimation/fake.rs b/crates/shared/src/gas_price_estimation/fake.rs index c874fc08da..19f8e56de9 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -1,7 +1,6 @@ use { - crate::gas_price_estimation::GasPriceEstimating, + crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, anyhow::Result, - crate::gas_price_estimation::price::GasPrice1559, }; #[derive(Default)] @@ -15,7 +14,7 @@ impl FakeGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for FakeGasPriceEstimator { - async fn estimate_with_limits(&self, _: f64, _: std::time::Duration) -> Result { + async fn estimate(&self) -> Result { Ok(self.0) } } diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 9c42789798..c457f528f5 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -30,17 +30,7 @@ pub const DEFAULT_TIME_LIMIT: Duration = Duration::from_secs(30); #[async_trait::async_trait] pub trait GasPriceEstimating: Send + Sync { /// Estimate the gas price for a transaction to be mined "quickly". - async fn estimate(&self) -> Result { - self.estimate_with_limits(DEFAULT_GAS_LIMIT, DEFAULT_TIME_LIMIT) - .await - } - /// Estimate the gas price for a transaction that uses to be mined - /// within . - async fn estimate_with_limits( - &self, - gas_limit: f64, - time_limit: std::time::Duration, - ) -> Result; + async fn estimate(&self) -> Result; } #[derive(Clone, Debug)] diff --git a/crates/shared/src/gas_price_estimation/priority.rs b/crates/shared/src/gas_price_estimation/priority.rs index 507fe7a192..efa1ab3e01 100644 --- a/crates/shared/src/gas_price_estimation/priority.rs +++ b/crates/shared/src/gas_price_estimation/priority.rs @@ -4,7 +4,6 @@ use { std::{ future::Future, sync::atomic::{AtomicUsize, Ordering}, - time::Duration, }, }; @@ -63,15 +62,6 @@ impl PriorityGasPriceEstimating { #[async_trait::async_trait] impl GasPriceEstimating for PriorityGasPriceEstimating { - async fn estimate_with_limits( - &self, - gas_limit: f64, - time_limit: Duration, - ) -> Result { - self.prioritize(|estimator| estimator.estimate_with_limits(gas_limit, time_limit)) - .await - } - async fn estimate(&self) -> Result { self.prioritize(|estimator| estimator.estimate()).await } diff --git a/crates/shared/src/price_estimation/competition/quote.rs b/crates/shared/src/price_estimation/competition/quote.rs index 6f097c347f..60ecaa2ff2 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -156,7 +156,7 @@ mod tests { use { super::*, crate::{ - gas_price_estimation::FakeGasPriceEstimator, + gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, price_estimation::{ MockPriceEstimating, QuoteVerificationMode, @@ -164,7 +164,6 @@ mod tests { }, }, alloy::primitives::U256, - crate::gas_price_estimation::price::GasPrice1559, model::order::OrderKind, }; From ea800d9d40048653b29c18c949a0fc5e3b3b2019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 13 Jan 2026 16:44:20 +0000 Subject: [PATCH 04/39] address gemini comment --- crates/shared/src/gas_price_estimation/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index c457f528f5..7174b1fc16 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -48,7 +48,7 @@ impl FromStr for GasEstimatorType { "web3" => Ok(GasEstimatorType::Web3), "alloy" => Ok(GasEstimatorType::Alloy), _ => Url::parse(s).map(GasEstimatorType::Driver).map_err(|e| { - format!("expected 'web3', 'native', or a valid driver URL; got {s:?}: {e}") + format!("expected 'web3', 'alloy', or a valid driver URL; got {s:?}: {e}") }), } } From 1e2e165829f063034f508c7ed6836cf6ae326cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 13 Jan 2026 16:46:00 +0000 Subject: [PATCH 05/39] compilation error --- crates/refunder/src/refund_service.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/refunder/src/refund_service.rs b/crates/refunder/src/refund_service.rs index cba146a1ba..f5bc1e28d2 100644 --- a/crates/refunder/src/refund_service.rs +++ b/crates/refunder/src/refund_service.rs @@ -309,7 +309,11 @@ impl RefundService { #[cfg(test)] mod tests { - use {super::*, shared::gas_price_estimation::eth_node::NodeGasPriceEstimator}; + use { + super::*, + alloy::primitives::address, + shared::gas_price_estimation::eth_node::NodeGasPriceEstimator, + }; /// Creates a minimal RefundService for testing purposes. fn new_test_service(web3: Web3) -> RefundService { From 93235571d802e5bf6e14d486f966247cd783c298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 14 Jan 2026 11:19:16 +0000 Subject: [PATCH 06/39] wip --- .../driver/src/domain/competition/auction.rs | 12 +- .../domain/competition/solution/settlement.rs | 7 +- crates/driver/src/domain/eth/gas.rs | 25 ++-- crates/driver/src/domain/mempools.rs | 15 ++- .../driver/src/infra/api/routes/gasprice.rs | 21 +--- crates/driver/src/infra/blockchain/gas.rs | 22 ++-- crates/driver/src/infra/blockchain/mod.rs | 15 +-- crates/driver/src/tests/setup/driver.rs | 3 + crates/driver/src/tests/setup/solver.rs | 21 ++-- crates/ethrpc/src/block_stream/mod.rs | 17 +-- crates/refunder/src/submitter.rs | 111 ++++++++++-------- crates/shared/src/gas_price.rs | 15 ++- .../shared/src/gas_price_estimation/alloy.rs | 22 ++-- .../shared/src/gas_price_estimation/driver.rs | 28 ++--- .../src/gas_price_estimation/eth_node.rs | 12 +- .../shared/src/gas_price_estimation/fake.rs | 19 ++- crates/shared/src/gas_price_estimation/mod.rs | 22 +++- .../shared/src/gas_price_estimation/price.rs | 22 +++- .../src/gas_price_estimation/priority.rs | 43 ++----- crates/shared/src/order_quoting.rs | 60 +++++----- .../src/price_estimation/competition/quote.rs | 22 ++-- 21 files changed, 283 insertions(+), 251 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index c91779d029..a328bafd55 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -2,12 +2,13 @@ use { crate::{ domain::{ competition::{self}, - eth, + eth::{self, GasPrice}, liquidity, time, }, infra::{Ethereum, blockchain, solver::Timeouts}, }, + alloy::primitives::U256, std::collections::{HashMap, HashSet}, thiserror::Error, }; @@ -56,11 +57,18 @@ impl Auction { true }); + let gas_est = eth.gas_price().await?; + let gas_price = GasPrice::new( + U256::from(gas_est.max_fee_per_gas).into(), + U256::from(gas_est.max_priority_fee_per_gas).into(), + None, + ); + Ok(Self { id, orders, tokens, - gas_price: eth.gas_price().await?, + gas_price, deadline, surplus_capturing_jit_order_owners, }) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 26a4327fb5..8bf6e3d93f 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -12,6 +12,7 @@ use { }, infra::{Simulator, blockchain::Ethereum, observe, solver::ManageNativeToken}, }, + alloy::primitives::U256, futures::future::try_join_all, std::collections::{BTreeSet, HashMap, HashSet}, tracing::instrument, @@ -174,7 +175,7 @@ impl Settlement { // Ensure that the solver has sufficient balance for the settlement to be mined // even if the gas price keeps climbing during the tx submission. - let required_eth_balance = gas.required_balance(price * 2.); + let required_eth_balance = gas.required_balance(U256::from(price.max_fee_per_gas * 2)); if eth.balance(solution.solver().address()).await? < required_eth_balance { return Err(Error::SolverAccountInsufficientBalance( required_eth_balance, @@ -398,7 +399,7 @@ impl Gas { /// The balance required to ensure settlement execution with the given gas /// parameters. - pub fn required_balance(&self, price: eth::GasPrice) -> eth::Ether { - self.limit * price.max() + pub fn required_balance(&self, max_fee_per_gas: U256) -> eth::Ether { + self.limit * max_fee_per_gas.into() } } diff --git a/crates/driver/src/domain/eth/gas.rs b/crates/driver/src/domain/eth/gas.rs index 1f93bf672e..5ac3400746 100644 --- a/crates/driver/src/domain/eth/gas.rs +++ b/crates/driver/src/domain/eth/gas.rs @@ -1,7 +1,8 @@ use { super::{Ether, U256}, + alloy::eips::eip1559::calc_effective_gas_price, derive_more::{Display, From, Into}, - std::{ops, ops::Add}, + std::ops::{self, Add}, }; /// Gas amount in gas units. @@ -37,16 +38,22 @@ pub struct GasPrice { tip: FeePerGas, /// The current base gas price that will be charged to all accounts on the /// next block. - base: FeePerGas, + base: Option, } impl GasPrice { /// Returns the estimated [`EffectiveGasPrice`] for the gas price estimate. pub fn effective(&self) -> EffectiveGasPrice { - let max = self.max.0.0; - let base = self.base.0.0; - let tip = self.tip.0.0; - max.min(base.saturating_add(tip)).into() + U256::from(calc_effective_gas_price( + u128::try_from(self.max.0.0).unwrap(), + u128::try_from(self.tip.0.0).unwrap(), + self.base, + )) + .into() + // let max = self.max.0.0; + // let base = self.base.0.0; + // let tip = self.tip.0.0; + // max.min(base.saturating_add(tip)).into() } pub fn max(&self) -> FeePerGas { @@ -57,11 +64,11 @@ impl GasPrice { self.tip } - pub fn base(&self) -> FeePerGas { + pub fn base(&self) -> Option { self.base } - pub fn new(max: FeePerGas, tip: FeePerGas, base: FeePerGas) -> Self { + pub fn new(max: FeePerGas, tip: FeePerGas, base: Option) -> Self { Self { max, tip, base } } } @@ -86,7 +93,7 @@ impl From for GasPrice { Self { max: value.into(), tip: value.into(), - base: value.into(), + base: u64::try_from(value).ok(), } } } diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index 40a32b9dc5..9d346987bf 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -4,11 +4,11 @@ use { domain::{ BlockNo, competition::solution::Settlement, - eth::{TxId, TxStatus}, + eth::{GasPrice, TxId, TxStatus}, }, infra::{self, Ethereum, observe, solver::Solver}, }, - alloy::consensus::Transaction, + alloy::{consensus::Transaction, primitives::U256}, anyhow::Context, ethrpc::block_stream::into_stream, futures::{FutureExt, StreamExt, future::select_ok}, @@ -146,11 +146,16 @@ impl Mempools { .await; let final_gas_price = match &replacement_gas_price { Ok(Some(replacement_gas_price)) - if replacement_gas_price.max() > current_gas_price.max() => + if replacement_gas_price.max() + > U256::from(current_gas_price.max_fee_per_gas).into() => { *replacement_gas_price } - _ => current_gas_price, + _ => GasPrice::new( + U256::from(current_gas_price.max_fee_per_gas).into(), + U256::from(current_gas_price.max_priority_fee_per_gas).into(), + self.ethereum.current_block().borrow().base_fee, + ), }; tracing::debug!( @@ -336,7 +341,7 @@ impl Mempools { ) })?) .into(), - eth::U256::from(pending_tx.max_fee_per_gas()).into(), + None, ); // in order to replace a tx we need to increase the price Ok(Some(pending_tx_gas_price * GAS_PRICE_BUMP)) diff --git a/crates/driver/src/infra/api/routes/gasprice.rs b/crates/driver/src/infra/api/routes/gasprice.rs index 2c66f40d7a..cb861bce0a 100644 --- a/crates/driver/src/infra/api/routes/gasprice.rs +++ b/crates/driver/src/infra/api/routes/gasprice.rs @@ -1,12 +1,7 @@ use { - crate::{ - domain::eth, - infra::{Ethereum, api::error::Error}, - util::serialize, - }, + crate::infra::{Ethereum, api::error::Error}, axum::Json, serde::{Deserialize, Serialize}, - serde_with::serde_as, tracing::instrument, }; @@ -15,16 +10,11 @@ pub(in crate::infra::api) fn gasprice(app: axum::Router) -> axum::Rout } /// Gas price components in EIP-1559 format. -#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GasPriceResponse { - #[serde_as(as = "serialize::U256")] - pub max_fee_per_gas: eth::U256, - #[serde_as(as = "serialize::U256")] - pub max_priority_fee_per_gas: eth::U256, - #[serde_as(as = "serialize::U256")] - pub base_fee_per_gas: eth::U256, + pub max_fee_per_gas: u128, + pub max_priority_fee_per_gas: u128, } #[instrument(skip(eth))] @@ -35,8 +25,7 @@ async fn route( let gas_price = eth.gas_price().await?; Ok(Json(GasPriceResponse { - max_fee_per_gas: gas_price.max().0.0, - max_priority_fee_per_gas: gas_price.tip().0.0, - base_fee_per_gas: gas_price.base().0.0, + max_fee_per_gas: gas_price.max_fee_per_gas, + max_priority_fee_per_gas: gas_price.max_priority_fee_per_gas, })) } diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index cc933effe3..313da0f246 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -8,8 +8,8 @@ use { domain::eth, infra::{config::file::GasEstimatorType, mempool}, }, + alloy::{eips::eip1559::Eip1559Estimation, primitives::U256}, ethrpc::Web3, - shared::gas_price_estimation::{ GasPriceEstimating, alloy::AlloyGasPriceEstimator, @@ -76,20 +76,21 @@ impl GasPriceEstimator { /// If additional tip is configured, it will be added to the gas price. This /// is to increase the chance of a transaction being included in a block, in /// case private submission networks are used. - pub async fn estimate(&self) -> Result { + pub async fn estimate(&self) -> Result { let estimate = self.gas.estimate().await.map_err(Error::GasPrice)?; let max_priority_fee_per_gas = { // the driver supports tweaking the tx gas price tip in case the gas // price estimator is systematically too low => compute configured tip bump let (max_additional_tip, tip_percentage_increase) = self.additional_tip; - let additional_tip = f64::from(max_additional_tip) - .min(estimate.max_priority_fee_per_gas * tip_percentage_increase); + let additional_tip = (max_additional_tip).min(U256::from( + (estimate.max_priority_fee_per_gas as f64) * tip_percentage_increase, + )); // make sure we tip at least some configurable minimum amount std::cmp::max( self.min_priority_fee, - eth::U256::from(estimate.max_priority_fee_per_gas + additional_tip), + eth::U256::from(estimate.max_priority_fee_per_gas) + additional_tip, ) }; @@ -105,10 +106,11 @@ impl GasPriceEstimator { ))); } - Ok(eth::GasPrice::new( - suggested_max_fee_per_gas.into(), - max_priority_fee_per_gas.into(), - eth::U256::from(estimate.base_fee_per_gas).into(), - )) + Ok(Eip1559Estimation { + max_fee_per_gas: u128::try_from(suggested_max_fee_per_gas) + .map_err(|err| Error::GasPrice(err.into()))?, + max_priority_fee_per_gas: u128::try_from(max_priority_fee_per_gas) + .map_err(|err| Error::GasPrice(err.into()))?, + }) } } diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index 0a0595ae4e..b93dbe307b 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -4,6 +4,7 @@ use { domain::{eth, eth::U256}, }, alloy::{ + eips::eip1559::Eip1559Estimation, network::TransactionBuilder, providers::Provider, rpc::types::{TransactionReceipt, TransactionRequest}, @@ -15,6 +16,7 @@ use { ethrpc::{Web3, block_stream::CurrentBlockWatcher}, shared::{ account_balances::{BalanceSimulator, SimulationError}, + gas_price_estimation::Eip1559EstimationExt, price_estimation::trade_verifier::balance_overrides::{ BalanceOverrides, BalanceOverriding, @@ -236,7 +238,7 @@ impl Ethereum { /// The gas price is determined based on the deadline by which the /// transaction must be included on-chain. A shorter deadline requires a /// higher gas price to increase the likelihood of timely inclusion. - pub async fn gas_price(&self) -> Result { + pub async fn gas_price(&self) -> Result { self.inner.gas.estimate().await } @@ -291,6 +293,7 @@ impl Ethereum { #[instrument(skip(self), ret(level = Level::DEBUG))] pub(super) async fn simulation_gas_price(&self) -> Option { + let base_fee = self.current_block().borrow().base_fee; // Some nodes don't pick a reasonable default value when you don't specify a gas // price and default to 0. Additionally some sneaky tokens have special code // paths that detect that case to try to behave differently during simulations @@ -298,15 +301,7 @@ impl Ethereum { // default value we estimate the current gas price upfront. But because it's // extremely rare that tokens behave that way we are fine with falling back to // the node specific fallback value instead of failing the whole call. - let gas_price = self.inner.gas.estimate().await.ok()?.effective().0.0; - u128::try_from(gas_price) - .inspect_err(|err| { - tracing::debug!( - ?err, - "failed to convert gas estimate to u128, returning None" - ); - }) - .ok() + Some(self.inner.gas.estimate().await.ok()?.effective(None)) } pub fn web3(&self) -> &Web3 { diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index bdd69f6fa7..5730040250 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -221,6 +221,9 @@ async fn create_config_file( .unwrap(); writeln!(file, "flashloans-enabled = true").unwrap(); writeln!(file, "tx-gas-limit = \"45000000\"").unwrap(); + // Use Web3 gas estimator for tests to match the mock solver's estimator + // and avoid mismatches in effective gas price calculations. + writeln!(file, "gas-estimator = {{ estimator = \"web3\" }}").unwrap(); write!( file, r#"[contracts] diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index a3b3cb38c1..45f1072903 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -449,7 +449,11 @@ impl Solver { let gas = Arc::new( infra::blockchain::GasPriceEstimator::new( rpc.web3(), - &Default::default(), + // NOTE: Using Web3 estimator for tests to avoid timing-dependent gas price + // variations. The Alloy estimator analyzes the last 10 blocks, which changes + // as blocks are mined during tests, causing mismatches between auction + // creation time and solver request validation time. + &infra::config::file::GasEstimatorType::Web3, &[infra::mempool::Config { min_priority_fee: Default::default(), gas_price_cap: eth::U256::MAX, @@ -495,14 +499,13 @@ impl Solver { axum::routing::post( move |axum::extract::State(state): axum::extract::State, axum::extract::Json(req): axum::extract::Json| async move { - let effective_gas_price = eth - .gas_price() - .await - .unwrap() - .effective() - .0 - .0 - .to_string(); + // Extract effectiveGasPrice from request instead of recalculating. + // Gas prices from the network can change between auction creation + // and solver request validation, causing mismatches in tests. + let effective_gas_price = req + .get("effectiveGasPrice") + .expect("effectiveGasPrice missing") + .clone(); let expected = json!({ "id": (!config.quote).then_some("1"), "tokens": tokens_json, diff --git a/crates/ethrpc/src/block_stream/mod.rs b/crates/ethrpc/src/block_stream/mod.rs index f746ad7fe4..45a5456df7 100644 --- a/crates/ethrpc/src/block_stream/mod.rs +++ b/crates/ethrpc/src/block_stream/mod.rs @@ -56,6 +56,7 @@ pub struct BlockInfo { pub timestamp: u64, pub gas_limit: U256, pub gas_price: U256, + pub base_fee: Option, /// When the system noticed the new block. pub observed_at: Instant, } @@ -69,6 +70,7 @@ impl Default for BlockInfo { timestamp: Default::default(), gas_limit: Default::default(), gas_price: Default::default(), + base_fee: Default::default(), observed_at: Instant::now(), } } @@ -89,19 +91,7 @@ impl TryFrom for BlockInfo { type Error = anyhow::Error; fn try_from(value: Block) -> std::result::Result { - Ok(Self { - number: value.header.number, - hash: value.header.hash, - parent_hash: value.header.parent_hash, - timestamp: value.header.timestamp, - gas_limit: U256::from(value.header.gas_limit), - gas_price: value - .header - .base_fee_per_gas - .map(U256::from) - .context("no gas price")?, - observed_at: Instant::now(), - }) + value.header.try_into() } } @@ -119,6 +109,7 @@ impl TryFrom for BlockInfo { .base_fee_per_gas .map(U256::from) .context("no gas price")?, + base_fee: value.base_fee_per_gas, observed_at: Instant::now(), }) } diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index 4872f1ab55..1282ad657f 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -9,14 +9,11 @@ // In the re-newed attempt for submission the same nonce is used as before. use { - alloy::{primitives::Address, providers::Provider}, + alloy::{eips::eip1559::Eip1559Estimation, primitives::Address, providers::Provider}, anyhow::{Context, Result}, contracts::alloy::CoWSwapEthFlow::{self, EthFlowOrder}, database::OrderUid, - shared::{ - ethrpc::Web3, - gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, - }, + shared::{ethrpc::Web3, gas_price_estimation::GasPriceEstimating}, std::time::Duration, }; @@ -24,22 +21,17 @@ use { // send out EIP1559 txs. // Example: If the prevailing gas is 10Gwei and the buffer factor is 1.20 // then the gas_price used will be 12. -const GAS_PRICE_BUFFER_FACTOR: f64 = 1.3; +const GAS_PRICE_BUFFER_PCT: u64 = 30; // In order to resubmit a new tx with the same nonce, the gas tip and // max_fee_per_gas needs to be increased by at least 10 percent. -const GAS_PRICE_BUMP: f64 = 1.125; - -/// Type safe cast to avoid unexpected issues due to type changes. -const fn f64_to_u128(n: f64) -> u128 { - n as u128 -} +const GAS_PRICE_BUMP_PML: u64 = 125; pub struct Submitter { pub web3: Web3, pub signer_address: Address, pub gas_estimator: Box, - pub gas_parameters_of_last_tx: Option, + pub gas_parameters_of_last_tx: Option, pub nonce_of_last_submission: Option, pub max_gas_price: u64, pub start_priority_fee_tip: u64, @@ -88,8 +80,8 @@ impl Submitter { let tx_result = ethflow_contract .invalidateOrdersIgnoringNotAllowed(encoded_ethflow_orders) // Gas conversions are lossy but technically the should not have decimal points even though they're floats - .max_priority_fee_per_gas(f64_to_u128(gas_price.max_priority_fee_per_gas)) - .max_fee_per_gas(f64_to_u128(gas_price.max_fee_per_gas)) + .max_priority_fee_per_gas(gas_price.max_priority_fee_per_gas) + .max_fee_per_gas(gas_price.max_fee_per_gas) .from(self.signer_address) .nonce(nonce) .send() @@ -109,20 +101,47 @@ impl Submitter { } } +trait ScaleExt { + fn scaled_by_pct(self, pct: u64) -> Self; + fn scaled_by_pml(self, pml: u64) -> Self; +} + +impl ScaleExt for u128 { + fn scaled_by_pct(self, pct: u64) -> Self { + self * (100 + pct as u128) / 100 + } + + fn scaled_by_pml(self, pml: u64) -> Self { + self * (1000 + pml as u128) / 1000 + } +} + +trait Eip1559EstimationExt { + fn scaled_by_pml(self, pml: u64) -> Self; +} + +impl Eip1559EstimationExt for Eip1559Estimation { + fn scaled_by_pml(mut self, pml: u64) -> Self { + self.max_fee_per_gas = self.max_fee_per_gas.scaled_by_pml(pml); + self.max_priority_fee_per_gas = self.max_priority_fee_per_gas.scaled_by_pml(pml); + self + } +} + fn calculate_submission_gas_price( - gas_price_of_last_submission: Option, - web3_gas_estimation: GasPrice1559, + gas_price_of_last_submission: Option, + web3_gas_estimation: Eip1559Estimation, newest_nonce: u64, nonce_of_last_submission: Option, max_gas_price: u64, start_priority_fee_tip: u64, -) -> Result { +) -> Result { // The gas price of the refund tx is the current prevailing gas price // of the web3 gas estimation plus a buffer. - let mut new_gas_price = web3_gas_estimation.bump(GAS_PRICE_BUFFER_FACTOR); + let mut new_gas_price = web3_gas_estimation.scaled_by_pct(GAS_PRICE_BUFFER_PCT); // limit the prio_fee to max_fee_per_gas as otherwise tx is invalid new_gas_price.max_priority_fee_per_gas = - (start_priority_fee_tip as f64).min(new_gas_price.max_fee_per_gas); + (start_priority_fee_tip as u128).min(new_gas_price.max_fee_per_gas); // If tx from the previous submission was not mined, // we incease the tip and max_gas_fee for miners @@ -130,7 +149,8 @@ fn calculate_submission_gas_price( if Some(newest_nonce) == nonce_of_last_submission && let Some(gas_price_of_last_submission) = gas_price_of_last_submission { - let gas_price_of_last_submission = gas_price_of_last_submission.bump(GAS_PRICE_BUMP); + let gas_price_of_last_submission = + gas_price_of_last_submission.scaled_by_pml(GAS_PRICE_BUMP_PML); new_gas_price.max_fee_per_gas = new_gas_price .max_fee_per_gas .max(gas_price_of_last_submission.max_fee_per_gas); @@ -139,7 +159,7 @@ fn calculate_submission_gas_price( .max(gas_price_of_last_submission.max_priority_fee_per_gas); } - if new_gas_price.max_fee_per_gas > max_gas_price as f64 { + if new_gas_price.max_fee_per_gas > max_gas_price as u128 { tracing::warn!( "Refunding txs are likely not mined in time, as the current gas price {:?} is higher \ than MAX_GAS_PRICE specified {:?}", @@ -147,9 +167,9 @@ fn calculate_submission_gas_price( max_gas_price ); new_gas_price.max_fee_per_gas = - f64::min(max_gas_price as f64, new_gas_price.max_fee_per_gas); + u128::min(max_gas_price as u128, new_gas_price.max_fee_per_gas); } - new_gas_price.max_priority_fee_per_gas = f64::min( + new_gas_price.max_priority_fee_per_gas = u128::min( new_gas_price.max_priority_fee_per_gas, new_gas_price.max_fee_per_gas, ); @@ -166,11 +186,10 @@ mod tests { const TEST_START_PRIORITY_FEE_TIP: u64 = 2_000_000_000; // First case: previous tx was successful - let max_fee_per_gas = 4_000_000_000f64; - let web3_gas_estimation = GasPrice1559 { - base_fee_per_gas: 2_000_000_000f64, + let max_fee_per_gas = 4_000_000_000_u128; + let web3_gas_estimation = Eip1559Estimation { max_fee_per_gas, - max_priority_fee_per_gas: 3_000_000_000f64, + max_priority_fee_per_gas: 3_000_000_000_u128, }; let newest_nonce = 1; let nonce_of_last_submission = None; @@ -184,19 +203,17 @@ mod tests { TEST_START_PRIORITY_FEE_TIP, ) .unwrap(); - let expected_result = GasPrice1559 { - max_fee_per_gas: max_fee_per_gas * GAS_PRICE_BUFFER_FACTOR, - max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as f64, - base_fee_per_gas: 2_000_000_000f64, + let expected_result = Eip1559Estimation { + max_fee_per_gas: max_fee_per_gas.scaled_by_pct(GAS_PRICE_BUFFER_PCT), + max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as u128, }; assert_eq!(result, expected_result); // Second case: Previous tx was not successful let nonce_of_last_submission = Some(newest_nonce); - let max_fee_per_gas_of_last_tx = max_fee_per_gas * 2f64; - let gas_price_of_last_submission = GasPrice1559 { + let max_fee_per_gas_of_last_tx = max_fee_per_gas * 2; + let gas_price_of_last_submission = Eip1559Estimation { max_fee_per_gas: max_fee_per_gas_of_last_tx, - max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as f64, - base_fee_per_gas: 2_000_000_000f64, + max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as u128, }; let result = calculate_submission_gas_price( Some(gas_price_of_last_submission), @@ -207,18 +224,17 @@ mod tests { TEST_START_PRIORITY_FEE_TIP, ) .unwrap(); - let expected_result = GasPrice1559 { - max_fee_per_gas: max_fee_per_gas_of_last_tx * GAS_PRICE_BUMP, - max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as f64 * GAS_PRICE_BUMP, - base_fee_per_gas: 2_000_000_000f64, + let expected_result = Eip1559Estimation { + max_fee_per_gas: max_fee_per_gas_of_last_tx.scaled_by_pml(GAS_PRICE_BUMP_PML), + max_priority_fee_per_gas: (TEST_START_PRIORITY_FEE_TIP as u128) + .scaled_by_pml(GAS_PRICE_BUMP_PML), }; assert_eq!(result, expected_result); // Thrid case: MAX_GAS_PRICE is not exceeded - let max_fee_per_gas = TEST_MAX_GAS_PRICE as f64 + 1000f64; - let web3_gas_estimation = GasPrice1559 { - base_fee_per_gas: 2_000_000_000f64, + let max_fee_per_gas = TEST_MAX_GAS_PRICE as u128 + 1000_u128; + let web3_gas_estimation = Eip1559Estimation { max_fee_per_gas, - max_priority_fee_per_gas: 3_000_000_000f64, + max_priority_fee_per_gas: 3_000_000_000_u128, }; let nonce_of_last_submission = None; let gas_price_of_last_submission = None; @@ -231,10 +247,9 @@ mod tests { TEST_START_PRIORITY_FEE_TIP, ) .unwrap(); - let expected_result = GasPrice1559 { - base_fee_per_gas: 2_000_000_000f64, - max_fee_per_gas: TEST_MAX_GAS_PRICE as f64, - max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as f64, + let expected_result = Eip1559Estimation { + max_fee_per_gas: TEST_MAX_GAS_PRICE as u128, + max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as u128, }; assert_eq!(result, expected_result); } diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index a3c8fa1d9b..5dc06f0c46 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -5,7 +5,8 @@ //! anomalies. use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, + crate::gas_price_estimation::GasPriceEstimating, + alloy::eips::eip1559::{Eip1559Estimation, calc_effective_gas_price}, anyhow::Result, tracing::instrument, }; @@ -34,11 +35,15 @@ where T: GasPriceEstimating, { #[instrument(skip_all)] - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { let estimate = self.inner.estimate().await?; - self.metrics - .gas_price - .set(estimate.effective_gas_price() / 1e9); + self.metrics.gas_price.set( + (calc_effective_gas_price( + estimate.max_fee_per_gas, + estimate.max_priority_fee_per_gas, + None, + ) / 10u128.pow(9)) as f64, + ); Ok(estimate) } } diff --git a/crates/shared/src/gas_price_estimation/alloy.rs b/crates/shared/src/gas_price_estimation/alloy.rs index a1215bd0a1..2dc687f77a 100644 --- a/crates/shared/src/gas_price_estimation/alloy.rs +++ b/crates/shared/src/gas_price_estimation/alloy.rs @@ -6,9 +6,9 @@ //! for the implementation details. use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, - alloy::providers::Provider, - anyhow::{Context, Result}, + crate::gas_price_estimation::GasPriceEstimating, + alloy::{eips::eip1559::Eip1559Estimation, providers::Provider}, + anyhow::Result, ethrpc::AlloyProvider, futures::TryFutureExt, tracing::instrument, @@ -25,24 +25,16 @@ impl AlloyGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for AlloyGasPriceEstimator { #[instrument(skip(self))] - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { let fees = self .0 .estimate_eip1559_fees() .map_err(|err| anyhow::anyhow!("could not estimate EIP 1559 fees: {err:?}")) .await?; - let max_fee_per_gas = u128_to_f64(fees.max_fee_per_gas) - .context("could not convert max_fee_per_gas to f64")?; - - Ok(GasPrice1559 { - // We reuse `max_fee_per_gas` since the base fee only actually - // exists in a mined block. For price estimates used to configure - // the gas price of a transaction the base fee doesn't matter. - base_fee_per_gas: max_fee_per_gas, - max_fee_per_gas, - max_priority_fee_per_gas: u128_to_f64(fees.max_priority_fee_per_gas) - .context("could not convert max_priority_fee_per_gas to f64")?, + Ok(Eip1559Estimation { + max_fee_per_gas: fees.max_fee_per_gas, + max_priority_fee_per_gas: fees.max_priority_fee_per_gas, }) } } diff --git a/crates/shared/src/gas_price_estimation/driver.rs b/crates/shared/src/gas_price_estimation/driver.rs index 8c3b98f85b..d21478285f 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -1,11 +1,9 @@ use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, - alloy::primitives::U256, + crate::gas_price_estimation::GasPriceEstimating, + alloy::eips::eip1559::Eip1559Estimation, anyhow::{Context, Result}, - number::serialization::HexOrDecimalU256, reqwest::Url, serde::Deserialize, - serde_with::serde_as, std::{ sync::Arc, time::{Duration, Instant}, @@ -25,21 +23,16 @@ pub struct DriverGasEstimator { #[derive(Debug, Clone)] struct CachedGasPrice { - price: GasPrice1559, + price: Eip1559Estimation, timestamp: Instant, } -#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] /// Gas price components in EIP-1559 format. struct GasPriceResponse { - #[serde_as(as = "HexOrDecimalU256")] - max_fee_per_gas: U256, - #[serde_as(as = "HexOrDecimalU256")] - max_priority_fee_per_gas: U256, - #[serde_as(as = "HexOrDecimalU256")] - base_fee_per_gas: U256, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, } const CACHE_DURATION: Duration = Duration::from_secs(5); @@ -54,7 +47,7 @@ impl DriverGasEstimator { } #[instrument(skip(self))] - async fn fetch_gas_price(&self) -> Result { + async fn fetch_gas_price(&self) -> Result { let response = self .client .get(self.url.clone()) @@ -67,10 +60,9 @@ impl DriverGasEstimator { .await .context("failed to parse driver response")?; - Ok(GasPrice1559 { - base_fee_per_gas: f64::from(response.base_fee_per_gas), - max_fee_per_gas: f64::from(response.max_fee_per_gas), - max_priority_fee_per_gas: f64::from(response.max_priority_fee_per_gas), + Ok(Eip1559Estimation { + max_fee_per_gas: response.max_fee_per_gas, + max_priority_fee_per_gas: response.max_priority_fee_per_gas, }) } } @@ -78,7 +70,7 @@ impl DriverGasEstimator { #[async_trait::async_trait] impl GasPriceEstimating for DriverGasEstimator { #[instrument(skip(self))] - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { // Lock cache for entire duration of this method to prevent concurrent network // requests let mut cache = self.cache.lock().await; diff --git a/crates/shared/src/gas_price_estimation/eth_node.rs b/crates/shared/src/gas_price_estimation/eth_node.rs index e202a79a1d..5cb19da4a5 100644 --- a/crates/shared/src/gas_price_estimation/eth_node.rs +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -1,8 +1,8 @@ //! Ethereum node `GasPriceEstimating` implementation. use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, - alloy::providers::Provider, + crate::gas_price_estimation::GasPriceEstimating, + alloy::{eips::eip1559::Eip1559Estimation, providers::Provider}, anyhow::{Context, Result}, ethrpc::AlloyProvider, }; @@ -17,16 +17,14 @@ impl NodeGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for NodeGasPriceEstimator { - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { let legacy = self .0 .get_gas_price() .await - .context("failed to get web3 gas price") - .map(u128_to_f64)??; + .context("failed to get web3 gas price")?; - Ok(GasPrice1559 { - base_fee_per_gas: 0.0, + Ok(Eip1559Estimation { max_fee_per_gas: legacy, max_priority_fee_per_gas: legacy, }) diff --git a/crates/shared/src/gas_price_estimation/fake.rs b/crates/shared/src/gas_price_estimation/fake.rs index 19f8e56de9..aebc8fd3b6 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -1,20 +1,29 @@ use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, + crate::gas_price_estimation::GasPriceEstimating, + alloy::eips::eip1559::Eip1559Estimation, anyhow::Result, }; -#[derive(Default)] -pub struct FakeGasPriceEstimator(pub GasPrice1559); +pub struct FakeGasPriceEstimator(pub Eip1559Estimation); + +impl Default for FakeGasPriceEstimator { + fn default() -> Self { + Self(Eip1559Estimation { + max_fee_per_gas: Default::default(), + max_priority_fee_per_gas: Default::default(), + }) + } +} impl FakeGasPriceEstimator { - pub fn new(gas_price: GasPrice1559) -> Self { + pub fn new(gas_price: Eip1559Estimation) -> Self { Self(gas_price) } } #[async_trait::async_trait] impl GasPriceEstimating for FakeGasPriceEstimator { - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { Ok(self.0) } } diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 7174b1fc16..4b0b0e1a0e 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -15,7 +15,10 @@ use { }, http_client::HttpClientFactory, }, - ::alloy::providers::Provider, + ::alloy::{ + eips::eip1559::{Eip1559Estimation, calc_effective_gas_price}, + providers::Provider, + }, anyhow::Result, std::{str::FromStr, time::Duration}, tracing::instrument, @@ -30,7 +33,7 @@ pub const DEFAULT_TIME_LIMIT: Duration = Duration::from_secs(30); #[async_trait::async_trait] pub trait GasPriceEstimating: Send + Sync { /// Estimate the gas price for a transaction to be mined "quickly". - async fn estimate(&self) -> Result; + async fn estimate(&self) -> Result; } #[derive(Clone, Debug)] @@ -87,9 +90,16 @@ pub async fn create_priority_estimator( Ok(PriorityGasPriceEstimating::new(estimators)) } -fn u128_to_f64(val: u128) -> Result { - if val > 2u128.pow(f64::MANTISSA_DIGITS) { - anyhow::bail!(format!("could not convert u128 to f64: {val}")); +pub trait Eip1559EstimationExt { + fn effective(self, base_fee: Option) -> u128; +} + +impl Eip1559EstimationExt for Eip1559Estimation { + fn effective(self, base_fee: Option) -> u128 { + calc_effective_gas_price( + self.max_fee_per_gas, + self.max_priority_fee_per_gas, + base_fee, + ) } - Ok(val as f64) } diff --git a/crates/shared/src/gas_price_estimation/price.rs b/crates/shared/src/gas_price_estimation/price.rs index cb410bdcb7..66f85c9521 100644 --- a/crates/shared/src/gas_price_estimation/price.rs +++ b/crates/shared/src/gas_price_estimation/price.rs @@ -1,7 +1,9 @@ // Vendored implementation of GasPrice1559 to start removing the dependency on // the gas_estimation crate use { + alloy::eips::eip1559::calc_effective_gas_price, anyhow::{Result, anyhow}, + num::Zero, serde::Serialize, }; @@ -22,11 +24,21 @@ impl GasPrice1559 { // different from estimated value in case of 1559 tx // (because base_fee_per_gas can change between estimation and mining the tx). pub fn effective_gas_price(&self) -> f64 { - std::cmp::min_by( - self.max_fee_per_gas, - self.max_priority_fee_per_gas + self.base_fee_per_gas, - |a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal), - ) + calc_effective_gas_price( + self.max_fee_per_gas as u128, + self.max_priority_fee_per_gas as u128, + if self.base_fee_per_gas.is_zero() { + None + } else { + Some(self.base_fee_per_gas as u64) + }, + ) as f64 + + // std::cmp::min_by( + // self.max_fee_per_gas, + // self.max_priority_fee_per_gas + self.base_fee_per_gas, + // |a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal), + // ) } // Validate against rules defined in https://eips.ethereum.org/EIPS/eip-1559 diff --git a/crates/shared/src/gas_price_estimation/priority.rs b/crates/shared/src/gas_price_estimation/priority.rs index efa1ab3e01..c7dff7925e 100644 --- a/crates/shared/src/gas_price_estimation/priority.rs +++ b/crates/shared/src/gas_price_estimation/priority.rs @@ -1,5 +1,6 @@ use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, + crate::gas_price_estimation::GasPriceEstimating, + alloy::eips::eip1559::Eip1559Estimation, anyhow::{Result, anyhow}, std::{ future::Future, @@ -35,10 +36,10 @@ impl PriorityGasPriceEstimating { Self { estimators } } - async fn prioritize<'a, T, F>(&'a self, operation: T) -> Result + async fn prioritize<'a, T, F>(&'a self, operation: T) -> Result where T: Fn(&'a dyn GasPriceEstimating) -> F, - F: Future>, + F: Future>, { for (i, estimator) in self.estimators.iter().enumerate() { match operation(estimator.estimator.as_ref()).await { @@ -62,7 +63,7 @@ impl PriorityGasPriceEstimating { #[async_trait::async_trait] impl GasPriceEstimating for PriorityGasPriceEstimating { - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { self.prioritize(|estimator| estimator.estimate()).await } } @@ -73,40 +74,22 @@ mod tests { crate::gas_price_estimation::{ GasPriceEstimating, MockGasPriceEstimating, - price::GasPrice1559, priority::PriorityGasPriceEstimating, }, + alloy::eips::eip1559::Eip1559Estimation, anyhow::anyhow, futures::future::FutureExt, }; - // Copied from the source: https://github.com/ashleygwilliams/assert_approx_eq/blob/master/src/lib.rs - // should be removed as we move away from expressing gas in f64 - macro_rules! assert_approx_eq { - ($a:expr, $b:expr) => {{ - let eps = 1.0e-6; - let (a, b) = (&$a, &$b); - assert!( - (*a - *b).abs() < eps, - "assertion failed: `(left !== right)` (left: `{:?}`, right: `{:?}`, expect diff: \ - `{:?}`, real diff: `{:?}`)", - *a, - *b, - eps, - (*a - *b).abs() - ); - }}; - } - #[test] fn prioritize_picks_first_if_first_succeeds() { let mut estimator_0 = MockGasPriceEstimating::new(); let estimator_1 = MockGasPriceEstimating::new(); estimator_0.expect_estimate().times(1).returning(|| { - Ok(GasPrice1559 { - base_fee_per_gas: 1.0, - ..Default::default() + Ok(Eip1559Estimation { + max_fee_per_gas: 10, + max_priority_fee_per_gas: 0, }) }); @@ -126,16 +109,16 @@ mod tests { .times(1) .returning(|| Err(anyhow!(""))); estimator_1.expect_estimate().times(1).returning(|| { - Ok(GasPrice1559 { - base_fee_per_gas: 2.0, - ..Default::default() + Ok(Eip1559Estimation { + max_fee_per_gas: 10, + max_priority_fee_per_gas: 0, }) }); let priority = PriorityGasPriceEstimating::new(vec![Box::new(estimator_0), Box::new(estimator_1)]); let result = priority.estimate().now_or_never().unwrap().unwrap(); - assert_approx_eq!(result.base_fee_per_gas, 2.0); + assert_eq!(result.max_fee_per_gas, 10); } #[test] diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 0500701f5a..3b61bd4922 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -14,7 +14,10 @@ use { price_estimation::{Estimate, QuoteVerificationMode, Verification}, trade_finding::external::dto, }, - alloy::primitives::{Address, U256, U512, ruint::UintTryFrom}, + alloy::{ + eips::eip1559::calc_effective_gas_price, + primitives::{Address, U256, U512, ruint::UintTryFrom}, + }, anyhow::{Context, Result}, chrono::{DateTime, Duration, Utc}, database::quotes::{Quote as QuoteRow, QuoteKind}, @@ -495,7 +498,11 @@ impl OrderQuoter { }; let fee_parameters = FeeParameters { gas_amount: trade_estimate.gas as _, - gas_price: gas_estimate.effective_gas_price(), + gas_price: calc_effective_gas_price( + gas_estimate.max_fee_per_gas, + gas_estimate.max_priority_fee_per_gas, + None, + ) as f64, sell_token_price, }; @@ -785,7 +792,8 @@ mod tests { super::*, crate::{ account_balances::MockBalanceFetching, - gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, + fee, + gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ HEALTHY_PRICE_ESTIMATION_TIME, MockPriceEstimating, @@ -794,6 +802,7 @@ mod tests { }, Address, U256 as AlloyU256, + alloy::eips::eip1559::Eip1559Estimation, chrono::Utc, futures::FutureExt, mockall::{Sequence, predicate::eq}, @@ -852,10 +861,9 @@ mod tests { additional_gas: 0, timeout: None, }; - let gas_price = GasPrice1559 { - base_fee_per_gas: 1.5, - max_fee_per_gas: 3.0, - max_priority_fee_per_gas: 0.5, + let gas_price = Eip1559Estimation { + max_fee_per_gas: 2, + max_priority_fee_per_gas: 1, }; let mut price_estimator = MockPriceEstimating::new(); @@ -993,10 +1001,9 @@ mod tests { additional_gas: 2, timeout: None, }; - let gas_price = GasPrice1559 { - base_fee_per_gas: 1.5, - max_fee_per_gas: 3.0, - max_priority_fee_per_gas: 0.5, + let gas_price = Eip1559Estimation { + max_fee_per_gas: 2, + max_priority_fee_per_gas: 1, }; let mut price_estimator = MockPriceEstimating::new(); @@ -1129,10 +1136,9 @@ mod tests { additional_gas: 0, timeout: None, }; - let gas_price = GasPrice1559 { - base_fee_per_gas: 1.5, - max_fee_per_gas: 3.0, - max_priority_fee_per_gas: 0.5, + let gas_price = Eip1559Estimation { + max_fee_per_gas: 2, + max_priority_fee_per_gas: 1, }; let mut price_estimator = MockPriceEstimating::new(); @@ -1266,10 +1272,9 @@ mod tests { additional_gas: 0, timeout: None, }; - let gas_price = GasPrice1559 { - base_fee_per_gas: 1., - max_fee_per_gas: 2., - max_priority_fee_per_gas: 0., + let gas_price = Eip1559Estimation { + max_fee_per_gas: 1, + max_priority_fee_per_gas: 0, }; let mut price_estimator = MockPriceEstimating::new(); @@ -1316,10 +1321,12 @@ mod tests { default_quote_timeout: HEALTHY_PRICE_ESTIMATION_TIME, }; - assert!(matches!( - quoter.calculate_quote(parameters).await.unwrap_err(), - CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } if fee_amount == U256::from(200), - )); + match quoter.calculate_quote(parameters).await.unwrap_err() { + CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } => { + assert_eq!(fee_amount, U256::from(200)) + } + _ => panic!("expected CalculateQuoteError::SellAmountDoesNotCoverFee"), + } } #[tokio::test] @@ -1340,10 +1347,9 @@ mod tests { additional_gas: 0, timeout: None, }; - let gas_price = GasPrice1559 { - base_fee_per_gas: 1., - max_fee_per_gas: 2., - max_priority_fee_per_gas: 0., + let gas_price = Eip1559Estimation { + max_fee_per_gas: 2, + max_priority_fee_per_gas: 0, }; let mut price_estimator = MockPriceEstimating::new(); diff --git a/crates/shared/src/price_estimation/competition/quote.rs b/crates/shared/src/price_estimation/competition/quote.rs index 60ecaa2ff2..872c317bd6 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -8,6 +8,7 @@ use { Query, QuoteVerificationMode, }, + alloy::eips::eip1559::calc_effective_gas_price, anyhow::Context, ethrpc::alloy::conversions::{IntoAlloy, IntoLegacy}, futures::future::{BoxFuture, FutureExt, TryFutureExt}, @@ -110,7 +111,13 @@ impl PriceRanking { let native = native.clone(); let gas = gas .estimate() - .map_ok(|gas| gas.effective_gas_price()) + .map_ok(|gas| { + calc_effective_gas_price( + gas.max_fee_per_gas, + gas.max_priority_fee_per_gas, + None, + ) + }) .map_err(PriceEstimationError::ProtocolInternal); let (native_price, gas_price) = futures::try_join!( native.estimate_native_price(token.into_alloy(), timeout), @@ -119,7 +126,7 @@ impl PriceRanking { Ok(RankingContext { native_price, - gas_price, + gas_price: gas_price as f64, }) } } @@ -156,14 +163,14 @@ mod tests { use { super::*, crate::{ - gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, + gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ MockPriceEstimating, QuoteVerificationMode, native::MockNativePriceEstimating, }, }, - alloy::primitives::U256, + alloy::{eips::eip1559::Eip1559Estimation, primitives::U256}, model::order::OrderKind, }; @@ -190,10 +197,9 @@ mod tests { native .expect_estimate_native_price() .returning(move |_, _| async { Ok(0.5) }.boxed()); - let gas = Arc::new(FakeGasPriceEstimator::new(GasPrice1559 { - base_fee_per_gas: 2.0, - max_fee_per_gas: 2.0, - max_priority_fee_per_gas: 2.0, + let gas = Arc::new(FakeGasPriceEstimator::new(Eip1559Estimation { + max_fee_per_gas: 2, + max_priority_fee_per_gas: 2, })); PriceRanking::BestBangForBuck { native: Arc::new(native), From a6a467c5e06f6e889683067c4dbeaeb1fd0e238b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 14 Jan 2026 16:36:28 +0000 Subject: [PATCH 07/39] Address river error --- crates/driver/src/tests/cases/settle.rs | 1 + crates/driver/src/tests/setup/solver.rs | 55 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/tests/cases/settle.rs b/crates/driver/src/tests/cases/settle.rs index 373c417eb3..6a69e68c50 100644 --- a/crates/driver/src/tests/cases/settle.rs +++ b/crates/driver/src/tests/cases/settle.rs @@ -205,6 +205,7 @@ async fn discards_excess_settle_and_solve_requests() { test.solve().await.err().kind("TooManyPendingSettlements"); // Enable auto mining to process all the settlement requests. + // *Note that processing the settlement requests will change the gas estimates!* test.set_auto_mining(true).await; // The first settlement must be successful. diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index a3b3cb38c1..fd862d685d 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -17,9 +17,11 @@ use { const_hex::ToHexExt, contracts::alloy::ERC20, itertools::Itertools, - serde_json::json, + serde_json::{Value, json}, + serde_with::{DisplayFromStr, serde_as}, solvers_dto::auction::FlashloanHint, std::{ + cmp::max, collections::{HashMap, HashSet}, net::SocketAddr, sync::{Arc, Mutex}, @@ -452,11 +454,11 @@ impl Solver { &Default::default(), &[infra::mempool::Config { min_priority_fee: Default::default(), - gas_price_cap: eth::U256::MAX, + gas_price_cap: eth::U256::from(1000000000000_u128), target_confirm_time: Default::default(), retry_interval: Default::default(), name: "default_rpc".to_string(), - max_additional_tip: eth::U256::ZERO, + max_additional_tip: eth::U256::from(3000000000_u128), additional_tip_percentage: 0., revert_protection: infra::mempool::RevertProtection::Disabled, nonce_block_number: None, @@ -512,7 +514,7 @@ impl Solver { "deadline": config.deadline.solvers(), "surplusCapturingJitOrderOwners": config.expected_surplus_capturing_jit_order_owners, }); - assert_eq!(req, expected, "unexpected /solve request"); + check_solve_request(req, expected); let mut state = state.0.lock().unwrap(); assert!( !state.called || state.allow_multiple_solve_requests, @@ -534,6 +536,51 @@ impl Solver { } } +/// Checks the provider /solve request against the expected values while keeping +/// some slack for the effective gas price, as it might vary between blockchain +/// requests. +/// +/// Context: when the gas-estimation crate was removed, the Alloy and Web3 +/// estimators started failing the driver tests: the request's effective gas +/// value did not match the expected. This did not happen with the previous +/// native estimator because it used a cache, and due to how short the test was +/// the cache always replied with the same value making the test pass. The new +/// estimators do not have a cache, as such the value might change; this check +/// takes that into account and validates the effective gas price within an +/// interval (15% at the time of writing). +fn check_solve_request(request: Value, expected: Value) { + #[serde_as] + #[derive(Debug, serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct SolveRequest { + #[serde_as(as = "DisplayFromStr")] + effective_gas_price: u128, + #[serde(flatten)] + rest: Value, + } + + let request: SolveRequest = serde_json::from_value(request).unwrap(); + let expected: SolveRequest = serde_json::from_value(expected).unwrap(); + assert_eq!( + request.rest, expected.rest, + "/solve request body does not match expectation" + ); + + const DIFF_PCT: f64 = 0.15; // 15% + // Assumes the u128 fits inside the i128, in case it doesn't, just upgrade it to + // U256 + let diff = (request.effective_gas_price as i128 - expected.effective_gas_price as i128).abs(); + let pct = diff as f64 / max(request.effective_gas_price, expected.effective_gas_price) as f64; + + assert!( + pct < DIFF_PCT, + "/solve request does not match expectactions, request: {}, expected: {} pct: {pct}, max \ + pct: {DIFF_PCT}", + request.effective_gas_price, + expected.effective_gas_price + ); +} + #[derive(Debug, Clone)] struct StateInner { /// Has this solver been called yet? If so, attempting to make another call From 5065765de2e218b15befda4fae2adb31ae3b3624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 14 Jan 2026 17:26:55 +0000 Subject: [PATCH 08/39] address clippy --- crates/driver/src/tests/setup/solver.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index fd862d685d..6cf8d806fe 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -569,7 +569,9 @@ fn check_solve_request(request: Value, expected: Value) { const DIFF_PCT: f64 = 0.15; // 15% // Assumes the u128 fits inside the i128, in case it doesn't, just upgrade it to // U256 - let diff = (request.effective_gas_price as i128 - expected.effective_gas_price as i128).abs(); + let diff = (request.effective_gas_price.cast_signed() + - expected.effective_gas_price.cast_signed()) + .abs(); let pct = diff as f64 / max(request.effective_gas_price, expected.effective_gas_price) as f64; assert!( From 05c2c01c435b2808ed456db4196a1023c95009ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= Date: Wed, 14 Jan 2026 19:02:05 +0000 Subject: [PATCH 09/39] Update crates/driver/src/tests/setup/solver.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- crates/driver/src/tests/setup/solver.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 6cf8d806fe..a7fc7da5ae 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -559,8 +559,10 @@ fn check_solve_request(request: Value, expected: Value) { rest: Value, } - let request: SolveRequest = serde_json::from_value(request).unwrap(); - let expected: SolveRequest = serde_json::from_value(expected).unwrap(); + let request: SolveRequest = serde_json::from_value(request) + .expect("failed to deserialize /solve request body"); + let expected: SolveRequest = serde_json::from_value(expected) + .expect("failed to deserialize expected /solve request body"); assert_eq!( request.rest, expected.rest, "/solve request body does not match expectation" From aa62ec243111b5ecc7921ab5495c0c97a5bec5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 11:23:48 +0000 Subject: [PATCH 10/39] address comments --- crates/driver/src/tests/cases/mod.rs | 27 +++++++++++++++++++ crates/driver/src/tests/setup/solver.rs | 23 +++++----------- crates/shared/src/gas_price_estimation/mod.rs | 5 +--- .../shared/src/gas_price_estimation/price.rs | 21 +-------------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index b9bbe5fc04..2a4a772d34 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -129,3 +129,30 @@ pub fn is_approximately_equal(executed_value: eth::U256, expected_value: eth::U2 expected_value * eth::U256::from(100000000001u128) / eth::U256::from(100000000000u128); // in percents = 100.000000001% executed_value >= lower && executed_value <= upper } + +#[cfg(test)] +pub trait ApproxEq { + // Implementation defined + fn is_approx_eq(&self, other: &Self, delta: Option) -> bool; +} + +#[cfg(test)] +impl ApproxEq for u128 { + fn is_approx_eq(&self, other: &Self, delta: Option) -> bool { + let (lower, upper) = match delta { + Some(percent) => { + // percent% tolerance (e.g., Some(1) means 1% delta) + let lower = other * (100 - percent as u128) / 100; + let upper = other * (100 + percent as u128) / 100; + (lower, upper) + } + None => { + // Default: very tight tolerance (approximately 0.000000001%) + let lower = other * 99999999999u128 / 100000000000u128; // in percents = 99.999999999% + let upper = other * 100000000001u128 / 100000000000u128; // in percents = 100.000000001% + (lower, upper) + } + }; + self >= &lower && self <= &upper + } +} diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index a7fc7da5ae..a79ed13a6d 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -11,7 +11,7 @@ use { time::{self}, }, infra::{self, Ethereum, blockchain::contracts::Addresses, config::file::FeeHandler}, - tests::setup::blockchain::Trade, + tests::{cases::ApproxEq, setup::blockchain::Trade}, }, alloy::{primitives::Address, signers::local::PrivateKeySigner}, const_hex::ToHexExt, @@ -21,7 +21,6 @@ use { serde_with::{DisplayFromStr, serde_as}, solvers_dto::auction::FlashloanHint, std::{ - cmp::max, collections::{HashMap, HashSet}, net::SocketAddr, sync::{Arc, Mutex}, @@ -559,8 +558,8 @@ fn check_solve_request(request: Value, expected: Value) { rest: Value, } - let request: SolveRequest = serde_json::from_value(request) - .expect("failed to deserialize /solve request body"); + let request: SolveRequest = + serde_json::from_value(request).expect("failed to deserialize /solve request body"); let expected: SolveRequest = serde_json::from_value(expected) .expect("failed to deserialize expected /solve request body"); assert_eq!( @@ -568,20 +567,10 @@ fn check_solve_request(request: Value, expected: Value) { "/solve request body does not match expectation" ); - const DIFF_PCT: f64 = 0.15; // 15% - // Assumes the u128 fits inside the i128, in case it doesn't, just upgrade it to - // U256 - let diff = (request.effective_gas_price.cast_signed() - - expected.effective_gas_price.cast_signed()) - .abs(); - let pct = diff as f64 / max(request.effective_gas_price, expected.effective_gas_price) as f64; - assert!( - pct < DIFF_PCT, - "/solve request does not match expectactions, request: {}, expected: {} pct: {pct}, max \ - pct: {DIFF_PCT}", - request.effective_gas_price, - expected.effective_gas_price + request + .effective_gas_price + .is_approx_eq(&expected.effective_gas_price, Some(15)), ); } diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 7174b1fc16..74d0a28efa 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -17,15 +17,12 @@ use { }, ::alloy::providers::Provider, anyhow::Result, - std::{str::FromStr, time::Duration}, + std::str::FromStr, tracing::instrument, url::Url, }; pub use {driver::DriverGasEstimator, fake::FakeGasPriceEstimator}; -pub const DEFAULT_GAS_LIMIT: f64 = 21000.0; -pub const DEFAULT_TIME_LIMIT: Duration = Duration::from_secs(30); - #[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] pub trait GasPriceEstimating: Send + Sync { diff --git a/crates/shared/src/gas_price_estimation/price.rs b/crates/shared/src/gas_price_estimation/price.rs index cb410bdcb7..8d1e3ec04f 100644 --- a/crates/shared/src/gas_price_estimation/price.rs +++ b/crates/shared/src/gas_price_estimation/price.rs @@ -1,9 +1,6 @@ // Vendored implementation of GasPrice1559 to start removing the dependency on // the gas_estimation crate -use { - anyhow::{Result, anyhow}, - serde::Serialize, -}; +use serde::Serialize; /// EIP1559 gas price #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Serialize)] @@ -29,22 +26,6 @@ impl GasPrice1559 { ) } - // Validate against rules defined in https://eips.ethereum.org/EIPS/eip-1559 - // max_fee_per_gas >= max_priority_fee_per_gas - // max_fee_per_gas >= base_fee_per_gas - pub fn is_valid(&self) -> bool { - self.max_fee_per_gas >= self.max_priority_fee_per_gas - && self.max_fee_per_gas >= self.base_fee_per_gas - } - - // Validate and build Result based on the validation result - pub fn validate(self) -> Result { - match self.is_valid() { - true => Ok(self), - false => Err(anyhow!("invalid gas price values: {:?}", self)), - } - } - // Bump gas price by factor. pub fn bump(self, factor: f64) -> Self { Self { From 3dd9f641188c9d58e1450803a7ad6ccdc47de787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 11:33:09 +0000 Subject: [PATCH 11/39] alleviate issue with the magic numbers --- crates/driver/src/infra/mempool/mod.rs | 21 ++++++++++++++++++++- crates/driver/src/tests/setup/solver.rs | 15 +++------------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/driver/src/infra/mempool/mod.rs b/crates/driver/src/infra/mempool/mod.rs index 035f933a2f..ac25841d3f 100644 --- a/crates/driver/src/infra/mempool/mod.rs +++ b/crates/driver/src/infra/mempool/mod.rs @@ -12,6 +12,7 @@ use { }, anyhow::Context, ethrpc::Web3, + url::Url, }; #[derive(Debug, Clone)] @@ -23,13 +24,31 @@ pub struct Config { /// Optional block number to use when fetching nonces. If None, uses the /// web3 lib's default behavior, which is `latest`. pub nonce_block_number: Option, - pub url: reqwest::Url, + pub url: Url, pub name: String, pub revert_protection: RevertProtection, pub max_additional_tip: eth::U256, pub additional_tip_percentage: f64, } +#[cfg(test)] +impl Config { + pub fn test_config(url: Url) -> Self { + Self { + min_priority_fee: Default::default(), + gas_price_cap: eth::U256::from(1000000000000_u128), + target_confirm_time: Default::default(), + retry_interval: Default::default(), + name: "default_rpc".to_string(), + max_additional_tip: eth::U256::from(3000000000_u128), + additional_tip_percentage: 0., + revert_protection: infra::mempool::RevertProtection::Disabled, + nonce_block_number: None, + url, + } + } +} + /// Don't submit transactions with high revert risk (i.e. transactions /// that interact with on-chain AMMs) to the public mempool. /// This can be enabled to avoid MEV when private transaction diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index a79ed13a6d..bfb7acf590 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -451,18 +451,9 @@ impl Solver { infra::blockchain::GasPriceEstimator::new( rpc.web3(), &Default::default(), - &[infra::mempool::Config { - min_priority_fee: Default::default(), - gas_price_cap: eth::U256::from(1000000000000_u128), - target_confirm_time: Default::default(), - retry_interval: Default::default(), - name: "default_rpc".to_string(), - max_additional_tip: eth::U256::from(3000000000_u128), - additional_tip_percentage: 0., - revert_protection: infra::mempool::RevertProtection::Disabled, - nonce_block_number: None, - url: config.blockchain.web3_url.parse().unwrap(), - }], + &[infra::mempool::Config::test_config( + config.blockchain.web3_url.parse().unwrap(), + )], ) .await .unwrap(), From ebf73de35c7527be0f4ae33ea5be2ff48966444d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 12:51:10 +0000 Subject: [PATCH 12/39] wip --- crates/driver/src/infra/blockchain/mod.rs | 2 +- crates/driver/src/tests/cases/mod.rs | 2 ++ crates/driver/src/tests/setup/solver.rs | 11 +++-------- crates/shared/src/order_quoting.rs | 11 ++++------- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index b93dbe307b..d3d58dcfb8 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -301,7 +301,7 @@ impl Ethereum { // default value we estimate the current gas price upfront. But because it's // extremely rare that tokens behave that way we are fine with falling back to // the node specific fallback value instead of failing the whole call. - Some(self.inner.gas.estimate().await.ok()?.effective(None)) + Some(self.inner.gas.estimate().await.ok()?.effective(base_fee)) } pub fn web3(&self) -> &Web3 { diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index 2a4a772d34..ae06824796 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -138,6 +138,7 @@ pub trait ApproxEq { #[cfg(test)] impl ApproxEq for u128 { + #[tracing::instrument] fn is_approx_eq(&self, other: &Self, delta: Option) -> bool { let (lower, upper) = match delta { Some(percent) => { @@ -153,6 +154,7 @@ impl ApproxEq for u128 { (lower, upper) } }; + tracing::debug!("lower: {lower}, self: {self}, upper: {upper}"); self >= &lower && self <= &upper } } diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 2e727d9550..7414171002 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -19,6 +19,7 @@ use { itertools::Itertools, serde_json::{Value, json}, serde_with::{DisplayFromStr, serde_as}, + shared::gas_price_estimation::Eip1559EstimationExt, solvers_dto::auction::FlashloanHint, std::{ collections::{HashMap, HashSet}, @@ -487,13 +488,8 @@ impl Solver { axum::routing::post( move |axum::extract::State(state): axum::extract::State, axum::extract::Json(req): axum::extract::Json| async move { - // Extract effectiveGasPrice from request instead of recalculating. - // Gas prices from the network can change between auction creation - // and solver request validation, causing mismatches in tests. - let effective_gas_price = req - .get("effectiveGasPrice") - .expect("effectiveGasPrice missing") - .clone(); + let base_fee = eth.current_block().borrow().base_fee; + let effective_gas_price = eth.gas_price().await.unwrap().effective(base_fee).to_string(); let expected = json!({ "id": (!config.quote).then_some("1"), "tokens": tokens_json, @@ -556,7 +552,6 @@ fn check_solve_request(request: Value, expected: Value) { request.rest, expected.rest, "/solve request body does not match expectation" ); - assert!( request .effective_gas_price diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 3b61bd4922..3080da1c33 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -792,7 +792,6 @@ mod tests { super::*, crate::{ account_balances::MockBalanceFetching, - fee, gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ HEALTHY_PRICE_ESTIMATION_TIME, @@ -1321,12 +1320,10 @@ mod tests { default_quote_timeout: HEALTHY_PRICE_ESTIMATION_TIME, }; - match quoter.calculate_quote(parameters).await.unwrap_err() { - CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } => { - assert_eq!(fee_amount, U256::from(200)) - } - _ => panic!("expected CalculateQuoteError::SellAmountDoesNotCoverFee"), - } + assert!(matches!( + quoter.calculate_quote(parameters).await.unwrap_err(), + CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } if fee_amount == U256::from(200), + )); } #[tokio::test] From a507ca9a4c0fe5250fb425a91efaf8c56caa2030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 15:38:21 +0000 Subject: [PATCH 13/39] is_approx_eq --- crates/driver/src/tests/cases/jit_orders.rs | 13 +++-- crates/driver/src/tests/cases/mod.rs | 54 +++++++++---------- .../driver/src/tests/cases/protocol_fees.rs | 9 ++-- crates/driver/src/tests/setup/mod.rs | 12 ++--- crates/driver/src/tests/setup/solver.rs | 2 +- 5 files changed, 41 insertions(+), 49 deletions(-) diff --git a/crates/driver/src/tests/cases/jit_orders.rs b/crates/driver/src/tests/cases/jit_orders.rs index 66a7e44782..f45ee4b0c8 100644 --- a/crates/driver/src/tests/cases/jit_orders.rs +++ b/crates/driver/src/tests/cases/jit_orders.rs @@ -5,7 +5,7 @@ use crate::{ }, tests::{ self, - cases::{EtherExt, is_approximately_equal}, + cases::EtherExt, setup::{ self, ExpectedOrderAmounts, @@ -55,6 +55,8 @@ struct TestCase { #[cfg(test)] async fn protocol_fee_test_case(test_case: TestCase) { + use crate::tests::cases::ApproxEq; + let test_name = format!("JIT Order: {:?}", test_case.solution.jit_order.order.side); // Adjust liquidity pools so that the order is executable at the amounts // expected from the solver. @@ -114,10 +116,11 @@ async fn protocol_fee_test_case(test_case: TestCase) { .await; let result = test.solve().await.ok(); - assert!(is_approximately_equal( - result.score(), - test_case.solution.expected_score, - )); + assert!( + result + .score() + .is_approx_eq(test_case.solution.expected_score, None), + ); result.jit_orders(&[jit_order]); } diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index 2a4a772d34..cd55a77128 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -119,40 +119,36 @@ impl EtherExt for f64 { } } -// because of rounding errors, it's good enough to check that the expected value -// is within a very narrow range of the executed value -#[cfg(test)] -pub fn is_approximately_equal(executed_value: eth::U256, expected_value: eth::U256) -> bool { - let lower = - expected_value * eth::U256::from(99999999999u128) / eth::U256::from(100000000000u128); // in percents = 99.999999999% - let upper = - expected_value * eth::U256::from(100000000001u128) / eth::U256::from(100000000000u128); // in percents = 100.000000001% - executed_value >= lower && executed_value <= upper -} - #[cfg(test)] pub trait ApproxEq { // Implementation defined - fn is_approx_eq(&self, other: &Self, delta: Option) -> bool; + fn is_approx_eq(self, other: Self, delta: Option) -> bool; } #[cfg(test)] -impl ApproxEq for u128 { - fn is_approx_eq(&self, other: &Self, delta: Option) -> bool { - let (lower, upper) = match delta { - Some(percent) => { - // percent% tolerance (e.g., Some(1) means 1% delta) - let lower = other * (100 - percent as u128) / 100; - let upper = other * (100 + percent as u128) / 100; - (lower, upper) - } - None => { - // Default: very tight tolerance (approximately 0.000000001%) - let lower = other * 99999999999u128 / 100000000000u128; // in percents = 99.999999999% - let upper = other * 100000000001u128 / 100000000000u128; // in percents = 100.000000001% - (lower, upper) - } - }; - self >= &lower && self <= &upper +impl ApproxEq for T +where + T: Into, +{ + fn is_approx_eq(self, other: Self, delta: Option) -> bool { + use {num::BigInt, std::cmp}; + + let self_: BigInt = (self).into(); + let self_ = BigRational::from_integer(self_); + + let other: BigInt = (other).into(); + let other = BigRational::from_integer(other); + + // because of rounding errors, it's good enough to check that the expected value + // is within a very narrow range of the executed value + let expected_delta = BigRational::from_f64(delta.unwrap_or(0.000000001)) + .expect("delta should be representable using BigRational"); + + let diff = (self_.clone() - other.clone()).abs(); + let calculated_delta = diff / cmp::max(self_, other); + + tracing::error!("{expected_delta} < {calculated_delta}"); + + calculated_delta <= expected_delta } } diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs index dbcbbddfb3..abf728077c 100644 --- a/crates/driver/src/tests/cases/protocol_fees.rs +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -3,7 +3,7 @@ use crate::{ infra::config::file::FeeHandler, tests::{ self, - cases::{EtherExt, is_approximately_equal}, + cases::EtherExt, setup::{ ExpectedOrderAmounts, Test, @@ -45,6 +45,8 @@ struct TestCase { #[cfg(test)] async fn protocol_fee_test_case(test_case: TestCase) { + use crate::tests::cases::ApproxEq; + let test_name = format!( "Protocol Fee: {:?} {:?}", test_case.order.side, test_case.fee_policy @@ -94,10 +96,7 @@ async fn protocol_fee_test_case(test_case: TestCase) { .await; let result = test.solve().await.ok(); - assert!(is_approximately_equal( - result.score(), - test_case.expected_score - )); + assert!(result.score().is_approx_eq(test_case.expected_score, None),); result.orders(&[order]); } diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index fb594b6d7a..3e98f4d87d 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -21,6 +21,7 @@ use { cases::{ AB_ORDER_AMOUNT, AD_ORDER_AMOUNT, + ApproxEq, CD_ORDER_AMOUNT, DEFAULT_POOL_AMOUNT_A, DEFAULT_POOL_AMOUNT_B, @@ -29,7 +30,6 @@ use { DEFAULT_SURPLUS_FACTOR, ETH_ORDER_AMOUNT, EtherExt, - is_approximately_equal, }, setup::{ blockchain::{Blockchain, Interaction, Trade}, @@ -1358,14 +1358,8 @@ impl SolveOk<'_> { Some(executed_amounts) => (executed_amounts.sell, executed_amounts.buy), None => (quoted_order.sell, quoted_order.buy), }; - assert!(is_approximately_equal( - u256(trade.get("executedSell").unwrap()), - expected_sell - )); - assert!(is_approximately_equal( - u256(trade.get("executedBuy").unwrap()), - expected_buy - )); + assert!(u256(trade.get("executedSell").unwrap()).is_approx_eq(expected_sell, None)); + assert!(u256(trade.get("executedBuy").unwrap()).is_approx_eq(expected_buy, None)); } self } diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index bfb7acf590..8b68145888 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -561,7 +561,7 @@ fn check_solve_request(request: Value, expected: Value) { assert!( request .effective_gas_price - .is_approx_eq(&expected.effective_gas_price, Some(15)), + .is_approx_eq(expected.effective_gas_price, Some(15.0)), ); } From ec38cd7caa8a9bb8cee82d6622ed216ef496e6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 16:05:22 +0000 Subject: [PATCH 14/39] approx_eq improvements --- crates/driver/src/tests/cases/mod.rs | 29 +++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index cd55a77128..d463968b2b 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -119,9 +119,20 @@ impl EtherExt for f64 { } } +/// Trait for approximate equality comparisons, useful for tests with rounding +/// errors. #[cfg(test)] pub trait ApproxEq { - // Implementation defined + /// Checks if two values are approximately equal within a relative error + /// threshold. + /// + /// # Examples + /// + /// ```ignore + /// assert!(100.is_approx_eq(101, Some(0.02))); // 1% diff, within 2% threshold + /// assert!(!100.is_approx_eq(150, Some(0.02))); // 50% diff, exceeds threshold + /// assert!(100.is_approx_eq(100, None)); // Default 1e-9 threshold + /// ``` fn is_approx_eq(self, other: Self, delta: Option) -> bool; } @@ -139,15 +150,23 @@ where let other: BigInt = (other).into(); let other = BigRational::from_integer(other); - // because of rounding errors, it's good enough to check that the expected value - // is within a very narrow range of the executed value + // Early equality check prevents division by zero when both values are 0 + if self_ == other { + return true; + } + + // Default to 1e-9 (0.0000001%) relative error threshold let expected_delta = BigRational::from_f64(delta.unwrap_or(0.000000001)) .expect("delta should be representable using BigRational"); + // We can't use num::Unsigned due to ruint::U256 not implementing it + // (due to limitations on const generics) + // Calculate relative error: |a - b| / max(|a|, |b|) + // Ensures correct behavior with negative numbers let diff = (self_.clone() - other.clone()).abs(); - let calculated_delta = diff / cmp::max(self_, other); + let calculated_delta = diff / cmp::max(self_.abs(), other.abs()); - tracing::error!("{expected_delta} < {calculated_delta}"); + tracing::debug!("{expected_delta} < {calculated_delta}"); calculated_delta <= expected_delta } From 876fd432f5c55a7ab0d217222c89d221f2d049e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 16:11:04 +0000 Subject: [PATCH 15/39] clippy --- crates/driver/src/tests/cases/jit_orders.rs | 2 +- crates/driver/src/tests/cases/mod.rs | 9 +++++---- crates/driver/src/tests/cases/protocol_fees.rs | 2 +- crates/driver/src/tests/setup/mod.rs | 4 ++-- crates/driver/src/tests/setup/solver.rs | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/driver/src/tests/cases/jit_orders.rs b/crates/driver/src/tests/cases/jit_orders.rs index f45ee4b0c8..f4aabe12e7 100644 --- a/crates/driver/src/tests/cases/jit_orders.rs +++ b/crates/driver/src/tests/cases/jit_orders.rs @@ -119,7 +119,7 @@ async fn protocol_fee_test_case(test_case: TestCase) { assert!( result .score() - .is_approx_eq(test_case.solution.expected_score, None), + .is_approx_eq(&test_case.solution.expected_score, None), ); result.jit_orders(&[jit_order]); } diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index d463968b2b..ac8a1e2ce6 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -133,21 +133,22 @@ pub trait ApproxEq { /// assert!(!100.is_approx_eq(150, Some(0.02))); // 50% diff, exceeds threshold /// assert!(100.is_approx_eq(100, None)); // Default 1e-9 threshold /// ``` - fn is_approx_eq(self, other: Self, delta: Option) -> bool; + fn is_approx_eq(&self, other: &Self, delta: Option) -> bool; } #[cfg(test)] impl ApproxEq for T where + Self: Copy, T: Into, { - fn is_approx_eq(self, other: Self, delta: Option) -> bool { + fn is_approx_eq(&self, other: &Self, delta: Option) -> bool { use {num::BigInt, std::cmp}; - let self_: BigInt = (self).into(); + let self_: BigInt = (*self).into(); let self_ = BigRational::from_integer(self_); - let other: BigInt = (other).into(); + let other: BigInt = (*other).into(); let other = BigRational::from_integer(other); // Early equality check prevents division by zero when both values are 0 diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs index abf728077c..af80cf4fb1 100644 --- a/crates/driver/src/tests/cases/protocol_fees.rs +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -96,7 +96,7 @@ async fn protocol_fee_test_case(test_case: TestCase) { .await; let result = test.solve().await.ok(); - assert!(result.score().is_approx_eq(test_case.expected_score, None),); + assert!(result.score().is_approx_eq(&test_case.expected_score, None),); result.orders(&[order]); } diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 3e98f4d87d..4681547e14 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -1358,8 +1358,8 @@ impl SolveOk<'_> { Some(executed_amounts) => (executed_amounts.sell, executed_amounts.buy), None => (quoted_order.sell, quoted_order.buy), }; - assert!(u256(trade.get("executedSell").unwrap()).is_approx_eq(expected_sell, None)); - assert!(u256(trade.get("executedBuy").unwrap()).is_approx_eq(expected_buy, None)); + assert!(u256(trade.get("executedSell").unwrap()).is_approx_eq(&expected_sell, None)); + assert!(u256(trade.get("executedBuy").unwrap()).is_approx_eq(&expected_buy, None)); } self } diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 8b68145888..9a52dac68e 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -561,7 +561,7 @@ fn check_solve_request(request: Value, expected: Value) { assert!( request .effective_gas_price - .is_approx_eq(expected.effective_gas_price, Some(15.0)), + .is_approx_eq(&expected.effective_gas_price, Some(15.0)), ); } From 113885872b941504aedbf2223db9dc65d5b72bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 16:20:04 +0000 Subject: [PATCH 16/39] cleanup --- crates/driver/src/domain/eth/gas.rs | 4 ---- crates/driver/src/domain/mempools.rs | 2 +- crates/driver/src/tests/setup/driver.rs | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/driver/src/domain/eth/gas.rs b/crates/driver/src/domain/eth/gas.rs index 5ac3400746..a098bdf555 100644 --- a/crates/driver/src/domain/eth/gas.rs +++ b/crates/driver/src/domain/eth/gas.rs @@ -50,10 +50,6 @@ impl GasPrice { self.base, )) .into() - // let max = self.max.0.0; - // let base = self.base.0.0; - // let tip = self.tip.0.0; - // max.min(base.saturating_add(tip)).into() } pub fn max(&self) -> FeePerGas { diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index 9d346987bf..fc526646e3 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -341,7 +341,7 @@ impl Mempools { ) })?) .into(), - None, + self.ethereum.current_block().borrow().base_fee, ); // in order to replace a tx we need to increase the price Ok(Some(pending_tx_gas_price * GAS_PRICE_BUMP)) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 5730040250..bdd69f6fa7 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -221,9 +221,6 @@ async fn create_config_file( .unwrap(); writeln!(file, "flashloans-enabled = true").unwrap(); writeln!(file, "tx-gas-limit = \"45000000\"").unwrap(); - // Use Web3 gas estimator for tests to match the mock solver's estimator - // and avoid mismatches in effective gas price calculations. - writeln!(file, "gas-estimator = {{ estimator = \"web3\" }}").unwrap(); write!( file, r#"[contracts] From c553a87c275910bbc10205d2b074ddb8ff176ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 15 Jan 2026 17:58:06 +0000 Subject: [PATCH 17/39] clippy --- crates/refunder/src/submitter.rs | 34 +++++++++++++------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index 1282ad657f..4c5caf795f 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -101,29 +101,20 @@ impl Submitter { } } -trait ScaleExt { - fn scaled_by_pct(self, pct: u64) -> Self; - fn scaled_by_pml(self, pml: u64) -> Self; -} - -impl ScaleExt for u128 { - fn scaled_by_pct(self, pct: u64) -> Self { - self * (100 + pct as u128) / 100 - } - - fn scaled_by_pml(self, pml: u64) -> Self { - self * (1000 + pml as u128) / 1000 - } -} - trait Eip1559EstimationExt { fn scaled_by_pml(self, pml: u64) -> Self; } impl Eip1559EstimationExt for Eip1559Estimation { fn scaled_by_pml(mut self, pml: u64) -> Self { - self.max_fee_per_gas = self.max_fee_per_gas.scaled_by_pml(pml); - self.max_priority_fee_per_gas = self.max_priority_fee_per_gas.scaled_by_pml(pml); + self.max_fee_per_gas = { + let n = self.max_fee_per_gas; + n * (1000 + pml as u128) / 1000 + }; + self.max_priority_fee_per_gas = { + let n = self.max_priority_fee_per_gas; + n * (1000 + pml as u128) / 1000 + }; self } } @@ -203,8 +194,9 @@ mod tests { TEST_START_PRIORITY_FEE_TIP, ) .unwrap(); + let expected_result = Eip1559Estimation { - max_fee_per_gas: max_fee_per_gas.scaled_by_pct(GAS_PRICE_BUFFER_PCT), + max_fee_per_gas: max_fee_per_gas * (100 + GAS_PRICE_BUFFER_PCT as u128) / 100, max_priority_fee_per_gas: TEST_START_PRIORITY_FEE_TIP as u128, }; assert_eq!(result, expected_result); @@ -225,9 +217,11 @@ mod tests { ) .unwrap(); let expected_result = Eip1559Estimation { - max_fee_per_gas: max_fee_per_gas_of_last_tx.scaled_by_pml(GAS_PRICE_BUMP_PML), + max_fee_per_gas: max_fee_per_gas_of_last_tx * (1000 + GAS_PRICE_BUMP_PML as u128) + / 1000, max_priority_fee_per_gas: (TEST_START_PRIORITY_FEE_TIP as u128) - .scaled_by_pml(GAS_PRICE_BUMP_PML), + * (1000 + GAS_PRICE_BUMP_PML as u128) + / 1000, }; assert_eq!(result, expected_result); // Thrid case: MAX_GAS_PRICE is not exceeded From 6737b8c1c524302dcdcda2b24163dcb551cbb409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 15:18:22 +0000 Subject: [PATCH 18/39] wip --- crates/driver/src/tests/setup/solver.rs | 1 + .../src/gas_price_estimation/eth_node.rs | 4 +- crates/shared/src/gas_price_estimation/mod.rs | 1 - .../shared/src/gas_price_estimation/price.rs | 193 ------------------ crates/shared/src/order_quoting.rs | 13 +- 5 files changed, 7 insertions(+), 205 deletions(-) delete mode 100644 crates/shared/src/gas_price_estimation/price.rs diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 87a146a274..adfce645da 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -19,6 +19,7 @@ use { itertools::Itertools, serde_json::{Value, json}, serde_with::{DisplayFromStr, serde_as}, + shared::gas_price_estimation::Eip1559EstimationExt, solvers_dto::auction::FlashloanHint, std::{ collections::{HashMap, HashSet}, diff --git a/crates/shared/src/gas_price_estimation/eth_node.rs b/crates/shared/src/gas_price_estimation/eth_node.rs index 2722f2c526..736d8c84d9 100644 --- a/crates/shared/src/gas_price_estimation/eth_node.rs +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -4,8 +4,8 @@ //! This approach is ported from the [`cowprotocol/gas-estimation`](https://github.com/cowprotocol/gas-estimation/tree/v0.7.3) crate's legacy estimation. use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, - alloy::providers::Provider, + crate::gas_price_estimation::GasPriceEstimating, + alloy::{eips::eip1559::Eip1559Estimation, providers::Provider}, anyhow::{Context, Result}, ethrpc::AlloyProvider, }; diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 6e6b539bca..37e98b21ff 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -2,7 +2,6 @@ pub mod alloy; pub mod driver; pub mod eth_node; pub mod fake; -pub mod price; pub mod priority; use { diff --git a/crates/shared/src/gas_price_estimation/price.rs b/crates/shared/src/gas_price_estimation/price.rs deleted file mode 100644 index a8f8d2c287..0000000000 --- a/crates/shared/src/gas_price_estimation/price.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Vendored implementation of GasPrice1559 to start removing the dependency on -// the gas_estimation crate -use serde::Serialize; -use {alloy::eips::eip1559::calc_effective_gas_price, num::Zero}; - -/// EIP1559 gas price -#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Serialize)] -pub struct GasPrice1559 { - // Estimated base fee for the pending block (block currently being mined) - pub base_fee_per_gas: f64, - // Maximum gas price willing to pay for the transaction. - pub max_fee_per_gas: f64, - // Priority fee used to incentivize miners to include the tx in case of network congestion. - pub max_priority_fee_per_gas: f64, -} - -impl GasPrice1559 { - pub fn effective_gas_price(&self) -> f64 { - calc_effective_gas_price( - self.max_fee_per_gas as u128, - self.max_priority_fee_per_gas as u128, - if self.base_fee_per_gas.is_zero() { - None - } else { - Some(self.base_fee_per_gas as u64) - }, - ) as f64 - } - - // Bump gas price by factor. - pub fn bump(self, factor: f64) -> Self { - Self { - max_fee_per_gas: self.max_fee_per_gas * factor, - max_priority_fee_per_gas: self.max_priority_fee_per_gas * factor, - ..self - } - } - - // Ceil gas price (since its defined as float). - pub fn ceil(self) -> Self { - Self { - max_fee_per_gas: self.max_fee_per_gas.ceil(), - max_priority_fee_per_gas: self.max_priority_fee_per_gas.ceil(), - ..self - } - } - - // If current cap if higher then the input, set to input. - pub fn limit_cap(self, cap: f64) -> Self { - Self { - max_fee_per_gas: self.max_fee_per_gas.min(cap), - max_priority_fee_per_gas: self - .max_priority_fee_per_gas - .min(self.max_fee_per_gas.min(cap)), /* enforce max_priority_fee_per_gas <= - * max_fee_per_gas */ - ..self - } - } -} - -impl std::fmt::Display for GasPrice1559 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let format_unit = |wei| { - let gwei: f64 = wei / 1e9; - if gwei >= 1.0 { - format!("{:.2} Gwei", gwei) - } else { - format!("{wei} wei") - } - }; - write!( - f, - "{{ max_fee: {}, max_priority_fee: {}, base_fee: {} }}", - format_unit(self.max_fee_per_gas), - format_unit(self.max_priority_fee_per_gas), - format_unit(self.base_fee_per_gas), - ) - } -} - -#[cfg(test)] -mod tests { - use crate::gas_price_estimation::price::GasPrice1559; - - // Copied from the source: https://github.com/ashleygwilliams/assert_approx_eq/blob/master/src/lib.rs - // should be removed as we move away from expressing gas in f64 - macro_rules! assert_approx_eq { - ($a:expr, $b:expr) => {{ - let eps = 1.0e-6; - let (a, b) = (&$a, &$b); - assert!( - (*a - *b).abs() < eps, - "assertion failed: `(left !== right)` (left: `{:?}`, right: `{:?}`, expect diff: \ - `{:?}`, real diff: `{:?}`)", - *a, - *b, - eps, - (*a - *b).abs() - ); - }}; - } - - #[test] - fn bump_and_ceil() { - let gas_price = GasPrice1559 { - max_fee_per_gas: 2.0, - max_priority_fee_per_gas: 3.0, - ..Default::default() - }; - - let gas_price_bumped = GasPrice1559 { - max_fee_per_gas: 2.25, - max_priority_fee_per_gas: 3.375, - ..Default::default() - }; - - let gas_price_bumped_and_ceiled = GasPrice1559 { - max_fee_per_gas: 3.0, - max_priority_fee_per_gas: 4.0, - ..Default::default() - }; - - assert_eq!(gas_price.bump(1.125), gas_price_bumped); - assert_eq!(gas_price.bump(1.125).ceil(), gas_price_bumped_and_ceiled); - } - - #[test] - fn limit_cap_only_max_fee_capped() { - let gas_price = GasPrice1559 { - max_fee_per_gas: 5.0, - max_priority_fee_per_gas: 3.0, - ..Default::default() - }; - - let gas_price_capped = GasPrice1559 { - max_fee_per_gas: 4.0, - max_priority_fee_per_gas: 3.0, - ..Default::default() - }; - - assert_eq!(gas_price.limit_cap(4.0), gas_price_capped); - } - - #[test] - fn limit_cap_max_fee_and_max_priority_capped() { - let gas_price = GasPrice1559 { - max_fee_per_gas: 5.0, - max_priority_fee_per_gas: 3.0, - ..Default::default() - }; - - let gas_price_capped = GasPrice1559 { - max_fee_per_gas: 2.0, - max_priority_fee_per_gas: 2.0, - ..Default::default() - }; - - assert_eq!(gas_price.limit_cap(2.0), gas_price_capped); - } - - #[test] - fn estimate_eip1559() { - assert_approx_eq!( - GasPrice1559 { - max_fee_per_gas: 10.0, - max_priority_fee_per_gas: 5.0, - base_fee_per_gas: 2.0 - } - .effective_gas_price(), - 7.0 - ); - - assert_approx_eq!( - GasPrice1559 { - max_fee_per_gas: 10.0, - max_priority_fee_per_gas: 8.0, - base_fee_per_gas: 2.0 - } - .effective_gas_price(), - 10.0 - ); - - assert_approx_eq!( - GasPrice1559 { - max_fee_per_gas: 10.0, - max_priority_fee_per_gas: 10.0, - base_fee_per_gas: 2.0 - } - .effective_gas_price(), - 10.0 - ); - } -} diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 6e1e60a79f..193c40eaeb 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -1,9 +1,6 @@ use { super::price_estimation::{ - self, - PriceEstimating, - PriceEstimationError, - native::NativePriceEstimating, + self, PriceEstimating, PriceEstimationError, native::NativePriceEstimating, }, crate::{ account_balances::{BalanceFetching, Query}, @@ -792,15 +789,13 @@ mod tests { super::*, crate::{ account_balances::MockBalanceFetching, - gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, + gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ - HEALTHY_PRICE_ESTIMATION_TIME, - MockPriceEstimating, + HEALTHY_PRICE_ESTIMATION_TIME, MockPriceEstimating, native::MockNativePriceEstimating, }, }, - Address, - U256 as AlloyU256, + Address, U256 as AlloyU256, alloy::eips::eip1559::Eip1559Estimation, chrono::Utc, futures::FutureExt, From e3767cce1d5145ba0c01a8d13083dfc7e56c9f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 15:51:03 +0000 Subject: [PATCH 19/39] Precision loss fix --- .../domain/competition/solution/settlement.rs | 4 +++- crates/driver/src/infra/blockchain/gas.rs | 19 ++++++++++++++----- .../shared/src/gas_price_estimation/fake.rs | 3 ++- crates/shared/src/gas_price_estimation/mod.rs | 3 ++- .../src/gas_price_estimation/priority.rs | 4 +++- crates/shared/src/order_quoting.rs | 11 ++++++++--- .../src/price_estimation/competition/quote.rs | 10 ++++++++-- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 8bf6e3d93f..f1374aa02d 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -175,7 +175,9 @@ impl Settlement { // Ensure that the solver has sufficient balance for the settlement to be mined // even if the gas price keeps climbing during the tx submission. - let required_eth_balance = gas.required_balance(U256::from(price.max_fee_per_gas * 2)); + let required_eth_balance = + // Converting to U256 first avoids possible overflow + gas.required_balance(U256::from(price.max_fee_per_gas) * U256::from(2)); if eth.balance(solution.solver().address()).await? < required_eth_balance { return Err(Error::SolverAccountInsufficientBalance( required_eth_balance, diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index bd0273a9e4..38143b52b6 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -8,10 +8,12 @@ use { domain::eth, infra::{config::file::GasEstimatorType, mempool}, }, - alloy::{eips::eip1559::Eip1559Estimation, primitives::U256}, + alloy::eips::eip1559::Eip1559Estimation, ethrpc::Web3, shared::gas_price_estimation::{ - GasPriceEstimating, alloy::Eip1559GasPriceEstimator, eth_node::NodeGasPriceEstimator, + GasPriceEstimating, + alloy::Eip1559GasPriceEstimator, + eth_node::NodeGasPriceEstimator, }, std::sync::Arc, }; @@ -81,9 +83,16 @@ impl GasPriceEstimator { // the driver supports tweaking the tx gas price tip in case the gas // price estimator is systematically too low => compute configured tip bump let (max_additional_tip, tip_percentage_increase) = self.additional_tip; - let additional_tip = (max_additional_tip).min(U256::from( - (estimate.max_priority_fee_per_gas as f64) * tip_percentage_increase, - )); + + // Calculate additional tip in integer space to avoid precision loss + // Convert percentage to basis points (multiply by 1000) to maintain precision + // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 125 + let tip_percentage_as_bps = (tip_percentage_increase * 1000.0) as u128; + let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas) + * eth::U256::from(tip_percentage_as_bps) + / eth::U256::from(1000u128); + + let additional_tip = max_additional_tip.min(calculated_tip); // make sure we tip at least some configurable minimum amount std::cmp::max( diff --git a/crates/shared/src/gas_price_estimation/fake.rs b/crates/shared/src/gas_price_estimation/fake.rs index e56d95e229..aebc8fd3b6 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -1,5 +1,6 @@ use { - crate::gas_price_estimation::GasPriceEstimating, alloy::eips::eip1559::Eip1559Estimation, + crate::gas_price_estimation::GasPriceEstimating, + alloy::eips::eip1559::Eip1559Estimation, anyhow::Result, }; diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 37e98b21ff..335da0b990 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -8,7 +8,8 @@ use { crate::{ ethrpc::Web3, gas_price_estimation::{ - alloy::Eip1559GasPriceEstimator, eth_node::NodeGasPriceEstimator, + alloy::Eip1559GasPriceEstimator, + eth_node::NodeGasPriceEstimator, priority::PriorityGasPriceEstimating, }, http_client::HttpClientFactory, diff --git a/crates/shared/src/gas_price_estimation/priority.rs b/crates/shared/src/gas_price_estimation/priority.rs index 72059c80cc..241019ca03 100644 --- a/crates/shared/src/gas_price_estimation/priority.rs +++ b/crates/shared/src/gas_price_estimation/priority.rs @@ -72,7 +72,9 @@ impl GasPriceEstimating for PriorityGasPriceEstimating { mod tests { use { crate::gas_price_estimation::{ - GasPriceEstimating, MockGasPriceEstimating, priority::PriorityGasPriceEstimating, + GasPriceEstimating, + MockGasPriceEstimating, + priority::PriorityGasPriceEstimating, }, alloy::eips::eip1559::Eip1559Estimation, anyhow::anyhow, diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 193c40eaeb..3080da1c33 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -1,6 +1,9 @@ use { super::price_estimation::{ - self, PriceEstimating, PriceEstimationError, native::NativePriceEstimating, + self, + PriceEstimating, + PriceEstimationError, + native::NativePriceEstimating, }, crate::{ account_balances::{BalanceFetching, Query}, @@ -791,11 +794,13 @@ mod tests { account_balances::MockBalanceFetching, gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ - HEALTHY_PRICE_ESTIMATION_TIME, MockPriceEstimating, + HEALTHY_PRICE_ESTIMATION_TIME, + MockPriceEstimating, native::MockNativePriceEstimating, }, }, - Address, U256 as AlloyU256, + Address, + U256 as AlloyU256, alloy::eips::eip1559::Eip1559Estimation, chrono::Utc, futures::FutureExt, diff --git a/crates/shared/src/price_estimation/competition/quote.rs b/crates/shared/src/price_estimation/competition/quote.rs index 6fa015ff9a..872c317bd6 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -1,7 +1,11 @@ use { super::{CompetitionEstimator, PriceRanking, compare_error}, crate::price_estimation::{ - Estimate, PriceEstimateResult, PriceEstimating, PriceEstimationError, Query, + Estimate, + PriceEstimateResult, + PriceEstimating, + PriceEstimationError, + Query, QuoteVerificationMode, }, alloy::eips::eip1559::calc_effective_gas_price, @@ -161,7 +165,9 @@ mod tests { crate::{ gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ - MockPriceEstimating, QuoteVerificationMode, native::MockNativePriceEstimating, + MockPriceEstimating, + QuoteVerificationMode, + native::MockNativePriceEstimating, }, }, alloy::{eips::eip1559::Eip1559Estimation, primitives::U256}, From 4ff8baa8b176d60e875f87bb10275f48169da039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= Date: Fri, 16 Jan 2026 17:07:18 +0000 Subject: [PATCH 20/39] Update crates/driver/src/domain/eth/gas.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- crates/driver/src/domain/eth/gas.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/eth/gas.rs b/crates/driver/src/domain/eth/gas.rs index a098bdf555..a0c402c30a 100644 --- a/crates/driver/src/domain/eth/gas.rs +++ b/crates/driver/src/domain/eth/gas.rs @@ -45,8 +45,8 @@ impl GasPrice { /// Returns the estimated [`EffectiveGasPrice`] for the gas price estimate. pub fn effective(&self) -> EffectiveGasPrice { U256::from(calc_effective_gas_price( - u128::try_from(self.max.0.0).unwrap(), - u128::try_from(self.tip.0.0).unwrap(), + u128::try_from(self.max.0.0).expect("max fee per gas should fit in a u128"), + u128::try_from(self.tip.0.0).expect("max priority fee per gas should fit in a u128"), self.base, )) .into() From 75ba21111b9a5398c2f95fb482ca631d9e6f8be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 17:10:17 +0000 Subject: [PATCH 21/39] Merge Eip1559Estimation extension traits --- crates/refunder/src/submitter.rs | 23 ++++--------------- crates/shared/src/gas_price_estimation/mod.rs | 17 ++++++++++++++ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index 4c5caf795f..a77cb66304 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -13,7 +13,10 @@ use { anyhow::{Context, Result}, contracts::alloy::CoWSwapEthFlow::{self, EthFlowOrder}, database::OrderUid, - shared::{ethrpc::Web3, gas_price_estimation::GasPriceEstimating}, + shared::{ + ethrpc::Web3, + gas_price_estimation::{Eip1559EstimationExt, GasPriceEstimating}, + }, std::time::Duration, }; @@ -101,24 +104,6 @@ impl Submitter { } } -trait Eip1559EstimationExt { - fn scaled_by_pml(self, pml: u64) -> Self; -} - -impl Eip1559EstimationExt for Eip1559Estimation { - fn scaled_by_pml(mut self, pml: u64) -> Self { - self.max_fee_per_gas = { - let n = self.max_fee_per_gas; - n * (1000 + pml as u128) / 1000 - }; - self.max_priority_fee_per_gas = { - let n = self.max_priority_fee_per_gas; - n * (1000 + pml as u128) / 1000 - }; - self - } -} - fn calculate_submission_gas_price( gas_price_of_last_submission: Option, web3_gas_estimation: Eip1559Estimation, diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 335da0b990..87e38a8395 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -86,8 +86,13 @@ pub async fn create_priority_estimator( Ok(PriorityGasPriceEstimating::new(estimators)) } +/// Extension trait for EIP-1559 gas price estimations. pub trait Eip1559EstimationExt { + /// Calculates the effective gas price that will be paid given the base fee. fn effective(self, base_fee: Option) -> u128; + + /// Scales fees by a multiplier in parts per thousand (e.g., 100 = +10%). + fn scaled_by_pml(self, pml: u64) -> Self; } impl Eip1559EstimationExt for Eip1559Estimation { @@ -98,4 +103,16 @@ impl Eip1559EstimationExt for Eip1559Estimation { base_fee, ) } + + fn scaled_by_pml(mut self, pml: u64) -> Self { + self.max_fee_per_gas = { + let n = self.max_fee_per_gas; + n * (1000 + pml as u128) / 1000 + }; + self.max_priority_fee_per_gas = { + let n = self.max_priority_fee_per_gas; + n * (1000 + pml as u128) / 1000 + }; + self + } } From c2e52c27f21a247bdcbd9d031798aed750443358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 17:12:17 +0000 Subject: [PATCH 22/39] Replace GasPriceResponse with the equivalent Eip1559Estimation --- crates/driver/src/infra/api/routes/gasprice.rs | 14 +++----------- crates/shared/src/gas_price_estimation/driver.rs | 11 +---------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/crates/driver/src/infra/api/routes/gasprice.rs b/crates/driver/src/infra/api/routes/gasprice.rs index cb861bce0a..175d13dec6 100644 --- a/crates/driver/src/infra/api/routes/gasprice.rs +++ b/crates/driver/src/infra/api/routes/gasprice.rs @@ -1,7 +1,7 @@ use { crate::infra::{Ethereum, api::error::Error}, + alloy::eips::eip1559::Eip1559Estimation, axum::Json, - serde::{Deserialize, Serialize}, tracing::instrument, }; @@ -9,22 +9,14 @@ pub(in crate::infra::api) fn gasprice(app: axum::Router) -> axum::Rout app.route("/gasprice", axum::routing::get(route)) } -/// Gas price components in EIP-1559 format. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GasPriceResponse { - pub max_fee_per_gas: u128, - pub max_priority_fee_per_gas: u128, -} - #[instrument(skip(eth))] async fn route( eth: axum::extract::State, -) -> Result, (hyper::StatusCode, axum::Json)> { +) -> Result, (hyper::StatusCode, axum::Json)> { // For simplicity we use the default time limit (None) let gas_price = eth.gas_price().await?; - Ok(Json(GasPriceResponse { + Ok(Json(Eip1559Estimation { max_fee_per_gas: gas_price.max_fee_per_gas, max_priority_fee_per_gas: gas_price.max_priority_fee_per_gas, })) diff --git a/crates/shared/src/gas_price_estimation/driver.rs b/crates/shared/src/gas_price_estimation/driver.rs index d21478285f..d922375a96 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -3,7 +3,6 @@ use { alloy::eips::eip1559::Eip1559Estimation, anyhow::{Context, Result}, reqwest::Url, - serde::Deserialize, std::{ sync::Arc, time::{Duration, Instant}, @@ -27,14 +26,6 @@ struct CachedGasPrice { timestamp: Instant, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -/// Gas price components in EIP-1559 format. -struct GasPriceResponse { - max_fee_per_gas: u128, - max_priority_fee_per_gas: u128, -} - const CACHE_DURATION: Duration = Duration::from_secs(5); impl DriverGasEstimator { @@ -56,7 +47,7 @@ impl DriverGasEstimator { .context("failed to send request to driver")? .error_for_status() .context("driver returned error status")? - .json::() + .json::() .await .context("failed to parse driver response")?; From 387ce1f80cd7ab1a794099ca9b76729b2aaff15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 17:13:15 +0000 Subject: [PATCH 23/39] Provide base fee in driver mempool --- crates/driver/src/domain/competition/auction.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index a328bafd55..10bc830db7 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -58,10 +58,11 @@ impl Auction { }); let gas_est = eth.gas_price().await?; + let base_fee = eth.current_block().borrow().base_fee; let gas_price = GasPrice::new( U256::from(gas_est.max_fee_per_gas).into(), U256::from(gas_est.max_priority_fee_per_gas).into(), - None, + base_fee, ); Ok(Self { From b4a7291c6bdc9019f31bcb3d58c74f33aa221062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 17:33:13 +0000 Subject: [PATCH 24/39] Fix approx_eq logs and tighten comparison margin --- crates/driver/src/tests/cases/mod.rs | 4 +--- crates/driver/src/tests/setup/solver.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index d531166520..33971c3af6 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -160,14 +160,12 @@ where let expected_delta = BigRational::from_f64(delta.unwrap_or(0.000000001)) .expect("delta should be representable using BigRational"); - // We can't use num::Unsigned due to ruint::U256 not implementing it - // (due to limitations on const generics) // Calculate relative error: |actual - expected| / |expected| // Ensures correct behavior with negative numbers let diff = (self_.clone() - other.clone()).abs(); let calculated_delta = diff / other.abs(); - tracing::debug!("{expected_delta} < {calculated_delta}"); + tracing::debug!("{calculated_delta} <= {expected_delta}",); calculated_delta <= expected_delta } diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index adfce645da..d5149e7abc 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -555,7 +555,7 @@ fn check_solve_request(request: Value, expected: Value) { assert!( request .effective_gas_price - .is_approx_eq(&expected.effective_gas_price, Some(15.0)), + .is_approx_eq(&expected.effective_gas_price, Some(1.0)), // 1.0% ); } From a28e5b543f0220371e1f275184a13a861ea03871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 16 Jan 2026 17:44:10 +0000 Subject: [PATCH 25/39] docs --- crates/driver/src/tests/cases/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index 33971c3af6..8b949b9b2e 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -126,6 +126,11 @@ pub trait ApproxEq { /// Checks if two values are approximately equal within a relative error /// threshold. /// + /// # Panics + /// + /// Panics if `other` is 0 but `self` is not 0, due to division by zero + /// in the relative error calculation. + /// /// # Examples /// /// ```ignore From 55c216d14cc426701d8b101e876281c886b8759c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 11:31:29 +0000 Subject: [PATCH 26/39] fix basis points --- crates/driver/src/infra/blockchain/gas.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index 38143b52b6..c7e68f05e2 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -85,12 +85,12 @@ impl GasPriceEstimator { let (max_additional_tip, tip_percentage_increase) = self.additional_tip; // Calculate additional tip in integer space to avoid precision loss - // Convert percentage to basis points (multiply by 1000) to maintain precision - // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 125 - let tip_percentage_as_bps = (tip_percentage_increase * 1000.0) as u128; + // Convert percentage to basis points (multiply by 10000) to maintain precision + // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 1250 + let tip_percentage_as_bps = (tip_percentage_increase * 10000.0) as u128; let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas) * eth::U256::from(tip_percentage_as_bps) - / eth::U256::from(1000u128); + / eth::U256::from(10000u128); let additional_tip = max_additional_tip.min(calculated_tip); From 26fbe9d4eecb1b4e7560519ca2f1026d0e9c8e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 11:44:15 +0000 Subject: [PATCH 27/39] Replace the GasPrice struct with Eip1559Estimation for mempool --- crates/driver/src/domain/mempools.rs | 49 ++++++++++++-------------- crates/driver/src/infra/mempool/mod.rs | 18 +++------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index fc526646e3..705fe1ee60 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -4,11 +4,11 @@ use { domain::{ BlockNo, competition::solution::Settlement, - eth::{GasPrice, TxId, TxStatus}, + eth::{TxId, TxStatus}, }, infra::{self, Ethereum, observe, solver::Solver}, }, - alloy::{consensus::Transaction, primitives::U256}, + alloy::{consensus::Transaction, eips::eip1559::Eip1559Estimation}, anyhow::Context, ethrpc::block_stream::into_stream, futures::{FutureExt, StreamExt, future::select_ok}, @@ -18,9 +18,9 @@ use { /// Factor by how much a transaction fee needs to be increased to override a /// pending transaction at the same nonce. The correct factor is actually -/// 1.125 but to avoid rounding issues on chains with very low gas prices +/// 12.5% but to avoid rounding issues on chains with very low gas prices /// we increase slightly more. -const GAS_PRICE_BUMP: f64 = 1.13; +const GAS_PRICE_BUMP_PCT: u64 = 13; /// The gas amount required to cancel a transaction. const CANCELLATION_GAS_AMOUNT: u64 = 21000; @@ -146,16 +146,14 @@ impl Mempools { .await; let final_gas_price = match &replacement_gas_price { Ok(Some(replacement_gas_price)) - if replacement_gas_price.max() - > U256::from(current_gas_price.max_fee_per_gas).into() => + if replacement_gas_price.max_fee_per_gas > current_gas_price.max_fee_per_gas => { *replacement_gas_price } - _ => GasPrice::new( - U256::from(current_gas_price.max_fee_per_gas).into(), - U256::from(current_gas_price.max_priority_fee_per_gas).into(), - self.ethereum.current_block().borrow().base_fee, - ), + _ => Eip1559Estimation { + max_fee_per_gas: current_gas_price.max_fee_per_gas, + max_priority_fee_per_gas: current_gas_price.max_priority_fee_per_gas, + }, }; tracing::debug!( @@ -273,11 +271,11 @@ impl Mempools { async fn cancel( &self, mempool: &infra::mempool::Mempool, - original_tx_gas_price: eth::GasPrice, + original_tx_gas_price: Eip1559Estimation, solver: &Solver, nonce: u64, ) -> Result { - let fallback_gas_price = original_tx_gas_price * GAS_PRICE_BUMP; + let fallback_gas_price = original_tx_gas_price.scaled_by_pct(GAS_PRICE_BUMP_PCT); let replacement_gas_price = self .minimum_replacement_gas_price(mempool, solver, nonce) .await; @@ -323,28 +321,27 @@ impl Mempools { mempool: &infra::Mempool, solver: &Solver, nonce: u64, - ) -> anyhow::Result> { - let pending_tx = match mempool + ) -> anyhow::Result> { + let Some(pending_tx) = mempool .find_pending_tx_in_mempool(solver.address(), nonce) .await? - { - Some(tx) => tx, - None => return Ok(None), + else { + return Ok(None); }; - let pending_tx_gas_price = eth::GasPrice::new( - eth::U256::from(pending_tx.max_fee_per_gas()).into(), - eth::U256::from(pending_tx.max_priority_fee_per_gas().with_context(|| { + let pending_tx_gas_price = Eip1559Estimation { + max_fee_per_gas: pending_tx.max_fee_per_gas(), + max_priority_fee_per_gas: pending_tx.max_priority_fee_per_gas().with_context(|| { format!( "pending tx is not EIP 1559 ({})", pending_tx.inner.tx_hash() ) - })?) - .into(), - self.ethereum.current_block().borrow().base_fee, - ); + })?, + } // in order to replace a tx we need to increase the price - Ok(Some(pending_tx_gas_price * GAS_PRICE_BUMP)) + .scaled_by_pct(GAS_PRICE_BUMP_PCT); + + Ok(Some(pending_tx_gas_price)) } } diff --git a/crates/driver/src/infra/mempool/mod.rs b/crates/driver/src/infra/mempool/mod.rs index ac25841d3f..3605280a2c 100644 --- a/crates/driver/src/infra/mempool/mod.rs +++ b/crates/driver/src/infra/mempool/mod.rs @@ -6,7 +6,7 @@ use { }, alloy::{ consensus::Transaction, - eips::BlockNumberOrTag, + eips::{BlockNumberOrTag, eip1559::Eip1559Estimation}, providers::{Provider, ext::TxPoolApi}, rpc::types::TransactionRequest, }, @@ -107,23 +107,13 @@ impl Mempool { pub async fn submit( &self, tx: eth::Tx, - gas_price: eth::GasPrice, + gas_price: Eip1559Estimation, gas_limit: eth::Gas, solver: &infra::Solver, nonce: u64, ) -> Result { - let max_fee_per_gas = gas_price - .max() - .0 - .0 - .try_into() - .map_err(anyhow::Error::from)?; - let max_priority_fee_per_gas = gas_price - .tip() - .0 - .0 - .try_into() - .map_err(anyhow::Error::from)?; + let max_fee_per_gas = gas_price.max_fee_per_gas; + let max_priority_fee_per_gas = gas_price.max_priority_fee_per_gas; let gas_limit = gas_limit.0.try_into().map_err(anyhow::Error::from)?; let tx_request = TransactionRequest::default() From 5e6f1050eeb3871fc802052022f1a2306be1782e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 11:47:42 +0000 Subject: [PATCH 28/39] Make base_fee mandatory --- crates/driver/src/domain/competition/auction.rs | 2 +- crates/driver/src/infra/blockchain/mod.rs | 9 ++++++++- crates/driver/src/tests/setup/solver.rs | 2 +- crates/ethrpc/src/block_stream/mod.rs | 6 ++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 10bc830db7..3558f06bb3 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -62,7 +62,7 @@ impl Auction { let gas_price = GasPrice::new( U256::from(gas_est.max_fee_per_gas).into(), U256::from(gas_est.max_priority_fee_per_gas).into(), - base_fee, + Some(base_fee), ); Ok(Self { diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index d3d58dcfb8..dea9537e04 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -301,7 +301,14 @@ impl Ethereum { // default value we estimate the current gas price upfront. But because it's // extremely rare that tokens behave that way we are fine with falling back to // the node specific fallback value instead of failing the whole call. - Some(self.inner.gas.estimate().await.ok()?.effective(base_fee)) + Some( + self.inner + .gas + .estimate() + .await + .ok()? + .effective(Some(base_fee)), + ) } pub fn web3(&self) -> &Web3 { diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index d5149e7abc..68e5ed6966 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -489,7 +489,7 @@ impl Solver { move |axum::extract::State(state): axum::extract::State, axum::extract::Json(req): axum::extract::Json| async move { let base_fee = eth.current_block().borrow().base_fee; - let effective_gas_price = eth.gas_price().await.unwrap().effective(base_fee).to_string(); + let effective_gas_price = eth.gas_price().await.unwrap().effective(Some(base_fee)).to_string(); let expected = json!({ "id": (!config.quote).then_some("1"), "tokens": tokens_json, diff --git a/crates/ethrpc/src/block_stream/mod.rs b/crates/ethrpc/src/block_stream/mod.rs index 45a5456df7..c1538574fa 100644 --- a/crates/ethrpc/src/block_stream/mod.rs +++ b/crates/ethrpc/src/block_stream/mod.rs @@ -56,7 +56,7 @@ pub struct BlockInfo { pub timestamp: u64, pub gas_limit: U256, pub gas_price: U256, - pub base_fee: Option, + pub base_fee: u64, /// When the system noticed the new block. pub observed_at: Instant, } @@ -109,7 +109,9 @@ impl TryFrom for BlockInfo { .base_fee_per_gas .map(U256::from) .context("no gas price")?, - base_fee: value.base_fee_per_gas, + base_fee: value + .base_fee_per_gas + .ok_or_else(|| anyhow!("no base fee available"))?, observed_at: Instant::now(), }) } From 61d7778313e357a638fdb1c4591f5408a660dd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 11:48:14 +0000 Subject: [PATCH 29/39] Rename PML -> PERMIL --- crates/refunder/src/submitter.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index a77cb66304..73d8a074af 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -28,7 +28,7 @@ const GAS_PRICE_BUFFER_PCT: u64 = 30; // In order to resubmit a new tx with the same nonce, the gas tip and // max_fee_per_gas needs to be increased by at least 10 percent. -const GAS_PRICE_BUMP_PML: u64 = 125; +const GAS_PRICE_BUMP_PERMIL: u64 = 125; pub struct Submitter { pub web3: Web3, @@ -126,7 +126,7 @@ fn calculate_submission_gas_price( && let Some(gas_price_of_last_submission) = gas_price_of_last_submission { let gas_price_of_last_submission = - gas_price_of_last_submission.scaled_by_pml(GAS_PRICE_BUMP_PML); + gas_price_of_last_submission.scaled_by_pml(GAS_PRICE_BUMP_PERMIL); new_gas_price.max_fee_per_gas = new_gas_price .max_fee_per_gas .max(gas_price_of_last_submission.max_fee_per_gas); @@ -202,10 +202,10 @@ mod tests { ) .unwrap(); let expected_result = Eip1559Estimation { - max_fee_per_gas: max_fee_per_gas_of_last_tx * (1000 + GAS_PRICE_BUMP_PML as u128) + max_fee_per_gas: max_fee_per_gas_of_last_tx * (1000 + GAS_PRICE_BUMP_PERMIL as u128) / 1000, max_priority_fee_per_gas: (TEST_START_PRIORITY_FEE_TIP as u128) - * (1000 + GAS_PRICE_BUMP_PML as u128) + * (1000 + GAS_PRICE_BUMP_PERMIL as u128) / 1000, }; assert_eq!(result, expected_result); From 8e26a48538eb85139cbaf28610fc6298f7f2fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 11:50:15 +0000 Subject: [PATCH 30/39] Make base_fee mandatory for effective() --- crates/driver/src/infra/blockchain/mod.rs | 9 +-------- crates/driver/src/tests/setup/solver.rs | 2 +- crates/shared/src/gas_price_estimation/mod.rs | 6 +++--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index dea9537e04..d3d58dcfb8 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -301,14 +301,7 @@ impl Ethereum { // default value we estimate the current gas price upfront. But because it's // extremely rare that tokens behave that way we are fine with falling back to // the node specific fallback value instead of failing the whole call. - Some( - self.inner - .gas - .estimate() - .await - .ok()? - .effective(Some(base_fee)), - ) + Some(self.inner.gas.estimate().await.ok()?.effective(base_fee)) } pub fn web3(&self) -> &Web3 { diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 68e5ed6966..d5149e7abc 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -489,7 +489,7 @@ impl Solver { move |axum::extract::State(state): axum::extract::State, axum::extract::Json(req): axum::extract::Json| async move { let base_fee = eth.current_block().borrow().base_fee; - let effective_gas_price = eth.gas_price().await.unwrap().effective(Some(base_fee)).to_string(); + let effective_gas_price = eth.gas_price().await.unwrap().effective(base_fee).to_string(); let expected = json!({ "id": (!config.quote).then_some("1"), "tokens": tokens_json, diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 87e38a8395..9fcaba277e 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -89,18 +89,18 @@ pub async fn create_priority_estimator( /// Extension trait for EIP-1559 gas price estimations. pub trait Eip1559EstimationExt { /// Calculates the effective gas price that will be paid given the base fee. - fn effective(self, base_fee: Option) -> u128; + fn effective(self, base_fee: u64) -> u128; /// Scales fees by a multiplier in parts per thousand (e.g., 100 = +10%). fn scaled_by_pml(self, pml: u64) -> Self; } impl Eip1559EstimationExt for Eip1559Estimation { - fn effective(self, base_fee: Option) -> u128 { + fn effective(self, base_fee: u64) -> u128 { calc_effective_gas_price( self.max_fee_per_gas, self.max_priority_fee_per_gas, - base_fee, + Some(base_fee), ) } From 457826b14b6027f669b19ff7af186ca5fc3fe5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 12:27:07 +0000 Subject: [PATCH 31/39] Add base_fee and effective_gas_price to estimators --- crates/shared/src/gas_price.rs | 4 ++++ .../shared/src/gas_price_estimation/alloy.rs | 17 +++++++++++++-- .../shared/src/gas_price_estimation/driver.rs | 21 ++++++++++++++++--- .../src/gas_price_estimation/eth_node.rs | 17 +++++++++++++-- .../shared/src/gas_price_estimation/fake.rs | 4 ++++ crates/shared/src/gas_price_estimation/mod.rs | 13 ++++++++++++ .../src/gas_price_estimation/priority.rs | 8 +++++-- crates/shared/src/order_quoting.rs | 15 ++++--------- .../src/price_estimation/competition/quote.rs | 10 +-------- 9 files changed, 80 insertions(+), 29 deletions(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 5dc06f0c46..c5dbb3ed75 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -46,6 +46,10 @@ where ); Ok(estimate) } + + async fn base_fee(&self) -> Result> { + self.inner.base_fee().await + } } #[derive(prometheus_metric_storage::MetricStorage)] diff --git a/crates/shared/src/gas_price_estimation/alloy.rs b/crates/shared/src/gas_price_estimation/alloy.rs index 7ada57385a..9ec1392f28 100644 --- a/crates/shared/src/gas_price_estimation/alloy.rs +++ b/crates/shared/src/gas_price_estimation/alloy.rs @@ -7,8 +7,11 @@ use { crate::gas_price_estimation::GasPriceEstimating, - alloy::{eips::eip1559::Eip1559Estimation, providers::Provider}, - anyhow::Result, + alloy::{ + eips::{BlockId, eip1559::Eip1559Estimation}, + providers::Provider, + }, + anyhow::{Result, anyhow}, ethrpc::AlloyProvider, futures::TryFutureExt, tracing::instrument, @@ -40,4 +43,14 @@ impl GasPriceEstimating for Eip1559GasPriceEstimator { max_priority_fee_per_gas: fees.max_priority_fee_per_gas, }) } + + async fn base_fee(&self) -> Result> { + Ok(self + .0 + .get_block(BlockId::latest()) + .await? + .ok_or_else(|| anyhow!("fecthed block does not have header"))? + .header + .base_fee_per_gas) + } } diff --git a/crates/shared/src/gas_price_estimation/driver.rs b/crates/shared/src/gas_price_estimation/driver.rs index d922375a96..5e1d7b1c16 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -1,7 +1,10 @@ use { crate::gas_price_estimation::GasPriceEstimating, - alloy::eips::eip1559::Eip1559Estimation, - anyhow::{Context, Result}, + alloy::{ + eips::{BlockId, eip1559::Eip1559Estimation}, + providers::{DynProvider, Provider}, + }, + anyhow::{Context, Result, anyhow}, reqwest::Url, std::{ sync::Arc, @@ -18,6 +21,7 @@ pub struct DriverGasEstimator { client: reqwest::Client, url: Url, cache: Arc>>, + provider: DynProvider, } #[derive(Debug, Clone)] @@ -29,11 +33,12 @@ struct CachedGasPrice { const CACHE_DURATION: Duration = Duration::from_secs(5); impl DriverGasEstimator { - pub fn new(client: reqwest::Client, driver_url: Url) -> Self { + pub fn new(client: reqwest::Client, driver_url: Url, provider: DynProvider) -> Self { Self { client, url: driver_url, cache: Arc::new(Mutex::new(None)), + provider, } } @@ -81,4 +86,14 @@ impl GasPriceEstimating for DriverGasEstimator { Ok(price) } + + async fn base_fee(&self) -> Result> { + Ok(self + .provider + .get_block(BlockId::latest()) + .await? + .ok_or_else(|| anyhow!("fecthed block does not have header"))? + .header + .base_fee_per_gas) + } } diff --git a/crates/shared/src/gas_price_estimation/eth_node.rs b/crates/shared/src/gas_price_estimation/eth_node.rs index 736d8c84d9..fa7fad9508 100644 --- a/crates/shared/src/gas_price_estimation/eth_node.rs +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -5,8 +5,11 @@ use { crate::gas_price_estimation::GasPriceEstimating, - alloy::{eips::eip1559::Eip1559Estimation, providers::Provider}, - anyhow::{Context, Result}, + alloy::{ + eips::{BlockId, eip1559::Eip1559Estimation}, + providers::Provider, + }, + anyhow::{Context, Result, anyhow}, ethrpc::AlloyProvider, }; @@ -35,4 +38,14 @@ impl GasPriceEstimating for NodeGasPriceEstimator { max_priority_fee_per_gas: legacy, }) } + + async fn base_fee(&self) -> Result> { + Ok(self + .0 + .get_block(BlockId::latest()) + .await? + .ok_or_else(|| anyhow!("fecthed block does not have header"))? + .header + .base_fee_per_gas) + } } diff --git a/crates/shared/src/gas_price_estimation/fake.rs b/crates/shared/src/gas_price_estimation/fake.rs index aebc8fd3b6..d19facce0f 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -26,4 +26,8 @@ impl GasPriceEstimating for FakeGasPriceEstimator { async fn estimate(&self) -> Result { Ok(self.0) } + + async fn base_fee(&self) -> Result> { + Ok(Default::default()) + } } diff --git a/crates/shared/src/gas_price_estimation/mod.rs b/crates/shared/src/gas_price_estimation/mod.rs index 9fcaba277e..b2efb5368c 100644 --- a/crates/shared/src/gas_price_estimation/mod.rs +++ b/crates/shared/src/gas_price_estimation/mod.rs @@ -30,6 +30,18 @@ pub use {driver::DriverGasEstimator, fake::FakeGasPriceEstimator}; pub trait GasPriceEstimating: Send + Sync { /// Estimate the gas price for a transaction to be mined "quickly". async fn estimate(&self) -> Result; + + async fn base_fee(&self) -> Result>; + + async fn effective_gas_price(&self) -> Result { + let estimate = self.estimate().await?; + let base_fee = self.base_fee().await?; + Ok(calc_effective_gas_price( + estimate.max_fee_per_gas, + estimate.max_priority_fee_per_gas, + base_fee, + )) + } } #[derive(Clone, Debug)] @@ -69,6 +81,7 @@ pub async fn create_priority_estimator( estimators.push(Box::new(DriverGasEstimator::new( http_factory.create(), url.clone(), + web3.alloy.clone(), ))); } GasEstimatorType::Web3 => { diff --git a/crates/shared/src/gas_price_estimation/priority.rs b/crates/shared/src/gas_price_estimation/priority.rs index 241019ca03..c6ad5fca19 100644 --- a/crates/shared/src/gas_price_estimation/priority.rs +++ b/crates/shared/src/gas_price_estimation/priority.rs @@ -36,10 +36,10 @@ impl PriorityGasPriceEstimating { Self { estimators } } - async fn prioritize<'a, T, F>(&'a self, operation: T) -> Result + async fn prioritize<'a, T, F, O>(&'a self, operation: T) -> Result where T: Fn(&'a dyn GasPriceEstimating) -> F, - F: Future>, + F: Future>, { for (i, estimator) in self.estimators.iter().enumerate() { match operation(estimator.estimator.as_ref()).await { @@ -66,6 +66,10 @@ impl GasPriceEstimating for PriorityGasPriceEstimating { async fn estimate(&self) -> Result { self.prioritize(|estimator| estimator.estimate()).await } + + async fn base_fee(&self) -> Result> { + self.prioritize(|estimator| estimator.base_fee()).await + } } #[cfg(test)] diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 3080da1c33..e0932eaad6 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -14,10 +14,7 @@ use { price_estimation::{Estimate, QuoteVerificationMode, Verification}, trade_finding::external::dto, }, - alloy::{ - eips::eip1559::calc_effective_gas_price, - primitives::{Address, U256, U512, ruint::UintTryFrom}, - }, + alloy::primitives::{Address, U256, U512, ruint::UintTryFrom}, anyhow::{Context, Result}, chrono::{DateTime, Duration, Utc}, database::quotes::{Quote as QuoteRow, QuoteKind}, @@ -464,9 +461,9 @@ impl OrderQuoter { }; let trade_query = Arc::new(parameters.to_price_query(self.default_quote_timeout)); - let (gas_estimate, trade_estimate, sell_token_price, _) = futures::try_join!( + let (effective_gas_price, trade_estimate, sell_token_price, _) = futures::try_join!( self.gas_estimator - .estimate() + .effective_gas_price() .map_err(|err| CalculateQuoteError::from(( EstimatorKind::Gas, PriceEstimationError::ProtocolInternal(err) @@ -498,11 +495,7 @@ impl OrderQuoter { }; let fee_parameters = FeeParameters { gas_amount: trade_estimate.gas as _, - gas_price: calc_effective_gas_price( - gas_estimate.max_fee_per_gas, - gas_estimate.max_priority_fee_per_gas, - None, - ) as f64, + gas_price: effective_gas_price as f64, sell_token_price, }; diff --git a/crates/shared/src/price_estimation/competition/quote.rs b/crates/shared/src/price_estimation/competition/quote.rs index 872c317bd6..ebdb96d326 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -8,7 +8,6 @@ use { Query, QuoteVerificationMode, }, - alloy::eips::eip1559::calc_effective_gas_price, anyhow::Context, ethrpc::alloy::conversions::{IntoAlloy, IntoLegacy}, futures::future::{BoxFuture, FutureExt, TryFutureExt}, @@ -110,14 +109,7 @@ impl PriceRanking { let gas = gas.clone(); let native = native.clone(); let gas = gas - .estimate() - .map_ok(|gas| { - calc_effective_gas_price( - gas.max_fee_per_gas, - gas.max_priority_fee_per_gas, - None, - ) - }) + .effective_gas_price() .map_err(PriceEstimationError::ProtocolInternal); let (native_price, gas_price) = futures::try_join!( native.estimate_native_price(token.into_alloy(), timeout), From b36d30c9d361eccb2ec592663b550987921e6c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 20 Jan 2026 17:28:04 +0000 Subject: [PATCH 32/39] wip --- crates/shared/src/gas_price.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index c5dbb3ed75..0b308d0877 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -37,11 +37,16 @@ where #[instrument(skip_all)] async fn estimate(&self) -> Result { let estimate = self.inner.estimate().await?; + + // do not use effective_gas_price here because it would duplicate the estimate call + let base_fee = self.inner.base_fee().await?; + self.metrics.base_fee.set(base_fee.unwrap_or(0) as i64); + self.metrics.gas_price.set( (calc_effective_gas_price( estimate.max_fee_per_gas, estimate.max_priority_fee_per_gas, - None, + base_fee, ) / 10u128.pow(9)) as f64, ); Ok(estimate) @@ -56,4 +61,6 @@ where struct Metrics { /// Last measured gas price in gwei gas_price: prometheus::Gauge, + /// Last measured base fee + base_fee: prometheus::IntGauge, } From 45b02cf5e7728ee672c41bc756da1915ef17d8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 21 Jan 2026 09:42:24 +0000 Subject: [PATCH 33/39] tracing for gas estimates --- crates/shared/src/gas_price.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 0b308d0877..1044f3ef1d 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -42,13 +42,19 @@ where let base_fee = self.inner.base_fee().await?; self.metrics.base_fee.set(base_fee.unwrap_or(0) as i64); - self.metrics.gas_price.set( - (calc_effective_gas_price( - estimate.max_fee_per_gas, - estimate.max_priority_fee_per_gas, - base_fee, - ) / 10u128.pow(9)) as f64, + let effective_gas_price = calc_effective_gas_price( + estimate.max_fee_per_gas, + estimate.max_priority_fee_per_gas, + base_fee, ); + + tracing::info!( + "estimate: {estimate:?}, base fee: {base_fee:?}, effective gas price: {effective_gas_price}" + ); + + self.metrics + .gas_price + .set((effective_gas_price / 10u128.pow(9)) as f64); Ok(estimate) } From 51d6acb33766cc113e46a5f5b333f1df42cd7514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 21 Jan 2026 10:44:29 +0000 Subject: [PATCH 34/39] proper metric --- crates/shared/src/gas_price.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 1044f3ef1d..9a1488b056 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -52,9 +52,7 @@ where "estimate: {estimate:?}, base fee: {base_fee:?}, effective gas price: {effective_gas_price}" ); - self.metrics - .gas_price - .set((effective_gas_price / 10u128.pow(9)) as f64); + self.metrics.gas_price.set(effective_gas_price as f64 / 1e9); Ok(estimate) } From 1e48694fbac1dc5f9e54505be1088123282327e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 21 Jan 2026 11:22:14 +0000 Subject: [PATCH 35/39] use effective instead of estimate --- crates/shared/src/gas_price.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 9a1488b056..cd2f103637 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -8,7 +8,6 @@ use { crate::gas_price_estimation::GasPriceEstimating, alloy::eips::eip1559::{Eip1559Estimation, calc_effective_gas_price}, anyhow::Result, - tracing::instrument, }; /// An instrumented gas price estimator that wraps an inner one. @@ -34,11 +33,17 @@ impl GasPriceEstimating for InstrumentedGasEstimator where T: GasPriceEstimating, { - #[instrument(skip_all)] async fn estimate(&self) -> Result { - let estimate = self.inner.estimate().await?; + self.inner.estimate().await + } + + async fn base_fee(&self) -> Result> { + self.inner.base_fee().await + } - // do not use effective_gas_price here because it would duplicate the estimate call + #[tracing::instrument(skip_all)] + async fn effective_gas_price(&self) -> Result { + let estimate = self.estimate().await?; let base_fee = self.inner.base_fee().await?; self.metrics.base_fee.set(base_fee.unwrap_or(0) as i64); @@ -53,11 +58,7 @@ where ); self.metrics.gas_price.set(effective_gas_price as f64 / 1e9); - Ok(estimate) - } - - async fn base_fee(&self) -> Result> { - self.inner.base_fee().await + Ok(effective_gas_price) } } From 671b2d2016a62535603f31b7004224e423966fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 21 Jan 2026 13:00:20 +0000 Subject: [PATCH 36/39] fmt --- crates/driver/src/infra/blockchain/gas.rs | 2 +- crates/shared/src/gas_price.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index c7e68f05e2..e20a27dc3d 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -87,7 +87,7 @@ impl GasPriceEstimator { // Calculate additional tip in integer space to avoid precision loss // Convert percentage to basis points (multiply by 10000) to maintain precision // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 1250 - let tip_percentage_as_bps = (tip_percentage_increase * 10000.0) as u128; + let tip_percentage_as_bps = tip_percentage_increase * 10000.0; let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas) * eth::U256::from(tip_percentage_as_bps) / eth::U256::from(10000u128); diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index cd2f103637..3386036965 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -53,10 +53,6 @@ where base_fee, ); - tracing::info!( - "estimate: {estimate:?}, base fee: {base_fee:?}, effective gas price: {effective_gas_price}" - ); - self.metrics.gas_price.set(effective_gas_price as f64 / 1e9); Ok(effective_gas_price) } @@ -64,7 +60,7 @@ where #[derive(prometheus_metric_storage::MetricStorage)] struct Metrics { - /// Last measured gas price in gwei + /// Last measured effective gas price in gwei gas_price: prometheus::Gauge, /// Last measured base fee base_fee: prometheus::IntGauge, From 1f59ff7896224da2f0bece65e9c81b0ce8294802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 21 Jan 2026 13:03:17 +0000 Subject: [PATCH 37/39] clippy --- crates/shared/src/gas_price.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/shared/src/gas_price.rs b/crates/shared/src/gas_price.rs index 3386036965..69ec87eaa2 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -45,7 +45,9 @@ where async fn effective_gas_price(&self) -> Result { let estimate = self.estimate().await?; let base_fee = self.inner.base_fee().await?; - self.metrics.base_fee.set(base_fee.unwrap_or(0) as i64); + self.metrics + .base_fee + .set(base_fee.unwrap_or(0).cast_signed()); let effective_gas_price = calc_effective_gas_price( estimate.max_fee_per_gas, From a667918a093633a289a719f7851e616d8ad5a6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Fri, 23 Jan 2026 09:27:25 +0000 Subject: [PATCH 38/39] apply suggestions --- crates/driver/src/domain/competition/solution/settlement.rs | 2 +- crates/driver/src/infra/blockchain/gas.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index f1374aa02d..1b64b133a1 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -177,7 +177,7 @@ impl Settlement { // even if the gas price keeps climbing during the tx submission. let required_eth_balance = // Converting to U256 first avoids possible overflow - gas.required_balance(U256::from(price.max_fee_per_gas) * U256::from(2)); + gas.required_balance(U256::from(price.max_fee_per_gas).saturating_mul(U256::from(2))); if eth.balance(solution.solver().address()).await? < required_eth_balance { return Err(Error::SolverAccountInsufficientBalance( required_eth_balance, diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index e20a27dc3d..16a5c77d7e 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -89,7 +89,7 @@ impl GasPriceEstimator { // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 1250 let tip_percentage_as_bps = tip_percentage_increase * 10000.0; let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas) - * eth::U256::from(tip_percentage_as_bps) + .saturating_sub(eth::U256::from(tip_percentage_as_bps)) / eth::U256::from(10000u128); let additional_tip = max_additional_tip.min(calculated_tip); From bfcc80852ae9ae4501fe95a49b828285d10d4ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Tue, 27 Jan 2026 12:58:40 +0000 Subject: [PATCH 39/39] address comments --- .../driver/src/domain/competition/auction.rs | 2 +- crates/driver/src/domain/eth/gas.rs | 19 ++++--------------- crates/driver/src/infra/blockchain/gas.rs | 9 ++++++++- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 3558f06bb3..10bc830db7 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -62,7 +62,7 @@ impl Auction { let gas_price = GasPrice::new( U256::from(gas_est.max_fee_per_gas).into(), U256::from(gas_est.max_priority_fee_per_gas).into(), - Some(base_fee), + base_fee, ); Ok(Self { diff --git a/crates/driver/src/domain/eth/gas.rs b/crates/driver/src/domain/eth/gas.rs index a0c402c30a..efb72ef7ae 100644 --- a/crates/driver/src/domain/eth/gas.rs +++ b/crates/driver/src/domain/eth/gas.rs @@ -38,7 +38,7 @@ pub struct GasPrice { tip: FeePerGas, /// The current base gas price that will be charged to all accounts on the /// next block. - base: Option, + base: u64, } impl GasPrice { @@ -47,7 +47,7 @@ impl GasPrice { U256::from(calc_effective_gas_price( u128::try_from(self.max.0.0).expect("max fee per gas should fit in a u128"), u128::try_from(self.tip.0.0).expect("max priority fee per gas should fit in a u128"), - self.base, + Some(self.base), )) .into() } @@ -60,11 +60,11 @@ impl GasPrice { self.tip } - pub fn base(&self) -> Option { + pub fn base(&self) -> u64 { self.base } - pub fn new(max: FeePerGas, tip: FeePerGas, base: Option) -> Self { + pub fn new(max: FeePerGas, tip: FeePerGas, base: u64) -> Self { Self { max, tip, base } } } @@ -83,17 +83,6 @@ impl std::ops::Mul for GasPrice { } } -impl From for GasPrice { - fn from(value: EffectiveGasPrice) -> Self { - let value = value.0.0; - Self { - max: value.into(), - tip: value.into(), - base: u64::try_from(value).ok(), - } - } -} - /// The amount of ETH to pay as fees for a single unit of gas. This is /// `{max,max_priority,base}_fee_per_gas` as defined by EIP-1559. /// diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index dcb554f3fd..8c8741fdd5 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -9,6 +9,7 @@ use { infra::{config::file::GasEstimatorType, mempool}, }, alloy::eips::eip1559::Eip1559Estimation, + anyhow::anyhow, ethrpc::Web3, shared::gas_price_estimation::{ GasPriceEstimating, @@ -96,9 +97,15 @@ impl GasPriceEstimator { // Calculate additional tip in integer space to avoid precision loss // Convert percentage to basis points (multiply by 10000) to maintain precision // e.g., tip_percentage_increase = 0.125 (12.5%) becomes 1250 + let overflow_err = || { + Error::GasPrice(anyhow!( + "overflow on multiplication (max_priority_fee_per_gas * tip_percentage_as_bps)" + )) + }; let tip_percentage_as_bps = tip_percentage_increase * 10000.0; let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas) - .saturating_sub(eth::U256::from(tip_percentage_as_bps)) + .checked_mul(eth::U256::from(tip_percentage_as_bps)) + .ok_or_else(overflow_err)? / eth::U256::from(10000u128); let additional_tip = max_additional_tip.min(calculated_tip);