← David Ryan's builder page

Uniswap V2 security audit

Ethereum mainnet: UniswapV2Pair, UniswapV2Factory and UniswapV2Router02
Reviewed 2026‑08‑20 against block 25,794,522

Uniswap V2 is an automated market maker. Each pair contract holds a reserve of two tokens and quotes a price from the ratio between them. A trader swaps against that reserve directly, and the price moves as the reserves change. The product of the two reserves stays constant across a swap, which is where the pricing curve comes from.

The reserve comes from liquidity providers. They deposit both tokens and receive LP tokens that record their share of the pool. Traders pay 0.3% on every swap which remains in the pair after the swap, so the value behind each LP token rises. A provider redeems their share whenever they choose.

There are three main contracts. A pair template holds the reserves and prices the swaps. A factory stamps out one copy of that template per token pair, and has produced 519,134 of them since May 2020 on the Ethereum mainnet deployment alone. A router sits in front of both and handles the transactions most users actually send. Every deployed pair runs byte-identical code, which this review proves by reproducing the CREATE2 derivation.

The Uniswap official deployments are on Ethereum mainnet and 15 other chains. There are also Uni V2 forks by multiple other organisations on mainnet and other chains. DefiLlama counted 767 forks in total, making Uni V2 the most forked protocol. This audit is for the Ethereum mainnet deployment alone. The Other deployments and forks section sets out what carries across to the other three, and how to check.

One function inside every pair sat dormant for 5.6 years. _mintFee collects a protocol fee for Uniswap governance, and it returns on its first line while no recipient is set. Governance had never named a recipient from protocol launch until 2025-12-28 under the UNIfication proposal. Code written in 2020 and never before executed in production began running on every deposit and every withdrawal across half a million pairs. This audit therefore focuses on that code.

All three contracts are immutable. No owner can patch them, and no governance vote can replace their code. Governance controls a single variable in the whole system, the address that receives the protocol fee. A flaw in any of the three contracts would have no fix, and the liquidity sitting in the pairs would have to be manually withdrawn.

This review found no exploitable vulnerability. The arithmetic on the newly live fee path is sound, and the report proves the intermediate products cannot overflow. The findings that matter are economic. The largest is that the fee cannot tell trading revenue apart from a token rebase, so it now takes one sixth of the staking yield in pools holding a yield-bearing asset.

How this audit was made

I commissioned this audit from myself, to find out how far an AI can take a smart contract review. Claude (Opus 5) read the bytecode, ran the analysis and wrote most of what you see below. I set the scope and the method, checked every finding against the chain myself, and decided what was worth publishing.

So read it as AI work with a human check on it, not as a firm's audit. The findings are reproducible: every figure is pinned to one block, and the method section says exactly how each claim was reached.

Audit summary

This review found no exploitable vulnerability in the three Uniswap V2 contracts in scope. All three are immutable. None holds an upgrade slot, a DELEGATECALL to a replaceable target, or a SELFDESTRUCT. Governance controls one variable across the whole system, which is the address that receives the protocol fee.

The review concentrated on UniswapV2Pair._mintFee, the protocol fee path. Uniswap governance set the fee recipient on 2025-12-28. Before that date the function returned early on every call, so code written in 2020 began executing in production after 5.6 years. The arithmetic on that path is sound. This report proves that the intermediate products cannot overflow, and confirms that the rounding runs in favour of liquidity providers.

The material findings are economic rather than arithmetic. _mintFee charges one sixth of the growth in the square root of the reserve product, whatever caused that growth. In a pool holding a rebasing token, the staking yield is growth of exactly this kind, so the protocol now takes one sixth of it. Finding M-1 quantifies this against the live stETH/WETH pair.

Three contracts, one template, 519,134 pairs on Ethereum mainnet. Section Template identity proves by CREATE2 derivation that every one of those pairs runs byte-identical code, so the pair findings apply to all of them. Uniswap's deployments on other chains, and the 767 forks other organisations run, sit outside this scope.

Findings at a glance

IDSeverityFinding
M-1MediumThe protocol fee charges token rebases as though they were trading revenue
M-2MediumA deposit that is not atomic with mint can be claimed by any caller
M-3MediumOracle consumers must use wrapping arithmetic, or the price feed reverts
L-1Lowmint and burn became more expensive when the protocol fee activated
L-2LowERC-20 tokens left in the router are claimable by the next caller
L-3LowETH left in the router is unrecoverable; 0.025 ETH is stranded now
L-4LowLP token transfers have no zero-address guard
L-5LowThe reserve ceiling of 2112 − 1 excludes very large token supplies
I-1InfoThe one sixth fee share is fixed in immutable code
I-2InfoFee rounding favours liquidity providers, bounded at one wei per call
I-3InfoFee-on-transfer tokens require the dedicated router entry points
I-4InfoThe EIP-712 domain separator is fixed at deployment

Counts: 0 critical, 0 high, 3 medium, 5 low, 4 informational.

Verified onchain facts

Every figure below was read from Ethereum mainnet at block 25,794,522, 2026-08-20 06:25:11 UTC.

UniswapV2PairUniswapV2FactoryUniswapV2Router02
Address0xB4e1…8C9Dc0x5C69…5aA6f0x7a25…2488D
DeployedBlock 10,008,355, 2020-05-05Block 10,000,835, 2020-05-04Block 10,207,858, 2020-06-05
Age6.29 years6.29 years6.20 years
Runtime code11,293 B (11,241 + 52 metadata)13,859 B (13,807 + 52)21,943 B (21,890 + 53)
Compilersolc 0.5.16+commit.9c3226cesolc 0.5.16+commit.9c3226cesolc 0.6.6+commit.6c089d02
Optimiserenabled, 999,999 runsenabled, 999,999 runsenabled, 999,999 runs
Instructions5,3616,45211,012
Selectors273524
Proxy slotsall three zeroall three zeroall three zero
Upgrade opcodesnoneone CREATE2none
Admin gettersnone respondfeeTo, feeToSetternone respond

The pair address above is the USDC/WETH instance, used as the read target for the template.

Governance. feeToSetter is 0x1a9c8182c09f50c8318d769245bea52c32be35bc, a timelock with a 172,800 second delay, which is 2 days. Its admin is 0x408ed6354d4973f66138c91495f2f2fcbd8724c3. feeTo is 0xf38521f130fccf29db1961597bc5d2b60f995f85, a 1,990 byte contract owned by the same timelock. allPairsLength() returns 519,134.

Pair storage layout, confirmed by direct slot reads:

SlotContents
0totalSupply
1balanceOf mapping root
2allowance mapping root
3DOMAIN_SEPARATOR
4nonces mapping root
5factory
6token0
7token1
8reserve0, reserve1, blockTimestampLast, packed into one slot
9price0CumulativeLast
10price1CumulativeLast
11kLast
12unlocked, the reentrancy guard

Method

I read the runtime bytecode of all three contracts over JSON-RPC and disassembled it with an opcode walker that skips PUSH immediates. Claims about what these contracts cannot do rest on that disassembly. I then read the verified source published for each deployed address and reconciled it against the bytecode. Every state figure comes from a single batch of 66 reads pinned to one block, so the numbers in this report reconcile with each other.

Four checks are worth naming, because each one produced a result used below.

Template identity. See the section below. This is what allows a single review to speak for 519,134 contracts.

Selector reconciliation. The pair's dispatcher carries 27 selectors. Its documented interface has 27 entries. The two sets match exactly, with no undocumented entry point and no missing one. The factory's dispatcher carries 35 selectors. That is its own 8 plus the pair template's 27, because the factory holds the pair's creation code inside its runtime.

Independent hash recomputation. I recomputed the pair's EIP-712 domain separator and permit type hash from their preimages and compared them against the values the contract returns. Both match:

DOMAIN_SEPARATOR  0xe8d93546d488d196c53f3e93ad73ba237e3fb527bddca6a240f54d03552dc70f
PERMIT_TYPEHASH   0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9

This matters for the compiler sweep below. The one known solc defect whose trigger conditions this build meets is a bytecode optimiser fault in Keccak-256 constant folding. Matching hashes, plus the matching CREATE2 derivation, give positive evidence that the hash sites in this bytecode compute correctly.

Compiler defect sweep. solc 0.5.16 carries 13 recorded defects and solc 0.6.6 carries 13. I assessed each against the language features these contracts actually use. None is reachable. The table records the reason for each class:

Defect classReason it cannot fire here
AbiReencodingHeadOverflow…, MissingEscapingInFormattingRequire ABIEncoderV2. All three contracts use the default encoder.
EmptyByteArrayCopy, DirtyBytesArrayToStorageRequire a bytes array copied into storage. No such copy exists.
DynamicArrayCleanupRequires a packed dynamic storage array that shrinks. The factory's address[] allPairs packs one element per slot and only grows.
MemoryArrayCreationOverflowRequires an array length above 2251. Router path lengths are bounded by calldata size.
privateCanBeOverriddenRequires a derived contract redeclaring a base contract's private function. The pair declares no function matching a private member of its ERC-20 base.
SignedImmutablesRequires a signed immutable narrower than 256 bits. The router's two immutables are address.
MissingSideEffectsOnSelectorAccessRequires .selector on a side-effecting expression. Selectors here are literals.
ArraySliceDynamicallyEncodedBaseType, NestedCalldataArray…, ABIDecodeTwoDimensionalArrayMemoryRequire array slices, nested calldata arrays or two-dimensional ABI decoding. None appears.
LostStorageArrayWriteOnSlotOverflowRequires a storage array based near slot 2256. Slots in use run 0 to 12.
ImplicitConstructorCallvalueCheckConcerns value sent during construction. The factory creates each pair with a value of zero.
KeccakCachingTrigger conditions are met. Addressed by the independent hash recomputation above.

Template identity

UniswapV2Factory.createPair deploys each pair with CREATE2, using the concatenated sorted token addresses as the salt. That makes every pair address a pure function of the factory address, the two tokens and the creation code. The derivation reproduces exactly:

init code                11,636 bytes
keccak256(init code)     0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
salt(USDC, WETH)         0x85053f65cd1ece2bb37b70c13d66eadebf2779df5ddd68cf12f3ccfdc6bfe760
derived address          0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc
deployed address         0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc

Three further facts complete the chain:

  1. That 11,636 byte init code appears verbatim inside the factory's runtime code at offset 2,171. The factory carries the template it stamps out.
  2. The pair's 11,293 byte runtime code sits inside that init code at offset 261.
  3. The same init code hash is hardcoded inside Router02's runtime at offset 16,742, which is how the router computes pair addresses without an external call.

Spot-checked across four live pairs, each one returns runtime code identical to the template and an address the derivation reproduces:

PairRuntime identicalCREATE2 derives its address
USDC/WETHyesyes
WETH/USDTyesyes
DAI/WETHyesyes
stETH/WETHyesyes

The pair findings in this report therefore apply to all 519,134 pairs the mainnet factory has deployed.

Other deployments and forks

The contracts audited here are one deployment of four kinds. Only the first is in scope.

DeploymentRead for this review
1Uniswap's own, on Ethereum mainnetYes. This is the audit.
2Uniswap's own, on 15 other networksNo
3Another organisation's fork, on Ethereum mainnetNo
4Another organisation's fork, on another chainNo

Category 2. Uniswap documents official V2 deployments on 16 networks, being 15 mainnets and the Sepolia testnet. Each carries its own factory address, so its pair addresses and its salts differ from mainnet's. DefiLlama attributes Uniswap V2 liquidity across 15 chains. None of those deployments was read here, and this report makes no claim about any of them.

Categories 3 and 4. Uniswap V2 is the most forked protocol DefiLlama tracks. It lists 767 protocols naming it as their source, against 238 for the next most copied design. Those forks hold 2.62 billion US dollars between them across 236 chains, where Uniswap V2 itself holds 0.97 billion. PancakeSwap AMM alone holds 1.94 billion, so the largest fork carries about twice the value of the original.

Deployment and fork figures come from the Uniswap developer documentation and the DefiLlama API, both read on 2026-09-07. They move, and they are the one set of numbers in this report that was not read from a chain directly.

A deployment outside category 1 inherits this report to the extent that it inherits the code. Two cases separate cleanly, and they apply to categories 2, 3 and 4 alike.

Identical code inherits every finding. The init code hash is the cheap test. Derive it from that deployment's own factory and compare it against 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f. A match means the pair template is byte-identical, and every pair finding here holds as written, including the overflow proof on _mintFee.

Modified code inherits the findings only where it still matches. The swap fee and the protocol fee share are compile-time constants, so a fork is free to set them differently. Any figure here that rests on the 0.3% swap fee or the one sixth protocol share needs recomputing against that deployment's own source. Changes to the ERC-20 base or to the wrapped native token each need their own reading.

The findings that follow from the design travel furthest. M-2, M-3, L-4 and L-5 describe how a constant product pair works at all, so they apply unless that deployment addressed them on purpose. The findings in L-2 and L-3 sit in the router, which forks rewrite more often than they rewrite the pair.

Governance and value figures do not travel at all. I-1 rests on Uniswap's own timelock, and every balance in this report was read from mainnet.

The protocol fee path

_mintFee runs at the start of mint and at the start of burn. It reads the recipient from the factory. It then compares the current reserve product against the stored kLast, and mints new LP tokens when the product has grown:

uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
    uint denominator = rootK.mul(5).add(rootKLast);
    uint liquidity = numerator / denominator;
    if (liquidity > 0) _mint(feeTo, liquidity);
}

The formula is correct. Solving for the minted amount L that gives the recipient one sixth of the growth in rootK:

L / (totalSupply + L) = (1/6) · (rootK − rootKLast) / rootK
L = totalSupply · (rootK − rootKLast) / (5 · rootK + rootKLast)

That is the expression in the code. The live USDC/WETH pair confirms it. An observed growth of 1.2991e-06 in rootK produces a pending charge of 0.00002165 percent of the pool. That is one sixth of the growth, to five significant figures.

The intermediate products cannot overflow. totalSupply never exceeds rootK. The two are equal when a pair is first seeded, since the first deposit mints sqrt(amount0 · amount1). Afterwards mint and burn scale both quantities by the same factor, and every other path raises the reserve product without raising the supply. So the ratio only falls. Both reserves are uint112, so rootK is at most 2112, and the largest product the function forms is:

totalSupply · (rootK − rootKLast)  ≤  rootK · rootK  ≤  2^224

That leaves 32 bits of headroom below the 2256 limit. The SafeMath guards on this path cannot trigger, which means _mintFee cannot revert and cannot brick mint or burn. The reserve product itself, uint(_reserve0).mul(_reserve1), is bounded by the same 2224.

Accrued growth is charged once and never retroactively. kLast is written at the end of mint and burn, and only while a recipient is set. When governance activated the fee on 2025-12-28, kLast held zero for every pair, and _mintFee skips the charge entirely when kLast is zero. Growth accumulated over the preceding 5.6 years therefore went uncharged.

Findings

M-1 · The protocol fee charges token rebases as though they were trading revenue Medium

Affects liquidity providers in pools holding a rebasing or yield-bearing token.

_mintFee measures growth in the square root of the reserve product. It does not distinguish where that growth came from. Trading fees raise the reserve product, and so does any other increase in the pair's token balances, including a positive rebase.

Take a token that rebases by a factor r on one side of a pool. The reserve product grows by (1 + r), so its square root grows by sqrt(1 + r). The recipient's resulting share of the pool is:

(1/6) · (1 − 1 / sqrt(1 + r))  ≈  r / 12

The rebase itself is worth about r / 2 of the pool, because the rebasing token is one of two sides. The protocol therefore captures one sixth of the staking yield, and the liquidity providers keep five sixths.

Evidence from the live stETH/WETH pair, 0x4028daac072e492d34a3afdbef0ba7e35d8b55c4:

MeasureValue
Growth in rootK since kLast was set6.8408e-05
Pending charge on the next mint or burn1,030,017,104,789,895 LP, being 0.00114005% of the pool
Recipient's current holding14,381,889,933,930,848 LP, being 0.015918% of supply

The pending charge is exactly one sixth of the growth. The implied one-sided growth of 1.368e-04 corresponds to about 1.7 days of rebase at a 3 percent annual staking yield, which matches the mechanism.

The comparison across pools shows the effect. The recipient holds 0.015918 percent of the stETH/WETH pool against 0.000906 percent of the USDC/WETH pool. That is a factor of 18, on a pool with far less trading volume. Yield drives the difference.

Impact. A liquidity provider in a rebasing-token pool now surrenders one sixth of the yield component of their return, on top of one sixth of trading fees. Before 2025-12-28 the whole rebase accrued to them. The same reasoning covers any pool where value reaches the pair by a route other than a swap. Airdrops paid to holders and direct donations both qualify.

Mitigation. Price the yield drag into the decision to provide liquidity in a rebasing-token pool. A wrapped form of the same asset avoids the charge. Its balance stays constant while the redemption rate moves, so the reserve product stays flat and the yield stays with the holder.

M-2 · A deposit that is not atomic with mint can be claimed by any caller Medium

Affects any contract or script that deposits without the router.

mint credits the caller with the difference between the pair's current token balances and its stored reserves:

uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);

The function attributes that difference to whoever calls it, and mints the LP tokens to whatever address that caller names. Tokens transferred to a pair in one transaction, with mint called in a later transaction, belong to the first caller of mint that follows.

Impact. An integrator who sends tokens to a pair and calls mint separately loses the deposit to a watcher. The window is one block.

Mitigation. Transfer and mint in a single transaction, which is what UniswapV2Router02.addLiquidity does. Treat direct pair calls as available only to contracts that control the whole sequence atomically.

M-3 · Oracle consumers must use wrapping arithmetic, or the price feed reverts Medium

Affects protocols reading the pair's time-weighted price.

_update maintains two cumulative price accumulators and the timestamp they were last written at:

uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;

Both the accumulators and the timestamp are designed to wrap. The contract's own arithmetic handles this correctly, because solc 0.5.16 wraps silently and the modular subtraction gives the right elapsed time across a wrap. blockTimestampLast is a uint32, so it wraps in February 2106.

The hazard sits in the consumer. A consumer compiled with solc 0.8.0 or later gets checked arithmetic by default. Subtracting a stored accumulator from a later reading then reverts on the wrap instead of returning the correct difference. The same applies to a consumer that stores its own uint256 timestamp and subtracts the pair's uint32 value.

At current values this is remote. price0CumulativeLast on USDC/WETH stands at about 2169 against a 2256 ceiling. The exposure scales with the price ratio. A pair whose accumulator advances at the maximum rate reaches the ceiling in about 136 years. A pair with extreme reserve asymmetry advances far faster than USDC/WETH does.

Impact. An oracle read that reverts at an arbitrary future point. For a lending protocol that treats a reverting oracle as a halt, that is a liveness failure. A protocol that falls back to a stale price instead faces a pricing error.

Mitigation. Wrap every accumulator and timestamp subtraction in unchecked, and store the counterpart timestamp as a uint32. Test the consumer against accumulator values near 2256 and against a blockTimestampLast that has just wrapped.

L-1 · mint and burn became more expensive when the protocol fee activated Low

Affects integrators with fixed gas budgets.

While no recipient was set, _mintFee returned after one external call and kLast stayed at zero. Both functions now perform additional storage work:

The current gas schedule gives the range. Rewriting kLast alone adds about 5,000 gas. The first charging call for a pair adds roughly 49,000, because kLast and the recipient's balance are both cold and both move away from zero.

Impact. Some calls that succeeded before 2025-12-28 now fail. The exposed cases are a contract that calls mint or burn through a fixed gas stipend, and a batched operation sitting near the block gas limit.

Mitigation. Re-measure gas for every path that reaches mint or burn, and size limits against the first-charge case rather than the steady state.

L-2 · ERC-20 tokens left in the router are claimable by the next caller Low

Affects anyone who transfers tokens to the router by mistake.

Two router functions pay out the router's whole balance of a token rather than the amount the current call produced:

// removeLiquidityETHSupportingFeeOnTransferTokens
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));

// swapExactTokensForETHSupportingFeeOnTransferTokens
uint amountOut = IERC20(WETH).balanceOf(address(this));

The router is designed to end every transaction holding nothing, so in normal operation the balance equals the amount just produced. Any token sent to the router directly, or left there by a failed integration, is swept to the next caller of the matching function.

Impact. Permanent loss of misdirected tokens to an unrelated party. Sums are whatever users have misdirected. All six major tokens checked held a zero balance at the review block.

Mitigation. Never transfer tokens to the router address. Integrators should treat a non-zero router balance as recoverable only by the party who acts first.

L-3 · ETH left in the router is unrecoverable Low

0.025 ETH is stranded at the review block.

The router's receive function accepts ETH from the WETH contract:

receive() external payable {
    assert(msg.sender == WETH);
}

Every ETH payment the router makes sends an amount computed by the current call. No function reads address(this).balance. ETH that arrives outside the normal unwrap flow, by a coinbase payment or a forced transfer, has no exit.

At block 25,794,522 the router holds 25,000,000,000,000,000 wei, being 0.025 ETH. That balance is permanently locked.

Impact. Small and bounded. It matters as a statement about the contract rather than as a loss.

Mitigation. Send ETH to the router only through its payable entry points.

L-4 · LP token transfers have no zero-address guard Low

Affects holders who mistype a destination.

The pair's ERC-20 implementation moves balances without checking the destination. A transfer to the zero address succeeds and removes the tokens from circulation, since no key exists to move them again.

The USDC/WETH pair holds 98,419,474,949 LP tokens at the zero address. Of that, 1,000 is the MINIMUM_LIQUIDITY the first deposit locks by design. The rest represents value sent there by holders, worth about 1.4 parts per million of the pool.

Impact. Silent, permanent loss on a mistyped transfer.

Mitigation. Validate the destination before calling transfer on an LP token. Reconcile the zero-address balance when computing circulating supply.

L-5 · The reserve ceiling of 2112 − 1 excludes very large token supplies Low

Affects tokens with very large supplies or high decimals.

_update rejects any balance above the uint112 ceiling:

require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');

The ceiling is 2112 − 1, which is about 5.19e33. For a token with 18 decimals that caps a pair at roughly 5.19e15 whole tokens.

Impact. A pair holding a token near that ceiling reverts on swap, mint, burn and sync once a deposit would cross it. Liquidity already in the pair stays withdrawable, since burn lowers balances. A token whose whole supply exceeds the ceiling cannot be fully pooled.

Mitigation. Check the token's maximum supply against 2112 − 1 in its own decimals before launching a pair.

I-1 · The one sixth fee share is fixed in immutable code Info

Recorded because it bounds the governance risk.

The divisors in _mintFee are compile-time constants inside immutable pair bytecode. UniswapV2Factory exposes exactly two authority functions, setFeeTo and setFeeToSetter, and both are gated on feeToSetter. Governance can change who receives the protocol fee, and can switch it off by setting the zero address.

Governance cannot raise the share above one sixth, reach liquidity provider principal, pause a pair, or replace pair code. The two-day timelock delay gives notice of any change to the recipient.

I-2 · Fee rounding favours liquidity providers, bounded at one wei per call Info

liquidity = numerator / denominator truncates. Math.sqrt truncates as well. The minted amount is therefore at most one LP token wei below the exact share, and the shortfall falls on the fee recipient.

A related case is a charge that truncates to zero, where kLast still advances and the growth goes uncharged. The threshold is tight. On USDC/WETH the growth in rootK would have to fall below 17.05 units against an actual value of 263,496,737,283. Reaching it needs two calls in close succession. Each such call forgives under one LP token wei, worth about 3e-10 US dollars at current pool size. The transaction costs many orders of magnitude more than it forgives.

I-3 · Fee-on-transfer tokens require the dedicated router entry points Info

The pair measures deposits and swap inputs from balance differences, so it handles a token that takes a cut on transfer without difficulty. The router's standard entry points compute expected amounts in advance and revert when the amount received falls short.

Router02 provides five functions ending in SupportingFeeOnTransferTokens for this case. They read the balance actually received and price the swap from it. The output check compares the recipient's balance before and after:

uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, ...);

A recipient that receives the same token from another source inside the same transaction inflates that difference and weakens the slippage check. Route output to an address that receives nothing else during the call.

I-4 · The EIP-712 domain separator is fixed at deployment Info

The pair computes DOMAIN_SEPARATOR in its constructor and stores it in slot 3. The chain id is read once, at deployment.

Should the chain split, both sides keep the same domain separator. A permit signature valid on one side would then be valid on the other. Two things bound the exposure: the per-holder nonces counter, and the signature deadline.

What these contracts cannot do

Each item below is confirmed against the disassembled runtime code, or against the state reads pinned to block 25,794,522.

Integration checklist

These contracts are immutable, so this section is the actionable part of the report.

Providing liquidity

Reading prices

Swapping and routing

Deriving pair addresses

Launching a pair

Limits of this review

State these alongside the findings whenever this audit is quoted.

Links