DEX Quoting: V2 Constant Product vs. V3 Concentrated Liquidity
TECH INSIGHT / EXPLAINED
A swap quote is a result for a particular amount, pool state and fee rule. V2 and V3 can both be modeled precisely, but they need different state and arithmetic. Keep token quantities in integer base units until presentation.
Local execution of the actual project
These captures show the existing project UI or functions executed locally with non-private sample inputs. Each caption states the scope of the execution.
V2: constant-product math with rounding
For original Uniswap V2’s 0.3% fee, the integer formula below matches the library’s output-flooring pattern. Other forks can have different fees. Reserves must come from the same block and be ordered by input/output token; obtaining and updating them still has a data cost. With fees retained in the pool, the raw reserve product can grow, so “k never changes” is only a simplified explanation.
def amount_out_v2(amount_in, reserve_in, reserve_out):
values = (amount_in, reserve_in, reserve_out)
if any(type(v) is not int or v <= 0 for v in values):
raise ValueError("positive integer base units required")
with_fee = amount_in * 997 # original Uniswap V2: 0.3%
return with_fee * reserve_out // (reserve_in * 1000 + with_fee)
unit = 10**6
print(amount_out_v2(unit, 1000*unit, 1000*unit))
print(amount_out_v2(500*unit, 1000*unit, 1000*unit))
# 996006 and 332665999 base unitsPrice impact is already in the quote
Assuming both example tokens have six decimals, 1 input unit returns 0.996006, while 500 returns 332.665999. The larger trade receives less per input unit because it moves along the curve. This predictable size effect is price impact. Slippage commonly refers to the difference between the quoted/expected outcome and actual execution; do not subtract the same modeled impact twice.
V3: piecewise liquidity, not V2 reserves
V3 uses sqrtPriceX96, active liquidity and initialized tick changes. A swap crossing boundaries must update liquidity and apply integer rounding at each step. The illustration’s staircase represents changes in available liquidity between ranges, not a claim that the pool’s price is a literal step function. Off-chain simulation is possible with full state and correct math; the Quoter is a convenient alternative.
A Quoter call is not a guaranteed fill
The original V3 Quoter uses swap/revert mechanics, and its methods are not normal Solidity view methods. Query it with eth_call or a suitable provider call at a pinned block; generated bindings depend on the Quoter version and ABI, so a generic CallOpts snippet may be wrong. Record amount, fee tier, pool and block, then impose minOut and deadline in the actual swap. State changes before inclusion can invalidate the quote.
A positive input can produce a zero quote
Consider a synthetic pool with 1,000 raw units in each reserve. Under the original V2 fee formula, input 1 produces output 0, input 2 produces output 1, and input 100 produces output 90. This is integer flooring, not display formatting. A library calculating zero does not mean the pair accepts that swap: the pair's swap implementation rejects requests where both output amounts are zero.
A mathematically completed quote therefore needs a separate minimum-usable-amount check before becoming an execution plan. Rounding zero up to one creates an unsupported payment promise. Conversely, failure at a tiny order size does not establish that the pool has no liquidity. An independent Python reproduction confirmed the three example outputs.
Compute required input for an exact output
In this example, obtaining output 90 requires input 100; input 99 produces only 89. Do not merely invert an exact-input result. Follow the original V2 library's input calculation, which adds one after integer division. The following code illustrates that positive-integer boundary.
def required_input(out, reserve_in, reserve_out):
values = (out, reserve_in, reserve_out)
if any(type(v) is not int or v <= 0 for v in values):
raise ValueError("positive integers required")
if out >= reserve_out:
raise ValueError("output exhausts reserve")
return reserve_in * out * 1000 // (
(reserve_out - out) * 997
) + 1
assert required_input(90, 1000, 1000) == 100
An output equal to the reserve makes the denominator zero and must be rejected at validation. Python expands large integers automatically, so this reproduction does not validate Solidity integer widths or overflow behavior. Transfer deductions and additional protocol-specific fees also sit outside this formula's scope.
A route that revisits the same pool
If an A → B → A route uses the same pool twice, its second swap must use the state left by the first. The original V3 Quoter reverts each pool call's changes while passing the calculated amount onward. Consequently, stitching independent quotes together, or relying on a simple Quoter loop for a repeated-pool route, can use different state from a continuously executed route.
The Quoter implementation supports this inference: carrying amounts forward does not establish state continuity. A route finder that permits repeated pools needs a model that carries their changed state forward. An implementation without that capability should explicitly exclude repeated-pool paths so that the meaning of its quote remains clear.
Diagnose even a one-unit discrepancy
When quotes disagree, first fix token order, raw amount, block hash, and fee configuration, then compare intermediate calculation steps. For V3, separate tests immediately before an initialized tick, exactly at the boundary, and after crossing it. SwapMath applies distinct integer calculations to input, output, and fees; rounding the final display cannot reproduce those decisions.
Record the first divergent step and its raw integers rather than hiding discrepancies behind an arbitrary percentage tolerance. Incomplete state from the chosen block requires recollection and should be distinguished from an arithmetic defect. The executed experiment here covers V2 arithmetic boundaries; it does not validate a V3 tick simulator. For a regression fixture, preserve the complete calculation inputs and intermediate results alongside the expected amount. That record lets a later implementation reproduce the same discrepancy instead of merely observing a failed final assertion.
Documentation and next steps
Content and examples reviewed: 2026-10-01
댓글
댓글 쓰기