diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index 445df3b604..d1920d1fe9 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -78,12 +78,25 @@ impl GasPriceEstimator { ) }; + // Add headroom on top of the estimate so a base-fee increase between + // submission and inclusion does not strand the tx below the new base + // fee. max_fee_per_gas is a ceiling, not the amount paid. + let factor_as_bps = self.adjustments.max_fee_per_gas_factor * 10000.0; + let suggested_max_fee_per_gas = eth::U256::from(estimate.max_fee_per_gas) + .checked_mul(eth::U256::from(factor_as_bps as u128)) + .ok_or_else(|| { + Error::GasPrice(anyhow!( + "overflow on multiplication (max_fee_per_gas * max_fee_per_gas_factor)" + )) + })? + / eth::U256::from(10000u128); + // make sure the used max fee per gas is at least big enough to cover // the tip - otherwise the tx will be rejected by the node // immediately - let suggested_max_fee_per_gas = eth::U256::from(estimate.max_fee_per_gas); let suggested_max_fee_per_gas = std::cmp::max(suggested_max_fee_per_gas, max_priority_fee_per_gas); + if suggested_max_fee_per_gas > self.adjustments.max_fee_per_gas { return Err(Error::GasPrice(anyhow::anyhow!( "suggested gas price is higher than maximum allowed gas price (network is too \ @@ -123,6 +136,9 @@ pub struct GasPriceParameters { max_fee_per_gas: eth::U256, /// We'll always tip at least this value for `max_priority_fee_per_gas`. min_priority_fee: eth::U256, + /// The `max_fee_per_gas` suggested by the estimator gets multiplied with + /// this factor to leave headroom for a base-fee increase before inclusion. + max_fee_per_gas_factor: f64, } pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters { @@ -133,6 +149,7 @@ pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters { let mut additional_tip_percentage = 0.0f64; let mut max_fee_per_gas = eth::U256::MAX; let mut min_priority_fee = eth::U256::ZERO; + let mut max_fee_per_gas_factor = 1.0f64; for mempool in mempools { max_additional_tip = max_additional_tip.max(mempool.max_additional_tip); @@ -140,6 +157,7 @@ pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters { additional_tip_percentage.max(mempool.additional_tip_percentage); max_fee_per_gas = max_fee_per_gas.min(mempool.gas_price_cap); min_priority_fee = min_priority_fee.max(mempool.min_priority_fee); + max_fee_per_gas_factor = max_fee_per_gas_factor.max(mempool.max_fee_per_gas_factor); } GasPriceParameters { @@ -147,5 +165,6 @@ pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters { additional_tip_factor: additional_tip_percentage, max_fee_per_gas, min_priority_fee, + max_fee_per_gas_factor, } } diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index d91a0c42ea..a9568f8248 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -334,6 +334,7 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config { .map(|(index, mempool)| mempool::Config { min_priority_fee: config.submission.min_priority_fee, gas_price_cap: config.submission.gas_price_cap, + max_fee_per_gas_factor: config.submission.max_fee_per_gas_factor, target_confirm_time: config.submission.target_confirm_time, retry_interval: config.submission.retry_interval, nonce_block_number: config.submission.nonce_block_number.map(Into::into), diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index c15de8de96..287967188a 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -111,6 +111,11 @@ struct SubmissionConfig { #[serde_as(as = "serde_ext::U256")] gas_price_cap: eth::U256, + /// Factor applied to the estimated `max_fee_per_gas` to leave headroom for + /// a base-fee increase between submission and inclusion. + #[serde(default = "default_max_fee_per_gas_factor")] + max_fee_per_gas_factor: f64, + /// The target confirmation time for settlement transactions used /// to estimate gas price. #[serde(with = "humantime_serde", default = "default_target_confirm_time")] @@ -213,6 +218,11 @@ fn default_gas_price_cap() -> eth::U256 { eth::U256::from(1000) * eth::U256::from(10).pow(eth::U256::from(9)) } +/// No headroom over the estimated `max_fee_per_gas` by default. +fn default_max_fee_per_gas_factor() -> f64 { + 1.0 +} + fn default_target_confirm_time() -> Duration { Duration::from_secs(30) } diff --git a/crates/driver/src/infra/mempool/mod.rs b/crates/driver/src/infra/mempool/mod.rs index f2b00c9126..2d02deeafa 100644 --- a/crates/driver/src/infra/mempool/mod.rs +++ b/crates/driver/src/infra/mempool/mod.rs @@ -22,6 +22,7 @@ use { pub struct Config { pub min_priority_fee: eth::U256, pub gas_price_cap: eth::U256, + pub max_fee_per_gas_factor: f64, pub target_confirm_time: std::time::Duration, pub retry_interval: std::time::Duration, /// Optional block number to use when fetching nonces. If None, uses the @@ -40,6 +41,7 @@ impl Config { Self { min_priority_fee: Default::default(), gas_price_cap: eth::U256::from(1000000000000_u128), + max_fee_per_gas_factor: 1., target_confirm_time: Default::default(), retry_interval: Default::default(), name: "default_rpc".to_string(),