Skip to main content

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}