Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2b6bf23
Migrate shared off of ethcontract
jmg-duarte Jan 12, 2026
f439af1
Vendor gas-estimation crate into shared module
jmg-duarte Jan 12, 2026
5e7e7a2
remove unused fn from trait
jmg-duarte Jan 13, 2026
fa90a4c
Merge branch 'main' into jmgd/alloy/gas
jmg-duarte Jan 13, 2026
ea800d9
address gemini comment
jmg-duarte Jan 13, 2026
1e2e165
compilation error
jmg-duarte Jan 13, 2026
9323557
wip
jmg-duarte Jan 14, 2026
a6a467c
Address river error
jmg-duarte Jan 14, 2026
5065765
address clippy
jmg-duarte Jan 14, 2026
05c2c01
Update crates/driver/src/tests/setup/solver.rs
jmg-duarte Jan 14, 2026
aa62ec2
address comments
jmg-duarte Jan 15, 2026
3dd9f64
alleviate issue with the magic numbers
jmg-duarte Jan 15, 2026
8a6a305
Merge branch 'jmgd/alloy/gas' into jmgd/alloy/gas-2
jmg-duarte Jan 15, 2026
ebf73de
wip
jmg-duarte Jan 15, 2026
a507ca9
is_approx_eq
jmg-duarte Jan 15, 2026
442db6d
Merge branch 'jmgd/alloy/gas' into jmgd/alloy/gas-2
jmg-duarte Jan 15, 2026
ec38cd7
approx_eq improvements
jmg-duarte Jan 15, 2026
876fd43
clippy
jmg-duarte Jan 15, 2026
49def52
Merge branch 'jmgd/alloy/gas' into jmgd/alloy/gas-2
jmg-duarte Jan 15, 2026
1138858
cleanup
jmg-duarte Jan 15, 2026
c553a87
clippy
jmg-duarte Jan 15, 2026
f42e13a
Merge branch 'main' into jmgd/alloy/gas-2
jmg-duarte Jan 16, 2026
6737b8c
wip
jmg-duarte Jan 16, 2026
e3767cc
Precision loss fix
jmg-duarte Jan 16, 2026
4ff8baa
Update crates/driver/src/domain/eth/gas.rs
jmg-duarte Jan 16, 2026
75ba211
Merge Eip1559Estimation extension traits
jmg-duarte Jan 16, 2026
c2e52c2
Replace GasPriceResponse with the equivalent Eip1559Estimation
jmg-duarte Jan 16, 2026
387ce1f
Provide base fee in driver mempool
jmg-duarte Jan 16, 2026
b4a7291
Fix approx_eq logs and tighten comparison margin
jmg-duarte Jan 16, 2026
a28e5b5
docs
jmg-duarte Jan 16, 2026
16b9b07
Merge branch 'main' into jmgd/alloy/gas-2
jmg-duarte Jan 20, 2026
55c216d
fix basis points
jmg-duarte Jan 20, 2026
26fbe9d
Replace the GasPrice struct with Eip1559Estimation for mempool
jmg-duarte Jan 20, 2026
5e6f105
Make base_fee mandatory
jmg-duarte Jan 20, 2026
61d7778
Rename PML -> PERMIL
jmg-duarte Jan 20, 2026
8e26a48
Make base_fee mandatory for effective()
jmg-duarte Jan 20, 2026
457826b
Add base_fee and effective_gas_price to estimators
jmg-duarte Jan 20, 2026
b36d30c
wip
jmg-duarte Jan 20, 2026
45b02cf
tracing for gas estimates
jmg-duarte Jan 21, 2026
51d6acb
proper metric
jmg-duarte Jan 21, 2026
1e48694
use effective instead of estimate
jmg-duarte Jan 21, 2026
671b2d2
fmt
jmg-duarte Jan 21, 2026
1f59ff7
clippy
jmg-duarte Jan 21, 2026
d474798
Merge branch 'main' into jmgd/alloy/gas-2
jmg-duarte Jan 23, 2026
a667918
apply suggestions
jmg-duarte Jan 23, 2026
eb96262
Merge branch 'main' into jmgd/alloy/gas-2
jmg-duarte Jan 26, 2026
a343fb5
Merge branch 'main' into jmgd/alloy/gas-2
jmg-duarte Jan 27, 2026
bfcc808
address comments
jmg-duarte Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/driver/src/domain/competition/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
);
Comment thread
jmg-duarte marked this conversation as resolved.

Ok(Self {
id,
orders,
tokens,
gas_price: eth.gas_price().await?,
gas_price,
deadline,
surplus_capturing_jit_order_owners,
})
Expand Down
9 changes: 6 additions & 3 deletions crates/driver/src/domain/competition/solution/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
}
}
30 changes: 11 additions & 19 deletions crates/driver/src/domain/eth/gas.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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 }
}
}
Expand All @@ -80,17 +83,6 @@ impl std::ops::Mul<f64> for GasPrice {
}
}

impl From<EffectiveGasPrice> 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.
///
Expand Down
37 changes: 20 additions & 17 deletions crates/driver/src/domain/mempools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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<TxId, Error> {
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;
Expand Down Expand Up @@ -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<eth::GasPrice> {
) -> Option<Eip1559Estimation> {
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.
Expand All @@ -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))
}
}
}
Expand Down
31 changes: 6 additions & 25 deletions crates/driver/src/infra/api/routes/gasprice.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
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,
};

pub(in crate::infra::api) fn gasprice(app: axum::Router<Ethereum>) -> axum::Router<Ethereum> {
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<Ethereum>,
) -> Result<Json<GasPriceResponse>, (hyper::StatusCode, axum::Json<Error>)> {
) -> Result<Json<Eip1559Estimation>, (hyper::StatusCode, axum::Json<Error>)> {
// 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,
}))
}
35 changes: 26 additions & 9 deletions crates/driver/src/infra/blockchain/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<eth::GasPrice, Error> {
pub async fn estimate(&self) -> Result<Eip1559Estimation, Error> {
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);
Comment on lines +106 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This math looks wrong to me. This should use a checked_mul instead of saturating_sub(), no?


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,
)
};

Expand All @@ -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()))?,
})
}
}
15 changes: 5 additions & 10 deletions crates/driver/src/infra/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use {
domain::{eth, eth::U256},
},
alloy::{
eips::eip1559::Eip1559Estimation,
network::TransactionBuilder,
providers::Provider,
rpc::types::{TransactionReceipt, TransactionRequest},
Expand All @@ -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,
Expand Down Expand Up @@ -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<eth::GasPrice, Error> {
pub async fn gas_price(&self) -> Result<Eip1559Estimation, Error> {
self.inner.gas.estimate().await
}

Expand Down Expand Up @@ -291,22 +293,15 @@ impl Ethereum {

#[instrument(skip(self), ret(level = Level::DEBUG))]
pub(super) async fn simulation_gas_price(&self) -> Option<u128> {
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
// than they normally would. To not rely on the node picking a reasonable
// 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 {
Expand Down
Loading
Loading