diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index c91779d029..10bc830db7 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,19 @@ impl Auction { true }); + 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(), + base_fee, + ); + 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 a53cb14a0b..63bf90be3c 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, @@ -176,7 +177,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(price * 2.); + let required_eth_balance = + // Converting to U256 first avoids possible overflow + 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, @@ -400,7 +403,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..efb72ef7ae 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,18 @@ pub struct GasPrice { tip: FeePerGas, /// The current base gas price that will be charged to all accounts on the /// next block. - base: FeePerGas, + base: u64, } 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).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"), + Some(self.base), + )) + .into() } pub fn max(&self) -> FeePerGas { @@ -57,11 +60,11 @@ impl GasPrice { self.tip } - pub fn base(&self) -> FeePerGas { + pub fn base(&self) -> u64 { self.base } - pub fn new(max: FeePerGas, tip: FeePerGas, base: FeePerGas) -> Self { + pub fn new(max: FeePerGas, tip: FeePerGas, base: u64) -> Self { Self { max, tip, base } } } @@ -80,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: value.into(), - } - } -} - /// 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/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index afd6fec97a..b5928a4f5e 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -8,7 +8,7 @@ use { }, infra::{self, Ethereum, observe, solver::Solver}, }, - alloy::consensus::Transaction, + 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,7 +146,7 @@ impl Mempools { .await; let final_gas_price = match &replacement_gas_price { Some(replacement_gas_price) - if replacement_gas_price.max() > current_gas_price.max() => + if replacement_gas_price.max_fee_per_gas > current_gas_price.max_fee_per_gas => { *replacement_gas_price } @@ -268,11 +268,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; @@ -314,15 +314,19 @@ impl Mempools { /// Computes minimum price to replace the last tx that was submitted /// with the given nonce. Returns `None` if no tx was submitted with /// that nonce yet. + #[tracing::instrument(skip_all)] async fn minimum_replacement_gas_price( &self, mempool: &infra::Mempool, solver: &Solver, next_nonce: u64, - ) -> Option { + ) -> Option { if let Some(last_submission) = mempool.last_submission(solver.address()) { - (last_submission.nonce == next_nonce) - .then_some(last_submission.gas_price * GAS_PRICE_BUMP) + if last_submission.nonce == next_nonce { + Some(last_submission.gas_price.scaled_by_pct(GAS_PRICE_BUMP_PCT)) + } else { + None + } } else { // If we don't have the last submission in-memory (i.e. first submission // attempt after a restart) we try to inspect the nodes transaction mempool. @@ -334,16 +338,15 @@ impl Mempools { .inspect_err(|err| tracing::debug!(?err, "could not inspect tx mempool")) .ok()??; - 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().or_else(|| { + 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().or_else(|| { tracing::error!(tx = ?pending_tx.inner.tx_hash(), "pending tx is not EIP 1559"); None - })?) - .into(), - eth::U256::from(pending_tx.max_fee_per_gas()).into(), - ); - Some(pending_tx_gas_price * GAS_PRICE_BUMP) + })?, + }; + + Some(pending_tx_gas_price.scaled_by_pct(GAS_PRICE_BUMP_PCT)) } } } diff --git a/crates/driver/src/infra/api/routes/gasprice.rs b/crates/driver/src/infra/api/routes/gasprice.rs index 2c66f40d7a..175d13dec6 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}, + alloy::eips::eip1559::Eip1559Estimation, axum::Json, - serde::{Deserialize, Serialize}, - serde_with::serde_as, tracing::instrument, }; @@ -14,29 +9,15 @@ 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. -#[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, -} - #[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 { - 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, + 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/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index 510eb1ace0..8c8741fdd5 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -8,6 +8,8 @@ use { domain::eth, infra::{config::file::GasEstimatorType, mempool}, }, + alloy::eips::eip1559::Eip1559Estimation, + anyhow::anyhow, ethrpc::Web3, shared::gas_price_estimation::{ GasPriceEstimating, @@ -84,20 +86,34 @@ 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); + + // 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) + .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); // 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, ) }; @@ -113,10 +129,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..d3d58dcfb8 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(base_fee)) } pub fn web3(&self) -> &Web3 { diff --git a/crates/driver/src/infra/mempool/mod.rs b/crates/driver/src/infra/mempool/mod.rs index 00d017908a..1b120a7e5c 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}, primitives::Address, providers::{Provider, ext::TxPoolApi}, rpc::types::TransactionRequest, @@ -73,7 +73,7 @@ pub struct Mempool { #[derive(Debug, Clone)] pub struct Submission { pub nonce: u64, - pub gas_price: eth::GasPrice, + pub gas_price: Eip1559Estimation, } impl std::fmt::Display for Mempool { @@ -121,23 +121,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() diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 15e89414dc..79f147657b 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -20,6 +20,7 @@ use { number::testing::ApproxEq, serde_json::{Value, json}, serde_with::{DisplayFromStr, serde_as}, + shared::gas_price_estimation::Eip1559EstimationExt, solvers_dto::auction::FlashloanHint, std::{ collections::{HashMap, HashSet}, @@ -488,14 +489,8 @@ 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(); + 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, @@ -558,11 +553,10 @@ fn check_solve_request(request: Value, expected: Value) { request.rest, expected.rest, "/solve request body does not match expectation" ); - 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% ); } diff --git a/crates/ethrpc/src/block_stream/mod.rs b/crates/ethrpc/src/block_stream/mod.rs index f746ad7fe4..c1538574fa 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: u64, /// 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,9 @@ impl TryFrom for BlockInfo { .base_fee_per_gas .map(U256::from) .context("no gas price")?, + base_fee: value + .base_fee_per_gas + .ok_or_else(|| anyhow!("no base fee available"))?, observed_at: Instant::now(), }) } diff --git a/crates/refunder/src/submitter.rs b/crates/refunder/src/submitter.rs index 4872f1ab55..73d8a074af 100644 --- a/crates/refunder/src/submitter.rs +++ b/crates/refunder/src/submitter.rs @@ -9,13 +9,13 @@ // 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}, + gas_price_estimation::{Eip1559EstimationExt, GasPriceEstimating}, }, std::time::Duration, }; @@ -24,22 +24,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_PERMIL: 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 +83,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() @@ -110,19 +105,19 @@ impl Submitter { } 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 +125,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_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); @@ -139,7 +135,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 +143,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 +162,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 +179,18 @@ 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 * (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); // 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 +201,19 @@ 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 * (1000 + GAS_PRICE_BUMP_PERMIL as u128) + / 1000, + max_priority_fee_per_gas: (TEST_START_PRIORITY_FEE_TIP as u128) + * (1000 + GAS_PRICE_BUMP_PERMIL as u128) + / 1000, }; 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 +226,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..69ec87eaa2 100644 --- a/crates/shared/src/gas_price.rs +++ b/crates/shared/src/gas_price.rs @@ -5,9 +5,9 @@ //! 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, }; /// An instrumented gas price estimator that wraps an inner one. @@ -33,18 +33,37 @@ impl GasPriceEstimating for InstrumentedGasEstimator where T: GasPriceEstimating, { - #[instrument(skip_all)] - async fn estimate(&self) -> Result { - let estimate = self.inner.estimate().await?; + async fn estimate(&self) -> Result { + self.inner.estimate().await + } + + async fn base_fee(&self) -> Result> { + self.inner.base_fee().await + } + + #[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 - .gas_price - .set(estimate.effective_gas_price() / 1e9); - Ok(estimate) + .base_fee + .set(base_fee.unwrap_or(0).cast_signed()); + + let effective_gas_price = calc_effective_gas_price( + estimate.max_fee_per_gas, + estimate.max_priority_fee_per_gas, + base_fee, + ); + + self.metrics.gas_price.set(effective_gas_price as f64 / 1e9); + Ok(effective_gas_price) } } #[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, } diff --git a/crates/shared/src/gas_price_estimation/configurable_alloy.rs b/crates/shared/src/gas_price_estimation/configurable_alloy.rs index 671497d7cf..5a0840b257 100644 --- a/crates/shared/src/gas_price_estimation/configurable_alloy.rs +++ b/crates/shared/src/gas_price_estimation/configurable_alloy.rs @@ -6,9 +6,9 @@ //! - Reward percentile to use use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559, u128_to_f64}, + crate::gas_price_estimation::GasPriceEstimating, alloy::{ - eips::BlockNumberOrTag, + eips::{BlockId, BlockNumberOrTag, eip1559::Eip1559Estimation}, providers::{Provider, utils::eip1559_default_estimator}, }, anyhow::{Context, Result}, @@ -49,11 +49,19 @@ impl ConfigurableGasPriceEstimator { #[async_trait::async_trait] impl GasPriceEstimating for ConfigurableGasPriceEstimator { + async fn base_fee(&self) -> Result> { + Ok(self + .provider + .get_block(BlockId::latest()) + .await? + .and_then(|block| block.header.base_fee_per_gas)) + } + #[instrument(skip(self), fields( past_blocks = %self.config.past_blocks, reward_percentile = %self.config.reward_percentile ))] - async fn estimate(&self) -> Result { + async fn estimate(&self) -> Result { // Fetch fee history with our configured parameters let fee_history = self .provider @@ -90,13 +98,9 @@ impl GasPriceEstimating for ConfigurableGasPriceEstimator { let estimation = eip1559_default_estimator(base_fee_per_gas, &fee_history.reward.unwrap_or_default()); - Ok(GasPrice1559 { - base_fee_per_gas: u128_to_f64(base_fee_per_gas) - .context("could not convert base_fee_per_gas to f64")?, - max_fee_per_gas: u128_to_f64(estimation.max_fee_per_gas) - .context("could not convert max_fee_per_gas to f64")?, - max_priority_fee_per_gas: u128_to_f64(estimation.max_priority_fee_per_gas) - .context("could not convert max_priority_fee_per_gas to f64")?, + Ok(Eip1559Estimation { + max_fee_per_gas: estimation.max_fee_per_gas, + max_priority_fee_per_gas: estimation.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..5e1d7b1c16 100644 --- a/crates/shared/src/gas_price_estimation/driver.rs +++ b/crates/shared/src/gas_price_estimation/driver.rs @@ -1,11 +1,11 @@ use { - crate::gas_price_estimation::{GasPriceEstimating, price::GasPrice1559}, - alloy::primitives::U256, - anyhow::{Context, Result}, - number::serialization::HexOrDecimalU256, + crate::gas_price_estimation::GasPriceEstimating, + alloy::{ + eips::{BlockId, eip1559::Eip1559Estimation}, + providers::{DynProvider, Provider}, + }, + anyhow::{Context, Result, anyhow}, reqwest::Url, - serde::Deserialize, - serde_with::serde_as, std::{ sync::Arc, time::{Duration, Instant}, @@ -21,40 +21,29 @@ pub struct DriverGasEstimator { client: reqwest::Client, url: Url, cache: Arc>>, + provider: DynProvider, } #[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, -} - 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, } } #[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()) @@ -63,14 +52,13 @@ 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")?; - 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 +66,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; @@ -98,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 6082c45db2..fa7fad9508 100644 --- a/crates/shared/src/gas_price_estimation/eth_node.rs +++ b/crates/shared/src/gas_price_estimation/eth_node.rs @@ -4,9 +4,12 @@ //! 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, - anyhow::{Context, Result}, + crate::gas_price_estimation::GasPriceEstimating, + alloy::{ + eips::{BlockId, eip1559::Eip1559Estimation}, + providers::Provider, + }, + anyhow::{Context, Result, anyhow}, ethrpc::AlloyProvider, }; @@ -23,18 +26,26 @@ impl NodeGasPriceEstimator { impl GasPriceEstimating for NodeGasPriceEstimator { /// Returns the result of calling the `eth_gasPrice` endpoint as the gas /// estimation. - 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, }) } + + 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 19f8e56de9..d19facce0f 100644 --- a/crates/shared/src/gas_price_estimation/fake.rs +++ b/crates/shared/src/gas_price_estimation/fake.rs @@ -1,20 +1,33 @@ 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) } + + 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 2aad505340..b7755cc1fd 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 configurable_alloy; pub mod driver; pub mod eth_node; pub mod fake; -pub mod price; pub mod priority; use { @@ -20,7 +19,10 @@ use { }, http_client::HttpClientFactory, }, - ::alloy::providers::Provider, + ::alloy::{ + eips::eip1559::{Eip1559Estimation, calc_effective_gas_price}, + providers::Provider, + }, anyhow::Result, std::str::FromStr, tracing::instrument, @@ -32,7 +34,19 @@ pub use {driver::DriverGasEstimator, fake::FakeGasPriceEstimator}; #[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; + + 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)] @@ -72,6 +86,7 @@ pub async fn create_priority_estimator( estimators.push(Box::new(DriverGasEstimator::new( http_factory.create(), url.clone(), + web3.alloy.clone(), ))); } GasEstimatorType::Web3 => { @@ -96,9 +111,33 @@ 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}")); +/// 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: 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: u64) -> u128 { + calc_effective_gas_price( + self.max_fee_per_gas, + self.max_priority_fee_per_gas, + Some(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 } - Ok(val as f64) } 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 8d1e3ec04f..0000000000 --- a/crates/shared/src/gas_price_estimation/price.rs +++ /dev/null @@ -1,192 +0,0 @@ -// Vendored implementation of GasPrice1559 to start removing the dependency on -// the gas_estimation crate -use 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), - ) - } - - // 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 index ba04669d69..c6ad5fca19 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, 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 { @@ -62,9 +63,13 @@ 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 } + + async fn base_fee(&self) -> Result> { + self.prioritize(|estimator| estimator.base_fee()).await + } } #[cfg(test)] @@ -73,40 +78,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 +113,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 7f844f4e6c..dd47e3f9d0 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -462,9 +462,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) @@ -496,7 +496,7 @@ impl OrderQuoter { }; let fee_parameters = FeeParameters { gas_amount: trade_estimate.gas as _, - gas_price: gas_estimate.effective_gas_price(), + gas_price: effective_gas_price as f64, sell_token_price, }; @@ -786,7 +786,7 @@ mod tests { super::*, crate::{ account_balances::MockBalanceFetching, - gas_price_estimation::{FakeGasPriceEstimator, price::GasPrice1559}, + gas_price_estimation::FakeGasPriceEstimator, price_estimation::{ HEALTHY_PRICE_ESTIMATION_TIME, MockPriceEstimating, @@ -795,6 +795,7 @@ mod tests { }, Address, U256 as AlloyU256, + alloy::eips::eip1559::Eip1559Estimation, chrono::Utc, futures::FutureExt, mockall::{Sequence, predicate::eq}, @@ -853,10 +854,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(); @@ -994,10 +994,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(); @@ -1130,10 +1129,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(); @@ -1267,10 +1265,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(); @@ -1341,10 +1338,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 b0ef01a748..8c6be7c544 100644 --- a/crates/shared/src/price_estimation/competition/quote.rs +++ b/crates/shared/src/price_estimation/competition/quote.rs @@ -112,8 +112,7 @@ impl PriceRanking { let gas = gas.clone(); let native = native.clone(); let gas = gas - .estimate() - .map_ok(|gas| gas.effective_gas_price()) + .effective_gas_price() .map_err(PriceEstimationError::ProtocolInternal); let (native_price, gas_price) = futures::try_join!( native.estimate_native_price(token.into_alloy(), timeout), @@ -122,7 +121,7 @@ impl PriceRanking { Ok(RankingContext { native_price, - gas_price, + gas_price: gas_price as f64, }) } } @@ -159,14 +158,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, }; @@ -193,10 +192,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),