Section 11 of 16
Library: Helpers and Math
Key takeaway: UniswapV2Library is the stateless math layer the Router depends on. It enforces canonical token ordering (
token0 < token1) so every token pair maps to exactly one CREATE2-deployed Pair address, computes that address off-chain via the samekeccak256formula the Factory uses, and providesgetAmountOutandgetAmountInwith the 0.30% fee baked in (multiply by 997, divide by 1000) plusquote()for proportional reserve math. Every Router function calls into this library; getting any helper wrong cascades through the entire periphery.
What You Are Building
UniswapV2Library is a stateless library that provides the mathematical foundation the Router depends on. Every swap amount calculation, every pair address lookup, every reserve fetch goes through this library. It is pure math and address computation. No state, no storage, no external calls except one reserve fetch. Eight functions, each building on the previous ones.
Token Sorting: Why Canonical Ordering Matters
function sortTokens(address tokenA, address tokenB)
internal pure returns (address token0, address token1)
Uniswap V2 always stores tokens in ascending address order. Token0 has the numerically smaller address. This is not arbitrary. It solves a fundamental problem: without canonical ordering, the same token pair could have multiple representations. WETH/USDC and USDC/WETH would be "different" pairs with separate liquidity pools, fragmenting liquidity and confusing users.
By enforcing token0 < token1, every token combination maps to exactly one pair. The Factory's createPair calls sortTokens and uses the sorted addresses as the CREATE2 salt, making duplicate creation impossible. The Library calls sortTokens at the start of nearly every function to ensure reserves are returned in the correct order relative to the caller's token arguments.
The function also validates two edge cases: the addresses must not be identical (IDENTICAL_ADDRESSES), and neither can be the zero address (ZERO_ADDRESS). Identical addresses would mean a pair trading a token with itself, which is nonsensical. Zero address tokens do not exist.
CREATE2 Address Calculation: pairFor
function pairFor(address factory, address tokenA, address tokenB)
internal pure returns (address pair)
This function computes the address of a pair contract without making any external calls. No CALL opcode, no gas for external execution, no dependency on the pair existing yet. It is pure arithmetic.
This works because the Factory deploys pairs using the CREATE2 opcode. CREATE2 produces deterministic addresses using this formula:
address = keccak256(0xff, factory, salt, init_code_hash)[12:]
The four components: 0xff is a constant prefix that distinguishes CREATE2 from CREATE. factory is the deployer's address. salt is keccak256(abi.encodePacked(token0, token1)), the hash of the sorted token pair. init_code_hash is the keccak256 of the Pair contract's creation bytecode.
The init code hash is the critical piece. It is a constant because the Pair constructor takes no arguments. Every pair has identical creation bytecode. If the constructor accepted parameters (like token addresses), the creation bytecode would differ for each pair, and the hash would not be constant. This is exactly why the Pair uses initialize() instead of constructor arguments, as covered in the Pair State section.
In Solidity, the computation looks like:
pair = address(uint160(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
INIT_CODE_HASH
)))));
The result is truncated to 160 bits (20 bytes) because Ethereum addresses are 20 bytes. The [12:] in the formula means taking the last 20 bytes of the 32 byte hash.
The Router uses pairFor on every swap, mint, and burn to find the correct pair without querying storage. Frontends use it to check if a pair exists before sending transactions. Anyone can compute where a pair for any token combination lives (or will live once created) using only the factory address and the constant init code hash.
Reserve Fetching: getReserves
function getReserves(address factory, address tokenA, address tokenB)
internal view returns (uint reserveA, uint reserveB)
This function calls pairFor to find the pair address, then calls the pair's getReserves() function. The critical detail: it reorders the returned reserves to match the caller's token order, not the pair's internal order.
The pair stores reserves as (reserve0, reserve1) where token0 < token1. But the caller might pass (USDC, WETH) where USDC > WETH. Without reordering, the caller would get WETH's reserve when they expect USDC's. The function checks if tokenA == token0 and returns reserves in the corresponding order.
The Quote Function
function quote(uint amountA, uint reserveA, uint reserveB)
internal pure returns (uint amountB)
Given an amount of tokenA and both reserves, return the proportional amount of tokenB at the current price ratio: amountB = amountA * reserveB / reserveA. This is used when adding liquidity. If you want to add 10 ETH to an ETH/USDC pool where the ratio is 1 ETH = 2000 USDC, quote(10, reserveETH, reserveUSDC) returns 20000 USDC.
Note that quote does not account for fees. It is a pure proportional calculation, appropriate for liquidity provision (which is fee free) but not for swaps.
getAmountOut: Deriving the Swap Formula
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
internal pure returns (uint amountOut)
This is the core trading math. Given an exact input amount, how much output do you get? Start from the constant product formula and derive it step by step.
The invariant is x * y = k, where x and y are reserves. After a swap with input dx (with a 0.3% fee), the new reserves must satisfy:
(x + dx * 0.997) * (y - dy) >= x * y
The 0.997 factor means only 99.7% of the input counts toward the product. The 0.3% fee stays in the pool. Solving for dy:
dy = (dx * 0.997 * y) / (x + dx * 0.997)
To avoid floating point, multiply numerator and denominator by 1000:
dy = (dx * 997 * y) / (x * 1000 + dx * 997)
In the code:
uint amountInWithFee = amountIn * 997;
uint numerator = amountInWithFee * reserveOut;
uint denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
Solidity integer division truncates (rounds down), which means the pool always keeps slightly more than the mathematical minimum. This is safe because it means the K invariant is satisfied with a tiny surplus.
Walk through with numbers: 1 ETH into a pool with 100 ETH and 200,000 USDC.
amountInWithFee = 1 * 997 = 997
numerator = 997 * 200000 = 199,400,000
denominator = 100 * 1000 + 997 = 100,997
amountOut = 199,400,000 / 100,997 = 1974 USDC
Without the fee, the output would be 1 * 200000 / 101 = 1980 USDC. The 6 USDC difference is the fee that stays in the pool.
getAmountIn: The Reverse, With Ceiling Division
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
internal pure returns (uint amountIn)
Given a desired output, how much input is needed? This is the algebraic inverse of getAmountOut, solved for dx:
dx = (x * dy * 1000) / ((y - dy) * 997) + 1
In code:
uint numerator = reserveIn * amountOut * 1000;
uint denominator = (reserveOut - amountOut) * 997;
amountIn = numerator / denominator + 1;
The + 1 is ceiling division. Integer division in Solidity always rounds down. For getAmountIn, rounding down would mean the user sends slightly less than needed. When the pair's swap function checks the K invariant, the adjusted product would be fractionally below the old K, and the swap would revert. The + 1 ensures the user always sends at least enough. They might overpay by 1 wei (the smallest unit), but the swap will succeed.
Without the + 1, certain swap amounts would be impossible to execute. The Router's swapTokensForExactTokens would intermittently fail depending on whether the exact math hit a rounding boundary.
Path Chaining: Multi Hop Swaps
function getAmountsOut(uint amountIn, address[] memory path)
function getAmountsIn(uint amountOut, address[] memory path)
Most trades are not single hops. If you want to swap LINK for DAI but there is no LINK/DAI pair, you route through WETH: LINK to WETH to DAI. The path is [LINK, WETH, DAI].
getAmountsOut processes the path forward. For path [A, B, C] with input amount amountIn:
- Fetch reserves for pair A/B. Call
getAmountOut(amountIn, reserveA, reserveB)to getamountB. - Fetch reserves for pair B/C. Call
getAmountOut(amountB, reserveB, reserveC)to getamountC. - Return the array
[amountIn, amountB, amountC].
Each hop's output becomes the next hop's input. The function returns an array with amounts at every step, which the Router uses to execute the actual swaps.
getAmountsIn works backward. For path [A, B, C] with desired output amountOut:
- Fetch reserves for pair B/C. Call
getAmountIn(amountOut, reserveB, reserveC)to getamountBneeded. - Fetch reserves for pair A/B. Call
getAmountIn(amountB, reserveA, reserveB)to getamountAneeded. - Return the array
[amountA, amountB, amountOut].
The path array must have at least 2 elements (a single hop). Each consecutive pair of addresses in the path must correspond to an existing pair with liquidity.
Your Task
Implement all eight functions in UniswapV2Library. The starter code gives you the library declaration and the INIT_CODE_HASH constant placeholder. You write everything else.