Constant-Product AMMs
Reserves, spot prices, constant-product swaps, price impact, input fees, and slippage protection in integer arithmetic.
View the tested implementationModule 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:
The reserve ratio gives the current marginal price of SOL:
The reciprocal quote is:
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:
For the initial pool:
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:
The AMM solves for the SOL reserve that preserves the invariant:
The trader receives the difference:
Equivalently:
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:
The new SOL reserve is:
The trader receives:
Their average execution price is:
The final marginal price is:
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:
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:
For a 3,000-USDC input:
Only the effective input determines output:
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:
Since the stored input reserve grows by more than the amount used to price the output:
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:
The user protects the transaction with a minimum output:
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:
The fee is then derived rather than calculated independently:
Therefore the accounting identity is exact:
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:
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,000base 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
- Uniswap, Uniswap v2 Core whitepaper.
- Uniswap, Uniswap v2 pair contract.
- Rust standard library,
u64andu128.