Skip to main content

degenbot_executor/
encoders.rs

1//! Command-stream primitives — opcode constants, `AddressTable`, and a `pub fn enc_*` builder
2//! for every opcode (`0x00`–`0x59`, `0xFF`).
3//!
4//! A pyo3-free *core leaf* implementing the tightly-packed command-bytecode
5//! layout. The authoritative contract layout is `contracts/README.md`
6//! "Command-Stream Executor" (opcode | name | encoding | description); Vyper
7//! source `executor/contracts/cmd_executor.vy` is the single source of
8//! truth for the wire format. These Rust `enc_*` builders are canonical
9//! (ADR-005); the per-opcode encoding tables below describe every field.
10//!
11//! # Scope
12//!
13//! The primitive encoders + `AddressTable` + `make_pool_key` ONLY (`pack_config`
14//! lives in `config`, re-exported here). The per-path-type composers (in `composers.rs`) and the PyO3
15//! wrappers are sibling / cutover tasks. `# Errors` doc sections appear on
16//! every `pub fn` returning `Result`.
17//!
18//! # Parity (§4.2 hard gate)
19//!
20//! Byte-for-byte parity vs the `cmd_executor` contract layout over a fixture
21//! corpus; the expected hex is embedded in the `tests` module below and is
22//! re-derived from these `enc_*` primitives, not from any external oracle.
23//!
24//! # V4 sign convention (§10.2)
25//!
26//! V4 uses negative `amountSpecified`; the compact uint96 amount accepted by
27//! `enc_v4_swap_compact` / `enc_v4_batch` is **positive** (exact-input) — the
28//! contract negates it internally. Documented per-fn.
29
30use std::collections::HashMap;
31
32use alloy::primitives::{Address, U256};
33
34// ── Sentinel address indices ────────────────────────────────────────────────
35// Resolved by the contract without SET_ADDRESS or TLOAD. Only 4 protocol
36// sentinels exist (0xFC–0xFF). Per-path tokens (USDC, WBTC, …) are NOT baked
37// into the contract — they go through the t_addresses table via SET_ADDRESS.
38
39/// PoolManager (immutable, set at deploy).
40pub const SENTINEL_PM: u8 = 0xFC;
41/// `self` / executor address.
42pub const SENTINEL_SELF: u8 = 0xFD;
43/// WETH (immutable, set at deploy).
44pub const SENTINEL_WETH: u8 = 0xFE;
45/// `address(0)` / `NATIVE_ADDRESS` — also the "no hooks" flag.
46pub const SENTINEL_NATIVE: u8 = 0xFF;
47/// `idx >= SENTINEL_THRESHOLD` is a protocol sentinel; `< it` is a table index.
48pub const SENTINEL_THRESHOLD: u8 = 0xFC;
49/// `t_addresses` table capacity — must match `MAX_INDEXED_ADDRESSES` in
50/// `cmd_executor.vy`.
51pub const MAX_INDEXED_ADDRESSES: usize = 32;
52
53/// `address(0)` — the native-ETH / "no address" sentinel address.
54pub const NATIVE_ADDRESS: Address = Address::ZERO;
55
56/// The largest V4 static `fee` the cmd_executor can encode .
57///
58/// Both `V4_SWAP_COMPACT` and `V4_SWAP_DYNAMIC` encode `fee` as a **2-byte**
59/// field (`push_u16`); the contract decodes `fee = (pkh >> 32) & 65535`,
60/// masking to `u16`. A static fee `> u16::MAX` (65535) is protocol-valid
61/// (`< 1 << 24`, not the dynamic-fee flag `0x800000`) but cannot be encoded by
62/// the executor. Such pools are also unprofitable (32%+ per swap) and are
63/// rejected at V4 admission (`BotState::register_v4_pool`) rather than wasting
64/// a solve + encode-fail cycle.
65///
66/// `0x1_0000 = 65_536` is the first fee value the 2-byte field cannot hold.
67pub const V4_FEE_ENCODER_MAX: u32 = 0x1_0000;
68
69// ── Command opcodes ─────────────────────────────────────────────────────────
70// Only 0x00 (SET_ADDRESS) and 0xFF (BEGIN_EXECUTION) are preprocessing
71// opcodes. 0x01–0x03 are reserved — their old SKIP_PROFIT_CHECK / BRIBE behavior
72// moved into the packed `config` ABI param of `execute()`; emitting them
73// reverts (InvalidCommand).
74
75/// `SET_ADDRESS` — append an address to the lookup table.
76pub const CMD_SET_ADDRESS: u8 = 0x00;
77
78/// `ERC20_TRANSFER` — transfer an ERC-20 (uint96 amount).
79pub const CMD_ERC20_TRANSFER: u8 = 0x10;
80/// `ERC20_XFER_BALANCE` — transfer an entire ERC-20 balance.
81pub const CMD_ERC20_XFER_BALANCE: u8 = 0x11;
82/// `WETH_DEPOSIT` — wrap ETH to WETH.
83pub const CMD_WETH_DEPOSIT: u8 = 0x12;
84/// `WETH_WITHDRAW` — unwrap WETH to ETH.
85pub const CMD_WETH_WITHDRAW: u8 = 0x13;
86/// `WETH_DEPOSIT_ALL` — wrap all ETH.
87pub const CMD_WETH_DEPOSIT_ALL: u8 = 0x14;
88/// `WETH_WITHDRAW_ALL` — unwrap all WETH.
89pub const CMD_WETH_WITHDRAW_ALL: u8 = 0x15;
90/// `SEND_ETH` — send uint96 ETH.
91pub const CMD_SEND_ETH: u8 = 0x16;
92/// `SEND_ETH_ALL` — send all ETH.
93pub const CMD_SEND_ETH_ALL: u8 = 0x17;
94
95/// `V2_SWAP_COMPACT` — V2 swap + forward data (uint96 amount).
96pub const CMD_V2_SWAP_COMPACT: u8 = 0x20;
97/// `V2_SWAP_CALC` — V2 swap from excess balance.
98pub const CMD_V2_SWAP_CALC: u8 = 0x21;
99/// `V2_SWAP_DIRECT` — V2 swap, explicit amount.
100pub const CMD_V2_SWAP_DIRECT: u8 = 0x22;
101
102/// `V3_SWAP_COMPACT` — V3 swap + auto-pay (uint96 amount).
103pub const CMD_V3_SWAP_COMPACT: u8 = 0x30;
104/// `V3_SWAP_DELTA` — V3 swap from PM exttload.
105pub const CMD_V3_SWAP_DELTA: u8 = 0x31;
106
107/// `V4_SWAP_COMPACT` — V4 swap, explicit amount (uint96).
108pub const CMD_V4_SWAP_COMPACT: u8 = 0x40;
109/// `V4_SWAP_DYNAMIC` — V4 swap from PM exttload.
110pub const CMD_V4_SWAP_DYNAMIC: u8 = 0x41;
111/// `V4_BATCH` — multi-swap + auto-settle (max 8).
112pub const CMD_V4_BATCH: u8 = 0x42;
113pub const CMD_V4_BATCH_OPEN_WETH: u8 = 0x43;
114
115/// `V4_UNLOCK` — enter PM unlock context.
116pub const CMD_V4_UNLOCK: u8 = 0x50;
117/// `V4_TAKE` — take from PM.
118pub const CMD_V4_TAKE: u8 = 0x51;
119/// `V4_TAKE_COMPACT` — take, uint96 amount.
120pub const CMD_V4_TAKE_COMPACT: u8 = 0x52;
121/// `V4_TAKE_DELTA` — take from PM exttload.
122pub const CMD_V4_TAKE_DELTA: u8 = 0x53;
123/// `V4_SYNC` — sync at PM (anytime).
124pub const CMD_V4_SYNC: u8 = 0x54;
125/// `V4_SETTLE` — settle at PM.
126pub const CMD_V4_SETTLE: u8 = 0x55;
127/// `V4_SETTLE_DELTA` — settle one currency from exttload.
128pub const CMD_V4_SETTLE_DELTA: u8 = 0x56;
129/// `V4_SETTLE_ALL` — settle all nonzero deltas.
130pub const CMD_V4_SETTLE_ALL: u8 = 0x57;
131/// `V4_MINT_COMPACT` — mint ERC6909 (no transfer).
132pub const CMD_V4_MINT_COMPACT: u8 = 0x58;
133/// `V4_BURN_COMPACT` — burn ERC6909 (no transfer).
134pub const CMD_V4_BURN_COMPACT: u8 = 0x59;
135
136/// `BEGIN_EXECUTION` — marks end of preprocessing / start of execution.
137pub const BEGIN_EXECUTION: u8 = 0xFF;
138
139/// The exclusive upper bound for a uint96 amount (`2^96`), used to validate
140/// every uint96 amount field (amounts ≥ this overflow the 12-byte field).
141const UINT96_BOUND: u128 = 1u128 << 96;
142
143/// Errors raised by the command-stream primitive encoders.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum EncoderError {
146    /// A uint96 amount field was given a value ≥ `2^96`.
147    Uint96Overflow(u128),
148    /// A `forward_data` slice exceeded the 1-byte length cap (255 bytes).
149    ForwardDataTooLong(usize),
150    /// The `AddressTable` is full (`MAX_INDEXED_ADDRESSES` reached).
151    AddressTableFull,
152    /// A `V4_BATCH` exceeded the contract's 8-swap cap.
153    TooManyV4BatchSwaps(usize),
154}
155
156impl std::fmt::Display for EncoderError {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            Self::Uint96Overflow(v) => write!(f, "uint96 amount {v} overflows the 12-byte field"),
160            Self::ForwardDataTooLong(n) => {
161                write!(f, "forward_data length {n} exceeds the 255-byte cap")
162            }
163            Self::AddressTableFull => write!(
164                f,
165                "address table full (max {MAX_INDEXED_ADDRESSES} entries)"
166            ),
167            Self::TooManyV4BatchSwaps(n) => {
168                write!(f, "V4_BATCH max 8 swaps, got {n}")
169            }
170        }
171    }
172}
173
174impl std::error::Error for EncoderError {}
175
176// ── Byte-pushing helpers (mirrors Python `_e(v, n, signed)` + `_address_to_bytes`) ──
177
178fn push_u8(out: &mut Vec<u8>, v: u8) {
179    out.push(v);
180}
181
182fn push_u16(out: &mut Vec<u8>, v: u16) {
183    out.extend_from_slice(&v.to_be_bytes());
184}
185
186/// Push an `int16` tick-spacing as two big-endian bytes. The contract decodes
187/// `tick_spacing` as a signed `int16`; V4 tick spacings are positive in
188/// practice, but the signed two's-complement layout is the wire format.
189fn push_i16(out: &mut Vec<u8>, v: i16) {
190    out.extend_from_slice(&v.to_be_bytes());
191}
192
193/// Push a uint96 amount (≤ `2^96 − 1`) as 12 big-endian bytes; anything larger
194/// is rejected as a `Uint96Overflow` (the 12-byte field cannot hold it).
195fn push_u96(out: &mut Vec<u8>, v: u128) -> Result<(), EncoderError> {
196    if v >= UINT96_BOUND {
197        return Err(EncoderError::Uint96Overflow(v));
198    }
199    let b = v.to_be_bytes();
200    out.extend_from_slice(&b[4..]); // last 12 of the 16-byte u128
201    Ok(())
202}
203
204/// Push a uint256 amount as 32 big-endian bytes (`_e(amount)`).
205fn push_u256(out: &mut Vec<u8>, v: U256) {
206    out.extend_from_slice(&v.to_be_bytes::<32>());
207}
208
209/// Push a 1-byte `forward_data` length prefix + the data, rejecting slices
210/// exceeding the 255-byte cap (the length is itself a `uint8`).
211fn push_forward_data(out: &mut Vec<u8>, data: &[u8]) -> Result<(), EncoderError> {
212    let len_u8 =
213        u8::try_from(data.len()).map_err(|_| EncoderError::ForwardDataTooLong(data.len()))?;
214    out.push(len_u8);
215    out.extend_from_slice(data);
216    Ok(())
217}
218
219// ── `pack_config` (re-exported from `config`) ─────────────────────────────────
220
221pub use crate::config::pack_config;
222
223// ── AddressTable ────────────────────────────────────────────────────────────
224
225/// Tracks addresses for compact index-based referencing in the command stream.
226///
227/// Each address is assigned a sequential index in insertion order
228/// (`0..MAX_INDEXED_ADDRESSES − 1`). Sentinel indices (`0xFC`–`0xFF`) resolve
229/// to the 4 protocol roles (PM / SELF / WETH / NATIVE) without `SET_ADDRESS` or
230/// `TLOAD`, saving ~476 gas per use. The table is built during preprocessing
231/// and referenced during execution.
232#[derive(Debug, Default)]
233pub struct AddressTable {
234    addresses: Vec<Address>,
235    index_map: HashMap<Address, u8>,
236    sentinel_map: HashMap<Address, u8>,
237}
238
239impl AddressTable {
240    /// Build a new table. `NATIVE_ADDRESS` (`address(0)`) always resolves to
241    /// [`SENTINEL_NATIVE`] without an explicit [`Self::add`] — the table is
242    /// pre-seeded with `sentinel_map[NATIVE_ADDRESS] = 0xFF` (and the same for
243    /// `ZERO_ADDRESS`, the same address).
244    #[must_use]
245    pub fn new() -> Self {
246        let mut sentinel_map = HashMap::new();
247        sentinel_map.insert(NATIVE_ADDRESS, SENTINEL_NATIVE);
248        // ZERO_ADDRESS == NATIVE_ADDRESS (both address(0)) — inserting both
249        // keys is a no-op overwrite of the same value.
250        sentinel_map.insert(Address::ZERO, SENTINEL_NATIVE);
251        Self {
252            addresses: Vec::new(),
253            index_map: HashMap::new(),
254            sentinel_map,
255        }
256    }
257
258    /// Build a table pre-seeded with the protocol sentinels: `pool_manager`
259    /// → [`SENTINEL_PM`], `executor` → [`SENTINEL_SELF`], `weth` →
260    /// [`SENTINEL_WETH`]. Any `None` sentinel is simply not registered.
261    #[must_use]
262    pub fn with_sentinels(
263        weth: Option<Address>,
264        executor: Option<Address>,
265        pool_manager: Option<Address>,
266    ) -> Self {
267        let mut table = Self::new();
268        if let Some(pm) = pool_manager {
269            table.sentinel_map.insert(pm, SENTINEL_PM);
270        }
271        if let Some(self_) = executor {
272            table.sentinel_map.insert(self_, SENTINEL_SELF);
273        }
274        if let Some(weth) = weth {
275            table.sentinel_map.insert(weth, SENTINEL_WETH);
276        }
277        table
278    }
279
280    /// Add an address, returning its index. Idempotent for duplicates.
281    ///
282    /// Sentinel addresses (WETH, PM, executor, NATIVE) return their fixed
283    /// sentinel index without adding to the table.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`EncoderError::AddressTableFull`] if the table already holds
288    /// [`MAX_INDEXED_ADDRESSES`] entries and `addr` is neither a sentinel nor
289    /// already present.
290    pub fn add(&mut self, addr: Address) -> Result<u8, EncoderError> {
291        // Check sentinel first.
292        if let Some(&idx) = self.sentinel_map.get(&addr) {
293            return Ok(idx);
294        }
295        if let Some(&idx) = self.index_map.get(&addr) {
296            return Ok(idx);
297        }
298        let idx = self.addresses.len();
299        if idx >= MAX_INDEXED_ADDRESSES {
300            return Err(EncoderError::AddressTableFull);
301        }
302        // `idx < MAX_INDEXED_ADDRESSES (32)` here, so the narrowing cannot fail;
303        // `unwrap_or` is panic-free and the fallback is unreachable.
304        let idx = u8::try_from(idx).unwrap_or(u8::MAX);
305        self.addresses.push(addr);
306        self.index_map.insert(addr, idx);
307        Ok(idx)
308    }
309
310    /// Return the table index (or sentinel index) for `addr`, or `None` if
311    /// `addr` was never added and is not a sentinel.
312    #[must_use]
313    pub fn index_of(&self, addr: Address) -> Option<u8> {
314        if let Some(&idx) = self.sentinel_map.get(&addr) {
315            Some(idx)
316        } else {
317            self.index_map.get(&addr).copied()
318        }
319    }
320
321    /// `true` if `addr` is a sentinel or a table entry.
322    #[must_use]
323    pub fn contains(&self, addr: Address) -> bool {
324        self.sentinel_map.contains_key(&addr) || self.index_map.contains_key(&addr)
325    }
326
327    /// Return only table addresses (not sentinels) for `SET_ADDRESS` encoding,
328    /// in insertion order.
329    #[must_use]
330    pub fn addresses(&self) -> &[Address] {
331        &self.addresses
332    }
333}
334
335// ── Preprocessing commands ──────────────────────────────────────────────────
336
337/// `SET_ADDRESS`: `[0x00][address:20]` — 21 bytes.
338#[must_use]
339pub fn enc_set_address(addr: Address) -> Vec<u8> {
340    let mut out = Vec::with_capacity(21);
341    out.push(CMD_SET_ADDRESS);
342    out.extend_from_slice(addr.as_slice());
343    out
344}
345
346/// Encode `SET_ADDRESS` commands for all table addresses (skip sentinels).
347#[must_use]
348pub fn enc_set_addresses(address_table: &AddressTable) -> Vec<u8> {
349    let mut out = Vec::with_capacity(address_table.addresses().len() * 21);
350    for &addr in address_table.addresses() {
351        out.extend_from_slice(&enc_set_address(addr));
352    }
353    out
354}
355
356/// Encode the full preprocessing section + separator: `[SET_ADDRESS commands][0xFF]`.
357///
358/// The stream starts directly with `SET_ADDRESS` commands — no `0xFE` prefix.
359/// Profit check and bribes are NO LONGER encoded in the stream — both are
360/// packed into the `config` ABI parameter of `execute()` (see [`pack_config`]).
361#[must_use]
362pub fn enc_preamble(address_table: &AddressTable) -> Vec<u8> {
363    let mut out = enc_set_addresses(address_table);
364    out.push(BEGIN_EXECUTION);
365    out
366}
367
368// ── ERC20 / ETH / Native commands (0x10–0x17) ───────────────────────────────
369
370/// `ERC20_TRANSFER`: `[0x10][token_idx:1][recipient_idx:1][amount:12]` — 15 bytes.
371///
372/// `amount` is `uint96` (max ~7.9e28 — covers all practical token amounts).
373///
374/// # Errors
375///
376/// Returns [`EncoderError::Uint96Overflow`] if `amount ≥ 2^96`.
377pub fn enc_erc20_transfer(
378    token_idx: u8,
379    recipient_idx: u8,
380    amount: u128,
381) -> Result<Vec<u8>, EncoderError> {
382    let mut out = Vec::with_capacity(15);
383    out.push(CMD_ERC20_TRANSFER);
384    push_u8(&mut out, token_idx);
385    push_u8(&mut out, recipient_idx);
386    push_u96(&mut out, amount)?;
387    Ok(out)
388}
389
390/// `ERC20_XFER_BALANCE`: `[0x11][token_idx:1][recipient_idx:1]` — 3 bytes.
391#[must_use]
392pub fn enc_erc20_xfer_balance(token_idx: u8, recipient_idx: u8) -> Vec<u8> {
393    vec![CMD_ERC20_XFER_BALANCE, token_idx, recipient_idx]
394}
395
396/// `WETH_DEPOSIT`: `[0x12][amount:32]` — 33 bytes.
397#[must_use]
398pub fn enc_weth_deposit(amount: U256) -> Vec<u8> {
399    let mut out = Vec::with_capacity(33);
400    out.push(CMD_WETH_DEPOSIT);
401    push_u256(&mut out, amount);
402    out
403}
404
405/// `WETH_WITHDRAW`: `[0x13][amount:32]` — 33 bytes.
406#[must_use]
407pub fn enc_weth_withdraw(amount: U256) -> Vec<u8> {
408    let mut out = Vec::with_capacity(33);
409    out.push(CMD_WETH_WITHDRAW);
410    push_u256(&mut out, amount);
411    out
412}
413
414/// `WETH_DEPOSIT_ALL`: `[0x14]` — 1 byte.
415#[must_use]
416pub fn enc_weth_deposit_all() -> Vec<u8> {
417    vec![CMD_WETH_DEPOSIT_ALL]
418}
419
420/// `WETH_WITHDRAW_ALL`: `[0x15]` — 1 byte.
421#[must_use]
422pub fn enc_weth_withdraw_all() -> Vec<u8> {
423    vec![CMD_WETH_WITHDRAW_ALL]
424}
425
426/// `SEND_ETH`: `[0x16][recipient_idx:1][amount:12]` — 14 bytes.
427///
428/// # Errors
429///
430/// Returns [`EncoderError::Uint96Overflow`] if `amount ≥ 2^96`.
431pub fn enc_send_eth(recipient_idx: u8, amount: u128) -> Result<Vec<u8>, EncoderError> {
432    let mut out = Vec::with_capacity(14);
433    out.push(CMD_SEND_ETH);
434    push_u8(&mut out, recipient_idx);
435    push_u96(&mut out, amount)?;
436    Ok(out)
437}
438
439/// `SEND_ETH_ALL`: `[0x17][recipient_idx:1]` — 2 bytes.
440#[must_use]
441pub fn enc_send_eth_all(recipient_idx: u8) -> Vec<u8> {
442    vec![CMD_SEND_ETH_ALL, recipient_idx]
443}
444
445// ── V2 commands (0x20–0x22) ─────────────────────────────────────────────────
446
447/// `V2_SWAP_COMPACT`: `[0x20][pool_idx:1][zfo:1][amount_out:12][recipient_idx:1][fee:2][fwd_len:1][fwd:N]` = 19 + N bytes.
448///
449/// `fee` is a fraction of 10000 (30 = 0.3% UniswapV2, 25 = 0.25% PancakeSwap),
450/// written to `t_v2_pair_fee[pool]` before `swap()` for correct auto-pay.
451/// `amount_out` is `uint96`. `forward_data` max 255 bytes.
452///
453/// # Errors
454///
455/// Returns [`EncoderError::Uint96Overflow`] if `amount_out ≥ 2^96`, or
456/// [`EncoderError::ForwardDataTooLong`] if `forward_data.len() > 255`.
457pub fn enc_v2_swap_compact(
458    pool_idx: u8,
459    zfo: bool,
460    amount_out: u128,
461    recipient_idx: u8,
462    fee: u16,
463    forward_data: &[u8],
464) -> Result<Vec<u8>, EncoderError> {
465    let mut out = Vec::with_capacity(19 + forward_data.len());
466    out.push(CMD_V2_SWAP_COMPACT);
467    push_u8(&mut out, pool_idx);
468    push_u8(&mut out, u8::from(zfo));
469    push_u96(&mut out, amount_out)?;
470    push_u8(&mut out, recipient_idx);
471    push_u16(&mut out, fee);
472    push_forward_data(&mut out, forward_data)?;
473    Ok(out)
474}
475
476/// `V2_SWAP_CALC`: `[0x21][pool_idx:1][zfo:1][recipient_idx:1][fee:2]` — 6 bytes.
477#[must_use]
478pub fn enc_v2_swap_calc(pool_idx: u8, zfo: bool, recipient_idx: u8, fee: u16) -> Vec<u8> {
479    let mut out = Vec::with_capacity(6);
480    out.push(CMD_V2_SWAP_CALC);
481    push_u8(&mut out, pool_idx);
482    push_u8(&mut out, u8::from(zfo));
483    push_u8(&mut out, recipient_idx);
484    push_u16(&mut out, fee);
485    out
486}
487
488/// `V2_SWAP_DIRECT`: `[0x22][pool_idx:1][zfo:1][amount_out:12][recipient_idx:1]` — 16 bytes.
489///
490/// V2 swap with explicit amount and no callback. `amount_out` is `uint96`. No
491/// fee field — the pair applies its stored fee.
492///
493/// # Errors
494///
495/// Returns [`EncoderError::Uint96Overflow`] if `amount_out ≥ 2^96`.
496pub fn enc_v2_swap_direct(
497    pool_idx: u8,
498    zfo: bool,
499    amount_out: u128,
500    recipient_idx: u8,
501) -> Result<Vec<u8>, EncoderError> {
502    let mut out = Vec::with_capacity(16);
503    out.push(CMD_V2_SWAP_DIRECT);
504    push_u8(&mut out, pool_idx);
505    push_u8(&mut out, u8::from(zfo));
506    push_u96(&mut out, amount_out)?;
507    push_u8(&mut out, recipient_idx);
508    Ok(out)
509}
510
511// ── V3 commands (0x30–0x31) ─────────────────────────────────────────────────
512
513/// `V3_SWAP_COMPACT`: `[0x30][pool_idx:1][zfo:1][amount_specified:12][recipient_idx:1][fwd_len:1][fwd:N]` = 17 + N bytes.
514///
515/// `amount_specified` is a **positive** `uint96` (exact-input — the contract
516/// negates it internally; see §10.2). Sqrt price limit auto-set to widest
517/// range. `forward_data` max 255 bytes.
518///
519/// # Errors
520///
521/// Returns [`EncoderError::Uint96Overflow`] if `amount_specified ≥ 2^96`, or
522/// [`EncoderError::ForwardDataTooLong`] if `forward_data.len() > 255`.
523pub fn enc_v3_swap_compact(
524    pool_idx: u8,
525    zfo: bool,
526    amount_specified: u128,
527    recipient_idx: u8,
528    forward_data: &[u8],
529) -> Result<Vec<u8>, EncoderError> {
530    let mut out = Vec::with_capacity(17 + forward_data.len());
531    out.push(CMD_V3_SWAP_COMPACT);
532    push_u8(&mut out, pool_idx);
533    push_u8(&mut out, u8::from(zfo));
534    push_u96(&mut out, amount_specified)?;
535    push_u8(&mut out, recipient_idx);
536    push_forward_data(&mut out, forward_data)?;
537    Ok(out)
538}
539
540/// `V3_SWAP_DELTA`: `[0x31][pool_idx:1][zfo:1][recipient_idx:1]` — 4 bytes.
541#[must_use]
542pub fn enc_v3_swap_delta(pool_idx: u8, zfo: bool, recipient_idx: u8) -> Vec<u8> {
543    let mut out = Vec::with_capacity(4);
544    out.push(CMD_V3_SWAP_DELTA);
545    push_u8(&mut out, pool_idx);
546    push_u8(&mut out, u8::from(zfo));
547    push_u8(&mut out, recipient_idx);
548    out
549}
550
551// ── V4 swap commands (0x40–0x42) ────────────────────────────────────────────
552
553/// `V4_SWAP_COMPACT`: `[0x40][c0_idx:1][c1_idx:1][fee:2][ts:2][hooks_idx:1][zfo:1][amount:12]` — 21 bytes.
554///
555/// `fee` is `uint16` (e.g. 3000 = 0.3%). `tick_spacing` is `int16` encoded as
556/// two big-endian bytes. `amount_u96` is a **positive** `uint96` exact-input
557/// amount — the contract negates it to a negative `amountSpecified` (§10.2).
558/// Use `hooks_idx = 0xFF` ([`SENTINEL_NATIVE`]) for "no hooks".
559///
560/// # Errors
561///
562/// Returns [`EncoderError::Uint96Overflow`] if `amount_u96 ≥ 2^96`.
563pub fn enc_v4_swap_compact(
564    c0_idx: u8,
565    c1_idx: u8,
566    fee: u16,
567    tick_spacing: i16,
568    hooks_idx: u8,
569    zfo: bool,
570    amount_u96: u128,
571) -> Result<Vec<u8>, EncoderError> {
572    let mut out = Vec::with_capacity(21);
573    out.push(CMD_V4_SWAP_COMPACT);
574    push_u8(&mut out, c0_idx);
575    push_u8(&mut out, c1_idx);
576    push_u16(&mut out, fee);
577    push_i16(&mut out, tick_spacing);
578    push_u8(&mut out, hooks_idx);
579    push_u8(&mut out, u8::from(zfo));
580    push_u96(&mut out, amount_u96)?;
581    Ok(out)
582}
583
584/// `V4_SWAP_DYNAMIC`: `[0x41][c0_idx:1][c1_idx:1][fee:2][ts:2][hooks_idx:1][zfo:1]` — 9 bytes.
585///
586/// Amount from PM `exttload`. `fee` is `uint16`, `tick_spacing` is `int16`.
587/// Use `hooks_idx = 0xFF` ([`SENTINEL_NATIVE`]) for "no hooks".
588#[must_use]
589pub fn enc_v4_swap_dynamic(
590    c0_idx: u8,
591    c1_idx: u8,
592    fee: u16,
593    tick_spacing: i16,
594    hooks_idx: u8,
595    zfo: bool,
596) -> Vec<u8> {
597    let mut out = Vec::with_capacity(9);
598    out.push(CMD_V4_SWAP_DYNAMIC);
599    push_u8(&mut out, c0_idx);
600    push_u8(&mut out, c1_idx);
601    push_u16(&mut out, fee);
602    push_i16(&mut out, tick_spacing);
603    push_u8(&mut out, hooks_idx);
604    push_u8(&mut out, u8::from(zfo));
605    out
606}
607
608/// A single entry in a `V4_BATCH` (`[c0_idx:1][c1_idx:1][fee:2][ts:2][hooks_idx:1][zfo:1][amount:12]` — 20 bytes).
609///
610/// `amount == 0` means dynamic (from PM `exttload`). `amount_u96` is a positive
611/// `uint96` exact-input amount (§10.2 — the contract negates internally).
612#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub struct V4BatchEntry {
614    /// Currency-0 table index.
615    pub c0_idx: u8,
616    /// Currency-1 table index.
617    pub c1_idx: u8,
618    /// Pool fee (`uint16`).
619    pub fee: u16,
620    /// Tick spacing (`int16`).
621    pub tick_spacing: i16,
622    /// Hooks address index (`0xFF` = no hooks).
623    pub hooks_idx: u8,
624    /// `zero_for_one` direction flag.
625    pub zfo: bool,
626    /// Positive `uint96` exact-input amount; `0` = dynamic.
627    pub amount_u96: u128,
628}
629
630/// `V4_BATCH`: `[0x42][num_swaps:1][entry_1:20]...[entry_N:20]`.
631///
632/// After all swaps, auto-settles native ETH and WETH deltas. Max 8 swaps
633/// (contract limit). Each 20-byte entry: `[c0_idx:1][c1_idx:1][fee:2][ts:2]`
634/// `[hooks_idx:1][zfo:1][amount:12]` — `amount == 0` means dynamic.
635///
636/// # Errors
637///
638/// Returns [`EncoderError::TooManyV4BatchSwaps`] if `swaps.len() > 8`, or
639/// [`EncoderError::Uint96Overflow`] if any entry's `amount_u96 ≥ 2^96`.
640pub fn enc_v4_batch(swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
641    v4_batch_stream(CMD_V4_BATCH, swaps)
642}
643
644/// `V4_BATCH_OPEN_WETH`: `[0x43][num_swaps:1][entry_1:20]...[entry_N:20]`.
645///
646/// Byte-identical layout to `V4_BATCH` (0x42) except the command byte: the
647/// PoolManager SKIPS the WETH tail-settle, leaving the positive WETH delta
648/// OPEN for a trailing `V4_MINT_COMPACT` (ERC6909 capture — TGUZCT/SW42JA);
649/// the native-ETH tail-settle still applies.
650///
651/// # Errors
652///
653/// Returns [`EncoderError::TooManyV4BatchSwaps`] if `swaps.len() > 8`, or
654/// [`EncoderError::Uint96Overflow`] if any entry's `amount_u96 ≥ 2^96`.
655pub fn enc_v4_batch_open_weth(swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
656    v4_batch_stream(CMD_V4_BATCH_OPEN_WETH, swaps)
657}
658
659/// Shared stream layout for `V4_BATCH` (0x42) / `V4_BATCH_OPEN_WETH` (0x43).
660///
661/// # Errors
662///
663/// Returns [`EncoderError::TooManyV4BatchSwaps`] if `swaps.len() > 8`, or
664/// [`EncoderError::Uint96Overflow`] if any entry's `amount_u96 ≥ 2^96`.
665fn v4_batch_stream(cmd: u8, swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
666    if swaps.len() > 8 {
667        return Err(EncoderError::TooManyV4BatchSwaps(swaps.len()));
668    }
669    let mut out = Vec::with_capacity(2 + swaps.len() * 20);
670    out.push(cmd);
671    // `swaps.len() ≤ 8` here, so the narrowing cannot fail; `unwrap_or` is
672    // panic-free and the fallback is unreachable.
673    push_u8(&mut out, u8::try_from(swaps.len()).unwrap_or(u8::MAX));
674    for s in swaps {
675        push_u8(&mut out, s.c0_idx);
676        push_u8(&mut out, s.c1_idx);
677        push_u16(&mut out, s.fee);
678        push_i16(&mut out, s.tick_spacing);
679        push_u8(&mut out, s.hooks_idx);
680        push_u8(&mut out, u8::from(s.zfo));
681        push_u96(&mut out, s.amount_u96)?;
682    }
683    Ok(out)
684}
685
686// ── V4 settlement / ERC6909 commands (0x50–0x59) ────────────────────────────
687
688/// `V4_UNLOCK`: `[0x50][len:1][data:N]` — 2 + N bytes.
689///
690/// Forward data max 255 bytes. Enters the PoolManager unlock context.
691///
692/// # Errors
693///
694/// Returns [`EncoderError::ForwardDataTooLong`] if `forward_data.len() > 255`.
695pub fn enc_v4_unlock(forward_data: &[u8]) -> Result<Vec<u8>, EncoderError> {
696    let mut out = Vec::with_capacity(2 + forward_data.len());
697    out.push(CMD_V4_UNLOCK);
698    push_forward_data(&mut out, forward_data)?;
699    Ok(out)
700}
701
702/// `V4_TAKE`: `[0x51][currency_idx:1][recipient_idx:1][amount:32]` — 35 bytes.
703///
704/// Rarely used — prefer [`enc_v4_take_compact`] (15 bytes) or
705/// [`enc_v4_take_delta`] (3 bytes).
706#[must_use]
707pub fn enc_v4_take(currency_idx: u8, recipient_idx: u8, amount: U256) -> Vec<u8> {
708    let mut out = Vec::with_capacity(35);
709    out.push(CMD_V4_TAKE);
710    push_u8(&mut out, currency_idx);
711    push_u8(&mut out, recipient_idx);
712    push_u256(&mut out, amount);
713    out
714}
715
716/// `V4_TAKE_COMPACT`: `[0x52][currency_idx:1][recipient_idx:1][amount:12]` — 15 bytes.
717///
718/// Preferred over [`enc_v4_take`] for all known amounts. `amount_u96` is `uint96`.
719///
720/// # Errors
721///
722/// Returns [`EncoderError::Uint96Overflow`] if `amount_u96 ≥ 2^96`.
723pub fn enc_v4_take_compact(
724    currency_idx: u8,
725    recipient_idx: u8,
726    amount_u96: u128,
727) -> Result<Vec<u8>, EncoderError> {
728    let mut out = Vec::with_capacity(15);
729    out.push(CMD_V4_TAKE_COMPACT);
730    push_u8(&mut out, currency_idx);
731    push_u8(&mut out, recipient_idx);
732    push_u96(&mut out, amount_u96)?;
733    Ok(out)
734}
735
736/// `V4_TAKE_DELTA`: `[0x53][currency_idx:1][recipient_idx:1]` — 3 bytes.
737#[must_use]
738pub fn enc_v4_take_delta(currency_idx: u8, recipient_idx: u8) -> Vec<u8> {
739    vec![CMD_V4_TAKE_DELTA, currency_idx, recipient_idx]
740}
741
742/// `V4_SYNC`: `[0x54][currency_idx:1]` — 2 bytes.
743#[must_use]
744pub fn enc_v4_sync(currency_idx: u8) -> Vec<u8> {
745    vec![CMD_V4_SYNC, currency_idx]
746}
747
748/// `V4_SETTLE`: `[0x55]` — 1 byte.
749#[must_use]
750pub fn enc_v4_settle() -> Vec<u8> {
751    vec![CMD_V4_SETTLE]
752}
753
754/// `V4_SETTLE_DELTA`: `[0x56][currency_idx:1]` — 2 bytes.
755#[must_use]
756pub fn enc_v4_settle_delta(currency_idx: u8) -> Vec<u8> {
757    vec![CMD_V4_SETTLE_DELTA, currency_idx]
758}
759
760/// `V4_SETTLE_ALL`: `[0x57]` — 1 byte.
761#[must_use]
762pub fn enc_v4_settle_all() -> Vec<u8> {
763    vec![CMD_V4_SETTLE_ALL]
764}
765
766/// `V4_MINT_COMPACT`: `[0x58][currency_idx:1][recipient_idx:1][amount:12]` — 15 bytes.
767///
768/// Convert a positive PM delta into an ERC6909 balance for `recipient` (no
769/// physical token transfer — the asset stays inside PoolManager). `amount_u96`
770/// is `uint96`.
771///
772/// # Errors
773///
774/// Returns [`EncoderError::Uint96Overflow`] if `amount_u96 ≥ 2^96`.
775pub fn enc_v4_mint_compact(
776    currency_idx: u8,
777    recipient_idx: u8,
778    amount_u96: u128,
779) -> Result<Vec<u8>, EncoderError> {
780    let mut out = Vec::with_capacity(15);
781    out.push(CMD_V4_MINT_COMPACT);
782    push_u8(&mut out, currency_idx);
783    push_u8(&mut out, recipient_idx);
784    push_u96(&mut out, amount_u96)?;
785    Ok(out)
786}
787
788/// `V4_BURN_COMPACT`: `[0x59][currency_idx:1][amount:12]` — 14 bytes.
789///
790/// Convert an ERC6909 balance into a payable PM delta (offsets a debt). No
791/// physical token transfer. `amount_u96` is `uint96`.
792///
793/// # Errors
794///
795/// Returns [`EncoderError::Uint96Overflow`] if `amount_u96 ≥ 2^96`.
796pub fn enc_v4_burn_compact(currency_idx: u8, amount_u96: u128) -> Result<Vec<u8>, EncoderError> {
797    let mut out = Vec::with_capacity(14);
798    out.push(CMD_V4_BURN_COMPACT);
799    push_u8(&mut out, currency_idx);
800    push_u96(&mut out, amount_u96)?;
801    Ok(out)
802}
803
804// ── Pool key helper ──────────────────────────────────────────────────────────
805
806/// A V4 pool key with currencies sorted so `currency0 < currency1`.
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
808pub struct V4PoolKey {
809    /// The numerically-smaller currency.
810    pub currency0: Address,
811    /// The numerically-larger currency.
812    pub currency1: Address,
813    /// Pool fee (`uint24` in the on-chain `PoolKey`; the compact encoders take
814    /// a `uint16` view).
815    pub fee: u32,
816    /// Tick spacing (signed; the compact encoders take an `int16` view).
817    pub tick_spacing: i32,
818    /// Hooks address (`address(0)` = no hooks).
819    pub hooks: Address,
820}
821
822/// Create a V4 pool key with currencies sorted by address.
823///
824/// Returns `(currency0, currency1, fee, tick_spacing, hooks)` with
825/// `currency0 < currency1` (lexicographic on the raw 20 bytes — equivalent to
826/// `Address`'s `Ord`, which compares the big-endian numeric value). Mirrors the
827/// currency-sort in [`crate`]'s `create2` precedent.
828#[must_use]
829pub fn make_pool_key(
830    currency0: Address,
831    currency1: Address,
832    fee: u32,
833    tick_spacing: i32,
834    hooks: Address,
835) -> V4PoolKey {
836    let (c0, c1) = if currency0 <= currency1 {
837        (currency0, currency1)
838    } else {
839        (currency1, currency0)
840    };
841    V4PoolKey {
842        currency0: c0,
843        currency1: c1,
844        fee,
845        tick_spacing,
846        hooks,
847    }
848}
849
850#[cfg(test)]
851#[expect(clippy::unwrap_used, clippy::cast_possible_truncation)]
852mod tests {
853    use super::*;
854    use alloy::primitives::address;
855
856    // ── uint96 boundary + overflow rejection ──
857
858    #[test]
859    fn uint96_max_is_accepted_overflow_is_rejected() {
860        // 2^96 − 1 is the largest valid uint96.
861        let max = u128::MAX >> 32;
862        assert_eq!(max, (1u128 << 96) - 1);
863        assert!(enc_erc20_transfer(1, 2, max).is_ok());
864        // 2^96 overflows the 12-byte field.
865        assert_eq!(
866            enc_erc20_transfer(1, 2, 1u128 << 96).unwrap_err(),
867            EncoderError::Uint96Overflow(1u128 << 96)
868        );
869    }
870
871    // ── AddressTable: sentinel resolution, dedup, cap ──
872
873    #[test]
874    fn address_table_sentinels_resolve_without_adding() {
875        let pm = address!("000000000004444c5dc75cB358380D2e3dE08A90");
876        let exec = address!("DeAd0000000000000000000000000000000000Be");
877        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
878        let mut table = AddressTable::with_sentinels(Some(weth), Some(exec), Some(pm));
879
880        assert_eq!(table.add(weth).unwrap(), SENTINEL_WETH);
881        assert_eq!(table.add(pm).unwrap(), SENTINEL_PM);
882        assert_eq!(table.add(exec).unwrap(), SENTINEL_SELF);
883        assert_eq!(table.add(Address::ZERO).unwrap(), SENTINEL_NATIVE);
884        // Sentinels are NOT listed for SET_ADDRESS.
885        assert!(table.addresses().is_empty());
886    }
887
888    #[test]
889    fn address_table_dedups_insertion_order() {
890        let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
891        let wbtc = address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
892        let mut table = AddressTable::new();
893
894        assert_eq!(table.add(usdc).unwrap(), 0);
895        assert_eq!(table.add(wbtc).unwrap(), 1);
896        // Duplicate returns the same index — no new entry.
897        assert_eq!(table.add(usdc).unwrap(), 0);
898        assert_eq!(table.add(wbtc).unwrap(), 1);
899        assert_eq!(table.addresses(), &[usdc, wbtc]);
900        // index_of mirrors add for present addresses.
901        assert_eq!(table.index_of(usdc), Some(0));
902        assert_eq!(table.index_of(wbtc), Some(1));
903        assert!(table
904            .index_of(address!("DeAd000000000000000000000000000000000001"))
905            .is_none());
906    }
907
908    #[test]
909    fn address_table_cap_rejects_beyond_32() {
910        // Fill the table to MAX_INDEXED_ADDRESSES, then the next add fails.
911        let mut table = AddressTable::new();
912        for i in 0u8..MAX_INDEXED_ADDRESSES as u8 {
913            // +1 so byte 0 (address(0) / NATIVE sentinel) is never produced.
914            let addr = Address::with_last_byte(i + 1);
915            assert_eq!(table.add(addr).unwrap(), i);
916        }
917        // Full.
918        let extra = Address::with_last_byte(0xAA);
919        assert_eq!(
920            table.add(extra).unwrap_err(),
921            EncoderError::AddressTableFull
922        );
923        assert_eq!(table.addresses().len(), MAX_INDEXED_ADDRESSES);
924    }
925
926    // ── make_pool_key currency sort (proper property) ──
927
928    #[test]
929    fn make_pool_key_sorts_and_is_symmetric() {
930        use proptest::prelude::*;
931        proptest!(|(a in 0u64..u64::MAX, b in 0u64..u64::MAX)| {
932            let ca = Address::with_last_byte((a & 0xFF) as u8);
933            let cb = Address::with_last_byte((b & 0xFF) as u8);
934            let k13 = make_pool_key(ca, cb, 3000, 60, Address::ZERO);
935            let k31 = make_pool_key(cb, ca, 3000, 60, Address::ZERO);
936            // Symmetric in argument order.
937            prop_assert_eq!(k13, k31);
938            // currency0 < currency1.
939            prop_assert!(k13.currency0 <= k13.currency1);
940        });
941    }
942
943    // ── proptest: AddressTable dedup is order-independent in membership ──
944
945    #[test]
946    fn property_address_table_membership_stable_under_reorder() {
947        use proptest::prelude::*;
948        proptest!(|(a in 0u64..256, b in 0u64..256, c in 0u64..256)| {
949            // The SET of members doesn't depend on insertion order.
950            let addrs: [Address; 3] = [
951                Address::with_last_byte(a as u8),
952                Address::with_last_byte(b as u8),
953                Address::with_last_byte(c as u8),
954            ];
955            let mut t1 = AddressTable::new();
956            let mut t2 = AddressTable::new();
957            for a_ in &addrs { t1.add(*a_).ok(); }
958            for a_ in addrs.iter().rev() { t2.add(*a_).ok(); }
959            // Same membership set.
960            for a_ in &addrs {
961                prop_assert_eq!(t1.contains(*a_), t2.contains(*a_));
962            }
963        });
964    }
965}