Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 26 additions & 1 deletion crates/rbuilder/src/building/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ use std::{
};
use thiserror::Error;
use time::OffsetDateTime;
use tracing::{error, trace};
use tracing::{error, trace, warn};
use tx_sim_cache::TxExecutionCache;

pub mod bid_adjustments;
Expand Down Expand Up @@ -864,6 +864,31 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
}
}

// flashbots/rbuilder#296: the proposer payout is sent from the builder
// coinbase, so the coinbase must hold at least the transferred value
// plus the gas the EVM locks up front (gas_limit * basefee, as the
// payout tx sets max_priority_fee_per_gas = 0 and max_fee_per_gas =
// basefee). If it does not, the payout tx is not sendable (in the
// limiting value-less case the floor is BASE_TX_GAS * basefee). Warn so
// the condition is visible up front rather than only as a downstream
// payout-tx revert. Checked after the refunds above, which also spend
// from the coinbase.
let builder_balance = fork
.state
.balance(builder_signer.address)
.map_err(CriticalCommitOrderError::Reth)?;
let payout_tx_cost =
proposer_payout_tx_cost(value, gas_limit, ctx.evm_env.block_env.basefee);
if builder_balance < payout_tx_cost {
warn!(
builder = %builder_signer.address,
%builder_balance,
%payout_tx_cost,
payout_value = %value,
"Builder coinbase balance is below the cost of the proposer payout tx; it will not be sendable"
);
}

let tx = create_payout_tx(
ctx.chain_spec.as_ref(),
ctx.evm_env.block_env.basefee,
Expand Down
60 changes: 60 additions & 0 deletions crates/rbuilder/src/building/payout_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ pub fn create_payout_tx(
signer.sign_tx(tx)
}

/// Minimum builder (coinbase) balance required to send the proposer payout
/// transaction: the transferred `value` plus the gas the EVM locks up front
/// (`gas_limit * basefee`, since the payout tx sets `max_fee_per_gas = basefee`
/// and `max_priority_fee_per_gas = 0`). A builder holding less than this cannot
/// cover the payout tx and it will not be sendable; in the limiting case of a
/// value-less payout the floor is `BASE_TX_GAS * basefee`. See
/// flashbots/rbuilder#296. Saturating so an implausible input cannot panic.
pub fn proposer_payout_tx_cost(value: U256, gas_limit: u64, basefee: u64) -> U256 {
value.saturating_add(U256::from(gas_limit).saturating_mul(U256::from(basefee)))
}

#[derive(Debug, thiserror::Error)]
pub enum PayoutTxErr {
#[error("Reth error: {0}")]
Expand Down Expand Up @@ -287,4 +298,53 @@ mod tests {
assert_matches!(estimate_result, Ok(_));
assert_eq!(estimate_result.unwrap().gas, 21_000);
}

#[test]
fn payout_tx_cost_covers_value_and_gas() {
let basefee = INITIAL_BASE_FEE;

// A value-less payout still needs BASE_TX_GAS * basefee: the "21k gas"
// floor from flashbots/rbuilder#296.
assert_eq!(
proposer_payout_tx_cost(U256::ZERO, BASE_TX_GAS, basefee),
U256::from(BASE_TX_GAS) * U256::from(basefee),
);

// The transferred value is added on top of the gas cost.
let value = U256::from(7_000_000_000u64);
assert_eq!(
proposer_payout_tx_cost(value, BASE_TX_GAS, basefee),
value + U256::from(BASE_TX_GAS) * U256::from(basefee),
);
}

#[test]
fn payout_tx_cost_matches_guard_boundary() {
let basefee = INITIAL_BASE_FEE;
let value = U256::from(7_000_000_000u64);
// Independently computed cost (NOT via the function under test) so the
// assertions below depend on proposer_payout_tx_cost's real output.
let exact = value + U256::from(BASE_TX_GAS) * U256::from(basefee);

// The guard in insert_refunds_and_proposer_payout_tx warns iff
// builder_balance < proposer_payout_tx_cost(...). A balance of exactly
// the cost is sufficient (no warn); one wei less is insufficient.
assert!(
exact >= proposer_payout_tx_cost(value, BASE_TX_GAS, basefee),
"balance equal to the cost is sufficient",
);
assert!(
exact - U256::from(1) < proposer_payout_tx_cost(value, BASE_TX_GAS, basefee),
"balance one wei below the cost is insufficient",
);
}

#[test]
fn payout_tx_cost_saturates() {
// Implausible inputs must saturate rather than panic on overflow.
assert_eq!(
proposer_payout_tx_cost(U256::MAX, u64::MAX, u64::MAX),
U256::MAX,
);
}
}