Section 14 of 16
Router: Swapping
What You Are Building
This is the heart of the Router. Every token trade on Uniswap V2 flows through these functions. There are six external swap variants covering every combination of exact input vs. exact output, and token vs. ETH on either side. All of them rely on a single internal _swap() function that handles the core routing logic. Understanding _swap() deeply is the key to understanding every swap variant.
The _swap Internal Function
_swap() takes two parameters: an array of pre-calculated amounts and a path of token addresses. The amounts array has one more element than the number of hops. For a path of [WETH, USDC, DAI], amounts might be [1e18, 2100e6, 2095e18], meaning: input 1 WETH, get 2100 USDC from the first pair, get 2095 DAI from the second pair.
The function loops through each consecutive pair in the path. For a path with N tokens, there are N-1 pairs and N-1 iterations. Here is what happens in each iteration.
Sorting tokens to determine direction. The function calls UniswapV2Library.sortTokens(input, output) to get the pair's canonical token0. Pairs always store reserves in sorted order (lower address = token0). The Router needs to know which slot the output token occupies to set the correct output parameter.
Setting amount0Out and amount1Out. The pair's swap() function takes amount0Out and amount1Out. One of these will be the swap output, the other will be zero. If the output token is token0 (the input token has the higher address), then amount0Out = amountOut and amount1Out = 0. If the output token is token1, the reverse. Getting this wrong means the pair sends the wrong token, or reverts because the invariant check fails.
Determining the to address. This is where the gas optimization lives. For intermediate hops (not the last swap), the to address is the next pair in the path: UniswapV2Library.pairFor(factory, output, path[i + 2]). For the final hop, the to address is the user's specified _to address. The critical insight: intermediate swaps send tokens directly to the next pair, not back through the Router. The Router never touches intermediate tokens. Token A goes to Pair 1, Pair 1 sends Token B directly to Pair 2, Pair 2 sends Token C to the user. This saves two transfer calls per intermediate hop (one from pair to router, one from router to next pair), which saves roughly 10,000-15,000 gas per hop.
Calling pair.swap(). Finally, the function calls IUniswapV2Pair(pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0)). The empty bytes parameter means no flash swap callback is triggered.
What goes wrong without _swap. If you tried to implement multi-hop swaps without this routing loop, you would need the user to approve every intermediate token, execute each swap separately, and transfer tokens between pairs manually. The gas cost would roughly double per hop, and the user experience would be terrible.
swapExactTokensForTokens
The user knows exactly how many input tokens they want to spend and sets a minimum acceptable output. This is the most common swap type.
Step 1: Calculate all amounts upfront. Call UniswapV2Library.getAmountsOut(factory, amountIn, path). This function walks the path, calling getAmountOut() for each pair using the current reserves. It returns the full amounts array: [amountIn, intermediate1, intermediate2, ..., finalOutput]. All calculations happen before any tokens move. If the reserves change between calculation and execution (because another transaction lands first), the pair's invariant check will catch any discrepancy.
Step 2: Validate slippage. Check require(amounts[amounts.length - 1] >= amountOutMin). If the calculated output is below the user's minimum, revert immediately. No tokens move, no gas wasted on transfers. The slippage check happens before any state changes.
Step 3: Transfer input to first pair. Call TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]). The input tokens go directly from the user to the first pair. The Router never holds them.
Step 4: Execute swaps. Call _swap(amounts, path, to). The routing loop handles everything from here.
Edge case: path length. The path must have at least 2 elements (a direct swap). There is no enforced maximum, but longer paths mean more gas and more slippage. In practice, paths longer than 3 are rare.
swapTokensForExactTokens
The user knows exactly how many output tokens they want and sets a maximum they are willing to pay. This is the reverse direction.
Step 1: Calculate required inputs. Call UniswapV2Library.getAmountsIn(factory, amountOut, path). This function works backwards through the path. Starting from the desired output, it calculates how much input each pair needs, accounting for the 0.3% fee at each hop. The result is the full amounts array, where amounts[0] is the total input required.
Step 2: Validate maximum input. Check require(amounts[0] <= amountInMax). If the required input exceeds the user's maximum, revert. This is the inverse of the slippage check in swapExactTokensForTokens. Instead of protecting against too little output, it protects against too much input.
Step 3: Transfer required input. Transfer exactly amounts[0] from the user to the first pair. Not amountInMax, but the calculated required amount. The user keeps whatever they did not need to spend.
Step 4: Execute swaps. Call _swap(amounts, path, to). Same routing loop, same logic.
Why both variants exist. "I want to spend exactly 1 ETH, give me as much USDC as possible" vs. "I need exactly 2000 USDC, take as little ETH as possible." Different use cases. The first is common for casual trading. The second is common when you need a precise amount (paying a bill, filling a position).
ETH Variants
Four functions handle swaps involving native ETH. The pattern is consistent: wrap ETH to WETH on input, unwrap WETH to ETH on output.
swapExactETHForTokens. The user sends ETH as msg.value. The function requires path[0] == WETH. It calculates amounts with getAmountsOut, using msg.value as the input amount. Then it calls IWETH(WETH).deposit{value: amounts[0]}() to wrap the ETH, followed by IWETH(WETH).transfer(firstPair, amounts[0]) to send WETH to the first pair. Finally it calls _swap(). There is no refund here. The user specifies an exact input, and msg.value must match exactly. If msg.value is less than needed, the deposit or transfer reverts.
swapTokensForExactETH. The user wants an exact amount of ETH out. The function requires path[path.length - 1] == WETH. It calculates required inputs with getAmountsIn, validates amounts[0] <= amountInMax, transfers input tokens to the first pair, and calls _swap() with address(this) as the final recipient. The Router receives WETH, calls IWETH(WETH).withdraw(amountOut) to unwrap it, and sends native ETH to the user via TransferHelper.safeTransferETH(to, amountOut).
swapExactTokensForETH. The user sends an exact token amount and wants ETH. Similar to the above, but the slippage check is on the output side (amounts[amounts.length - 1] >= amountOutMin). The swap routes to the Router, which unwraps and forwards ETH.
swapETHForExactTokens. The user sends ETH and wants an exact token output. This one has a refund. The function calculates required inputs with getAmountsIn. amounts[0] might be less than msg.value. The function validates amounts[0] <= msg.value, wraps exactly amounts[0] worth of ETH to WETH, transfers it to the first pair, executes the swap, and refunds the excess: if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]). Without this refund, excess ETH would be locked in the Router. This is the only swap variant that can refund ETH, because it is the only one where the user sends more ETH than might be needed.
Why the path must start or end with WETH. The pair contracts only deal in ERC-20 tokens. They have no concept of native ETH. The Router bridges this gap by wrapping and unwrapping. If the path does not start with WETH but the user sends ETH, the wrapped WETH would be sent to a pair that expects a different token, and the swap would revert (or worse, succeed with wrong tokens).
Your Task
Implement _swap() and all six external swap functions. The starter code gives you the function signatures and the infrastructure. The internal _swap() function is the foundation. Get that right, and the external functions are straightforward wrappers that handle token transfers, ETH wrapping/unwrapping, and slippage checks around the core routing loop.