AMMs and Liquidity
Day 6··10 min read

Constant-Product AMMs

Reserves, spot prices, constant-product swaps, price impact, input fees, and slippage protection in integer arithmetic.

View the tested implementation

Module 1 accounted for ownership inside a vault. Module 2 begins with a different machine: a pool that holds two assets and quotes trades directly from its reserves.

Day 6 covers the smallest useful constant-product AMM—reserves, prices, exact-input swaps, fees, and slippage protection—then translates those equations into checked integer arithmetic and 29 tests.

The complete Rust implementation and test suite live in the DeFi AMM repository.

An AMM does not promise one fixed price. It offers a path of prices along a curve.

Two reserves define the market

Consider a pool containing:

x = 200 SOL
y = 30,000 USDC

The reserve ratio gives the current marginal price of SOL:

P_SOL = y / x = 30,000 / 200 = 150 USDC

The reciprocal quote is:

P_USDC = x / y = 200 / 30,000 = 0.00666667 SOL

These are two views of the same pool state. They are not promises that an arbitrarily large trade can execute at either number.

The constant-product curve

A basic constant-product AMM maintains:

x × y = k

For the initial pool:

k = 200 × 30,000 = 6,000,000

If a trader adds USDC to buy SOL, the USDC reserve rises and the SOL reserve falls. Ignoring fees, the new reserves remain on the same curve.

This model is the foundation described by the official Uniswap v2 whitepaper: each pair stores reserves of two assets and requires their product not to decrease.

Deriving an exact-input swap

Let a trader supply Δy USDC. The post-trade USDC reserve is:

y' = y + Δy

The AMM solves for the SOL reserve that preserves the invariant:

x' = k / y'

The trader receives the difference:

amount out = x − x'

Equivalently:

amount out = x × Δy / (y + Δy)

The implementation uses the second form because it expresses the output directly and delays division until after multiplication.

One swap moves through many prices

Starting from 200 SOL and 30,000 USDC, a trader supplies 3,000 USDC.

The new USDC reserve is:

y' = 30,000 + 3,000 = 33,000

The new SOL reserve is:

x' = 6,000,000 / 33,000 = 181.81818...

The trader receives:

200 − 181.81818... = 18.18182 SOL

Their average execution price is:

3,000 / 18.18182 = 165 USDC per SOL

The final marginal price is:

33,000 / 181.81818 = 181.50 USDC per SOL

Three prices now matter:

  • Initial spot price: 150 USDC per SOL.
  • Average execution price: 165 USDC per SOL.
  • Final spot price: 181.50 USDC per SOL.

The trader did not purchase every SOL at 150 or 181.50. Their order moved continuously along the curve, and 165 was its average price.

Price impact

Price impact measures how much the trader's own order worsened their average execution relative to the starting spot price:

price impact = (165 − 150) / 150 = 10%

Larger trades consume more of one reserve, move farther along the curve, and generally receive a worse average price. Deeper liquidity makes the same trade smaller relative to the pool and therefore reduces its impact.

This is endogenous to the trade. Even if no other transaction appears between quote and execution, price impact still exists.

Input fees

Now apply a 30-basis-point fee:

30 bps = 0.30%

For a 3,000-USDC input:

fee = 3,000 × 0.003 = 9 USDC
effective input = 3,000 − 9 = 2,991 USDC

Only the effective input determines output:

amount out = 200 × 2,991 / (30,000 + 2,991)
amount out ≈ 18.13222 SOL

The complete 3,000 USDC still enters the pool. The 9-USDC fee remains in reserves for liquidity providers, while the trader receives less SOL than in the fee-free quote.

That separation matters:

pricing reserve = old reserve + effective input
stored reserve = old reserve + gross input

Since the stored input reserve grows by more than the amount used to price the output:

k_after > k_before

For the scaled worked example, the implementation produces a post-fee product of 6,001,636,839,000,000,000, compared with 6,000,000,000,000,000,000 before the trade.

Uniswap v2 likewise charges a 30-basis-point trading fee that goes to liquidity providers under its default fee configuration (Uniswap v2 whitepaper). The educational model stops there: it does not yet include a protocol-fee split.

Price impact is not slippage

Price impact is caused by the trade itself. Slippage is the difference between a quote and the state available when the transaction actually executes.

Suppose the quote promises 18.13222 SOL. Other trades execute first and the updated pool can now return only 17.90 SOL:

slippage = (18.13222 − 17.90) / 18.13222 ≈ 1.28%

The user protects the transaction with a minimum output:

actual amount out ≥ minimum amount out

If the quoted output falls below that minimum, the swap fails without changing either reserve. A limit equal to the current quote succeeds; one unit above it fails.

Integer implementation

The Rust library stores token quantities and reserves as u64 base units. All products and divisions widen to checked u128 intermediates.

The fee scale is:

pub const FEE_SCALE: u16 = 10_000;

Effective input is rounded down:

effective = floor(gross × (10,000 − fee_bps) / 10,000)

The fee is then derived rather than calculated independently:

fee = gross − effective

Therefore the accounting identity is exact:

gross input = effective input + fee

Swap output also rounds down. Both remainders favor the pool. Rust's checked integer methods return None on overflow, allowing the operation to reject an unrepresentable transition rather than wrap it (Rust u128).

Exact ratios instead of floating-point prices

The library represents a price as an exact numerator and denominator. Comparisons use cross-multiplication:

a / b ≤ c / d ⇔ a × d ≤ c × b

Human-readable fixed-point prices are derived only when requested, with explicit decimal normalization for both tokens. This matters because raw SOL-like and USDC-like token amounts need not use the same number of decimal places.

The public SwapQuote explains the complete transition without mutation:

  • Gross input, fee, and effective input.
  • Output amount.
  • Initial, average-execution, and final price ratios.
  • Projected final reserves.
  • Constant product before and after.
  • Price impact in basis points.

swap_exact_input uses the same quote and validates the minimum output before committing either reserve.

The worked example in base units

Using six decimal places for both example assets:

const UNIT: u64 = 1_000_000;
 
let reserve_x = 200 * UNIT;
let reserve_y = 30_000 * UNIT;
let amount_in = 3_000 * UNIT;

Without fees, integer arithmetic returns 18,181,818 base units of X: 18.181818 tokens. The tiny difference from the real-number result stays in the pool.

At 30 basis points:

  • Gross input: 3,000,000,000 base units.
  • Fee: 9,000,000.
  • Effective input: 2,991,000,000.
  • Output: 18,132,217, or 18.132217 tokens.

The fee-inclusive average execution price is approximately 165.45136 USDC per SOL.

A property that sounded true but was not

One initial property asserted that a larger input must always have a weakly worse average execution price.

That is true over real numbers for a constant-product curve, but the implemented price performs two independent floor operations: first on output, then when comparing average execution ratios. In a heavily lopsided pool, two extremely small trades can land on different integer boundaries and reverse the apparent ordering by less than one base unit of meaningful signal.

The property test was wrong; the swap implementation was not.

The corrected property generates trades as meaningful percentages of the input reserve, from 0.5% to 100%, where curve movement dominates unit-level quantization. Exact universal properties—such as reserve positivity, quote purity, atomic failure, and nondecreasing k—remain unqualified.

This is the same lesson Module 1 ended on: a property is only as sound as its mathematical domain.

What the tests cover

The first AMM implementation contains 29 tests: 20 example tests and 9 generated properties.

They verify:

  • Valid and invalid pool construction.
  • Spot prices in both directions.
  • Fee-free and fee-paying worked scenarios.
  • Symmetric X-to-Y and Y-to-X swaps.
  • Fee retention and nondecreasing constant product.
  • Zero-input, zero-effective-input, zero-output, and overflow rejection.
  • Minimum-output success and atomic failure.
  • Quote purity and quote/execution agreement.
  • Reserve positivity and output bounds.
  • Directional spot-price movement.
  • Fee-paying quotes never outperform equivalent fee-free quotes.

The property suite passes at 2,000 generated cases, and the complete suite passes five repeated runs at 512 cases.

What I learned

  • A reserve ratio is the current marginal price, not the execution price for an entire trade.
  • Constant-product swaps move continuously along xy = k.
  • Average execution price lies between the initial and final marginal prices in the real-number model.
  • Price impact comes from the trade; slippage comes from state changing after the quote.
  • Input fees reduce effective input while the gross amount enters reserves.
  • Fee retention and output rounding make the stored constant product nondecreasing.
  • Token decimals must be explicit when turning raw reserve ratios into human prices.
  • Quotes and state-changing swaps should share one arithmetic path.
  • Integer boundaries can invalidate a real-number property when its test domain is too fine.

Day 7 asks how external markets pull an AMM back toward equilibrium: arbitrage, target-price trades, fee-adjusted bounds, and sequential swaps.

References