degenbot_executor/lib.rs
1//! The `cmd_executor` command-stream grammar — solver result → the `bytes`
2//! the on-chain `cmd_executor.execute(bytes, config)` contract runs.
3//!
4//! A pyo3-free *core leaf* (ADR-005 standalone surface) with two modules:
5//!
6//! - **The command grammar** (the bulk of this crate): the per-protocol hop
7//! facts + the plan walker that derives the enclosure and emits a
8//! [`grammar_plan::Plan`] ([`grammar_walker`]), the ledger validator that
9//! gates every Plan ([`grammar_ledger`]), the shape-class deriver
10//! ([`grammar_shape`]), the opcode builders ([`encoders`]), the
11//! `execute` config packing ([`config`]), and the public intake —
12//! [`composers::EncodeContext`] + [`composers::EncodeRequest`] →
13//! [`composers::encode_cmd_stream`] (ADR-033). The ADR-029/030/031 record
14//! records own the grammar's invariants (axes, tri-state outcomes, the
15//! facts-driven walker).
16//! - **The warmup-slot math** ([`WarmupSlots`] /
17//! [`compute_simulation_warmup_slots`]): the Solidity storage-slot math
18//! that feeds simulation state-override warmup — three slots pre-warmed so
19//! injected runtime bytecode (no `initialize()` call) sees warm storage,
20//! replicating `cmd_executor.initialize()`'s cold-SSTORE avoidance
21//! (~22,100 gas/slot).
22//!
23//! # Warmup-slot storage layout
24//!
25//! - **WETH9** `balanceOf` at mapping slot **3** (`name`@0, `symbol`@1,
26//! `decimals`@2 — all occupy storage, not `constant`).
27//! - **PoolManager** ERC6909 `balanceOf` at mapping slot **4** (C3 linearization
28//! of `PoolManager is ProtocolFees, ERC6909Claims, …`: `owner`@0,
29//! `pendingOwner`@1, `isOperator`@2, `protocolFeesAccrued`@3, `balanceOf`@4).
30//! - **ERC6909 id** = `uint160(currency)` per `CurrencyLibrary.toId()` — the
31//! native id is `uint160(address(0)) = 0`.
32//!
33//! [`WarmupSlots`] carries the three computed **slot addresses** only
34//! (`U256`); the warmed balance values + the `eth_simulateV1`
35//! `{address: {"stateDiff": {slot_hex: value_hex}}}` dict shape are a thin
36//! PyO3 adapter concern, not a core one.
37//!
38//! # Parity
39//!
40//! The warmup slots are byte-for-byte parity vs
41//! `cmd_executor.initialize()`'s storage layout (§4.2) — canonical mainnet
42//! WETH (`0xC02…`) / PoolManager (`0x0000…444c`). The grammar's byte-identity
43//! is pinned by the golden corpus + the revm runtime matrix
44//! (degenbot-simulation).
45
46// Solidity/ERC identifiers (balanceOf, ERC6909, protocolFeesAccrued, …) are
47// ubiquitous in this crate's docs; allow the pedantic doc-markdown lint to
48// match the peer math crates (degenbot-solidly-math, -balancer-math, …).
49#![expect(clippy::doc_markdown)]
50
51use alloy::primitives::{keccak256, Address, U256};
52
53pub mod composers;
54pub mod config;
55pub mod encoders;
56pub mod grammar_ledger;
57pub mod grammar_plan;
58pub mod grammar_shape;
59
60pub mod grammar_walker;
61
62/// The WETH9 `balanceOf` mapping storage slot (`name`@0, `symbol`@1,
63/// `decimals`@2, `balanceOf`@3).
64pub const WETH9_BALANCE_OF_SLOT: u64 = 3;
65
66/// The PoolManager ERC6909 `balanceOf` mapping storage slot (C3 linearization:
67/// `owner`@0, `pendingOwner`@1, `isOperator`@2, `protocolFeesAccrued`@3,
68/// `balanceOf`@4).
69pub const POOL_MANAGER_ERC6909_BALANCE_OF_SLOT: u64 = 4;
70
71/// The three computed warmup **slot addresses** for `eth_simulateV1`
72/// `stateDiff` overrides.
73///
74/// Carries slot addresses only — the warmed balance value (1 wei) and the
75/// `stateDiff` dict shape are the cutover-task PyO3 adapter concern.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct WarmupSlots {
78 /// WETH9 `balanceOf(executor)` mapping slot — warmed so the executor's WETH
79 /// ERC20 balance slot is hot.
80 pub weth_balance: U256,
81 /// PoolManager ERC6909 `balanceOf(executor, weth_id)` nested-mapping slot —
82 /// the primary warmup slot enabling gas-efficient `V4_MINT` profit capture.
83 pub erc6909_weth: U256,
84 /// PoolManager ERC6909 `balanceOf(executor, native_id)` nested-mapping slot
85 /// — warmed for paths that mint native ETH as ERC6909.
86 pub erc6909_native: U256,
87}
88
89/// Compute a Solidity mapping storage slot: `keccak256(key ‖ base_slot)`,
90/// both 32-byte big-endian.
91///
92/// Ports `examples/cmd_stream.py::mapping_slot` (L884–L891). **Layout note:**
93/// the key is concatenated *before* the base slot (`key ‖ base_slot`, not
94/// `base_slot ‖ key`) — this is the parity-sensitive point.
95#[must_use]
96pub fn mapping_slot(base_slot: U256, key: U256) -> U256 {
97 let mut preimage = [0u8; 64];
98 preimage[0..32].copy_from_slice(&key.to_be_bytes::<32>());
99 preimage[32..64].copy_from_slice(&base_slot.to_be_bytes::<32>());
100 U256::from_be_bytes(keccak256(preimage).0)
101}
102
103/// Compute the storage slot for `mapping[key1][key2]` at `base_slot`:
104/// `mapping_slot(mapping_slot(base_slot, key1), key2)`.
105///
106/// Ports `examples/cmd_stream.py::_nested_mapping_slot` (L894–L916).
107#[must_use]
108pub fn nested_mapping_slot(base_slot: U256, key1: U256, key2: U256) -> U256 {
109 mapping_slot(mapping_slot(base_slot, key1), key2)
110}
111
112/// Build a `U256` from a 64-char big-endian hex string (no `0x` prefix), at
113/// const-eval time. Used for the parity fixtures + the `MASK_160` const.
114const fn u256_from_hex(hex: &str) -> U256 {
115 let bytes = hex.as_bytes();
116 let mut b = [0u8; 32];
117 let mut i = 0;
118 while i < 32 {
119 let hi = hex_digit(bytes[2 * i]);
120 let lo = hex_digit(bytes[2 * i + 1]);
121 b[i] = hi * 16 + lo;
122 i += 1;
123 }
124 U256::from_be_bytes(b)
125}
126
127const fn hex_digit(c: u8) -> u8 {
128 match c {
129 b'0'..=b'9' => c - b'0',
130 b'a'..=b'f' => c - b'a' + 10,
131 b'A'..=b'F' => c - b'A' + 10,
132 _ => 0,
133 }
134}
135
136/// The low-160-bit mask (`2^160 − 1`) for the ERC6909 `uint160(currency)`
137/// truncation. Built as a const so the shift never touches the overflowing
138/// `u128` domain.
139const MASK_160: U256 =
140 u256_from_hex("000000000000000000000000ffffffffffffffffffffffffffffffffffffffff");
141
142/// Derive the ERC6909 token id for a currency: `uint160(currency)` per
143/// `CurrencyLibrary.toId()` (the low 160 bits of the address's integer form).
144///
145/// The native id (`uint160(address(0))`) is `0`.
146#[must_use]
147pub fn erc6909_id(currency: Address) -> U256 {
148 // address(0) → 0; any address → its low 160 bits (a no-op on a valid
149 // `Address`, which is type-guaranteed ≤160 bits, but mirrors the Python
150 // oracle's `int(addr,16) & ((1<<160)-1)` for fidelity).
151 U256::from_be_bytes(currency.into_word().0) & MASK_160
152}
153
154/// Compute the three `eth_simulateV1` warmup slot addresses that replicate the
155/// effect of `cmd_executor.initialize()`.
156///
157/// Ports `examples/cmd_stream.py::compute_simulation_warmup_slots`
158/// (L919–L1054). Returns the **slot addresses** only (typed struct); the 1-wei
159/// warmed values + the `stateDiff` dict shape are the cutover-task PyO3
160/// adapter.
161///
162/// # Arguments
163///
164/// - `executor` — the cmd_executor contract address.
165/// - `weth` — the WETH9 contract address (the ERC6909 WETH id derives from
166/// it; the native id is 0).
167#[must_use]
168pub fn compute_simulation_warmup_slots(executor: Address, weth: Address) -> WarmupSlots {
169 let executor_slot_key = U256::from_be_bytes(executor.into_word().0);
170 let weth_id = erc6909_id(weth);
171 let native_id = U256::ZERO; // uint160(address(0)) = 0
172
173 let base_slot = U256::from(WETH9_BALANCE_OF_SLOT);
174 let pm_slot = U256::from(POOL_MANAGER_ERC6909_BALANCE_OF_SLOT);
175
176 WarmupSlots {
177 weth_balance: mapping_slot(base_slot, executor_slot_key),
178 erc6909_weth: nested_mapping_slot(pm_slot, executor_slot_key, weth_id),
179 erc6909_native: nested_mapping_slot(pm_slot, executor_slot_key, native_id),
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use alloy::primitives::{address, Address};
187
188 /// Canonical mainnet WETH9 and PoolManager (per the parity corpus
189 /// `tests/arbitrage/test_warmup_slots_gas.py`).
190 const WETH: Address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
191
192 /// `uint160(WETH)` — the ERC6909 WETH id.
193 const WETH_ID: U256 =
194 u256_from_hex("000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
195
196 // ---------------------------------------------------------------------------
197 // Parity fixtures — the slot hexes this leaf computes over the three
198 // executor addresses in the parity corpus:
199 // - `0xAA…AA` (the canonical `EXECUTOR_ADDRESS` in test_warmup_slots_gas.py)
200 // - `0xDeAd…0001` / `0xDeAd…0002` (the eth_simulateV1 test executors)
201 // Each row: (executor, WETH, PM) → (weth_balance, erc6909_weth,
202 // erc6909_native) as U256 + the 064x hex the stateDiff dict uses.
203 // ---------------------------------------------------------------------------
204 type Fixture = (
205 Address, // executor
206 U256, // weth_balance_slot
207 &'static str, // weth_balance_slot_hex (064x)
208 U256, // erc6909_weth_slot
209 &'static str, // erc6909_weth_slot_hex
210 U256, // erc6909_native_slot
211 &'static str, // erc6909_native_slot_hex
212 );
213
214 const FIXTURES: &[Fixture] = &[
215 (
216 address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
217 u256_from_hex("ca0453669a7127ce38f304ce121e552d78c30286022ebefeef6884684816084d"),
218 "ca0453669a7127ce38f304ce121e552d78c30286022ebefeef6884684816084d",
219 u256_from_hex("c87651b1e38cc90cedd910b68ad3a33c54f8132e8e50beaae3ca68aafafe854a"),
220 "c87651b1e38cc90cedd910b68ad3a33c54f8132e8e50beaae3ca68aafafe854a",
221 u256_from_hex("27b77f9e86613e8da78f13fe38575cf532b4167ab8d8d5303c689cc9fd0ed7ff"),
222 "27b77f9e86613e8da78f13fe38575cf532b4167ab8d8d5303c689cc9fd0ed7ff",
223 ),
224 (
225 address!("dead000000000000000000000000000000000001"),
226 u256_from_hex("f35974400be343ad66717b2a38de57c05ec39411b31023551415263aad6916c0"),
227 "f35974400be343ad66717b2a38de57c05ec39411b31023551415263aad6916c0",
228 u256_from_hex("95c453f6b7d6cf8b9823c709b3f0fedc6e6a884d3f9f5bdc510e8822c1fb31d5"),
229 "95c453f6b7d6cf8b9823c709b3f0fedc6e6a884d3f9f5bdc510e8822c1fb31d5",
230 u256_from_hex("b2e80833314f92ac980f4c8d9255290f04bbb025949387963ffc8a62ffb875c1"),
231 "b2e80833314f92ac980f4c8d9255290f04bbb025949387963ffc8a62ffb875c1",
232 ),
233 (
234 address!("dead000000000000000000000000000000000002"),
235 u256_from_hex("f189b9f9855f3e9ba1ed2b62d0daf3b7a19d5a80bc3be3cf7a43f4d3f7324366"),
236 "f189b9f9855f3e9ba1ed2b62d0daf3b7a19d5a80bc3be3cf7a43f4d3f7324366",
237 u256_from_hex("5dff84ecba6fdee6e82672cd2d229a766608bc3d85c630594b0ac4bd1082be3c"),
238 "5dff84ecba6fdee6e82672cd2d229a766608bc3d85c630594b0ac4bd1082be3c",
239 u256_from_hex("394274c6b1bd4f5ef5f3123f467627e5d50a0fcb9c065c4b31a2b16774aaf2a6"),
240 "394274c6b1bd4f5ef5f3123f467627e5d50a0fcb9c065c4b31a2b16774aaf2a6",
241 ),
242 ];
243
244 #[test]
245 fn parity_vs_python_oracle() {
246 for &(executor, weth_slot, weth_hex, erc_weth, erc_weth_hex, erc_native, erc_native_hex) in
247 FIXTURES
248 {
249 let slots = compute_simulation_warmup_slots(executor, WETH);
250 assert_eq!(
251 slots.weth_balance, weth_slot,
252 "WETH balance slot (executor {executor:?})"
253 );
254 assert_eq!(
255 slots.erc6909_weth, erc_weth,
256 "ERC6909 WETH slot (executor {executor:?})"
257 );
258 assert_eq!(
259 slots.erc6909_native, erc_native,
260 "ERC6909 native slot (executor {executor:?})"
261 );
262
263 // The stateDiff dict keys are the 064x hex of the slot — confirm the
264 // hex round-trip matches the oracle's `f"0x{slot:064x}"`.
265 assert_eq!(
266 format!("{:064x}", slots.weth_balance),
267 weth_hex,
268 "WETH balance slot hex (executor {executor:?})"
269 );
270 assert_eq!(
271 format!("{:064x}", slots.erc6909_weth),
272 erc_weth_hex,
273 "ERC6909 WETH slot hex (executor {executor:?})"
274 );
275 assert_eq!(
276 format!("{:064x}", slots.erc6909_native),
277 erc_native_hex,
278 "ERC6909 native slot hex (executor {executor:?})"
279 );
280 }
281 }
282
283 // ---------------------------------------------------------------------------
284 // mapping_slot / nested_mapping_slot primitives — direct parity + layout.
285 // ---------------------------------------------------------------------------
286
287 /// The mapping_slot composition is `keccak256(key ‖ base_slot)` (key FIRST),
288 /// matching the Solidity layout `key.to_bytes(32,"big") ‖ base_slot.to_bytes(32,"big")`.
289 /// Cross-checks against the parity corpus's manual keccak (L271–L278):
290 /// keccak256(executor_32bytes ‖ 3_32bytes).
291 #[test]
292 fn mapping_slot_layout_is_key_then_base_slot() {
293 let executor = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
294 let executor_u = U256::from_be_bytes(executor.into_word().0);
295 let got = mapping_slot(U256::from(3u64), executor_u);
296
297 // Manual: keccak256(executor_bytes32 ‖ 3_bytes32).
298 let mut preimage = [0u8; 64];
299 preimage[0..32].copy_from_slice(&executor.into_word().0);
300 preimage[32..64].copy_from_slice(&U256::from(3u64).to_be_bytes::<32>());
301 let want = U256::from_be_bytes(keccak256(preimage).0);
302
303 assert_eq!(got, want);
304 // And the mainnet-parity WETH balance slot value:
305 assert_eq!(
306 got,
307 u256_from_hex("ca0453669a7127ce38f304ce121e552d78c30286022ebefeef6884684816084d")
308 );
309 }
310
311 /// `nested_mapping_slot` = `mapping_slot(mapping_slot(base, k1), k2)`.
312 #[test]
313 fn nested_mapping_slot_composition() {
314 let base = U256::from(4u64);
315 let k1 = U256::from_be_bytes(
316 address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
317 .into_word()
318 .0,
319 );
320 let k2 = WETH_ID;
321
322 let got = nested_mapping_slot(base, k1, k2);
323 let want = mapping_slot(mapping_slot(base, k1), k2);
324 assert_eq!(got, want);
325 // Mainnet parity: the ERC6909 WETH slot for executor 0xAA…AA.
326 assert_eq!(
327 got,
328 u256_from_hex("c87651b1e38cc90cedd910b68ad3a33c54f8132e8e50beaae3ca68aafafe854a")
329 );
330 }
331
332 /// The nested slot with `key2 = native_id (0)` is *not* the same as a
333 /// single-level `mapping_slot(base, executor)` — confirms the double hash.
334 #[test]
335 fn nested_native_slot_is_double_hashed() {
336 let base = U256::from(4u64);
337 let executor = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
338 let k1 = U256::from_be_bytes(executor.into_word().0);
339
340 let native = nested_mapping_slot(base, k1, U256::ZERO);
341 let single = mapping_slot(base, k1);
342 assert_ne!(native, single);
343 // Mainnet parity: the ERC6909 native slot for executor 0xAA…AA.
344 assert_eq!(
345 native,
346 u256_from_hex("27b77f9e86613e8da78f13fe38575cf532b4167ab8d8d5303c689cc9fd0ed7ff")
347 );
348 }
349
350 // ---------------------------------------------------------------------------
351 // ERC6909 id derivation.
352 // ---------------------------------------------------------------------------
353
354 #[test]
355 fn erc6909_id_is_uint160_of_currency() {
356 // uint160(WETH) — the canonical mainnet WETH id.
357 assert_eq!(erc6909_id(WETH), WETH_ID);
358 assert_eq!(
359 erc6909_id(WETH),
360 U256::from_be_bytes(WETH.into_word().0) & MASK_160
361 );
362 // Native: uint160(address(0)) == 0.
363 assert_eq!(erc6909_id(Address::ZERO), U256::ZERO);
364 }
365
366 // ---------------------------------------------------------------------------
367 // Property tests.
368 // ---------------------------------------------------------------------------
369
370 /// Invariant: `mapping_slot` is deterministic — same inputs → same slot.
371 #[test]
372 fn property_mapping_slot_deterministic() {
373 use proptest::prelude::*;
374 proptest!(|(base in 0u64..u64::MAX, key in 0u64..u64::MAX)| {
375 let a = mapping_slot(U256::from(base), U256::from(key));
376 let b = mapping_slot(U256::from(base), U256::from(key));
377 prop_assert_eq!(a, b);
378 });
379 }
380
381 /// Invariant: `mapping_slot` outputs are uniformly spread across the U256
382 /// space (a keccak collision / extreme clustering would fail this).
383 #[test]
384 fn property_mapping_slot_spread() {
385 use proptest::prelude::*;
386 proptest!(|(base in 0u64..u64::MAX, key in 0u64..u64::MAX)| {
387 let slot = mapping_slot(U256::from(base), U256::from(key));
388 // keccak outputs are effectively uniform — assert the slot is not
389 // trivially small (it should exceed a u64 for any input, since
390 // keccak256 of a 64-byte preimage never lands below ~2^190 in
391 // practice; this catches accidental truncation to low bytes).
392 prop_assert!(slot > U256::from(u64::MAX));
393 });
394 }
395
396 /// Invariant: distinct keys yield distinct slots (no collisions over a
397 /// modest sweep — keccak behaves as a random oracle).
398 #[test]
399 fn property_mapping_slot_no_collision_over_sweep() {
400 let base = U256::from(3u64);
401 let mut seen = std::collections::HashSet::new();
402 for key in 0u64..1000 {
403 assert!(
404 seen.insert(mapping_slot(base, U256::from(key))),
405 "collision at base=3, key={key}"
406 );
407 }
408 }
409}