From d3d7fb31d75ade4b7ed68c37ae75e95ac850783e Mon Sep 17 00:00:00 2001 From: Delweng Date: Sat, 14 Mar 2026 15:56:10 +0800 Subject: [PATCH] fix(txpool): use ceiling division for replacement tx price bump check (#23012) Signed-off-by: Delweng --- crates/transaction-pool/src/pool/txpool.rs | 29 +++++++++++++++++++++ crates/transaction-pool/src/validate/mod.rs | 9 ++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index 05a69471b0..3003c42da0 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -3104,6 +3104,35 @@ mod tests { assert_eq!(pool.len(), 1); } + #[test] + fn insert_replace_underpriced_rounds_up_minimum_bump() { + let on_chain_balance = U256::ZERO; + let on_chain_nonce = 0; + let mut f = MockTransactionFactory::default(); + let mut pool = AllTransactions { minimal_protocol_basefee: 0, ..Default::default() }; + let mut tx = MockTransaction::eip1559().inc_price().inc_limit(); + tx.set_priority_fee(1); + tx.set_max_fee(1); + + let first = f.validated(tx.clone()); + let _ = pool.insert_tx(first.clone(), on_chain_balance, on_chain_nonce).unwrap(); + + let mut replacement = f.validated(tx.rng_hash().inc_price()); + replacement.transaction.set_priority_fee(1); + replacement.transaction.set_max_fee(2); + let err = + pool.insert_tx(replacement.clone(), on_chain_balance, on_chain_nonce).unwrap_err(); + assert!(matches!(err, InsertErr::Underpriced { .. })); + assert!(pool.contains(first.hash())); + assert_eq!(pool.len(), 1); + + replacement.transaction.set_priority_fee(2); + replacement.transaction.set_max_fee(2); + let replaced = pool.insert_tx(replacement, on_chain_balance, on_chain_nonce).unwrap(); + assert!(replaced.replaced_tx.is_some()); + assert_eq!(pool.len(), 1); + } + #[test] fn insert_conflicting_type_normal_to_blob() { let on_chain_balance = U256::from(10_000); diff --git a/crates/transaction-pool/src/validate/mod.rs b/crates/transaction-pool/src/validate/mod.rs index d5c3f05312..379c0985f7 100644 --- a/crates/transaction-pool/src/validate/mod.rs +++ b/crates/transaction-pool/src/validate/mod.rs @@ -455,9 +455,11 @@ impl ValidPoolTransaction { // // The bump is different for EIP-4844 and other transactions. See `PriceBumpConfig`. let price_bump = price_bumps.price_bump(self.tx_type()); + let required_bumped_fee = + |existing_fee: u128| existing_fee.saturating_mul(100 + price_bump).div_ceil(100); // Check if the max fee per gas is underpriced. - if maybe_replacement.max_fee_per_gas() < self.max_fee_per_gas() * (100 + price_bump) / 100 { + if maybe_replacement.max_fee_per_gas() < required_bumped_fee(self.max_fee_per_gas()) { return true } @@ -470,7 +472,7 @@ impl ValidPoolTransaction { if existing_max_priority_fee_per_gas != 0 && replacement_max_priority_fee_per_gas != 0 && replacement_max_priority_fee_per_gas < - existing_max_priority_fee_per_gas * (100 + price_bump) / 100 + required_bumped_fee(existing_max_priority_fee_per_gas) { return true } @@ -480,8 +482,7 @@ impl ValidPoolTransaction { // This enforces that blob txs can only be replaced by blob txs let replacement_max_blob_fee_per_gas = maybe_replacement.transaction.max_fee_per_blob_gas().unwrap_or_default(); - if replacement_max_blob_fee_per_gas < - existing_max_blob_fee_per_gas * (100 + price_bump) / 100 + if replacement_max_blob_fee_per_gas < required_bumped_fee(existing_max_blob_fee_per_gas) { return true }