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 implementationDay 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:
The pool's marginal SOL price is:
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:
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:
The constant-product constraint must also hold:
Substituting y' = P_external × x' gives:
At an external price of 180:
The theoretical arbitrage trade is therefore:
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,421quote units. - Output:
17,425,814base units. - Profit:
273,293,099quote 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:
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,733base units. - Output:
3,167,176,500quote units. - Profit:
334,368,540quote units.
Fees create a no-arbitrage band
Let the input fee be 30 basis points:
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:
At a pool price of 150:
In the other direction:
This creates an approximate no-arbitrage band:
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:
The SOL received is:
Profit before execution cost is:
The continuous optimum occurs when one additional unit of input creates exactly one unit of external value. Solving that condition gives:
For P_e = 180 and a 30-basis-point fee:
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,833quote units. - Output:
17,151,335base units. - Profit:
264,751,467quote 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:
- The exact theoretical target may be unreachable.
- One more input unit can produce no additional output.
- The continuous formula supplies a search centre, not the final executable answer.
Near a quantisation boundary:
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:
For X-to-Y arbitrage, the external acquisition cost rounds up:
Both conventions prevent the quote from overstating realizable profit. Execution cost and profit are always denominated in quote-asset base units.
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:
- obtains the pure quote;
- rejects a missing opportunity;
- enforces the caller's minimum profit;
- executes through the existing slippage-protected swap path; and
- 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:
Because fees stay in the pool:
Repeated profitable trades reduce the available opportunity until the best executable integer trade has no positive net profit:
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 = kwith 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
- Uniswap, Uniswap v2 Core whitepaper.
- Uniswap, Uniswap v2 pair contract.
- Rust standard library,
u128::isqrtand checked arithmetic onu128.