Skip to content

Swap

This page covers the necessary concepts to understand token swap flow in Uniswap V2.

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 xswapx_{swap} of token X. To do so, they must deposit an amount yswapy_{swap} of token Y

k=xafteryafter=xbeforeybefore(xbeforexswap)(ybefore+yswap)=xbeforeybefore(xbefore+xswap)(ybeforeyswap)=xbeforeybefore(reserveIn+amountIn)(reserveOutamountOut)=reserveInreserveOut\begin{aligned} k = x_{after} \cdot y_{after} &= x_{before} \cdot y_{before} \\ (x_{before} - x_{swap})(y_{before} + y_{swap}) &= x_{before} \cdot y_{before} \\ (x_{before} + x_{swap})(y_{before} - y_{swap}) &= x_{before} \cdot y_{before} \\ (reserveIn + amountIn)(reserveOut - amountOut) &= reserveIn \cdot reserveOut \\ \end{aligned}amountIn is known, amountOut is unknown:amountOut=reserveOutamountInreserveIn+amountIn(1)amountOut is known, amountIn is unknown:amountIn=reserveInamountOutreserveOutamountOut(2)\begin{aligned} \text{amountIn is known, amountOut is unknown:} \\ amountOut &= \frac{reserveOut \cdot amountIn}{reserveIn + amountIn} & (1) \\ \\ \text{amountOut is known, amountIn is unknown:} \\ amountIn &= \frac{reserveIn \cdot amountOut}{reserveOut - amountOut} & (2) \end{aligned}

Interpreting the equations:

  • (1) trader wants to deposit amountInamountIn of token A => will receive amountOutamountOut of token B
  • (2) trader wants to receive amountOutamountOut of token A => should deposit amountInamountIn of token B

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)
amountOut=reserveOut0.997amountInreserveIn+0.997amountInamountOut = \frac{reserveOut \cdot 0.997 \cdot amountIn}{reserveIn + 0.997 \cdot amountIn}
// 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 asset
function 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)
amountIn=reserveInamountOut0.997(reserveOutamountOut)amountIn = \frac{reserveIn \cdot amountOut}{ 0.997 \cdot (reserveOut - amountOut)}
// 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 asset
function 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

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.

(x+Δx)(yΔy)=(x+0.997Δx)(yΔy)+0.003Δx(yΔy).(x+\Delta x)(y-\Delta y) = (x+0.997\,\Delta x)(y-\Delta y) + 0.003\,\Delta x\,(y-\Delta y).xy=xy+0.003Δx(yΔy)>xy.x'y' = xy + 0.003\,\Delta x\,(y-\Delta y) > xy.

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:

peffective=yafterybeforexbeforexafterp_{effective} = \frac{ y_{after} - y_{before} } { x_{before} - x_{after} }

ybeforey_{before} / yaftery_{after} reserve balance of token Y before/after trade

xbeforex_{before} / xafterx_{after} reserve balance of token X before/after trade

The effective price is always higher (worse) than the starting spot price.

The theoretical exchange rate for an infinitely small trade (compared to the reserves).

pspot=ybeforexbeforep_{spot} = \frac{y_{before}}{x_{before}}

ybeforey_{before} / xbeforex_{before} reserve balance of token Y/X before trade

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
  • 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

CaseInitial USDC reserveInitial ETH reserveStarting spot priceamountIn (USDC)amountOut (ETH)Effective priceFinal USDC reserveFinal ETH reserveSpot price after trade
A2,000,00021,000,000 USDC/ETH10,0000.009950251,005,000.00 USDC/ETH2,010,0001.990049751,010,025.00 USDC/ETH
B200,0002010,000 USDC/ETH10,0000.9523809510,500.00 USDC/ETH210,00019.0476190511,025.00 USDC/ETH
C20,000200100 USDC/ETH10,00066.66666667150.00 USDC/ETH30,000133.33333333225.00 USDC/ETH
  • 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.

PoolInitial USDC reserveInitial ETH reserveStarting spot priceETH boughtFraction of ETH pool boughtUSDC paidEffective priceFinal USDC reserveFinal ETH reserveSpot price after trade
Deep pool40,000,00010,0004,000.00 USDC/ETH100.1%40,040.044,004.00 USDC/ETH40,040,040.049,9904,008.01 USDC/ETH
Shallow pool400,0001004,000.00 USDC/ETH1010%44,444.444,444.44 USDC/ETH444,444.44904,938.27 USDC/ETH

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:

  • amountOutMin for exact-input swaps
  • amountInMax for exact-output swaps
  • plus a deadline to avoid stale execution.

All router swap methods call UniswapV2Pair.swap() method.

  • works for either direction on the same pair
  • amount0Out/amount1Out is how much token0/token1 the pair should send out
  • usually one is zero and 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 checks
function 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);
}

In order for swap to be successful, the new balances must preserve or increase the old invariant (xy=kxy=k).

// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:180:182
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');
StepValue / checkExample
1Trader sendssends 2,100 token1
2Trader wants outamount0Out = 1, amount1Out = 0
3Old reservesreserve0 = 100, reserve1 = 200,000
4Old k100 * 200,000 = 20,000,000
5Pool sends out1 token0
6Balances after user sends inputbalance0 = 99, balance1 = 202,100
7Compute inputamount0In = 99 - (100 - 1) = 0
8Compute inputamount1In = 202,100 - (200,000 - 0) = 2,100
9Fee-adjust balance0balance0Adjusted = 99 _ 1000 - 0 _ 3 = 99,000
10Fee-adjust balance1balance1Adjusted = 202,100 _ 1000 - 2,100 _ 3 = 202,093,700
11Check99,000 _ 202,093,700 >= 100 _ 200,000 * 1000^2
12ResultTrue, so swap succeeds

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)

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 element defines 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
HopPair usedInput to that pairOutput from that pair
1DAI/WETHDAIWETH
2WETH/USDCWETHUSDC
3USDC/UNIUSDCUNI
4UNI/LINKUNILINK
Loading graph...
// UniswapV2Workflows:swap:118:136
async 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:225
private 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
);
}
EVM LENS

Function Trace

WorkflowSwap
DescriptionSee the function trace and protocol source code
NETWORK STATUS: ONLINE
BROWSER VM