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}