wp-evm-v3-core 0.1.1

Pure data + quote + plan for v3-family (CL) DEXes — no async dependencies
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
//! Pure quote functions for the v3 family.
//!
//! Inputs: a hydrated `PoolState` + parameters.
//! Outputs: a `Quote` (amount_out, sqrt price after, price impact).
//!
//! No I/O. Caller hydrates state once via `hydrate::pool_state`, then can
//! call quote functions any number of times against the in-memory snapshot.

use crate::{
    data::{ExactInParams, ExactOutParams, PoolState, Quote},
    swap,
};
use alloy_primitives::{I256, U256};
use thiserror::Error;
use wp_evm_amm_math::AmmMathError;

#[derive(Error, Debug)]
pub enum QuoteError {
    #[error("input token does not match pool")]
    UnknownToken,
    #[error("amm math error: {0}")]
    Math(#[from] AmmMathError),
    #[error("pool has insufficient liquidity to fulfil the requested swap")]
    InsufficientLiquidity,
    /// Invariant violation inside the quote/swap pipeline. Used for
    /// conditions the surrounding code proves cannot occur (e.g. a
    /// zero-amount or invalid-price-limit signal bubbling up from `swap`
    /// after the quote layer has already guarded them). Kept as a distinct
    /// variant — rather than being folded into `Math` — so diagnostics
    /// accurately reflect the failure category.
    #[error("quote pipeline internal invariant violated: {0}")]
    Internal(&'static str),
}

impl From<swap::SwapError> for QuoteError {
    fn from(e: swap::SwapError) -> Self {
        match e {
            swap::SwapError::Math(m) => QuoteError::Math(m),
            swap::SwapError::InsufficientLiquidity => QuoteError::InsufficientLiquidity,
            // ZeroAmount and InvalidPriceLimit are unreachable from quote
            // entry points (zero amount is short-circuited above; limits are
            // set by quote to the protocol boundary). If a future guard
            // regression triggers them, surface the real cause via
            // `Internal` rather than masquerading as a math overflow.
            swap::SwapError::ZeroAmount => {
                QuoteError::Internal("swap returned ZeroAmount after quote-level guard")
            }
            swap::SwapError::InvalidPriceLimit => {
                QuoteError::Internal("swap returned InvalidPriceLimit with quote-supplied sentinel")
            }
            swap::SwapError::Internal(msg) => QuoteError::Internal(msg),
        }
    }
}

pub fn exact_in(state: &PoolState, params: &ExactInParams) -> Result<Quote, QuoteError> {
    exact_in_with_fee_fn(state, params, |s| s.fee)
}

/// Exact-in quote with an injectable fee computation.
///
/// The closure is called once with the pool state and must return the effective
/// swap fee in pips (e.g. 3000 for 0.3%). For protocols with static fees, use
/// `exact_in` which delegates here with `|s| s.fee`. For protocols with dynamic
/// fees (e.g. Algebra Integral), pass a closure that computes the current fee
/// from a fresh oracle read; the computed value is then used for the swap math.
///
/// This function is pure. The fee closure must itself be pure (no I/O).
pub fn exact_in_with_fee_fn<F>(
    state: &PoolState,
    params: &ExactInParams,
    fee_fn: F,
) -> Result<Quote, QuoteError>
where
    F: Fn(&PoolState) -> u32,
{
    let zero_for_one = params.token_in == state.token0;
    if !zero_for_one && params.token_in != state.token1 {
        return Err(QuoteError::UnknownToken);
    }

    // Preserve the SDK-era contract: zero amount_in is a no-op, not an error.
    // Documented by `exact_in_zero_amount_returns_zero_out` test.
    if params.amount_in.is_zero() {
        return Ok(Quote {
            amount_in: U256::ZERO,
            amount_out: U256::ZERO,
            sqrt_price_x96_after: state.sqrt_price_x96,
            price_impact_bps: 0,
        });
    }

    let effective_fee = fee_fn(state);

    let amount_specified = I256::try_from(params.amount_in)
        .map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
    let sqrt_price_limit_x96 = if zero_for_one {
        swap::min_sqrt_ratio_plus_one()
    } else {
        swap::max_sqrt_ratio_minus_one()
    };

    let result =
        swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;

    // The quote entry point sets `sqrt_price_limit_x96` to the protocol
    // boundary sentinel, so any partial fill at this layer means the pool
    // cannot satisfy the requested input (not a user-chosen early exit).
    // Without this check, a swap that walks to `MIN_SQRT_RATIO + 1` /
    // `MAX_SQRT_RATIO - 1` with remainder would return Ok with
    // `amount_in < params.amount_in` — silent degradation. Fail loud.
    if result.amount_in != params.amount_in {
        return Err(QuoteError::InsufficientLiquidity);
    }

    Ok(Quote {
        amount_in: result.amount_in,
        amount_out: result.amount_out,
        sqrt_price_x96_after: result.sqrt_price_x96_after,
        price_impact_bps: compute_price_impact_bps(
            state.sqrt_price_x96,
            result.sqrt_price_x96_after,
        ),
    })
}

pub fn exact_out(state: &PoolState, params: &ExactOutParams) -> Result<Quote, QuoteError> {
    exact_out_with_fee_fn(state, params, |s| s.fee)
}

/// Exact-out quote with an injectable fee computation.
///
/// Mirrors `exact_in_with_fee_fn`. The closure returns the effective fee in
/// pips used by the native swap loop. Default `exact_out` delegates here
/// with `|s| s.fee`. For dynamic-fee protocols, supply a closure that returns
/// the current fee from state.
///
/// This function is pure. The fee closure must itself be pure (no I/O).
pub fn exact_out_with_fee_fn<F>(
    state: &PoolState,
    params: &ExactOutParams,
    fee_fn: F,
) -> Result<Quote, QuoteError>
where
    F: Fn(&PoolState) -> u32,
{
    let zero_for_one = params.token_in == state.token0;
    if !zero_for_one && params.token_in != state.token1 {
        return Err(QuoteError::UnknownToken);
    }

    // Behavioural parity with `exact_in_with_fee_fn`'s zero-amount
    // short-circuit: zero `amount_out` is a no-op, not an error.
    if params.amount_out.is_zero() {
        return Ok(Quote {
            amount_in: U256::ZERO,
            amount_out: U256::ZERO,
            sqrt_price_x96_after: state.sqrt_price_x96,
            price_impact_bps: 0,
        });
    }

    let effective_fee = fee_fn(state);

    // Exact-out sign convention: `amount_specified` is the NEGATIVE of the
    // desired output (see `UniswapV3Pool.sol::swap` + `SwapMath.sol`).
    // `I256::try_from` can only fail for U256 >= 2^255, and `checked_neg`
    // only fails at `I256::MIN` (whose magnitude is 2^255). Both guards
    // surface as Math(MulDivOverflow) — shouldn't happen for any real pool.
    let amount_out_i = I256::try_from(params.amount_out)
        .map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
    let amount_specified =
        amount_out_i.checked_neg().ok_or(QuoteError::Math(AmmMathError::MulDivOverflow))?;

    let sqrt_price_limit_x96 = if zero_for_one {
        swap::min_sqrt_ratio_plus_one()
    } else {
        swap::max_sqrt_ratio_minus_one()
    };

    let result =
        swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;

    // Mirror of `exact_in_with_fee_fn`'s partial-fill guard. The quote layer
    // always passes the protocol-boundary sentinel as the limit, so any
    // shortfall means the pool genuinely cannot supply the requested output.
    // Without this check, a swap that walks to the boundary with unsatisfied
    // demand would silently return `amount_out < params.amount_out`.
    if result.amount_out != params.amount_out {
        return Err(QuoteError::InsufficientLiquidity);
    }

    Ok(Quote {
        amount_in: result.amount_in,
        amount_out: result.amount_out,
        sqrt_price_x96_after: result.sqrt_price_x96_after,
        price_impact_bps: compute_price_impact_bps(
            state.sqrt_price_x96,
            result.sqrt_price_x96_after,
        ),
    })
}

/// Compute absolute price impact as basis points.
///
/// `|after - before| / before * 10_000`, clamped to `u16::MAX`.
/// Returns 0 if `before == 0` (should not happen for a valid pool).
///
/// Note: this is computed on sqrt_price deltas. Since price ∝ sqrt_price²,
/// the bps reading understates true price-change bps by roughly half for
/// small moves. It is still useful as a monotone proxy for impact severity.
fn compute_price_impact_bps(before: U256, after: U256) -> u16 {
    if before == U256::ZERO {
        return 0;
    }
    let (num, denom) =
        if after > before { (after - before, before) } else { (before - after, before) };
    let bps = (num * U256::from(10_000u64)) / denom;
    bps.saturating_to::<u16>()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data::{ExactInParams, ExactOutParams, PoolState, TickInfo};
    use alloy_primitives::{address, U256};

    /// Fixture: USDC/WETH 0.3% pool.
    ///
    /// `sqrt_price_x96 = 3543191142285914205922034323214` encodes a price of
    /// roughly 2000 token1/token0 (in raw 18-decimal SDK units).
    /// `get_tick_at_sqrt_ratio(sqrt_price_x96)` gives **76012** — the value
    /// stored in `tick` exactly matches what the SDK derives, so the pool is
    /// internally consistent.
    ///
    /// Two overlapping liquidity ranges create three initialized ticks:
    ///
    ///   - Wide range  [74940, 76020]: L = 1e21  (1249 × 60 to 1267 × 60)
    ///   - Narrow range [75960, 76020]: L = 1e21  (1266 × 60 to 1267 × 60)
    ///
    /// At tick 76012 (in the overlap zone [75960, 76020]), active liquidity
    /// = 2e21. Tick structure:
    ///
    ///   74940: net = +1e21, gross = 1e21   (wide range lower bound)
    ///   75960: net = +1e21, gross = 1e21   (narrow range lower bound)
    ///   76020: net = -2e21, gross = 2e21   (shared upper bound for both)
    ///
    /// A `zero_for_one` swap (token0 in, price DOWN) crosses tick 75960 once
    /// ~1.2e17 tokens are consumed (SqrtPriceMath delta on 2e21 liquidity over
    /// a 52-tick gap). After crossing, liquidity drops to 1e21 (wide range
    /// only). The wide range holds ~6.7e18 tokens down to tick 74940, so a
    /// 1e18-input swap crosses 75960 and stops well before 74940.
    fn fixture_usdc_weth_03() -> PoolState {
        // sqrt_price_x96 encodes tick 76012; the SDK re-derives this tick from
        // the value when constructing Pool::new_with_tick_data_provider.
        let sqrt_price_x96: U256 =
            U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
        PoolState {
            token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
            token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
            fee: 3000,
            tick_spacing: 60,
            sqrt_price_x96,
            liquidity: 2_000_000_000_000_000_000_000u128, // 2e21 (wide + narrow range)
            tick: 76012, // matches get_tick_at_sqrt_ratio(sqrt_price_x96)
            ticks: vec![
                TickInfo {
                    // 1249 * 60 = 74940 — lower bound of wide range.
                    // get_sqrt_ratio_at_tick(74940) = 3358146572400655475063989961326
                    tick: 74940,
                    liquidity_net: 1_000_000_000_000_000_000_000i128,
                    liquidity_gross: 1_000_000_000_000_000_000_000u128,
                },
                TickInfo {
                    // 1266 * 60 = 75960 — lower bound of narrow range.
                    // A zero_for_one swap crosses this downward (~1.2e17 tokens
                    // needed from the 2e21 active liquidity in [75960, 76020]).
                    // get_sqrt_ratio_at_tick(75960) = 3533845506420911390540068078527
                    tick: 75960,
                    liquidity_net: 1_000_000_000_000_000_000_000i128,
                    liquidity_gross: 1_000_000_000_000_000_000_000u128,
                },
                TickInfo {
                    // 1267 * 60 = 76020 — shared upper bound for both ranges.
                    tick: 76020,
                    liquidity_net: -2_000_000_000_000_000_000_000i128,
                    liquidity_gross: 2_000_000_000_000_000_000_000u128,
                },
            ],
        }
    }

    #[test]
    fn exact_in_one_usdc_for_weth_within_tick() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::from(1_000_000u64), // 1 USDC (6 decimals)
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &p).expect("quote ok");
        assert!(q.amount_out > U256::ZERO);
        assert!(q.amount_out < U256::from(1_000_000_000_000_000u64));
        assert_eq!(q.amount_in, p.amount_in);
    }

    #[test]
    fn exact_in_rejects_unknown_token() {
        let s = fixture_usdc_weth_03();
        let bogus_token = address!("0x000000000000000000000000000000000000dead");
        let p = ExactInParams {
            token_in: bogus_token,
            token_out: s.token1,
            amount_in: U256::from(1_000_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_in(&s, &p).expect_err("should reject unknown token");
        assert!(matches!(err, QuoteError::UnknownToken));
    }

    #[test]
    fn exact_in_reverse_direction_weth_for_usdc() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token1,                              // WETH
            token_out: s.token0,                             // USDC
            amount_in: U256::from(1_000_000_000_000_000u64), // 0.001 WETH
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &p).expect("reverse quote ok");
        // The pool is constructed with both tokens hardcoded to 18 decimals.
        // The fixture sqrt_price_x96 encodes token1_per_token0 ≈ 2000 in raw
        // (both-18-decimal) units, so swapping WETH (token1) → USDC (token0)
        // at price 1/2000 yields: 1e15 / 2000 × (1 - 0.003) ≈ 4.985e11 raw units.
        // Allow ±3% tolerance for tick math rounding.
        assert!(
            q.amount_out > U256::from(480_000_000_000u64),
            "amount_out too low: {}",
            q.amount_out
        );
        assert!(
            q.amount_out < U256::from(515_000_000_000u64),
            "amount_out too high: {}",
            q.amount_out
        );
        assert_eq!(q.amount_in, p.amount_in);
    }

    #[test]
    fn exact_in_large_swap_crosses_tick() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,  // USDC (token0) in → zero_for_one → price moves DOWN
            token_out: s.token1, // WETH out
            // 1e18 is ~17× the amount needed to walk from tick 76012 down through
            // tick 75960 (SqrtPriceMath delta ≈ 6e16 at L=1e21), so this swap
            // will cross the 75960 boundary.
            amount_in: U256::from(1_000_000_000_000_000_000u64), // 1e18
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &p).expect("large swap ok");
        assert!(q.amount_out > U256::ZERO, "amount_out should be > 0");
        // sqrt_price at tick 75960 = 3533845506420911390540068078527.
        // After crossing, sqrt_price_after must be below that value.
        let sqrt_at_75960 = U256::from_str_radix("3533845506420911390540068078527", 10).unwrap();
        assert!(
            q.sqrt_price_x96_after < sqrt_at_75960,
            "expected sqrt_price to cross below tick 75960 (sqrt {}), got {}",
            sqrt_at_75960,
            q.sqrt_price_x96_after
        );
        assert_eq!(q.amount_in, p.amount_in);
    }

    #[test]
    fn exact_out_round_trip_against_exact_in() {
        let s = fixture_usdc_weth_03();
        let p_in = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::from(1_000_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q_in = exact_in(&s, &p_in).unwrap();

        let p_out = ExactOutParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_out: q_in.amount_out,
            recipient: p_in.recipient,
        };
        let q_out = exact_out(&s, &p_out).unwrap();

        // Round-trip should be within a few wei of the original input.
        // Allow up to 1000 wei difference to cover integer-division rounding
        // in the SDK's swap math without masking real bugs.
        let diff = if q_out.amount_in > p_in.amount_in {
            q_out.amount_in - p_in.amount_in
        } else {
            p_in.amount_in - q_out.amount_in
        };
        assert!(diff <= U256::from(1_000u64), "round-trip diff = {}", diff);
    }

    #[test]
    fn exact_in_reports_price_impact() {
        let s = fixture_usdc_weth_03();
        let big = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            // 1e18 crosses tick 75960 (see exact_in_large_swap_crosses_tick).
            // The sqrt_price moves by ~52 ticks worth (~0.26%), which is ≥ 1 bps
            // in the sqrt-price-delta metric, so price_impact_bps must be > 0.
            amount_in: U256::from(1_000_000_000_000_000_000u64), // 1e18
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &big).unwrap();
        assert!(q.price_impact_bps > 0, "expected nonzero price impact, got 0");
    }

    #[test]
    fn exact_in_small_swap_has_minimal_price_impact() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::from(1_000u64), // tiny
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &p).unwrap();
        // Tiny swap: price impact should be at most a few bps (certainly < 100 = 1%).
        assert!(q.price_impact_bps < 100, "expected tiny impact, got {}", q.price_impact_bps);
    }

    #[test]
    fn exact_out_rejects_unknown_token() {
        let s = fixture_usdc_weth_03();
        let bogus = address!("0x000000000000000000000000000000000000dead");
        let p = ExactOutParams {
            token_in: bogus,
            token_out: s.token1,
            amount_out: U256::from(1_000_000_000_000_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_out(&s, &p).expect_err("should reject unknown token");
        assert!(matches!(err, QuoteError::UnknownToken));
    }

    #[test]
    fn exact_in_with_fee_fn_uses_injected_fee() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::from(1_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        // With the normal 3000-pip (0.3%) fee:
        let q_normal = exact_in(&s, &p).expect("normal quote ok");
        // With an injected 100-pip (0.01%) fee — smallest valid SDK tier.
        // FeeAmount::from(0) triggers a tick-spacing invariant in the SDK, so
        // 100 is the lowest usable value for this test. It still demonstrates
        // that the closure result overrides state.fee.
        let q_low_fee = exact_in_with_fee_fn(&s, &p, |_| 100).expect("low-fee quote ok");
        // Lower-fee output must be strictly greater (less fee deducted).
        assert!(
            q_low_fee.amount_out > q_normal.amount_out,
            "low-fee output should exceed normal output: {} vs {}",
            q_low_fee.amount_out,
            q_normal.amount_out
        );
    }

    // ── Issue #4: edge case tests ─────────────────────────────────────────────

    #[test]
    fn exact_in_token_in_equals_token_out_returns_unknown_token() {
        // token_in == token_out: neither branch of `zero_for_one` matches,
        // so the guard at `!zero_for_one && token_in != token1` triggers.
        // token_in = token0, zero_for_one = true → we bypass the guard and
        // reach SDK pool construction. The SDK will accept token0 as input
        // and return a quote from token0 to token1; it does NOT see the
        // mismatch on token_out because exact_in_with_fee_fn only checks
        // token_in against the pool. Token_out is passed through to ABI
        // encoding but not validated by the SDK math path.
        //
        // Behaviour today: exact_in produces a valid Quote when token_in ==
        // token_out == token0, because the guard only checks token_in.
        // This test documents that known limitation rather than asserting
        // the ideal UnknownToken error.
        //
        // To surface UnknownToken we use a token not in the pool at all.
        let s = fixture_usdc_weth_03();
        let bogus = address!("0x000000000000000000000000000000000000dead");
        let p = ExactInParams {
            token_in: bogus,
            token_out: bogus, // same bogus token — token_in not in pool
            amount_in: U256::from(1_000_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_in(&s, &p).expect_err("should reject token not in pool");
        assert!(matches!(err, QuoteError::UnknownToken));
    }

    #[test]
    fn exact_out_token_in_equals_token_out_returns_unknown_token() {
        // Mirror of exact_in variant: bogus token_in triggers the guard.
        let s = fixture_usdc_weth_03();
        let bogus = address!("0x000000000000000000000000000000000000dead");
        let p = ExactOutParams {
            token_in: bogus,
            token_out: bogus,
            amount_out: U256::from(1_000_000u64),
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_out(&s, &p).expect_err("should reject token not in pool");
        assert!(matches!(err, QuoteError::UnknownToken));
    }

    #[test]
    fn exact_in_very_large_amount_returns_insufficient_liquidity() {
        // A swap of 1e40 exceeds the fixture's tick range entirely. The native
        // loop walks to the lowest tick, exhausts liquidity, and errors with
        // InsufficientLiquidity (the native loop reports this directly once
        // it walks to the MIN_SQRT_RATIO boundary with remainder).

        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
                .unwrap(), // 1e40
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_in(&s, &p).expect_err("should error on oversized amount");
        assert!(
            matches!(err, QuoteError::InsufficientLiquidity),
            "expected InsufficientLiquidity, got {:?}",
            err
        );
    }

    #[test]
    fn exact_out_very_large_amount_returns_insufficient_liquidity() {
        // Requesting output that exceeds the fixture's total WETH supply
        // forces the native loop to walk to the MIN_SQRT_RATIO boundary
        // with demand still unsatisfied, surfacing InsufficientLiquidity.
        // The native loop walks to the MIN_SQRT_RATIO boundary with demand
        // still unsatisfied and reports `InsufficientLiquidity` directly.

        let s = fixture_usdc_weth_03();
        let p = ExactOutParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_out: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
                .unwrap(), // 1e40
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let err = exact_out(&s, &p).expect_err("should error on oversized output request");
        assert!(
            matches!(err, QuoteError::InsufficientLiquidity),
            "expected InsufficientLiquidity, got {:?}",
            err
        );
    }

    #[test]
    fn exact_out_zero_amount_returns_zero() {
        // Zero `amount_out`: native path short-circuits to a zero-quote,
        // matching the exact_in zero-amount contract. No swap invoked.
        let s = fixture_usdc_weth_03();
        let p = ExactOutParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_out: U256::ZERO,
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_out(&s, &p).expect("zero-amount exact_out should not error");
        assert_eq!(q.amount_in, U256::ZERO);
        assert_eq!(q.amount_out, U256::ZERO);
        assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
        assert_eq!(q.price_impact_bps, 0);
    }

    #[test]
    fn exact_in_zero_amount_returns_zero_out() {
        // Zero amount_in: the uniswap-v3-sdk treats this as a no-op swap and
        // returns amount_out = 0 without erroring. Document this behaviour.
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token0,
            token_out: s.token1,
            amount_in: U256::ZERO,
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        // The SDK accepts zero input and returns zero output.
        let q = exact_in(&s, &p).expect("zero-amount swap should not error");
        assert_eq!(q.amount_out, U256::ZERO, "zero-in should produce zero-out");
        assert_eq!(q.amount_in, U256::ZERO);
    }

    #[test]
    fn exact_in_zero_amount_reverse_direction_returns_zero_out() {
        let s = fixture_usdc_weth_03();
        let p = ExactInParams {
            token_in: s.token1,
            token_out: s.token0,
            amount_in: U256::ZERO,
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_in(&s, &p).expect("reverse-direction zero-input should not error");
        assert_eq!(q.amount_in, U256::ZERO);
        assert_eq!(q.amount_out, U256::ZERO);
        assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
    }

    #[test]
    fn exact_out_zero_amount_reverse_direction_returns_zero() {
        let s = fixture_usdc_weth_03();
        let p = ExactOutParams {
            token_in: s.token1,
            token_out: s.token0,
            amount_out: U256::ZERO,
            recipient: address!("0x0000000000000000000000000000000000000099"),
        };
        let q = exact_out(&s, &p).expect("reverse-direction zero-output should not error");
        assert_eq!(q.amount_in, U256::ZERO);
        assert_eq!(q.amount_out, U256::ZERO);
        assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
    }
}