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.
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.
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.
| ID | Severity | Finding |
|---|---|---|
| M-1 | Medium | The protocol fee charges token rebases as though they were trading revenue |
| M-2 | Medium | A deposit that is not atomic with mint can be claimed by any caller |
| M-3 | Medium | Oracle consumers must use wrapping arithmetic, or the price feed reverts |
| L-1 | Low | mint and burn became more expensive when the protocol fee activated |
| L-2 | Low | ERC-20 tokens left in the router are claimable by the next caller |
| L-3 | Low | ETH left in the router is unrecoverable; 0.025 ETH is stranded now |
| L-4 | Low | LP token transfers have no zero-address guard |
| L-5 | Low | The reserve ceiling of 2112 − 1 excludes very large token supplies |
| I-1 | Info | The one sixth fee share is fixed in immutable code |
| I-2 | Info | Fee rounding favours liquidity providers, bounded at one wei per call |
| I-3 | Info | Fee-on-transfer tokens require the dedicated router entry points |
| I-4 | Info | The EIP-712 domain separator is fixed at deployment |
Counts: 0 critical, 0 high, 3 medium, 5 low, 4 informational.
Every figure below was read from Ethereum mainnet at block 25,794,522, 2026-08-20 06:25:11 UTC.
| UniswapV2Pair | UniswapV2Factory | UniswapV2Router02 | |
|---|---|---|---|
| Address | 0xB4e1…8C9Dc | 0x5C69…5aA6f | 0x7a25…2488D |
| Deployed | Block 10,008,355, 2020-05-05 | Block 10,000,835, 2020-05-04 | Block 10,207,858, 2020-06-05 |
| Age | 6.29 years | 6.29 years | 6.20 years |
| Runtime code | 11,293 B (11,241 + 52 metadata) | 13,859 B (13,807 + 52) | 21,943 B (21,890 + 53) |
| Compiler | solc 0.5.16+commit.9c3226ce | solc 0.5.16+commit.9c3226ce | solc 0.6.6+commit.6c089d02 |
| Optimiser | enabled, 999,999 runs | enabled, 999,999 runs | enabled, 999,999 runs |
| Instructions | 5,361 | 6,452 | 11,012 |
| Selectors | 27 | 35 | 24 |
| Proxy slots | all three zero | all three zero | all three zero |
| Upgrade opcodes | none | one CREATE2 | none |
| Admin getters | none respond | feeTo, feeToSetter | none 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:
| Slot | Contents |
|---|---|
| 0 | totalSupply |
| 1 | balanceOf mapping root |
| 2 | allowance mapping root |
| 3 | DOMAIN_SEPARATOR |
| 4 | nonces mapping root |
| 5 | factory |
| 6 | token0 |
| 7 | token1 |
| 8 | reserve0, reserve1, blockTimestampLast, packed into one slot |
| 9 | price0CumulativeLast |
| 10 | price1CumulativeLast |
| 11 | kLast |
| 12 | unlocked, the reentrancy guard |
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 class | Reason it cannot fire here |
|---|---|
AbiReencodingHeadOverflow…, MissingEscapingInFormatting | Require ABIEncoderV2. All three contracts use the default encoder. |
EmptyByteArrayCopy, DirtyBytesArrayToStorage | Require a bytes array copied into storage. No such copy exists. |
DynamicArrayCleanup | Requires a packed dynamic storage array that shrinks. The factory's address[] allPairs packs one element per slot and only grows. |
MemoryArrayCreationOverflow | Requires an array length above 2251. Router path lengths are bounded by calldata size. |
privateCanBeOverridden | Requires a derived contract redeclaring a base contract's private function. The pair declares no function matching a private member of its ERC-20 base. |
SignedImmutables | Requires a signed immutable narrower than 256 bits. The router's two immutables are address. |
MissingSideEffectsOnSelectorAccess | Requires .selector on a side-effecting expression. Selectors here are literals. |
ArraySliceDynamicallyEncodedBaseType, NestedCalldataArray…, ABIDecodeTwoDimensionalArrayMemory | Require array slices, nested calldata arrays or two-dimensional ABI decoding. None appears. |
LostStorageArrayWriteOnSlotOverflow | Requires a storage array based near slot 2256. Slots in use run 0 to 12. |
ImplicitConstructorCallvalueCheck | Concerns value sent during construction. The factory creates each pair with a value of zero. |
KeccakCaching | Trigger conditions are met. Addressed by the independent hash recomputation above. |
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:
Spot-checked across four live pairs, each one returns runtime code identical to the template and an address the derivation reproduces:
| Pair | Runtime identical | CREATE2 derives its address |
|---|---|---|
| USDC/WETH | yes | yes |
| WETH/USDT | yes | yes |
| DAI/WETH | yes | yes |
| stETH/WETH | yes | yes |
The pair findings in this report therefore apply to all 519,134 pairs the mainnet factory has deployed.
The contracts audited here are one deployment of four kinds. Only the first is in scope.
| Deployment | Read for this review | |
|---|---|---|
| 1 | Uniswap's own, on Ethereum mainnet | Yes. This is the audit. |
| 2 | Uniswap's own, on 15 other networks | No |
| 3 | Another organisation's fork, on Ethereum mainnet | No |
| 4 | Another organisation's fork, on another chain | No |
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.
_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.
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:
| Measure | Value |
|---|---|
Growth in rootK since kLast was set | 6.8408e-05 |
Pending charge on the next mint or burn | 1,030,017,104,789,895 LP, being 0.00114005% of the pool |
| Recipient's current holding | 14,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.
mint can be claimed by any caller MediumAffects 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.
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.
mint and burn became more expensive when the protocol fee activated LowAffects 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:
kLast = uint(reserve0).mul(reserve1) on every
mint and burn.totalSupply, a write to the
recipient's balance, and a Transfer log.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Each item below is confirmed against the disassembled runtime code, or against the state reads pinned to block 25,794,522.
DELEGATECALL to a replaceable target. There is no
upgrade path.SELFDESTRUCT in any of the three, so none can be removed
and no balance can be swept that way.CREATE2 in the factory, which is createPair. It
stamps out the fixed template whose init code hash is
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f,
and it cannot deploy anything else.setFeeTo and
setFeeToSetter, both gated on feeToSetter behind a
two-day timelock. It cannot raise the fee share above one sixth, reach
liquidity provider principal, pause a pair, or replace pair code._mintFee cannot revert. Its SafeMath guards sit
32 bits below the point where they could trigger, so activating the protocol
fee could not brick mint or burn on any pair.unlocked.These contracts are immutable, so this section is the actionable part of the report.
mint and
burn.mint and burn
path against the first-charge case (L-1).unchecked
(M-3).uint32, matching
blockTimestampLast.getReserves for a spot price only where manipulation
within one block is acceptable. Use the accumulators for anything that
governs value.SupportingFeeOnTransferTokens entry points for any
token that takes a cut on transfer (I-3).deadline. The router enforces
deadline >= block.timestamp and nothing more.CREATE2 from the factory address,
the sorted token pair and init code hash
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f.State these alongside the findings whenever this audit is quoted.
CREATE2 derivation and the init code hash
cover all 519,134 pairs by construction. Runtime code was read and compared
for four of them, not for every one.0x1a9c8182c09f50c8318d769245bea52c32be35bc and its admin were
read for their parameters. Neither contract was reviewed, and nor was the
1,990 byte feeTo recipient. Where the protocol fee goes after
it arrives is out of scope.kLast semantics in the code, not on an
archive read._mintFee arithmetic explained