<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Adi</title>
    <description>The latest articles on Forem by Adi (@ranting_sage).</description>
    <link>https://forem.com/ranting_sage</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3861351%2F9a0d5283-aa22-4574-a6d1-f32d551424e9.png</url>
      <title>Forem: Adi</title>
      <link>https://forem.com/ranting_sage</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ranting_sage"/>
    <language>en</language>
    <item>
      <title>From Intent to LP Tokens: The Uniswap V2 Mint Process</title>
      <dc:creator>Adi</dc:creator>
      <pubDate>Tue, 07 Apr 2026 16:06:40 +0000</pubDate>
      <link>https://forem.com/ranting_sage/from-intent-to-lp-tokens-the-uniswap-v2-mint-process-1ke3</link>
      <guid>https://forem.com/ranting_sage/from-intent-to-lp-tokens-the-uniswap-v2-mint-process-1ke3</guid>
      <description>&lt;p&gt;If you've been following along, we've covered what AMMs are and how the constant product formula works, then took a detour to map out the contract architecture before diving in. This is the third post in the series, and the first one where we actually get into a flow end to end. We're starting with Mint — adding liquidity to a pool.&lt;/p&gt;

&lt;p&gt;In practice, this is where a lot of DeFi begins. Liquidity providers deposit tokens into pools and earn a share of the fees generated by swaps. Protocols build on top of this — routing trades, sourcing prices, or deploying their own liquidity as part of larger strategies. A surprising amount of “yield” in the ecosystem traces back to this one operation.&lt;/p&gt;

&lt;p&gt;The full path for this operation, as a reminder from the map post: &lt;strong&gt;User → Router → Pair → User&lt;/strong&gt;. That's the skeleton. Everything below is what happens inside each of those steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Starting at the Router: &lt;code&gt;addLiquidity()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everything starts when a user calls &lt;code&gt;addLiquidity()&lt;/code&gt; on &lt;code&gt;UniswapV2Router02&lt;/code&gt;. The function signature looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Walking through the parameters: &lt;code&gt;tokenA&lt;/code&gt; and &lt;code&gt;tokenB&lt;/code&gt; are the two tokens you want to deposit into. &lt;code&gt;amountADesired&lt;/code&gt; and &lt;code&gt;amountBDesired&lt;/code&gt; are the amounts you'd &lt;em&gt;like&lt;/em&gt; to put in (your intention). &lt;code&gt;amountAMin&lt;/code&gt; and &lt;code&gt;amountBMin&lt;/code&gt; are the minimums you're willing to accept (your floor). &lt;code&gt;to&lt;/code&gt; is the address that will receive the LP tokens. &lt;code&gt;deadline&lt;/code&gt; is a timestamp after which the transaction should revert rather than execute, which prevents your transaction from sitting in the mempool and executing at a price you no longer want.&lt;/p&gt;

&lt;p&gt;The reason there are both "desired" and "minimum" amounts will become clear in a moment.&lt;/p&gt;


&lt;h2&gt;
  
  
  The ratio problem: &lt;code&gt;_addLiquidity()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before anything gets transferred anywhere, the Router calls its own internal function &lt;code&gt;_addLiquidity()&lt;/code&gt; to figure out the exact amounts that should actually go into the pool. This is where the ratio enforcement happens.&lt;/p&gt;

&lt;p&gt;Remember from the first post that the spot price of a pool is just &lt;code&gt;reserveB / reserveA&lt;/code&gt; — the ratio of the two balances. That ratio &lt;em&gt;is&lt;/em&gt; the price. If you could just deposit any arbitrary amounts of both tokens, you'd be changing the ratio, which means you'd be changing the price. Adding liquidity shouldn't move the price. So the deposits have to respect the existing ratio.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_addLiquidity()&lt;/code&gt; first calls the &lt;strong&gt;Factory&lt;/strong&gt; to get the pool address for the pair. If the pool doesn't exist yet, the Factory deploys one. Then it fetches the current reserves &lt;code&gt;reserveA&lt;/code&gt; and &lt;code&gt;reserveB&lt;/code&gt;. Two paths from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If this is a brand new pool with zero reserves, there's no existing ratio to preserve. Your &lt;code&gt;amountADesired&lt;/code&gt; and &lt;code&gt;amountBDesired&lt;/code&gt; get returned as-is. Whatever you put in first sets the initial price, which is exactly why first-deposit pricing is something to be intentional about.&lt;/li&gt;
&lt;li&gt;If the pool already exists, the Router calls &lt;code&gt;quote()&lt;/code&gt; on &lt;code&gt;UniswapV2Library&lt;/code&gt;, passing your &lt;code&gt;amountADesired&lt;/code&gt; alongside the current &lt;code&gt;reserveA&lt;/code&gt; and &lt;code&gt;reserveB&lt;/code&gt;. The formula is straightforward:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;amountB=amountA×reserveBreserveA
amountB = amountA \times \frac{reserveB}{reserveA}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;tB&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;Rearranged to show what it's actually enforcing:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;amountBamountA=reserveBreserveA
\frac{amountB}{amountA} = \frac{reserveB}{reserveA}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;tB&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Your deposit ratio must equal the existing pool ratio. Same logic as the constant product intuition from the first post: the ratio of the pool is the price, and you're not allowed to move it just by adding liquidity.&lt;/p&gt;

&lt;p&gt;The result of &lt;code&gt;quote()&lt;/code&gt; is called &lt;code&gt;amountBOptimal&lt;/code&gt; (the amount of B that must accompany your desired amount of A). Now there's a check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;amountBOptimal ≤ amountBDesired&lt;/code&gt;, you have enough B and you're not being asked for more than you offered. The function returns &lt;code&gt;amountADesired&lt;/code&gt; and &lt;code&gt;amountBOptimal&lt;/code&gt; as the final deposit amounts.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;amountBOptimal &amp;gt; amountBDesired&lt;/code&gt;, your B is the limiting factor. The function flips it: call &lt;code&gt;quote()&lt;/code&gt; again treating &lt;code&gt;amountBDesired&lt;/code&gt; as the anchor, calculating the optimal A to go alongside it. The returned pair becomes &lt;code&gt;amountAOptimal&lt;/code&gt; and &lt;code&gt;amountBDesired&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where &lt;code&gt;amountAMin&lt;/code&gt; and &lt;code&gt;amountBMin&lt;/code&gt; come in. After working out the optimal amounts, the Router checks that neither falls below the user's stated floor. If the pool's ratio has moved since the user submitted the transaction (because someone else swapped in the meantime) and the optimal amounts have shifted below the minimums, the transaction reverts. Slippage protection, applied to liquidity provision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sending the tokens: &lt;code&gt;safeTransferFrom()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;_addLiquidity()&lt;/code&gt; returns the final &lt;code&gt;amountA&lt;/code&gt; and &lt;code&gt;amountB&lt;/code&gt;, the Router needs the pool's address. Since the Factory uses &lt;code&gt;CREATE2&lt;/code&gt; for deterministic deployment (covered in the previous post), the Router can compute the pair address using &lt;code&gt;pairFor()&lt;/code&gt; on &lt;code&gt;UniswapV2Library&lt;/code&gt; without an external call, just a hash.&lt;/p&gt;

&lt;p&gt;Then it transfers both tokens from the user directly to the pair contract using &lt;code&gt;safeTransferFrom()&lt;/code&gt; from &lt;code&gt;TransferHelper&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things are worth unpacking here. First, why &lt;code&gt;safeTransferFrom()&lt;/code&gt; and not a plain transfer? The architecture post touched on the non-returning token problem, but there's a deeper reason the Router is the one initiating this transfer at all rather than asking the user to send tokens themselves.&lt;/p&gt;

&lt;p&gt;In smart contract security, there's a strong preference for &lt;em&gt;pulling&lt;/em&gt; funds over &lt;em&gt;pushing&lt;/em&gt; them. Pushing means the user sends tokens directly to a contract before calling it. Pulling means the contract pulls the tokens in as part of its own execution, after being given approval to do so. The practical difference matters: if you push first and the transaction fails halfway through, your tokens are already sitting in the contract with nothing to claim them. With a pull pattern, the transfer and the logic happen atomically inside a single call. Either both succeed or neither does.&lt;/p&gt;

&lt;p&gt;There's also reentrancy to consider. Reentrancy is when an external contract call made during your function's execution triggers a callback back into your function before it has finished, letting an attacker exploit the in-between state. Pushing funds from outside the call opens a window where state and balances can be inconsistent in ways that reentrancy can exploit. Pulling inside a controlled execution, combined with a reentrancy lock (which the pair contract uses via its &lt;code&gt;lock&lt;/code&gt; modifier), keeps that window closed. The general rule in web3: avoid letting funds sit unaccounted in a contract whenever possible, and never push when you can pull.&lt;/p&gt;

&lt;p&gt;Second, why &lt;code&gt;safeTransferFrom()&lt;/code&gt; specifically rather than the standard ERC-20 &lt;code&gt;transferFrom()&lt;/code&gt;? As covered in the architecture post, certain tokens (USDT being the most notorious) don't return a boolean from &lt;code&gt;transfer()&lt;/code&gt; even though the ERC-20 spec says they should. A naive &lt;code&gt;require(token.transferFrom(...))&lt;/code&gt; would revert on these tokens even when the transfer succeeded. &lt;code&gt;TransferHelper.safeTransferFrom()&lt;/code&gt; handles both cases gracefully by using a low-level call and checking the result regardless of whether a return value was provided.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inside the Pair: &lt;code&gt;mint()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With the tokens sitting in the pair contract, the Router calls &lt;code&gt;mint(to)&lt;/code&gt; on &lt;code&gt;UniswapV2Pair&lt;/code&gt;. This is where LP tokens get minted and handed to the user. The function does several things in sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;_mintFee()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The very first thing &lt;code&gt;mint()&lt;/code&gt; does is call &lt;code&gt;_mintFee(_reserve0, _reserve1)&lt;/code&gt;. This is the protocol fee mechanism, and it's worth understanding separately before the rest of &lt;code&gt;mint()&lt;/code&gt; makes full sense.&lt;/p&gt;

&lt;p&gt;Uniswap V2 has a protocol fee switch. When enabled by governance, 1/6th of the 0.3% swap fee (so 0.05% of every trade) is redirectable to a &lt;code&gt;feeTo&lt;/code&gt; address set in the Factory. The other 5/6ths always go to LPs.&lt;/p&gt;

&lt;p&gt;The clever thing about how this is implemented: the protocol fee isn't collected on every swap, because that would add gas cost to every single trade. Instead, it's calculated lazily — only when someone adds or removes liquidity. &lt;code&gt;_mintFee()&lt;/code&gt; figures out how much the pool has grown from swap fees since the last liquidity event, and mints LP tokens to the &lt;code&gt;feeTo&lt;/code&gt; address representing that 1/6th cut.&lt;/p&gt;

&lt;p&gt;The mechanism uses &lt;code&gt;kLast&lt;/code&gt;, a value stored in the pair contract representing &lt;code&gt;reserve0 × reserve1&lt;/code&gt; at the time of the last liquidity event. Liquidity events here means mint or burn only. Swaps deliberately do not update &lt;code&gt;kLast&lt;/code&gt;. That distinction is the entire point: between two liquidity events, the only thing affecting reserves is swaps, and every swap deposits a small fee into the pool. So the gap between &lt;code&gt;kLast&lt;/code&gt; and the current &lt;code&gt;reserve0 × reserve1&lt;/code&gt; represents exactly the fees accumulated from trading activity since the last time anyone added or removed liquidity.&lt;/p&gt;

&lt;p&gt;But wait. Wasn't &lt;em&gt;k&lt;/em&gt; supposed to stay constant? That was the whole point of the constant product formula.&lt;/p&gt;

&lt;p&gt;Yes and no. The invariant check during a swap ensures that &lt;code&gt;k&lt;/code&gt; does not decrease as a result of the swap itself. But the 0.3% fee that stays in the pool after each swap means &lt;code&gt;k&lt;/code&gt; actually grows slightly with every trade. The invariant is a floor, not a ceiling. Each swap pushes the pool to a new, slightly higher value of &lt;code&gt;reserve0 × reserve1&lt;/code&gt;. &lt;code&gt;kLast&lt;/code&gt; captures what that product was at the last liquidity event, and &lt;code&gt;reserve0 × reserve1&lt;/code&gt; now is larger because of all the fees collected in between. The difference between them is the accumulated fee, expressed as growth in &lt;code&gt;k&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, why measure this as growth in &lt;code&gt;sqrt(k)&lt;/code&gt; rather than growth in &lt;code&gt;k&lt;/code&gt; directly? Because LP token supply is proportional to &lt;code&gt;sqrt(k)&lt;/code&gt; (recall from the fresh pool formula: &lt;code&gt;liquidity = sqrt(amount0 × amount1)&lt;/code&gt;). To talk about what share of the pool someone is entitled to, you need to work in the same units as LP tokens. &lt;code&gt;sqrt(k)&lt;/code&gt; is that unit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F99ktfncpmobafsicwzi0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F99ktfncpmobafsicwzi0.png" alt="Increase in k due to swaps, subsequently adding fee" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is what half an hour of wrestling an AI, given the current RAM prices, produces. I know it’s not only not perfect, its downright terrible. But the only thing that matters is this: fees show up as growth in √k.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let &lt;code&gt;rootK = sqrt(reserve0 × reserve1)&lt;/code&gt; now and &lt;code&gt;rootKLast = sqrt(kLast)&lt;/code&gt; from the last liquidity event. The growth &lt;code&gt;rootK - rootKLast&lt;/code&gt; is the liquidity added to the pool purely from fees. The protocol wants 1/6th of that.&lt;/p&gt;

&lt;p&gt;Here's why you can't just compute &lt;code&gt;(rootK - rootKLast) / 6&lt;/code&gt; and call it done. The protocol doesn't receive tokens directly. It receives LP tokens, which are a &lt;em&gt;claim&lt;/em&gt; on the pool. Minting new LP tokens dilutes the existing ones. So you need to figure out: how many new LP tokens, when added to the existing supply, would entitle their holder to exactly 1/6th of the fee growth?&lt;/p&gt;

&lt;p&gt;Let's call the LP tokens to mint &lt;code&gt;s&lt;/code&gt;. After minting, the new total supply is &lt;code&gt;totalSupply + s&lt;/code&gt;. The protocol's share of the pool would be &lt;code&gt;s / (totalSupply + s)&lt;/code&gt;. That share, multiplied by the current pool size &lt;code&gt;rootK&lt;/code&gt;, must equal 1/6th of the fee growth:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;stotalSupply+s×rootK=16×(rootK−rootKLast)
\frac{s}{totalSupply + s} \times rootK = \frac{1}{6} \times (rootK - rootKLast)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;lS&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ppl&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;6&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Solving for &lt;code&gt;s&lt;/code&gt;:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;s=totalSupply×(rootK−rootKLast)5×rootK+rootKLast
s = \frac{totalSupply \times (rootK - rootKLast)}{5 \times rootK + rootKLast}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;5&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;lS&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ppl&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;roo&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;This is exactly the formula in the contract. The &lt;code&gt;5 × rootK + rootKLast&lt;/code&gt; denominator isn't arbitrary: it falls out of the algebra when you solve for &lt;code&gt;s&lt;/code&gt; while ensuring the protocol gets 1/6th and not a wei more. Working through the algebra yourself is worth doing once.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;feeTo&lt;/code&gt; is the zero address (protocol fee is off, which is the default), &lt;code&gt;_mintFee()&lt;/code&gt; skips all of this and returns &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reserves vs balances
&lt;/h3&gt;

&lt;p&gt;Back in &lt;code&gt;mint()&lt;/code&gt;, the function reads two sets of values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_reserve0&lt;/code&gt; and &lt;code&gt;_reserve1&lt;/code&gt;: the last recorded balances, what the contract &lt;em&gt;remembered&lt;/em&gt; from its previous interaction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;balance0&lt;/code&gt; and &lt;code&gt;balance1&lt;/code&gt;: the actual current token balances sitting in the contract right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are different because the Router just sent tokens to the pair via a plain ERC-20 transfer. That updated the token contract's ledger but since the pair hasn't processed it yet, the balances just sit there idly in the contract unaccounted for. The difference is what the user just deposited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amount0 = balance0 - _reserve0
amount1 = balance1 - _reserve1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  LP token calculation
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;amount0&lt;/code&gt; and &lt;code&gt;amount1&lt;/code&gt; known, the contract works out how many LP tokens to mint. The approach splits based on whether this pool has ever had liquidity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh pool (&lt;code&gt;totalSupply == 0&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;liquidity=amount0×amount1−MINIMUMLIQUIDITY
liquidity = \sqrt{amount0 \times amount1} - MINIMUM_LIQUIDITY
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;l&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;q&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;M&lt;/span&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="mord mathnormal"&gt;M&lt;/span&gt;&lt;span class="mord mathnormal"&gt;U&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;M&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;L&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mord mathnormal"&gt;U&lt;/span&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="mord mathnormal"&gt;D&lt;/span&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Y&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;The geometric mean of the two deposits. This works better than a simple sum because it treats both tokens symmetrically: if you swap which token is token0 and which is token1, the geometric mean gives the same result. It also scales sensibly: a pool with 10x the deposits produces 10x the LP tokens.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MINIMUM_LIQUIDITY&lt;/code&gt; (1000 LP tokens, hardcoded) gets minted to the zero address instead of the user, permanently locked. This one-time burn ensures the pool can never be fully emptied — there will always be at least 1000 shares outstanding, meaning the pool always holds some non-zero amount of both tokens. Without this, a malicious first depositor could manipulate the ratio so severely after draining the pool that future LPs get griefed &lt;em&gt;(there are several attack vectors that come under the first deposit attack category. We’ll cover these separately someday)&lt;/em&gt;. 1000 shares is economically negligible at 10^-18 token precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing pool (&lt;code&gt;totalSupply &amp;gt; 0&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;liquidity=min⁡(amount0×totalSupplyreserve0, amount1×totalSupplyreserve1)
liquidity = \min\left(
\frac{amount0 \times totalSupply}{reserve0},\ 
\frac{amount1 \times totalSupply}{reserve1}
\right)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;l&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;q&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;min&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;lS&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ppl&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;reser&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;am&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;lS&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ppl&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Each token independently says: given how much I deposited relative to what's already here, I'm entitled to this share of the pool. The &lt;code&gt;min()&lt;/code&gt; of the two is taken. Deposit in exactly the right ratio and both values are equal, no penalty. Deposit in the wrong ratio — too much token0 relative to what the pool holds — and the token1 side gives fewer LP tokens. The excess token0 gets absorbed into the pool with no compensation. The &lt;code&gt;min()&lt;/code&gt; is what silently enforces ratio compliance. Deposit sloppily and you're effectively donating to everyone else in the pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;_mint()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;liquidity&lt;/code&gt; calculated, the pair calls &lt;code&gt;_mint(to, liquidity)&lt;/code&gt;, the standard ERC-20 mint inherited from &lt;code&gt;UniswapV2ERC20&lt;/code&gt;. &lt;code&gt;totalSupply&lt;/code&gt; increases by &lt;code&gt;liquidity&lt;/code&gt; and the &lt;code&gt;to&lt;/code&gt; address receives that many LP tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;_update()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;_update(balance0, balance1, _reserve0, _reserve1)&lt;/code&gt; syncs the stored reserves to the current actual balances. New &lt;code&gt;reserve0&lt;/code&gt; becomes &lt;code&gt;balance0&lt;/code&gt;, new &lt;code&gt;reserve1&lt;/code&gt; becomes &lt;code&gt;balance1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;_update()&lt;/code&gt; also does something less obvious, connecting back to the TWAP oracle mention in the architecture post. If any time has elapsed since the last block that interacted with this pool, it updates the cumulative price accumulators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;price0CumulativeLast += (reserve1 / reserve0) × timeElapsed
price1CumulativeLast += (reserve0 / reserve1) × timeElapsed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The price at the &lt;em&gt;old&lt;/em&gt; reserves, multiplied by how long those reserves were in effect, gets added to a running total. An external oracle contract can snapshot these accumulators at two points in time and divide by elapsed time to get a time-weighted average price (a TWAP) that's much harder to manipulate than a spot price reading. This accumulation happens silently inside every &lt;code&gt;_update()&lt;/code&gt; call, whether anyone is using the oracle or not. TWAP will be covered in a separate post entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kLast&lt;/code&gt; and the &lt;code&gt;Mint&lt;/code&gt; event
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;_update()&lt;/code&gt;, if &lt;code&gt;_mintFee()&lt;/code&gt; returned &lt;code&gt;true&lt;/code&gt; (protocol fee is on), &lt;code&gt;kLast&lt;/code&gt; gets updated to &lt;code&gt;reserve0 × reserve1&lt;/code&gt; using the freshly synced values. This records the new baseline for the next fee calculation.&lt;/p&gt;

&lt;p&gt;Finally, the &lt;code&gt;Mint&lt;/code&gt; event is emitted and the function returns.&lt;/p&gt;




&lt;p&gt;To summarise what actually happened across the full flow: the user called the Router, which calculated the right amounts to maintain the pool's existing price ratio, pulled the tokens into the pair, and then the pair figured out the fair number of LP tokens to mint. It accounted for any protocol fee owed, computed the geometric mean for a new pool or the proportional min for an existing one, minted the tokens, and quietly updated the price accumulators in the background.&lt;/p&gt;

&lt;p&gt;Next up: Swap, where the constant product formula stops being theory and the fee math actually runs live.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>ethereum</category>
      <category>uniswap</category>
    </item>
    <item>
      <title>Before the Swaps: A Map of Uniswap V2</title>
      <dc:creator>Adi</dc:creator>
      <pubDate>Sun, 05 Apr 2026 10:00:13 +0000</pubDate>
      <link>https://forem.com/ranting_sage/before-the-swaps-a-map-of-uniswap-v2-2ihf</link>
      <guid>https://forem.com/ranting_sage/before-the-swaps-a-map-of-uniswap-v2-2ihf</guid>
      <description>&lt;p&gt;Recently I did a &lt;a href="https://dev.to/ranting_sage/x-y-k-and-other-things-i-shouldve-learned-sooner-ofe"&gt;blog post&lt;/a&gt; where we did a quick breakdown of what AMMs are, Uniswap specifically, and built a basic intuition of how things work under the hood. It was initially just supposed to be a quick intro, but it eventually turned out more technical. We covered the &lt;strong&gt;Constant Product&lt;/strong&gt; formula, how it came to be, and how it's been the cornerstone of almost the entire DeFi space &lt;em&gt;(the actual decentralized space, not the protocols that keep showing up with a multisig and claiming to be DeFi, until somehow they get compromised and begin crying that the protocol itself is decentralized and we aren't at fault — it was the keys. Just shut up, if something can put that big of a dent in your core logic then stop calling yourself a DeFi protocol. Yeah, it's DeFi until it isn't. It's unbelievable how there have been so many such exploits in the past few years, but that doesn't mean it prevents new protocols, following similar architecture, from not coming up, not getting funded and somehow not growing to a quintillion dollars in TVL. Sorry I got sidetracked, but it is what it is)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So this is the second post in the series. But before we get into the actual flows — how swaps work, how liquidity gets added, all of that — it's worth taking five minutes to understand how Uniswap V2 is laid out. Not the contracts in detail, we'll get to those one by one in the posts that follow. Just the structure. Think of this as the blueprint of a building you've never been in. You don't need to know where every room is, just where the front door is and which staircase goes where.&lt;/p&gt;

&lt;p&gt;If some of the terms or concepts here feel unfamiliar, don't worry about it. By the time we're done with this series, everything here will have clicked into place naturally. This post is just context — a map you can come back to if you ever want to orient yourself while following along.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two repos, one protocol
&lt;/h2&gt;

&lt;p&gt;The first thing that trips people up when they open the Uniswap V2 GitHub is that there are two repositories: &lt;code&gt;v2-core&lt;/code&gt; and &lt;code&gt;v2-periphery&lt;/code&gt;. It's tempting to assume this is just an organisational choice (at least thats what I initially thought it was) i.e. someone decided to split the code into two folders and gave them dramatic names. It's actually a deliberate architectural decision, and understanding &lt;em&gt;why&lt;/em&gt; it's split that way makes the whole thing easier to reason about.&lt;/p&gt;

&lt;p&gt;Core holds the contracts that actually hold your money. That's why its the core. Nothing fancy, nothing extraneous. Because these contracts custody real funds, they are intentionally minimal and completely immutable after deployment. There's no admin key, no upgrade mechanism, no way to patch them after the fact. If a bug were found in Core, the only option would be to deploy a new version entirely. Sounds scary at first, until you flip it around: it also means nobody can change these contracts to do something unexpected with your funds. Ever.&lt;/p&gt;

&lt;p&gt;Periphery, well, as the name suggests, lies on the periphery of the core logic. It holds everything that makes Core usable. The routing logic, the slippage checks, the optimal amount calculations, the multi-hop path resolution, all of that lives here. And we will get to it. And crucially, Periphery can be replaced. If a better router gets deployed tomorrow, users can switch to it without the underlying pools changing at all. The pools don't care who calls them.&lt;/p&gt;

&lt;p&gt;The split is a trust boundary. Core is what you have to trust. Periphery is just convenience.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's actually in each repo
&lt;/h2&gt;

&lt;p&gt;Core has three contracts that matter: &lt;code&gt;UniswapV2Factory&lt;/code&gt;, &lt;code&gt;UniswapV2Pair&lt;/code&gt;, and &lt;code&gt;UniswapV2ERC20&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Factory&lt;/strong&gt; is the registry. It deploys new pool contracts and keeps a record of every pool that exists - a mapping of token pair to pool address. When you want to find or create a pool for two tokens, the factory is where you go. One interesting detail: it uses a deterministic deployment method (&lt;code&gt;CREATE2&lt;/code&gt;) which means any pool's address can be computed off-chain just from the factory address and the two token addresses, without ever querying the chain. Internally, &lt;code&gt;CREATE2&lt;/code&gt; is just a hash of multiple parameters, one of them being the &lt;code&gt;salt&lt;/code&gt;. That means the same parameters generate the same result (the contract address, in this case) every time. If you want to deploy again, just change the salt and you'll get a new address. &lt;em&gt;I'll try to write a separate post on deterministic deployment: the types, the when and why, and when to avoid it altogether.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Pair&lt;/strong&gt; is the pool itself. Each deployed pair contract holds exactly two tokens and handles everything: minting LP tokens when liquidity is added, burning them when it's removed, executing swaps, flash loans, all of it. It also stores the price accumulators that make Uniswap's built-in TWAP oracle possible. One thing worth noting: &lt;code&gt;UniswapV2Pair&lt;/code&gt; is itself an ERC-20. The LP tokens &lt;em&gt;are&lt;/em&gt; the pair contract's own token. Holding LP tokens for a pool means literally holding shares of that contract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UniswapV2ERC20&lt;/code&gt; is just the base ERC-20 that Pair inherits from, standard token functionality plus EIP-2612 permit support, which lets you approve and act in a single transaction rather than two.&lt;/p&gt;

&lt;p&gt;Core also has a couple of utility libraries — &lt;code&gt;Math.sol&lt;/code&gt; for the integer square root used in initial LP token calculations, and &lt;code&gt;UQ112x112.sol&lt;/code&gt; for the fixed-point arithmetic used in price accumulation. Nothing you need to think about much right now.&lt;/p&gt;

&lt;p&gt;Periphery is anchored by &lt;code&gt;UniswapV2Router02&lt;/code&gt; — the contract users actually interact with. Every function you call when using a DEX frontend, or any other integrator, sits here. It handles all four operations, wraps and unwraps ETH for ETH-involving swaps, enforces deadlines, and calculates the optimal amounts before anything touches Core. There's also a &lt;code&gt;Router01&lt;/code&gt; in the repo, but it's effectively deprecated. Router02 replaced it with better support for a broader range of token types.&lt;/p&gt;

&lt;p&gt;Supporting the Router is &lt;code&gt;UniswapV2Library&lt;/code&gt;, a stateless utility library that handles all the computational work: looking up pair addresses, fetching reserves, calculating swap outputs, walking multi-hop paths. None of this needs to touch state, so it's compiled inline rather than deployed as a standalone contract. And there's &lt;code&gt;UniswapV2OracleLibrary&lt;/code&gt;, which provides helpers for building TWAP oracles on top of the pair's price accumulators — if you've ever wondered how protocols get manipulation-resistant on-chain price feeds, this is part of that story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The helpers that don't live in either repo
&lt;/h2&gt;

&lt;p&gt;Two external utility packages get pulled into V2 that are worth knowing about, mostly because you'll see them imported in the contracts as you follow along.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;TransferHelper&lt;/code&gt; from &lt;code&gt;@uniswap/solidity-lib&lt;/code&gt;. The problem it solves is a real one: ERC-20's &lt;code&gt;transfer()&lt;/code&gt; is &lt;em&gt;supposed&lt;/em&gt; to return a boolean indicating success, but a surprising number of tokens, USDT being the most notorious, don't return anything at all. A naive &lt;code&gt;require(token.transfer(...))&lt;/code&gt; would just revert on these tokens even when the transfer worked fine. &lt;code&gt;TransferHelper&lt;/code&gt; wraps transfers in a low-level call that handles both cases gracefully. It's a small thing, but without it the Router would silently fail on a large class of tokens that people actually use.&lt;/p&gt;

&lt;p&gt;The second is &lt;code&gt;FixedPoint&lt;/code&gt; from &lt;code&gt;@uniswap/lib&lt;/code&gt;, which provides fixed-point number types for the oracle library. Solidity has no native floating-point, so cumulative price values need a way to accumulate fractional ratios over time without losing precision. That's what this handles.&lt;/p&gt;




&lt;h2&gt;
  
  
  The call flow in one line
&lt;/h2&gt;

&lt;p&gt;For every operation, the path is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User → Router → Pair → User&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Factory sits to the side, consulted by the Router to find or deploy pair addresses when needed, but not in the critical path of every transaction. The libraries are silent infrastructure, compiled into the Router and never called directly.&lt;/p&gt;

&lt;p&gt;That's the map. Keep it somewhere in the back of your head as we get into the flows. Whenever you see the Router calling something on the Pair, or the Pair checking something via the Library, you'll know exactly where you are.&lt;/p&gt;




&lt;p&gt;Here are all the contracts and repos referenced in this post, if you want to poke around yourself, although you definitely don't need to do that just yet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Router&lt;/strong&gt;: &lt;a href="https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol" rel="noopener noreferrer"&gt;https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair&lt;/strong&gt;: &lt;a href="https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol" rel="noopener noreferrer"&gt;https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Factory&lt;/strong&gt;: &lt;a href="https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol" rel="noopener noreferrer"&gt;https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TransferHelper&lt;/strong&gt;: &lt;a href="https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol" rel="noopener noreferrer"&gt;https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FixedPoint&lt;/strong&gt;: &lt;a href="https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/FixedPoint.sol" rel="noopener noreferrer"&gt;https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/FixedPoint.sol&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: adding liquidity to a pool. This is where the constant product formula stops being theory and starts getting its hands dirty.&lt;br&gt;
In case you missed the intro: &lt;a href="https://dev.to/ranting_sage/x-y-k-and-other-things-i-shouldve-learned-sooner-ofe"&gt;x * y = k, and Other Things I Should've Learned Sooner &lt;/a&gt; &lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>ethereum</category>
      <category>uniswap</category>
    </item>
    <item>
      <title>x * y = k, and Other Things I Should've Learned Sooner</title>
      <dc:creator>Adi</dc:creator>
      <pubDate>Sat, 04 Apr 2026 23:01:33 +0000</pubDate>
      <link>https://forem.com/ranting_sage/x-y-k-and-other-things-i-shouldve-learned-sooner-ofe</link>
      <guid>https://forem.com/ranting_sage/x-y-k-and-other-things-i-shouldve-learned-sooner-ofe</guid>
      <description>&lt;p&gt;Getting into blockchain as a smart contract dev is a different experience than entering almost any other sector. Every field has its niche things, sure, but you mostly learn by understanding well-known patterns and implementing them. In smart contract development, though, you actually have to read other people's contracts. Not just snippets, the whole thing. That's how you grow, and the best way to do it is to go through some of the most battle-tested protocols out there.&lt;/p&gt;

&lt;p&gt;And Uniswap is the obvious place to start. It's been one of the cornerstones of DeFi for as long as I can remember, long before I even got into the space. So every once in a while I'd think, alright, today's the day. Then I'd open the repos. Two of them, and a couple of utility repos too. Multiple contracts cross-referencing each other. An actual whitepaper with actual math in it &lt;em&gt;(and with my just-above-average mathematics, I wasn't exactly jumping for joy. I still am not)&lt;/em&gt;. The tab would go into hibernation, and I'd tell myself I'd pick it up properly once I got through whatever I was currently working on.&lt;/p&gt;

&lt;p&gt;Procrastination, basically.&lt;/p&gt;

&lt;p&gt;If you're in the same boat, whether for the same reasons or just because you &lt;em&gt;actually&lt;/em&gt; have other things that keep getting in the way, this series is what I wish I had. I went through the entire thing, cross-referenced everything against the actual contracts, and wrote it down in plain English with math I could understand myself. The goal was to cover it completely, without cutting corners, but in a way where you could open the contracts side by side, follow along, and realize by the end that you'd essentially read the whole thing without it ever feeling like you were.&lt;/p&gt;

&lt;p&gt;Turns out Uniswap is one of those things where the more you understand it, the simpler it gets. Not the other way around. The surface looks intimidating. Underneath it is one equation. Once that clicks, the rest assembles itself pretty quickly.&lt;/p&gt;

&lt;p&gt;So. Let's get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that I kept pushing to "later"
&lt;/h2&gt;

&lt;p&gt;Uniswap launched in November 2018. Quietly. No token, no VC fanfare, just a blog post and a GitHub repo. &lt;em&gt;It really must have been different times altogether, feels medieval almost.&lt;/em&gt; By the time DeFi Summer hit in 2020 and the entire space lost its mind over yield farming, Uniswap was already the backbone of a significant chunk of on-chain trading volume. While Aave and Compound were building decentralised lending, and Curve was figuring out how to swap stablecoins without destroying the peg, Uniswap was the place where the long tail of tokens actually got traded. New project launching and wants liquidity on day one without negotiating with a centralised exchange? Uniswap. Protocol needed a price feed that couldn't easily be manipulated? Uniswap. It became infrastructure almost by accident.&lt;/p&gt;

&lt;p&gt;The protocol has gone through four versions now, and the version history actually tells you something useful about what problems were being solved at each step.&lt;/p&gt;

&lt;p&gt;V1 was the proof of concept - launched in November 2018, it worked, but every token could only trade against ETH. Want to swap USDC for DAI? That's USDC → ETH → DAI. Two hops, two fees, and you're briefly exposed to ETH's price in the middle of a transaction you intended to be completely stable. It proved the core idea was sound. It didn't quite prove the model was ready for everything people would want to throw at it.&lt;/p&gt;

&lt;p&gt;V2 fixed that. Direct ERC-20 to ERC-20 pairs, better resistance to price manipulation, flash swaps, and a cleaner architecture overall. It shipped in May 2020 and became what most of the DeFi ecosystem actually built on top of. The forks alone tell the story: Sushiswap, Pancakeswap, Quickswap, and dozens of others across every EVM chain that launched afterward were essentially V2 with a different coat of paint. If you've used a DEX on any EVM chain other than Ethereum mainnet, there's a good chance it was running V2 logic underneath.&lt;/p&gt;

&lt;p&gt;V3 arrived in May 2021 and introduced concentrated liquidity — LPs can now choose specific price ranges to deploy their capital into rather than spreading it uniformly across the entire curve. More capital-efficient, but meaningfully more complex to reason about. &lt;em&gt;We’ll cover this too. Sometime. Soon. Hopefully.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;V4 launched in January 2025 and is a different kind of upgrade altogether — it turns Uniswap into a developer platform. The headline feature is hooks: modular contracts that let developers inject custom logic into pool creation, swaps, and liquidity management. On top of that, a singleton architecture consolidates all pools into one contract, making pool creation up to 99.99% cheaper than before. It's powerful, and it's going to be interesting to watch what gets built on top of it. &lt;em&gt;Yeah, considering me, covering this one “soon” might be a stretch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;V2 is still the right foundation to understand first. V3 and V4 both build on concepts that V2 establishes i.e. concentrated liquidity, which makes no sense without first understanding uniform liquidity, and hooks customize a swap flow you need to already understand. You can reason through V1 in about five minutes once you have V2. Everything else follows.&lt;/p&gt;

&lt;p&gt;So V2 is where this series lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does it actually work?
&lt;/h2&gt;

&lt;p&gt;On a centralised exchange, there's an order book, which is a live list of people willing to buy at various prices and people willing to sell at various prices, with a matching engine pairing them up in real time. The price at any moment is just wherever the last match landed. It works because there are enough participants that someone is almost always willing to take the other side of your trade.&lt;/p&gt;

&lt;p&gt;Uniswap has none of that. No order book, no matching engine, no counterparty. Just a smart contract sitting on-chain with two tokens locked inside it, ready to trade with you any time you want. You want USDC, send it ETH. It'll give you USDC back. The question that actually matters is: how does it decide how much?&lt;/p&gt;

&lt;p&gt;Think about it from the pool's perspective for a second. It holds some amount of ETH and some amount of USDC, and it wants to behave like a reasonable market. It has no external price feed. It has no idea what Binance is doing right now. All it has is the ratio of what it holds. And that ratio is actually enough, because the ratio &lt;em&gt;is&lt;/em&gt; the price. If the pool holds 100 ETH and 200,000 USDC, the implied price of ETH is 200,000 / 100 = 2,000 USDC. Simple division.&lt;/p&gt;

&lt;p&gt;The problem is keeping that relationship stable in a sensible way as trades happen. If you just let people take whatever they want, the pool drains. So the pool needs a rule. And the rule it uses is this: whatever you do to this pool, the product of the two token balances cannot decrease.&lt;/p&gt;

&lt;p&gt;If the pool holds &lt;em&gt;x&lt;/em&gt; of one token and &lt;em&gt;y&lt;/em&gt; of the other, then at all times:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;x · y = k&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the rule. &lt;em&gt;k&lt;/em&gt; stays constant. You want to take some ETH out? Fine, but you have to put in enough USDC that the product of the new balances still equals &lt;em&gt;k&lt;/em&gt;. The pool doesn't care what the "right" price is — it just enforces that one equation, and the price falls out automatically.&lt;/p&gt;

&lt;p&gt;Work through a concrete example. Pool holds 100 ETH and 200,000 USDC. &lt;em&gt;k&lt;/em&gt; = 20,000,000. You want to buy 10 ETH. After giving you 10 ETH, the pool has 90 ETH. To keep the product at 20,000,000, it now needs 20,000,000 / 90 ≈ 222,222 USDC. It had 200,000, so you owe it 22,222 USDC for those 10 ETH.&lt;/p&gt;

&lt;p&gt;At the pool's implied price before the trade, 10 ETH should have cost 20,000 USDC. You paid 22,222. The extra 2,222 is what buying 10% of the pool's ETH supply in a single trade does to the math, and it points to something important that we'll come back to in a moment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10ild33n4hz8gvs6b3wl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10ild33n4hz8gvs6b3wl.jpeg" alt="The hyperbolic curve for x*y=k" width="761" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s an image I found that visualises this nicely. The quantities of token A and B act as x and y, and every valid state of the pool lies somewhere on this curve defined by k. Trades don’t change the curve — they just move the pool along it.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;Constant Product Formula&lt;/strong&gt;. The name, at this point, is fairly self-explanatory. The product stays constant. &lt;em&gt;k&lt;/em&gt; doesn't move. And the system built on top of this - a smart contract that always quotes a price, always accepts a trade, at any size, at any time, with no humans or order books involved, is an &lt;strong&gt;Automated Market Maker&lt;/strong&gt;. It makes markets. Automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Whose tokens are in there, and why
&lt;/h2&gt;

&lt;p&gt;Uniswap doesn't own anything. Every token in every pool was put there by someone who wanted to earn a return on their capital, and those people are called &lt;strong&gt;liquidity providers&lt;/strong&gt;, or LPs.&lt;/p&gt;

&lt;p&gt;The word liquidity here means roughly the same thing it means in traditional finance, i.e. the availability of an asset for exchange without significantly moving its price. A pool with $10 million worth of tokens in it has a lot of liquidity. A pool with $50,000 in it doesn't. When you deposit into a pool, you're increasing its liquidity, which is where the term comes from.&lt;/p&gt;

&lt;p&gt;To deposit, you have to provide &lt;em&gt;both&lt;/em&gt; tokens, in proportion to the current ratio of the pool. You can't just throw in ETH and call it done. You need the matching USDC too. This is to preserve the existing ratio, because depositing in a different ratio would shift the pool's implied price, and that's not something the pool will just let happen without the math demanding compensation for it. In return for depositing, the pool mints &lt;strong&gt;LP tokens&lt;/strong&gt; and hands them to you. These represent your ownership stake. Deposit 5% of the total pool value, you get tokens representing 5% of the supply. They're regular ERC-20 tokens which are fully transferable, and your ticket to getting your deposit back whenever you want.&lt;/p&gt;

&lt;p&gt;Every swap that goes through the pool charges a 0.3% fee. That fee doesn't get sent anywhere; it just stays inside the pool, incrementally growing the balances above what &lt;em&gt;k&lt;/em&gt; alone would predict. Since LP tokens represent a share of everything in the pool, and the pool is quietly getting larger with each swap, those LP tokens are gradually becoming worth more. When you eventually do want out, a process the contracts call &lt;em&gt;burning&lt;/em&gt;, you hand back your LP tokens and receive your proportional share of the current pool: original deposit, plus your cut of every fee collected since you put the money in.&lt;/p&gt;

&lt;p&gt;The total value of all tokens across all pools in the protocol is what people refer to as &lt;strong&gt;TVL&lt;/strong&gt; — Total Value Locked. It's the number that gets cited when people talk about how large a DeFi protocol is, and it matters practically because it determines how smoothly the pool handles large trades without moving the price too much.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happens to the price when you trade
&lt;/h2&gt;

&lt;p&gt;Back to that example. The pool had 100 ETH and 200,000 USDC, and you bought 10 ETH for 22,222 USDC instead of the "expected" 20,000. That gap exists because of something called &lt;strong&gt;price impact&lt;/strong&gt;, and understanding it is probably the most useful intuition to take away from this entire post.&lt;/p&gt;

&lt;p&gt;The spot price at any moment is just &lt;em&gt;y&lt;/em&gt; / &lt;em&gt;x&lt;/em&gt; — the ratio of the two balances. Before your trade: 200,000 / 100 = 2,000 USDC per ETH. After your trade: 222,222 / 90 ≈ 2,469 USDC per ETH. Your single trade moved the price by about 23%.&lt;/p&gt;

&lt;p&gt;That's an extreme example. Buying 10% of the pool's ETH supply in one go will always hurt you, but the shape of the relationship is what matters. Look at the formula: spot price is &lt;em&gt;y&lt;/em&gt; / &lt;em&gt;x&lt;/em&gt;. When you buy ETH, &lt;em&gt;x&lt;/em&gt; (ETH in the pool) shrinks and &lt;em&gt;y&lt;/em&gt; (USDC in the pool) grows. Smaller denominator, bigger numerator, price goes up. When you sell ETH, the opposite happens. The pool is always drifting toward whatever ratio reflects the last trade, which is what keeps it loosely aligned with external market prices. If the pool's implied price drifts too far from, say, Binance's price, arbitrageurs come in and trade it back into alignment, profiting from the gap and correcting the pool's ratio in the process.&lt;/p&gt;

&lt;p&gt;The gap between the price you saw when you submitted your transaction and the price you actually executed at is called &lt;strong&gt;slippage&lt;/strong&gt;. It comes from two sources: your own trade moving the pool (price impact), and other transactions executing before yours and moving it first. Larger pools suffer less from both. A 10 ETH trade against a pool with 10,000 ETH barely registers. Against a pool with 100 ETH, it's significant. This is why liquidity depth matters, and why a newly launched token with a thin pool on Uniswap can swing 20% on a single moderate trade.&lt;/p&gt;

&lt;p&gt;There's one more property the formula has that's worth appreciating: the pool can never be fully drained. To get all the ETH out you'd need to send in an infinite amount of USDC, since as &lt;em&gt;x&lt;/em&gt; approaches zero, the price climbs toward infinity. The curve is a hyperbola, and the pool always lives somewhere on it. Every trade just moves you to a different point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uniswap didn't invent AMMs but it defined the template
&lt;/h2&gt;

&lt;p&gt;Once V2 proved the constant product model worked at scale, a reasonable question was: what if the formula were different?&lt;/p&gt;

&lt;p&gt;Curve Finance, which had launched around the same time, looked at the stablecoin problem and noticed something. If you're swapping USDC for USDT, two assets that are both supposed to be worth $1, the constant product formula still moves the price noticeably on large trades, even though economically, the price essentially &lt;em&gt;shouldn't&lt;/em&gt; move at all. Curve replaced the formula with a hybrid that behaves like constant product near the edges of the curve but much more like a flat line near the peg, where stablecoin trades actually happen. The result is dramatically tighter pricing for like-valued assets. Curve became the dominant venue for stablecoin and liquid staking swaps for exactly this reason, and still is.&lt;/p&gt;

&lt;p&gt;Balancer took a different angle entirely. Instead of two-token pools with equal weighting, it generalized the model to support up to eight tokens with arbitrary weight ratios. A pool could hold 80% ETH and 20% USDC, and the AMM mechanics would keep it at that ratio automatically as people traded against it. Effectively a self-rebalancing index fund that earns swap fees. &lt;em&gt;Don’t worry if some of these terms feel a bit unknown, by the end of the series, you’ll be flowing through these like a proper crypto bro.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both of these, and the dozens of AMM protocols that came after, are built on the same underlying insight Uniswap established: encode your pricing rule as a mathematical invariant, let the market trade against it, and price discovery happens by itself. The specific formula is a design choice. What Uniswap figured out first was the architecture. Four versions and nearly $3 trillion in cumulative trading volume later, that original insight is still at the centre of all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four things you can actually do
&lt;/h2&gt;

&lt;p&gt;Everything on Uniswap V2 is one of four operations, and this series is going to go deep on each one in the posts that follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swapping&lt;/strong&gt; is the one everyone has used — send in one token, get back another, rate determined by the constant product formula, 0.3% fee taken from your input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding liquidity&lt;/strong&gt;, which the contracts call &lt;em&gt;minting&lt;/em&gt; — deposit both tokens in the right ratio, receive LP tokens representing your ownership stake, start earning fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removing liquidity&lt;/strong&gt;, which the contracts call &lt;em&gt;burning&lt;/em&gt; — hand back your LP tokens, get your proportional share of the current pool returned to you, fees and all.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;flash loans&lt;/strong&gt; — borrow any amount of tokens from the pool with zero collateral, do whatever you want with them within the same transaction, return them before the transaction closes. No approval process. No credit check. Nothing. If the money isn't back by the end of the transaction, the whole thing reverts as if it never happened. The EVM's atomicity is the security model, and the whole thing was designed this way deliberately. It's used for things like arbitrage across protocols, liquidating underwater positions in lending protocols, and swapping collateral types without needing upfront capital. It's one of those things that sounds like it shouldn't work until you think about it for a minute and realize it couldn't possibly not work.&lt;/p&gt;

&lt;p&gt;Each of those four flows has a lot going on underneath — the actual contracts, the exact math at each step, and a handful of design decisions that look strange at first and make complete sense once you trace through them. That's what the upcoming posts are for.&lt;/p&gt;

&lt;p&gt;If the constant product formula is starting to feel intuitive rather than just symbolically familiar, you're in exactly the right place to continue.&lt;/p&gt;

&lt;p&gt;Next up: &lt;a href="https://dev.to/ranting_sage/before-the-swaps-a-map-of-uniswap-v2-2ihf"&gt;Before the Swaps: A Map of Uniswap V2&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>ethereum</category>
      <category>uniswap</category>
    </item>
  </channel>
</rss>
