DeFi

Building an AMM from Scratch: Constant Product Market Makers in Solidity

Learn to build a fully functional automated market maker in Solidity, implementing the x*y=k invariant, liquidity provision, and swap mechanics.

Mudaser Iqbal··13 min read

The Mathematics of Constant Product AMMs

Automated Market Makers replaced order books in DeFi by implementing a simple but powerful invariant: x * y = k, where x and y are the reserves of two tokens and k is a constant. Any trade must preserve this invariant — you can buy Token Y with Token X as long as the product of the new reserves equals k.

This formula has elegant properties: it provides liquidity at every price point (the curve is asymptotic — reserves never hit zero), it is deterministic (given reserves and a trade size, the output is computable), and it is manipulation-resistant in a relative sense (moving the price requires committing capital).

The price at any point on the curve is x/y (the ratio of reserves). Large trades relative to pool size cause significant price impact — this is slippage. The deeper the liquidity (larger k), the less slippage for a given trade size.

Before writing code, internalize the invariant: x * y = (x + dx) * (y - dy), where dx is the input amount and dy is the output amount. Solving for dy: dy = y * dx / (x + dx). This is the core swap formula.

Building the Core AMM Contract

The minimal AMM contract needs four functions: addLiquidity, removeLiquidity, swap, and getAmountOut.

State: uint256 public reserveA, uint256 public reserveB for reserve tracking. ERC-20 tokenA and tokenB for the token pair. A totalSupply and mapping(address => uint256) balanceOf for LP token tracking (or inherit ERC-20 for LP tokens).

addLiquidity: transfers tokenA and tokenB from the caller, calculates LP shares to mint, updates reserves, mints LP tokens. For the first deposit, shares = sqrt(amountA * amountB) — this sets the initial price. For subsequent deposits, shares must be proportional to the existing ratio to prevent dilution: shares = min(amountA * totalSupply / reserveA, amountB * totalSupply / reserveB).

removeLiquidity: burns LP tokens, calculates proportional token amounts, transfers tokens to caller, updates reserves.

swap: takes one token, calculates output using the formula (accounting for the fee), transfers output token, updates reserves. A 0.3% fee means: amountInWithFee = amountIn * 997; output = reserveOut * amountInWithFee / (reserveIn * 1000 + amountInWithFee).

Critically: always update reserves AFTER transfers to prevent reentrancy. Check the invariant holds at the end of swap: require(newReserveA * newReserveB >= reserveA * reserveB).

Handling Precision and Overflow

Solidity integer arithmetic requires careful handling to avoid precision loss and overflow.

Overflow: Solidity 0.8.x reverts on overflow by default. However, intermediate multiplication of large reserve values can overflow uint256. Reserves near the uint256 max multiplied together overflow. Use SafeMath for critical paths or restructure calculations to minimize intermediate values.

Practical mitigation: cap maximum reserves at uint112 (as Uniswap v2 does). uint112 maximum is ~5 * 10^33, which is sufficient for any realistic token quantity including 18-decimal tokens. Store reserves as uint112 but compute in uint256 for intermediate calculations.

Precision loss in LP share calculation: integer division truncates. A tiny first deposit can set a price that causes precision loss for all subsequent depositors. Uniswap v2 burns the first MINIMUM_LIQUIDITY (1000) shares to dead address on initialization — this prevents precision attacks by ensuring the LP supply is never trivially small.

Reentrancy: follow checks-effects-interactions. Update state (balances, reserves) before making external calls (token transfers). Use ReentrancyGuard from OpenZeppelin for belt-and-suspenders protection.

Flash Loans and Price Oracles

A constant product AMM naturally supports flash loans. In a single transaction, a caller can borrow the pool's entire token balance, use it for arbitrage or liquidations, and return it (plus a fee) by the end of the transaction. The invariant check at the end ensures solvency.

Flash loan implementation: allow the caller to receive tokens before payment, execute arbitrary logic in a callback, then verify the invariant holds after the callback. If it does not, revert.

On-chain price oracles from AMM reserves: the instantaneous spot price (reserveA / reserveB) is easily manipulated. Uniswap v2's TWAP oracle accumulates price * time in a cumulative sum, allowing external contracts to compute a time-weighted average over any window. Implement a similar accumulator by storing price0Cumulative and price1Cumulative, updating them on every reserve change with: price0Cumulative += reserveA / reserveB * timeElapsed.

Any contract that reads this pool's price should use a TWAP over at minimum 10 minutes. Never use the instantaneous spot price for any economic decision — this is how oracle manipulation attacks happen.

Testing and Security Checklist

A production-ready AMM requires exhaustive testing and security review.

Invariant tests: write a Foundry invariant test that asserts reserveA * reserveB >= k after every sequence of operations. The fuzzer will find edge cases your unit tests miss.

Fuzz the swap formula: test swap() with random input amounts from 1 wei to 10^24. Verify the output never exceeds the reserve, the invariant holds, and the price impact is calculable.

Attack vectors to test explicitly:
Donation attack: sending tokens directly to the contract without calling addLiquidity increases the product but not LP shares, diluting existing LPs. Decide whether to allow this and document the behavior.
Sandwich attack simulation: simulate a bot front-running a large swap. Verify slippage protection reverts the trade if the output is below the minimum acceptable.
Reentrancy: attempt to reenter swap during a transfer callback.
Zero amount: call swap with zero input.

Gas optimization targets: the swap function should be under 50,000 gas for a simple two-token pool. Profile with forge test --gas-report and optimize hot paths.

Never deploy an AMM handling significant value without a professional audit. The constant product formula is simple, but the attack surface around precision, reentrancy, and oracle use is wide.

One Solidity tip + 1 case study per month

Building an AMM from Scratch: Constant Product Market Makers in Solidity | Crypto Hawking