Swap
This page covers the necessary concepts to understand token swap flow in Uniswap V2.
Concepts
Section titled “Concepts”Exchange Amounts
Section titled “Exchange Amounts”A swap is the exchange of one token for another through an automated liquidity pool in a single transaction, with no need for a traditional order book or counterparty.
You specify either an exact amount to send or an exact amount to receive, and the pool calculates the other side using its current reserves.
Suppose that a trader wants to buy an amount of token X. To do so, they must deposit an amount of token Y
Interpreting the equations:
- (1) trader wants to deposit of token A => will receive of token B
- (2) trader wants to receive of token A => should deposit of token B
Adding Fees
Section titled “Adding Fees”Uniswap v2 charges fees on trader deposits, and then the remaining amount is actually traded.
A 0.3% fee is deducted from each swap and distributed to liquidity providers.
- Applying fees to (1)
// contracts/uniswap-v2/v2-periphery/contracts/libraries/UniswapV2Library.sol:42:50// given an input amount of an asset and pair reserves, returns the maximum output amount of the other assetfunction getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint amountInWithFee = amountIn.mul(997); uint numerator = amountInWithFee.mul(reserveOut); uint denominator = reserveIn.mul(1000).add(amountInWithFee); amountOut = numerator / denominator;}- Applying fees to (2)
// contracts/uniswap-v2/v2-periphery/contracts/libraries/UniswapV2Library.sol:52:59// given an output amount of an asset and pair reserves, returns a required input amount of the other assetfunction getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint numerator = reserveIn.mul(amountOut).mul(1000); uint denominator = reserveOut.sub(amountOut).mul(997); amountIn = (numerator / denominator).add(1);}Note: Notice both numerator and denominator are multiplied by 1000 to account for 0.3% fees from the input amount
Fees Increase K
Section titled “Fees Increase K”The swap is priced using only 99.7% of the input, but 100% enters the pool; the extra 0.3% stays with LPs and increases k.
Effective Price
Section titled “Effective Price”Because the pool balances must move along a CPMM curve, every trade shifts the balance of reserves and results in a new price.
So the effective price is the actual exchange rate the trader pays:
/ reserve balance of token Y before/after trade
/ reserve balance of token X before/after trade
The effective price is always higher (worse) than the starting spot price.
Spot Price
Section titled “Spot Price”The theoretical exchange rate for an infinitely small trade (compared to the reserves).
/ reserve balance of token Y/X before trade
Price Impact
Section titled “Price Impact”It is the difference between
- the amount that a trader pays (or deposits) because of effective price
- the amount that the trader would have paid at spot price
Initial Reserve Ratio
Section titled “Initial Reserve Ratio”- Effective price increase depends on the starting spot price
- When you push the reserves towards the edges, the curve becomes much steeper in price terms
Example: Assume the trader always inputs 10,000 USDC to buy ETH. k = 4,000,000
| Case | Initial USDC reserve | Initial ETH reserve | Starting spot price | amountIn (USDC) | amountOut (ETH) | Effective price | Final USDC reserve | Final ETH reserve | Spot price after trade |
|---|---|---|---|---|---|---|---|---|---|
| A | 2,000,000 | 2 | 1,000,000 USDC/ETH | 10,000 | 0.00995025 | 1,005,000.00 USDC/ETH | 2,010,000 | 1.99004975 | 1,010,025.00 USDC/ETH |
| B | 200,000 | 20 | 10,000 USDC/ETH | 10,000 | 0.95238095 | 10,500.00 USDC/ETH | 210,000 | 19.04761905 | 11,025.00 USDC/ETH |
| C | 20,000 | 200 | 100 USDC/ETH | 10,000 | 66.66666667 | 150.00 USDC/ETH | 30,000 | 133.33333333 | 225.00 USDC/ETH |
Relative Trade Size
Section titled “Relative Trade Size”- Effective price increase depends on the fraction of the pool being swapped
- in a deep pool, buying a token amount might barely move the price,
- in a shallow pool, the same trade can move the price noticeably
Example: Assume both pools start at the same spot price: 4,000 USDC per ETH.
| Pool | Initial USDC reserve | Initial ETH reserve | Starting spot price | ETH bought | Fraction of ETH pool bought | USDC paid | Effective price | Final USDC reserve | Final ETH reserve | Spot price after trade |
|---|---|---|---|---|---|---|---|---|---|---|
| Deep pool | 40,000,000 | 10,000 | 4,000.00 USDC/ETH | 10 | 0.1% | 40,040.04 | 4,004.00 USDC/ETH | 40,040,040.04 | 9,990 | 4,008.01 USDC/ETH |
| Shallow pool | 400,000 | 100 | 4,000.00 USDC/ETH | 10 | 10% | 44,444.44 | 4,444.44 USDC/ETH | 444,444.44 | 90 | 4,938.27 USDC/ETH |
Slippage
Section titled “Slippage”It is the difference between the quoted effective price of a trade and the final execution price.
This is because other trades might be executed before the trader’s, changing spot price before the trade takes place.
In Uniswap V2, slippage protection is at the router level, by parameters like:
amountOutMinfor exact-input swapsamountInMaxfor exact-output swaps- plus a
deadlineto avoid stale execution.
Contracts
Section titled “Contracts”Pair: Swap method
Section titled “Pair: Swap method”All router swap methods call UniswapV2Pair.swap() method.
- works for
either directionon the same pair amount0Out/amount1Outis how muchtoken0/token1the pair should send out- usually
one is zeroand the other is the token you want to receive - the success criteria (invariant) is that fee-adjusted k must not go down (fees increase k)
// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:158:187// this low-level function should be called from a contract which performs important safety checksfunction swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); }
_update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);}Invariant
Section titled “Invariant”In order for swap to be successful, the new balances must preserve or increase the old invariant ().
// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:180:182uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');Example
Section titled “Example”| Step | Value / check | Example |
|---|---|---|
| 1 | Trader sends | sends 2,100 token1 |
| 2 | Trader wants out | amount0Out = 1, amount1Out = 0 |
| 3 | Old reserves | reserve0 = 100, reserve1 = 200,000 |
| 4 | Old k | 100 * 200,000 = 20,000,000 |
| 5 | Pool sends out | 1 token0 |
| 6 | Balances after user sends input | balance0 = 99, balance1 = 202,100 |
| 7 | Compute input | amount0In = 99 - (100 - 1) = 0 |
| 8 | Compute input | amount1In = 202,100 - (200,000 - 0) = 2,100 |
| 9 | Fee-adjust balance0 | balance0Adjusted = 99 _ 1000 - 0 _ 3 = 99,000 |
| 10 | Fee-adjust balance1 | balance1Adjusted = 202,100 _ 1000 - 2,100 _ 3 = 202,093,700 |
| 11 | Check | 99,000 _ 202,093,700 >= 100 _ 200,000 * 1000^2 |
| 12 | Result | True, so swap succeeds |
Router: Swap Methods
Section titled “Router: Swap Methods”UniswapV2Router02 contract (periphery) provides multiple swap methods to support different combinations of swaps.
It calculates optimal amounts before calling UniswapV2Pair.swap() method.
The core distinction across all functions is:
- who fixes what (exact input vs. exact output)
- whether ETH or ERC-20 tokens are on either end
- tokens whose balance decreases on transfer (SupportingFeeOnTransferTokens)
Multihop swap
Section titled “Multihop swap”One trade can pass through multiple token pairs/pools in sequence instead of using one direct swap.
Router uses path argument to support this type of trade.
The order of the path does matter:
path[0]is the token you are paying with,path[path.length - 1]is the token you want to receive- every
intermediate elementdefines the hop sequence the router will follow
Example path
Take this path: [DAI, WETH, USDC, UNI, LINK]
That means:
- Overall input token: DAI.
- Overall output token: LINK.
- Intermediate hops: WETH, then USDC, then UNI
| Hop | Pair used | Input to that pair | Output from that pair |
|---|---|---|---|
| 1 | DAI/WETH | DAI | WETH |
| 2 | WETH/USDC | WETH | USDC |
| 3 | USDC/UNI | USDC | UNI |
| 4 | UNI/LINK | UNI | LINK |
Function trace
Section titled “Function trace”// UniswapV2Workflows:swap:118:136async swap({ user = USER_1.address, amountIn = 50n * _1e18, amountOutMin = 50n * _1e18 } = {}) { const { erc20TokenA, erc20TokenB } = await this.initialLiquidity({ user: USER_1.address, amountADesired: 200n * _1e18, amountBDesired: 400n * _1e18, traceTx: false, });
const pairAddress = await this.calculatePairAddress(erc20TokenA, erc20TokenB); this.lensClient.addressLabeler.markContractAddress( pairAddress, 'contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:UniswapV2Pair' );
const path: Address[] = [erc20TokenA.address, erc20TokenB.address]; const trace = await this._swap(erc20TokenA, amountIn, amountOutMin, path, user, this.maxUint256(), user);
return { trace, erc20TokenA, erc20TokenB };}
// UniswapV2Workflows:_swap:202:225private async _swap( tokenIn: UniswapV2ERC20, amountIn: bigint, amountOutMin: bigint, path: Address[], to: Address, deadline: bigint, user: Address, trace = true) { const { router2 } = this.deployment;
await this.transferErc20(tokenIn, USER_0.address, user, amountIn); await this.approve(tokenIn, router2.address, amountIn, user);
return await this.lensClient.contract( router2, 'swapExactTokensForTokens', [amountIn, amountOutMin, path, to, deadline], user, undefined, trace );}