evm_fork_cache/bundle.rs
1//! Multi-transaction **bundle simulation** over cumulative block state, plus
2//! **coinbase / miner-payment accounting** (Phase 6 Track A+B).
3//!
4//! A *bundle* is an ordered sequence of `Call`-kind transactions applied to a
5//! single overlay so that transaction `i` observes the committed writes of
6//! transactions `0..i` — the minimal primitive an MEV searcher needs to value a
7//! candidate set of transactions as a unit. It is intentionally *not* a block
8//! builder: there is no mempool, ordering auction, or `Create`-kind support (see
9//! the Phase 6 spec non-goals).
10//!
11//! The execution itself lives in [`EvmOverlay::simulate_bundle`] (and the
12//! cache-side convenience [`EvmCache::simulate_bundle`]); this module owns the
13//! public vocabulary those methods speak.
14//!
15//! # Coinbase accounting
16//!
17//! [`BundleResult::coinbase_payment`] is the block beneficiary's balance delta
18//! across the bundle — the honest miner payment. Under EIP-1559 (London+, which
19//! this engine runs by default) revm credits the beneficiary only the **priority
20//! fee** (`(effective_gas_price − basefee) × gas_used`) and burns the base-fee
21//! portion in-EVM, so the delta already excludes the base fee. It also captures
22//! any **direct value transfers to the beneficiary** (an explicit coinbase tip).
23//! So `coinbase_payment = Σ priority_feeᵢ × gas_usedᵢ + direct coinbase tips`,
24//! over the transactions whose effects are kept. Set the base fee with
25//! [`EvmCache::set_basefee`](crate::cache::EvmCache::set_basefee) to model a
26//! non-zero base fee (a higher base fee lowers the priority fee, and thus the
27//! payment, for a fixed `gas_price`). All arithmetic is saturating.
28//!
29//! [`EvmCache::simulate_bundle`]: crate::cache::EvmCache::simulate_bundle
30//! [`EvmOverlay::simulate_bundle`]: crate::cache::EvmOverlay::simulate_bundle
31
32use alloy_primitives::{Address, Bytes, Log, U256};
33use revm::context::result::ExecutionResult;
34
35use crate::cache::TxConfig;
36
37/// One transaction in a bundle.
38///
39/// `Call`-kind only for Phase 6 (`Create`/`Create2` bundle transactions are a
40/// documented follow-up). The [`tx`](BundleTx::tx) field reuses the existing
41/// [`TxConfig`] vocabulary (`value` / `gas_limit` / `gas_price` / `nonce` /
42/// `access_list`), so a bundle transaction can carry native value, be
43/// gas-bounded, or pre-warm an EIP-2930 access list exactly like a single
44/// [`call_raw_with_access_list_with`](crate::cache::EvmOverlay::call_raw_with_access_list_with)
45/// call.
46#[derive(Clone, Debug)]
47pub struct BundleTx {
48 /// Sender of the call (the `from` / caller address).
49 pub from: Address,
50 /// Call target (`Call`-kind only for Phase 6).
51 pub to: Address,
52 /// ABI-encoded calldata for the call.
53 pub calldata: Bytes,
54 /// Per-transaction environment overrides (value / gas / nonce / access list).
55 pub tx: TxConfig,
56}
57
58impl BundleTx {
59 /// A bundle transaction with a default [`TxConfig`] (zero value, default
60 /// gas/nonce, no access list).
61 pub fn new(from: Address, to: Address, calldata: Bytes) -> Self {
62 Self {
63 from,
64 to,
65 calldata,
66 tx: TxConfig::default(),
67 }
68 }
69
70 /// A bundle transaction carrying an explicit [`TxConfig`] — e.g. to send
71 /// native `value` (a direct coinbase transfer), set a `gas_price`, or
72 /// pre-warm an access list.
73 pub fn with_config(from: Address, to: Address, calldata: Bytes, tx: TxConfig) -> Self {
74 Self {
75 from,
76 to,
77 calldata,
78 tx,
79 }
80 }
81}
82
83/// What happens when a bundle transaction reverts (or halts).
84///
85/// Defaults to [`Atomic`](RevertPolicy::Atomic).
86#[derive(Clone, Debug, Default)]
87pub enum RevertPolicy {
88 /// Any transaction revert/halt reverts the **whole** bundle to the outer
89 /// checkpoint and sets [`BundleResult::succeeded`] to `false`. Execution
90 /// stops at the failing transaction.
91 #[default]
92 Atomic,
93 /// The listed transaction indices may revert without aborting the bundle:
94 /// their state effects are rolled back **individually** (inner checkpoint
95 /// revert) and later transactions still execute. A revert at an index *not*
96 /// in the list behaves like [`Atomic`](RevertPolicy::Atomic).
97 AllowReverts(Vec<usize>),
98}
99
100/// Options controlling a bundle simulation.
101#[derive(Clone, Debug, Default)]
102pub struct BundleOptions {
103 /// Revert handling for individual transactions. Default
104 /// [`Atomic`](RevertPolicy::Atomic).
105 pub revert_policy: RevertPolicy,
106 /// Whether the bundle's cumulative state is folded into the overlay's dirty
107 /// layer (`true`) or reverted so the overlay is left unchanged (`false`).
108 /// Default `false` (evaluate, don't persist).
109 pub commit: bool,
110}
111
112/// Outcome of a single transaction executed within a bundle.
113#[derive(Clone, Debug)]
114pub struct TxOutcome {
115 /// The raw revm [`ExecutionResult`] (`Success` / `Revert` / `Halt`).
116 pub result: ExecutionResult,
117 /// Gas consumed by this transaction.
118 pub gas_used: u64,
119 /// `true` if this transaction reverted or halted.
120 pub reverted: bool,
121 /// Logs emitted by this transaction (empty for a revert/halt).
122 pub logs: Vec<Log>,
123}
124
125/// Result of a bundle simulation.
126#[derive(Clone, Debug)]
127pub struct BundleResult {
128 /// One [`TxOutcome`] per executed transaction. Length equals `txs.len()`
129 /// unless an [`Atomic`](RevertPolicy::Atomic) bundle aborted early, in which
130 /// case it ends at the failing transaction.
131 pub per_tx: Vec<TxOutcome>,
132 /// Miner payment: the block beneficiary's balance delta across the kept
133 /// transactions (priority fee + direct coinbase tips; the base fee is already
134 /// excluded by revm — see the [module docs](self#coinbase-accounting)).
135 /// Saturating; `0` for an [`Atomic`](RevertPolicy::Atomic) bundle that aborted.
136 pub coinbase_payment: U256,
137 /// Total gas used across the executed transactions.
138 pub gas_used: u64,
139 /// `false` iff an [`Atomic`](RevertPolicy::Atomic) bundle aborted on a
140 /// revert/halt (or an `AllowReverts` bundle hit a revert at a non-whitelisted
141 /// index).
142 pub succeeded: bool,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn revert_policy_defaults_to_atomic() {
151 assert!(matches!(RevertPolicy::default(), RevertPolicy::Atomic));
152 }
153
154 #[test]
155 fn bundle_options_default_is_evaluate_only_atomic() {
156 let opts = BundleOptions::default();
157 assert!(matches!(opts.revert_policy, RevertPolicy::Atomic));
158 assert!(!opts.commit, "default must not persist");
159 }
160
161 #[test]
162 fn bundle_tx_new_uses_default_tx_config() {
163 let from = Address::repeat_byte(0x01);
164 let to = Address::repeat_byte(0x02);
165 let tx = BundleTx::new(from, to, Bytes::from(vec![0xaa]));
166 assert_eq!(tx.from, from);
167 assert_eq!(tx.to, to);
168 assert_eq!(tx.calldata, Bytes::from(vec![0xaa]));
169 assert_eq!(tx.tx.value, U256::ZERO);
170 assert!(tx.tx.gas_price.is_none());
171 assert!(tx.tx.access_list.is_none());
172 }
173
174 #[test]
175 fn bundle_tx_with_config_carries_value_and_gas_price() {
176 let tx = BundleTx::with_config(
177 Address::ZERO,
178 Address::ZERO,
179 Bytes::new(),
180 TxConfig {
181 value: U256::from(42u64),
182 gas_price: Some(7),
183 ..Default::default()
184 },
185 );
186 assert_eq!(tx.tx.value, U256::from(42u64));
187 assert_eq!(tx.tx.gas_price, Some(7));
188 }
189}