Section 5 of 16

Build
+15 Lynx

Pair: Adding Liquidity (mint)

What You Are Building

The mint() function is how liquidity enters the pool. A user sends both tokens to the pair contract, then calls mint(). The function calculates how many LP tokens to issue based on the amounts deposited relative to the existing reserves. These LP tokens represent the user's share of the pool.

This is the first function that actually moves value. Everything you built in the previous sections (reserves, the lock modifier, safe transfers, the oracle) comes together here. Every design choice in mint() exists because of a specific attack vector or accounting requirement. Understanding the why behind each line is what separates someone who can fork Uniswap from someone who can build a DEX from scratch.

The Transfer First Pattern

Uniswap V2 uses an unusual interaction model. Tokens are transferred to the pair contract before mint() is called. The function then reads the contract's actual token balances and compares them to the stored reserves to determine what was deposited:

balance0 = IERC20(token0).balanceOf(address(this));
amount0 = balance0 - _reserve0;

Why not pass amounts as parameters and use transferFrom inside mint()? Two reasons. First, the pair contract never needs to hold approvals. Users approve the Router, which handles transfers and then calls the pair. This separation means the pair contract has a smaller attack surface. Second, this pattern unifies the accounting model across mint(), burn(), and swap(). Every function simply asks: "what changed since the last time reserves were updated?" This makes flash operations possible and keeps the pair logic stateless with respect to callers.

The tradeoff: if someone transfers tokens to the pair without calling mint(), those tokens sit in the contract and get absorbed by the next caller. They become a donation. The Router protects normal users from this by bundling transfers and calls into a single transaction.

First Deposit: The Geometric Mean

When totalSupply == 0, there are no existing LP tokens and no reserves to compare against. The first depositor sets the initial price by choosing how many of each token to provide. The formula for initial liquidity is:

liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY

The geometric mean sqrt(amount0 * amount1) is chosen specifically because it makes LP token value independent of the price ratio between the two tokens. Consider the alternative: if you used amount0 + amount1, a depositor could game the system by depositing 1 wei of token0 and 1 billion of token1, inflating the LP supply artificially. With the geometric mean, the LP tokens scale with the product of the amounts. Doubling one token and halving the other gives the same result. This means the LP token supply is denominated in units that reflect the geometric average of both token quantities, which maps cleanly to the constant product invariant k = x * y.

The Uniswap V2 whitepaper proves that this choice ensures the value of a liquidity pool share can never decrease (in the absence of impermanent loss), regardless of what ratio the first depositor chose. Any other formula would allow manipulation of LP token value by choosing a skewed initial deposit.

MINIMUM_LIQUIDITY: Preventing the Share Inflation Attack

On the first deposit, 1000 wei of LP tokens are minted to address(0), permanently locking them. This looks wasteful, but it prevents a critical attack.

The attack works like this. An attacker creates a pool by depositing 1 wei of each token. They receive sqrt(1) = 1 LP token, and they are the sole holder. Now totalSupply = 1. They then donate a massive amount of tokens directly to the pair (without calling mint()), say 1e18 of each token. Now the pair holds 1e18 tokens, but totalSupply is still 1. When the next user tries to deposit, say, 2e18 of each token, their LP calculation is min(2e18 * 1 / 1e18, 2e18 * 1 / 1e18) = 2. But the integer division 2e18 * 1 / 1e18 could round to 1 or even 0 depending on exact amounts. The attacker now holds 1 out of 2 or 3 total LP tokens, but the pool contains the attacker's donated tokens plus the victim's deposit. The victim's share is diluted.

By burning 1000 LP tokens to address(0), the minimum totalSupply is always at least 1000. To execute the share inflation attack profitably, the attacker would need to donate millions of dollars worth of tokens to make rounding errors meaningful against a totalSupply of 1000. This makes the attack economically impractical.

Subsequent Deposits: The min() Rule

When totalSupply > 0, the pool already has a price ratio established by the reserves. The formula is:

liquidity = min(amount0 * totalSupply / _reserve0, amount1 * totalSupply / _reserve1)

Each ratio amountX * totalSupply / reserveX computes what share of the pool the deposit represents for that token. Taking the min() means the depositor gets LP tokens based on whichever token they provided less of relative to the current ratio.

Why min() instead of max() or the average? Because it enforces that depositors match the current ratio or lose value. If you deposit 100 USDC and 200 ETH into a 1:1 pool, you get LP tokens proportional to the 100 USDC side. The extra 100 ETH stays in the pool and benefits existing LPs. This is not a flaw. It is a mechanism that incentivizes depositors to always deposit at the correct ratio, which is exactly what the Router's addLiquidity function calculates for them.

If max() were used instead, depositors could inflate their LP share by dumping extra tokens on one side. That would dilute existing LPs unfairly.

The _mintFee() Call and Its Ordering

Before calculating liquidity, mint() calls _mintFee(). This function checks whether protocol fees are enabled (by reading feeTo from the factory) and, if so, mints LP tokens to the fee recipient based on the growth of sqrt(k) since the last fee collection.

The ordering here is critical. _mintFee() may increase totalSupply by minting protocol fee LP tokens. If you read totalSupply before calling _mintFee(), your liquidity calculation uses a stale denominator, and the new depositor gets slightly more LP tokens than they should, effectively stealing value from the protocol fee recipient.

The correct order: call _mintFee() first, then read totalSupply. The feeOn boolean returned by _mintFee() is stored so that at the end of the function, you know whether to update kLast.

The Math Library

The starter code includes a Math library with min() and sqrt(). The sqrt() implementation is the Babylonian method (Newton's method for square roots), which is the same algorithm Uniswap V2 uses in production. You do not need to write these. They are provided.

Your Task

Write the complete mint() function body. Here is the logic flow, line by line:

  1. Read the current reserves from getReserves(). Store them as local variables _reserve0 and _reserve1.
  2. Read the actual token balances of the contract via IERC20(tokenX).balanceOf(address(this)).
  3. Calculate amount0 and amount1 by subtracting the stored reserves from the actual balances. This is what was deposited since the last update.
  4. Call _mintFee(_reserve0, _reserve1). Store the returned boolean as feeOn.
  5. Read totalSupply into a local variable. This MUST happen after _mintFee because the fee mint may have changed totalSupply.
  6. If _totalSupply == 0: calculate liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY, then mint MINIMUM_LIQUIDITY to address(0).
  7. If _totalSupply > 0: calculate liquidity = min(amount0 * _totalSupply / _reserve0, amount1 * _totalSupply / _reserve1).
  8. Require liquidity > 0. A zero liquidity mint means the deposit was too small to represent any pool share.
  9. Mint liquidity LP tokens to the to address.
  10. Call _update(balance0, balance1, _reserve0, _reserve1) to sync reserves and update the oracle.
  11. If feeOn is true, set kLast = uint(reserve0) * reserve1. This snapshot lets _mintFee() compute fee growth on the next call.
  12. Emit the Mint event with amount0 and amount1.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

mint reads reserves via getReserves
mint reads token balances
mint calculates amount deposited
mint calls _mintFee before calculation
mint uses sqrt for first deposit
mint subtracts MINIMUM_LIQUIDITY on first deposit
mint permanently locks MINIMUM_LIQUIDITY
mint uses Math.min for subsequent deposits
mint requires liquidity > 0
mint calls _update
mint emits Mint event