AMMs and Liquidity
Day 7··12 min read

Arbitrage and Pool-Price Restoration

Deriving fee-aware arbitrage, no-arbitrage bands, discrete profit maximisation, and atomic execution for a constant-product AMM.

View the tested implementation

Day 6 built a constant-product pool that can quote and execute swaps. Day 7 connects that isolated pool to an external market.

The central question is not merely whether two prices differ. It is whether an executable trade can turn that difference into positive profit after fees, rounding, and execution cost.

The complete Rust implementation and its 63 passing tests live in the DeFi AMM repository.

Arbitrage does not force an AMM to one perfect price. It removes the profitable trades that exist between the AMM and the rest of the market.

Begin with a price discrepancy

Return to the Day 6 pool:

x = 200 SOL
y = 30,000 USDC
k = xy = 6,000,000

The pool's marginal SOL price is:

P_pool = y / x = 150 USDC per SOL

Suppose SOL trades externally for 180 USDC. The pool is selling SOL below the external price. An arbitrageur can send USDC into the pool, receive SOL, and sell that SOL externally.

That trade moves both reserves in the restoring direction:

y increases · x decreases · y/x increases

The direction rule is symmetric:

  • If P_pool < P_external, send the quote asset Y and remove the base asset X.
  • If P_pool > P_external, send X and remove Y.

For the 150-to-180 discrepancy, the absolute gap is 30 USDC and the external price is 20% above the pool price.

Fee-free target reserves

Ignoring fees and integer rounding, arbitrage continues until the pool's final marginal price equals the external price:

y' / x' = P_external

The constant-product constraint must also hold:

x'y' = k

Substituting y' = P_external × x' gives:

x' = sqrt(k / P_external)
y' = sqrt(k × P_external)

At an external price of 180:

x' = sqrt(6,000,000 / 180) ≈ 182.5742 SOL
y' = sqrt(6,000,000 × 180) ≈ 32,863.35 USDC

The theoretical arbitrage trade is therefore:

USDC in = 32,863.35 − 30,000 ≈ 2,863.35
SOL out = 200 − 182.5742 ≈ 17.4258

Selling that SOL externally returns approximately 3,136.64 USDC, leaving about 273.29 USDC of profit before execution costs.

The base-unit Rust result is exact under the library's integer rules:

  • Input: 2,863,353,421 quote units.
  • Output: 17,425,814 base units.
  • Profit: 273,293,099 quote units.

Marginal price decides when to stop

The average execution price determines profit on the completed trade. The final marginal price answers a different question: should the arbitrageur trade one more unit?

  • Below 180, another tiny purchase from the pool remains profitable.
  • At 180, its marginal external value equals its marginal pool cost.
  • Above 180, continuing loses money.

The last marginal unit can earn zero while the entire trade remains profitable. Earlier units were acquired at better prices along the curve.

The opposite direction

Now keep the pool at 150 but let SOL trade externally at 120. SOL is overpriced inside the pool, so the arbitrageur buys SOL externally, sends it into the pool, and removes USDC.

The target reserves become:

x' = sqrt(6,000,000 / 120) ≈ 223.6068 SOL
y' = sqrt(6,000,000 × 120) ≈ 26,832.82 USDC

The arbitrageur supplies approximately 23.6068 SOL and receives 3,167.18 USDC. Buying that SOL externally costs about 2,832.82 USDC, leaving approximately 334.36 USDC.

The implementation produces:

  • Input: 23,606,733 base units.
  • Output: 3,167,176,500 quote units.
  • Profit: 334,368,540 quote units.

Fees create a no-arbitrage band

Let the input fee be 30 basis points:

f = 0.003 · gamma = 1 − f = 0.997

One unit of gross input provides only gamma units of effective purchasing power. Price equality is no longer the threshold for profitable arbitrage.

When the external price is above the pool price, the approximate break-even condition is:

P_external > P_pool / gamma

At a pool price of 150:

upper boundary = 150 / 0.997 ≈ 150.45135

In the other direction:

P_external < P_pool × gamma
lower boundary = 150 × 0.997 = 149.55

This creates an approximate no-arbitrage band:

149.55 ≤ P_external ≤ 150.45135

Inside the band, a price discrepancy exists but does not pay for the swap fee. Raising the fee widens the band: the lower boundary moves down and the upper boundary moves up.

The official Uniswap v2 whitepaper describes the same constant-product foundation and 30-basis-point trading fee. Its deployed pair logic enforces the invariant using fee-adjusted balances in the official Uniswap v2 pair contract.

Optimising a fee-paying trade

Suppose the arbitrageur sends Delta y USDC into the 150-priced pool while SOL trades externally at P_e. Only gamma × Delta y participates in swap pricing:

x' = k / (y + gamma × Delta y)

The SOL received is:

Delta x = x − k / (y + gamma × Delta y)

Profit before execution cost is:

profit(Delta y) = P_e(x − k/(y + gamma × Delta y)) − Delta y

The continuous optimum occurs when one additional unit of input creates exactly one unit of external value. Solving that condition gives:

y + gamma × Delta y = sqrt(P_e × k × gamma)
Delta y* = (sqrt(P_e × k × gamma) − y) / gamma

For P_e = 180 and a 30-basis-point fee:

Delta y* ≈ 2,822.49 USDC

That is smaller than the fee-free input of approximately 2,863.35 USDC. Fees make the final portion of the original trade unprofitable.

The implemented base-unit result is:

  • Input: 2,822,488,833 quote units.
  • Output: 17,151,335 base units.
  • Profit: 264,751,467 quote units.
  • Final raw pool price: approximately 179.506 USDC per SOL.

The pool deliberately stops short of raw equality with 180. Another trade would have to pay another fee.

Continuous mathematics only finds a candidate

Programs execute integer token units, not real numbers. The theoretical optimum can lie between two representable inputs, and swap output must be floored.

Three consequences follow:

  1. The exact theoretical target may be unreachable.
  2. One more input unit can produce no additional output.
  3. The continuous formula supplies a search centre, not the final executable answer.

Near a quantisation boundary:

quote(n + 1) = quote(n)

The arbitrageur then pays an extra unit for no extra output, strictly reducing profit.

The implementation uses checked u128 arithmetic and Rust's floor u128::isqrt to locate the analytical centre. It then evaluates real swap quotes around that centre, plus the relevant input boundaries.

Every candidate goes through the Day 6 quote_exact_input path. There is no second, subtly different swap formula hiding inside the optimiser.

Profit rounds against the arbitrageur

The external price is stored as an exact positive ratio of quote units to base units. Pool and external prices are compared by cross-multiplication rather than floating point.

For Y-to-X arbitrage, external sale proceeds round down:

external value = floor(X out × quote units / base units)

For X-to-Y arbitrage, the external acquisition cost rounds up:

external cost = ceil(X in × quote units / base units)

Both conventions prevent the quote from overstating realizable profit. Execution cost and profit are always denominated in quote-asset base units.

profit = external value out − external value in − execution cost

A candidate with zero profit, negative profit, or checked-subtraction failure is discarded rather than wrapped into an unsigned value.

Search guarantees must be stated honestly

The optimiser evaluates 1, the maximum allowed input, the analytical centre, and a bounded window around that centre. Its radius scales with fee quantisation and is capped for performance.

That is a documented local search—not a universal proof of global optimality over every pathological u64 pool.

Testing exposed why this distinction matters. In one counterexample, floor rounding created a wide plateau where profit remained exactly 273,293,099 across search radii from 50 through 2,000. Expanding the window found a smaller input with the same profit, but did not improve the profit itself.

The tie-breaker therefore prefers the smaller input. Small generated domains compare the production optimiser against an exhaustive search over every allowed input. The implementation passes that brute-force oracle without pretending the bounded production search proves more than it does.

Quote first, mutate once

quote_arbitrage is pure. It determines direction, searches candidates, applies conservative external valuation, includes execution cost, and returns None when no strictly positive trade exists.

execute_arbitrage then:

  1. obtains the pure quote;
  2. rejects a missing opportunity;
  3. enforces the caller's minimum profit;
  4. executes through the existing slippage-protected swap path; and
  5. commits reserve changes only after every check succeeds.

A minimum-profit threshold equal to the quote succeeds. One unit above it fails without changing either reserve.

Sequential arbitrage converges economically

One arbitrageur may be constrained by capital, a maximum input, slippage protection, or a moving external price. Integer rounding and fees can also leave a residual discrepancy.

After every completed swap, the next quote must use the new reserves:

k_i = x_i × y_i

Because fees stay in the pool:

k_(i+1) ≥ k_i

Repeated profitable trades reduce the available opportunity until the best executable integer trade has no positive net profit:

max over Delta > 0 of profit(Delta) ≤ 0

This—not necessarily P_pool = P_external—is the real discrete equilibrium after fees, rounding, input caps, and execution costs.

What the tests found

Day 7 adds 34 tests: 22 examples and 12 generated properties. Together with the untouched 29 Day 6 tests, the crate now has 63 passing tests.

The suite covers:

  • Both arbitrage directions and exact price comparison.
  • Fee-free and 30-basis-point worked scenarios.
  • No trade at equality, inside the fee band, or after execution cost consumes profit.
  • Maximum-input and minimum-profit protection.
  • Conservative external proceeds and acquisition costs.
  • Quote purity, quote/execution agreement, and atomic failures.
  • Deterministic tie-breaking.
  • Sequential convergence over constrained economic domains.
  • Production optimiser agreement with exhaustive search on small domains.

Two tempting properties failed under legitimate integer arithmetic:

  • Larger input does not produce monotonically better or worse profit at every tiny scale.
  • Repeated execution cannot have one universal iteration bound when the external-price gap is enormous but the input cap is negligible.

The generators were constrained by economically meaningful reserve fractions and realistic price perturbations, with the counterexamples preserved in comments. No arbitrary tolerance was added and no failing invariant was quietly weakened.

What I learned

  • Arbitrage direction follows an exact comparison between pool and external prices.
  • Fee-free target reserves come from combining xy = k with the target reserve ratio.
  • Average execution price explains total profit; marginal price explains whether to continue.
  • Fees create a no-arbitrage band and make the optimal trade stop before raw equality.
  • Continuous mathematics locates a candidate; integer arithmetic determines the executable trade.
  • Proceeds should round down and costs should round up when reporting profit.
  • A bounded optimiser must document its actual guarantee.
  • Economic equilibrium means no profitable executable trade remains.

Day 8 introduces the other side of an AMM: liquidity providers, LP shares, proportional deposits and withdrawals, initial-liquidity rules, and ownership accounting.

References