V2-as-degenerate-CL swap-step parity probe

Probe harness: rust/crates/engine/degenbot-solvers/examples/v2_cl_parity_probe.rs Run: cargo run -p degenbot-solvers --example v2_cl_parity_probe

This probe answers the decision question in ADR-060 (“Collapse the V2 walk dispatch — representation vs interface”): can a V2 constant-product hop be represented as a degenerate single-range CL tick sequence without changing the per-step arithmetic? It measures the swap-step level, which is the site that flips the ADR decision; the full-path returned-tuple bar is a downstream concern.

The probe runs the production code paths unchanged: the baseline is degenbot_math::v2::IntHopState::swap, the candidate is degenbot_solvers::cl::simulate_v3_range_swap over the projected range. No special rounding, no configuration switch.

Mapping construction

For a V2 hop (R_in, R_out, gamma_numer, fee_denom) and a chosen direction:

  • Liquidity: L = floor(sqrt(R_in * R_out)) — the integer geometric mean. The real geometric mean is irrational for any non-square R_in * R_out, so this floor is lossy by construction and is the first expected parity break.

  • Spot price: sqrt_price_x96 = floor(L * 2^96 / R_in), placing the projected spot at the reserve ratio. Also generally non-integral.

  • Range span: one range with empty interior word boundaries, so the CL step degenerates to a single compute_swap_step_v3 to the exit boundary. The span is widened by 2^32 on the swap side ([sqrtP >> 32, sqrtP] for zero-for-one, [sqrtP, sqrtP << 32] for one-for-zero). Every tested input is orders of magnitude below the resulting capacity, so the step stays in the target-unreachable (open-AMM) branch — the only branch with a V2 analogue.

  • Fee: the V2 fraction is mapped onto the identical CL fraction, fee_pips = 1e6 * (fee_denom - gamma_numer) / fee_denom, giving gamma_CL = 1e6 - fee_pips over fee_denom_CL = 1e6. Exact for 997/1000, 9975/10000, 9995/10000 and 99/100.

  • Direction: both projections are measured. Zero-for-one projects R_in -> token0, one-for-zero projects R_out -> token0. The two CL rounding paths differ (zero-for-one floors amount1; one-for-zero floors amount0), so a representation must clear both.

The candidate’s net input is floor(x * gamma/fee_denom) (the CL amount_remaining_less_fee); the baseline keeps the fee inside the rational floor(x * gamma * R_out / (fee_denom * R_in + x * gamma)).

Corpus inventory

Source

V2 hop states

Notes

tests/fixtures/heavy_mixed_solve_captures.jsonl.zst

27 unique (369 occurrences)

real mixed V2+CL solver captures

tests/fixtures/path5000_v2v4v3_block25704509.json

1

real UniV2 hop with recorded input

tests/fixtures/path110302_v3v4v2_block25711761.json

1

real UniV2 hop, recorded 58,233,015 input

tests/fixtures/path182449_v4v4v2_block25731019.json

1

real SushiV2 hop, recorded 9,085,365 input

real total

30

fee tiers 0.30% and 0.25%

synthetic grid

36

fees {0.30%, 1%, 0.05%} × reserve ratios {~1x, 10x, 1000x} × magnitudes {1e18, 1e21, 1e24, 1e27}

Each state is swept over an 8-point log-spaced input grid up to the order of magnitude of R_in, plus the 3 recorded hotspot inputs, in both directions: 1236 comparisons.

Coverage gaps (methodology limitations):

  • No boundary-exact cases. The range is deliberately widened past every tested input to keep the step target-unreachable; a synthetic range-boundary hit is a projection artifact, not a V2 behaviour. range_boundary_hits = 0.

  • No input beyond ~R_in and no multi-hop composition; the probe is a per-step comparison by scope.

  • Real captures cover only 0.30%/0.25% fee tiers and two ratio regimes (~2.5e-9 and ~2e2..5e2). The 1% and 0.05% tiers are synthetic only.

  • Per-hop input amounts are not captured in the mixed JSONL, so the input grid plus the recorded single-path hotspots stand in for solved amounts.

Divergence distribution (1236 comparisons)

Bucket

n

divergent

%

negative

positive

min delta

median (non-zero)

max delta

overall

1236

225

18.20%

192

33

-530,594,942

-11

+7,255

real

660

66

10.00%

58

8

-530,594,942

-211

+7,255

synthetic

576

159

27.60%

134

25

-999

-9

+148

zero-for-one

618

103

16.67%

103

0

-530,594,942

-14

-1

one-for-zero

618

122

19.74%

89

33

-530,594,942

-9

+7,255

ratio >=100x

110

66

60.00%

58

8

-530,594,942

-211

+7,255

ratio 1000x

192

85

44.27%

72

13

-999

-208

+148

ratio 10x

192

64

33.33%

52

12

-10

-5

+2

ratio ~1x

192

10

5.21%

10

0

-1

-1

-1

ratio <1e-3

550

0

0.00%

0

0

0

0

0

0.30% (synthetic)

192

48

25.00%

40

8

-996

-9

+138

1.00% (synthetic)

192

46

23.96%

38

8

-989

-9

+148

0.05% (synthetic)

192

65

33.85%

56

9

-999

-9

+134

997/1000 (real)

594

66

11.11%

58

8

-530,594,942

-211

+7,255

9975/10000 (real)

66

0

0.00%

0

0

0

0

0

consumed_input matched on every comparison (consumed_mismatch = 0): with the range never reached, both simulators consume the full x. Divergence is therefore entirely in output. It is not one-sided overall: zero-for-one is strictly non-positive, one-for-zero can overshoot.

Representative real divergences (all consumed_v2 == consumed_cl):

x=1            R_in=7456337076646   R_out=3886685670626625517819   V2=519695605   CL=0
x=1            R_in=272235829501    R_out=144881599035718159565    V2=530594942   CL=0
x=9085365      R_in=1835250620298287096280  R_out=4569594589161887004337777
               V2=22553805448 CL=22553803195 delta=-2253
x=58233015     R_in=7456337076646   R_out=3886685670626625517819
               V2=144559529641 CL=144559527263 delta=-2378

Structural reasons for the divergence

  1. Post-fee net input is floored before the invariant. This is the dominant and decisive break. V2’s getAmountOut keeps the fee as an exact rational: out = floor(x·gamma·R_out / (fee_denom·R_in + x·gamma)). The CL exact-in step first computes amount_remaining_less_fee = floor(x·gamma/fee_denom) (a muldiv), then derives the price move from that floored quantity. For a non-unit gamma, small inputs floor to zero: at x = 1 and 0.3% the CL step sees floor(0.997) = 0 and returns zero output while V2 returns hundreds of millions of wei. This produces the -530,594,942 real-fixture miss and the zero-for-one one-sidedness.

  2. Geometric-mean liquidity floor. L = floor(sqrt(R_in·R_out)) equals the real geometric mean only for perfect squares; the projected constant-product invariant therefore differs by up to one unit of L. The output effect scales with R_out and with the reserve ratio.

  3. Spot-price floor. floor(L·2^96/R_in) sits off the exact reserve ratio by up to one Q96 unit, shifting the projected price.

  4. Per-step rounding shape. Even with exact L and sqrt price, the CL step rounds sqrt_price_next up and floors the output delta in two stages, while V2 performs a single terminal DIV. That asymmetry is why the zero-for-one arm is one-sided (candidate ≤ baseline) but the one-for-zero arm can exceed the baseline (the amount1 input path rounds differently), yielding the +7,255 maximum overshoot.

The states that matched everywhere (ratio < 1e-3, including the three real 0.25% pools and the deep-ratio 0.3% pools) do so because their outputs are small relative to the reserve magnitude: the L, sqrt-price and net-input floors all fall below the 1-wei output quantum at the tested inputs. The projection is arithmetically identical there by magnitude, not by construction. Divergence concentrates where R_out >= 100·R_in (60% of comparisons).

Verdict

B — keep the interface collapse; the representation collapse fails the zero-tolerance bar.

ADR-060 clears option A only if every compared step is byte-identical. The probe found 225 of 1236 comparisons (18.2%) with a non-zero output delta, including a real captured hop whose x = 1 output differs by 530,594,942 wei and a real x = 58,233,015 comparison differing by 2,378 wei. No wei tolerance applies: the structural cause — flooring the post-fee input before the invariant, plus the geometric-mean and spot-price floors — is inherent to the CL step, not an accumulation a probe could erase. A representation swap would therefore change landed range geometry and any candidate sitting on a crossing_gross_input boundary, which is an observable result change dressed as a refactor.

Collapsing the V2 walk dispatch at the piece-view seam (interface collapse) removes the enum match sites without touching either production simulator and remains byte-identical by construction. This probe is the evidence record for that choice; it does not modify any production source.