Section 7 of 16
Pair: The Swap
What You Are Building
The swap() function is the heart of the protocol. Every trade on Uniswap V2 goes through this function. It also enables flash loans. You will also implement _mintFee(), which handles protocol fee collection. Together, these two functions complete the UniswapV2Pair contract.
How swap() Works
The swap function takes output amounts as parameters, not input amounts. The caller specifies how much of token0 and/or token1 they want to receive. The function transfers those amounts out optimistically, then verifies that enough input tokens were sent to maintain the constant product invariant (accounting for the 0.3% fee).
This design is unintuitive at first. Why specify outputs instead of inputs? Because it enables flash loans. A caller can receive tokens, do something with them (arbitrage, liquidation, anything), and then return tokens. The function does not care what happens between the optimistic transfer and the invariant check.
The Swap Flow
- Validate outputs: At least one output must be greater than zero. You cannot call swap with both outputs as zero.
- Check reserves: Both outputs must be strictly less than the current reserves. You cannot withdraw more than the pool has.
- Optimistic transfer: Transfer the requested output amounts to the recipient. The tokens leave the contract before any input is verified.
- Flash loan callback: If the caller passed non-empty
data, calluniswapV2Call()on the recipient. This is the flash loan hook. The recipient can use the tokens and return them (plus fee) in the same transaction. - Read new balances: After the transfers and callback, read the actual token balances.
- Calculate input amounts:
amountIn = balance > reserve - amountOut ? balance - (reserve - amountOut) : 0. At least one input must be greater than zero. - Fee-adjusted K check: The core invariant.
balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 1000000, wherebalanceAdjusted = balance * 1000 - amountIn * 3. The factor of 3/1000 is the 0.3% fee. - Update reserves: Call
_update()with the new balances. - Emit Swap.
The K Invariant with Fees
The constant product formula x * y = k is the textbook version. In practice, Uniswap V2 enforces:
(balance0 * 1000 - amount0In * 3) * (balance1 * 1000 - amount1In * 3) >= reserve0 * reserve1 * 1000000
This means 0.3% of every input is excluded from the product calculation. After the swap, the effective K is slightly larger than before (the fee stays in the pool). Over time, this grows K and increases the value of LP tokens.
The _mintFee() Function
Uniswap V2 has an optional protocol fee. When enabled, 1/6th of the 0.3% swap fee (0.05%) goes to a protocol-designated address (feeTo). Instead of skimming tokens on every swap, the protocol fee is collected lazily by minting LP tokens.
The math works like this:
- Read
kLast(the K value at the last mint/burn). - Compute
rootK = sqrt(reserve0 * reserve1)androotKLast = sqrt(kLast). - If
rootK > rootKLast(K grew from swap fees since the last collection):numerator = totalSupply * (rootK - rootKLast)denominator = rootK * 5 + rootKLastliquidity = numerator / denominator- Mint
liquidityLP tokens tofeeTo.
Why * 5? Because the protocol takes 1/6th. The denominator is constructed so that the minted LP tokens dilute existing LPs by exactly the right amount.
If feeTo is address(0), the protocol fee is disabled. In that case, _mintFee() sets kLast to zero (to save gas on future calls) and returns false.
The IUniswapV2Callee Interface
For flash loans, the pair calls uniswapV2Call(msg.sender, amount0Out, amount1Out, data) on the recipient. Any contract that wants to use flash loans must implement this interface.
Your Task
This is the biggest section. You need to implement two functions:
- swap(): The complete swap logic with all validations, optimistic transfers, optional callback, input calculation, fee-adjusted K check, and reserve update.
- _mintFee(): Protocol fee calculation and LP token minting.
Take it step by step. The swap has many moving parts, but each step is straightforward on its own.