Back to Blog
How to Build Uniswap V2 From Scratch (Line by Line, 207 Tests)
AMMUniswapSolidityTutorialDeFi

How to Build Uniswap V2 From Scratch (Line by Line, 207 Tests)

17 min
The best way to understand the protocol you want to fork is to rebuild it yourself first.

TL;DR — Quick Summary

  • Rebuilding Uniswap V2 from scratch is the fastest way to actually understand what a fork inherits — the good, the bad, and the subtle design decisions that matter.
  • The protocol is smaller than people think: one Factory contract, one Pair contract (the AMM itself), a Router, and a Library. That is the whole thing.
  • The core math is the constant product formula x * y = k, with a 0.3% fee wrapped around every swap to keep the invariant drifting upward.
  • You will implement: reserves tracking, minting and burning LP tokens, the swap function with its K-invariant check, flash swaps via callback, TWAP price accumulators, and proper safety rails against fee-on-transfer and rebasing tokens.
  • Zealynx Academy's Uniswap V2 module walks you through the build across 8 sections with 207 automated tests. You do not finish until every test passes.
  • You emerge understanding exactly what a fork is actually copying — which is the point.

Why Build From Scratch Before You Fork

The Uniswap V2 codebase has been forked thousands of times. Most forks ship with bugs the original team spent years hardening against. Integer overflow guards removed. Reentrancy protections half-disabled. Fee mechanisms that break the invariant. Missing flash-swap callbacks. Fee-on-transfer token handling that corrupts reserves silently.
These bugs are not subtle when you have built the protocol yourself. They become obvious because you remember writing the function that should have caught them.
The fastest way to truly understand Uniswap V2 — to read a fork's diff and immediately know what's safe, what's dangerous, and what's just stylistic — is to rebuild it once from scratch. Not to copy-paste. Not to "follow along" watching someone else type. To write every function yourself until the tests pass.
That is exactly what Zealynx Academy's Uniswap V2 module walks you through.

Uniswap V2 Architecture: The Four Contracts That Matter

Before writing any code, understand what you are building. Uniswap V2 is four contracts plus a small library:

UniswapV2Factory

A factory that deploys new Pair contracts, one per token pair. The Factory owns no state beyond a registry of deployed pairs and a feeTo address for the optional protocol fee. When someone calls createPair(tokenA, tokenB), the Factory deploys a new Pair contract using CREATE2 with a deterministic salt, so the pair's address can be computed in advance anywhere without needing to ask the Factory.

UniswapV2Pair (the AMM)

This is the actual AMM. Each Pair holds two ERC-20 tokens in its reserves and lets users swap between them according to the constant product formula. It also tracks time-weighted cumulative prices (for TWAP oracles), mints and burns LP tokens for liquidity providers, and implements flash swaps via a callback pattern.
The Pair contract inherits from UniswapV2ERC20, which is the LP token logic — a standard ERC-20 with an additional permit() function for gasless approvals (EIP-2612 style).

UniswapV2Router

The Router is a convenience wrapper. It does not hold state. It sits in front of the Pair contracts and exposes a simpler interface: addLiquidity, removeLiquidity, swapExactTokensForTokens, swapTokensForExactTokens, plus ETH variants. Under the hood, the Router calculates amounts, calls into the Pair's swap(), mint(), and burn() functions, and enforces slippage deadlines.
Users interact with the Router. Pairs and the Factory are lower-level plumbing.

UniswapV2Library

A pure helper library with no state. It provides the math: converting amount-in to amount-out for a swap, computing optimal liquidity amounts, sorting token addresses. The Router calls it extensively.
That is the whole protocol. Read the original Uniswap V2 whitepaper and you will see this same architecture.

The Core Math: Constant Product With a Fee Twist

The heart of Uniswap V2 is one equation: x * y = k.
x is the reserve of token A. y is the reserve of token B. k is an invariant the pool must preserve across every swap.

How a Swap Works Conceptually

Before swap: pool has 100 DAI and 1 ETH. k = 100 * 1 = 100.
User wants to trade DAI for ETH. They send 10 DAI into the pool. New DAI balance = 110.
For k to stay equal to 100, the new ETH balance must be 100 / 110 = 0.909. So the user receives 1 - 0.909 = 0.091 ETH.
The 0.3% fee: actually the contract holds 10 DAI but only 10 * 0.997 = 9.97 DAI counts as the input for the k calculation. The remaining 0.03 DAI stays in the pool as fee, increasing k on every swap.
So over time, k drifts upward. Every swap makes the pool slightly richer. That is how LPs earn fees — the value of their LP tokens grows as k grows.

Why This Matters for Your Build

Implementing this math incorrectly is the #1 way forks get drained. Specific traps:
  • Forgetting the fee on the k check. Some forks apply fees correctly on the numerator but forget to adjust when re-checking the invariant. The result: the invariant can decrease, and arbitrageurs drain the pool.
  • Integer rounding in the wrong direction. Solidity integer division rounds toward zero. In a swap, the output amount must always round DOWN (favoring the protocol) to avoid leaking value. Getting this wrong in one direction costs LPs. Getting it wrong in the other direction enables draining.
  • Using balance vs reserve incorrectly. balance is the current ERC-20 balance of the token in the Pair. reserve is the stored state from the last sync. They diverge any time someone sends tokens directly to the Pair without calling a function (which happens a lot — this is how flash swaps and optimistic transfers work).

Mapping to Zealynx Academy's 8 Sections

The Uniswap V2 module breaks the build into 8 sections. Here is what each one covers and what you will learn by the end.

Section 1: Introduction + Scaffolding

You start with starter code: imports, storage layout, function signatures, but the bodies are TODO. Your job is to understand the structure before you write any logic.
What you learn: the overall architecture, why each contract exists, how they fit together.

Section 2: The Library Math

Implement UniswapV2Library: quote(), getAmountOut(), getAmountIn(), sortTokens(), and the math for optimal liquidity addition.
Why it matters: the math here shows up inside the Pair's swap function. Getting the formulas right here saves you from debugging the swap later.
Common trap: getAmountOut must use amountInWithFee = amountIn * 997 and divide by reserveIn * 1000 + amountInWithFee. Miss the fee anywhere and the pool drains.

Section 3: The ERC-20 Pair Token

Implement UniswapV2ERC20 — the LP token logic the Pair inherits. Standard ERC-20 plus permit() for EIP-2612 gasless approvals, which requires the EIP-712 domain separator and nonce tracking.
Common trap: the permit signature must match the exact EIP-712 typed-data hash. One character off in the type string and every permit silently fails.

Section 4: Reserves and Sync

The Pair tracks reserve0, reserve1, and blockTimestampLast in a single storage slot (packed) to save gas. You implement _update() which syncs these after every operation, and sync() which lets anyone force a reserve refresh.
What you learn: how Uniswap fits three values into one storage slot using uint112 for reserves and uint32 for the timestamp. This is why the protocol tops out at reserves of 2^112.

Section 5: Minting and Burning LP Tokens

Implement mint() (add liquidity, receive LP tokens) and burn() (destroy LP tokens, receive tokens back pro-rata).
Common trap: the first liquidity provider gets special treatment — they set the initial price ratio and receive sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY LP tokens. The MINIMUM_LIQUIDITY (typically 1000 wei) is locked forever to prevent a first-depositor inflation attack. Forget this and you have shipped a known vulnerability.

Section 6: The Swap Function

This is the heart of the protocol. You implement swap(amount0Out, amount1Out, to, data):
  1. Validate amounts are positive and below reserves.
  2. Optimistically transfer tokens to the recipient.
  3. If data.length > 0, trigger a flash swap callback on the recipient.
  4. Read balances (not reserves — balances, because someone may have sent tokens in).
  5. Compute fee-adjusted balances.
  6. Check the K-invariant: balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 1000^2.
  7. Update reserves via _update().
Common trap: the K-invariant check uses * 1000^2 on the right side to match the fee-adjusted left side. Mix up the exponent and the check is trivially bypassable.

Section 7: TWAP Price Accumulators

Every time _update() runs, it advances price0CumulativeLast and price1CumulativeLast by the current price times the time elapsed. External oracles read these cumulatives at two timestamps and divide the difference to get a TWAP.
What you learn: Uniswap V2's oracle is not a push-based feed. It is a pull-based accumulator that any contract can query. This pattern shows up in many subsequent protocols.

Section 8: Final Build + Test Suite

By section 8 you have the whole protocol. You run the full test suite. 207 tests. They check:
  • K-invariant holds after every swap
  • Reentrancy guards are in place
  • Flash-swap callback is triggered correctly
  • LP token accounting is pro-rata after mint and burn
  • Fee-on-transfer tokens do not corrupt reserves
  • Sync recovers from direct-transfer token donations
  • Permit signatures verify against EIP-712 domain
You do not finish until all 207 pass. No partial credit.

What 207 Tests Actually Test For

The test suite is not a formality. Each test corresponds to a real category of bug that has been exploited in Uniswap V2 forks over the years. A sample of what gets checked:
Math correctness
1function test_swap_preserves_k_invariant() public {
2 uint256 kBefore = reserve0 * reserve1;
3 pair.swap(0, 1 ether, bob, "");
4 uint256 kAfter = reserve0 * reserve1;
5 assertGe(kAfter, kBefore); // K must only grow
6}
Reentrancy
1function test_swap_blocks_reentrancy() public {
2 // Malicious token that calls back into swap during transfer
3 // Expect revert
4}

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

Flash-swap callback invocation
1function test_flashswap_triggers_callback() public {
2 pair.swap(1 ether, 0, address(callback), "trigger");
3 // Expect callback.uniswapV2Call was called with correct args
4}
First-depositor attack
1function test_firstDepositor_cannot_manipulate_ratio() public {
2 // Attempting to deposit 1 wei first and set unfair ratio should fail
3 // Because MINIMUM_LIQUIDITY is locked
4}
Fee-on-transfer token handling
1function test_swap_handles_fee_on_transfer_tokens() public {
2 // Tokens that deduct a fee on transfer should not break K
3}
If you have not handled any of these cases, the tests fail and tell you exactly which invariant broke. You do not need a separate debugger — the tests guide the fix.

The Common Pitfalls When Building

Patterns we have seen builders hit across the first 3 completions of the Academy's Uniswap V2 module:
Missing reentrancy lock on swap(). The swap function triggers a callback, which means an attacker can call back into the Pair before the swap completes. Without the lock modifier, an attacker can drain the pool. The test suite catches this, but only if you remember the modifier exists.
Wrong fee arithmetic in getAmountOut. The formula is (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997). Miss the 997 in the numerator and the pool leaks value on every swap.
Forgetting the MINIMUM_LIQUIDITY burn. The first liquidity provider must receive sqrt(k) - 1000 LP tokens, with 1000 LP tokens burned (sent to address 0). Skip this and the first-depositor inflation attack is live.
Packing reserves incorrectly. uint112 reserve0 and uint112 reserve1 plus uint32 blockTimestampLast must sum to 256 bits. Use uint256 instead and you double the gas cost per swap and lose compatibility with downstream protocols that expect the packed slot.
Using balance instead of reserve (or vice versa) in the wrong place. The swap function uses balance to detect direct transfers. The K-invariant check uses balance (not reserve) on the left side and reserve on the right. Mix them up and the check either always passes or always fails.
Every one of these is caught by specific tests. The test failures tell you exactly what went wrong. This is why the test-driven approach works — you do not need to be a Uniswap expert before you start. You need to pay attention to what fails.

What You Understand After

By the time all 207 tests pass, you can answer questions like:
  • Why does Uniswap V2 use uint112 for reserves instead of uint256? (Packing + uint224 for cumulative price math.)
  • What happens if someone transfers tokens directly to a Pair without calling a function? (The extra balance can be swept by calling sync() or used in the next swap's K check.)
  • Why is the flash-swap callback triggered before the balance check? (So the borrower can use the tokens, put them back, and close the swap atomically.)
  • How does the TWAP oracle prevent short-term manipulation? (By using accumulators over time intervals, short-lived price spikes average out.)
  • Why does the first liquidity provider lock MINIMUM_LIQUIDITY? (To prevent a first-depositor donating a tiny amount of tokens to manipulate the price ratio and stealing subsequent deposits.)
This is what a builder who understands the protocol sounds like. Not someone who has read the whitepaper. Someone who has written the code, failed the tests, understood why, and shipped a working version.

Where to Start

Open Zealynx Academy and start the Uniswap V2 module. The first section loads immediately in your browser — no setup, no clone command, no forge install. The test suite runs in the browser against your code. The environment handles compilation.
Go at your own pace. Most builders finish the full 8 sections in roughly a week of focused sessions. If you get stuck, the feedback repo at github.com/ZealynxSecurity/zealynx-academy-feedback is where you can ask questions.
When the tests pass, you have a complete Uniswap V2 in your browser, understood end to end. You are ready to think seriously about what a fork would change, what it would keep, what edge cases matter for your design.
After finishing the build, the natural next step is to enter the Shadow Arena and review a real past audit contest on a Uniswap V2 fork — Basin, ElasticSwap, or Velodrome. Now that you built the original, the fork's deviations become obvious. That is where the deepest learning happens.

Zealynx Academy Is Part of the Ethereum Security QF Round

A note for readers who care about public-goods funding in Ethereum: Zealynx Academy is accepted into the Giveth Ethereum Security QF round backed by TheDAO Security Fund's 500 ETH matching pool. The round runs April 21 – May 12, 2026. Small donations from many supporters unlock significantly more matching than a few large donations. If the Academy's mission of making Web3 builders actually understand the protocols they ship resonates with you, a $5 donation before the round closes compounds meaningfully. Details and donation guide here.

Conclusion

Forking Uniswap V2 is a one-line operation. Understanding it takes actually building it. The 8 sections in Zealynx Academy's module are structured to give you that understanding the fastest way we know how: guided starter code, line-by-line writing, and 207 tests that tell you precisely when you got something wrong.
After this module, forking becomes a deliberate act. You know what you are copying. You know what you want to change. You know what bugs to avoid. That is the threshold between a builder who ships forks and a builder who ships protocols.
Announcement and full platform: Zealynx Academy Is Public
Support the QF round: giveth.io/project/zealynx-academy

FAQ

1. Do I need experience with Solidity to start the Uniswap V2 module?
Yes, basic Solidity. You should understand functions, mappings, events, and how storage works. You do not need advanced skills like inline assembly or gas-optimization tricks — the module teaches those through the build. If you are completely new to Solidity, start with CryptoZombies or Cyfrin Updraft first, then come back.
2. How long does it take to finish all 8 sections?
Most builders finish in a focused week, spending 2-3 hours per section. Sections 6 (swap function) and 3 (ERC-20 + permit) take the longest. Sections 1, 2, and 4 are faster. The first three builders to complete the full module in the Academy's private phase each took 5-10 days, working at their own pace.
3. What if a test fails and I cannot figure out why?
The test names describe what they check, and the revert message usually tells you which invariant broke. If you genuinely get stuck, open an issue on the feedback repo with the section, the test name, and your current code. The team responds usually within a day.
4. Is this the same as forking from GitHub?
No. Forking copies the final code without forcing you to think. The Academy module gives you starter scaffolding but leaves every function body empty. You write the logic. You hit the bugs. You fix them. By the end you have the same code a fork would have, but you also understand every line.
5. Does this work for Uniswap V3?
A Uniswap V3 module is on the roadmap. V3 is substantially more complex — concentrated liquidity, tick math, hooks — so the V3 module will be longer and is built on top of the V2 foundation. Do V2 first. It is the prerequisite for thinking clearly about V3.
6. What should I do after finishing the Uniswap V2 module?
Enter the Shadow Arena. The Basin, ElasticSwap, and Velodrome targets are all Uniswap V2-family forks. Now that you built the original, you can compare the fork's diff and spot exactly what changed and why it matters. That is where pattern recognition for security starts building.
7. Do I need to run a local development environment?
No. The module runs in the browser. Compilation, testing, and feedback happen in-platform. You do not need to install Foundry, Hardhat, or anything else. If you want to export your code and run it in your own setup, that is supported — but it is not required.

Glossary

TermDefinition
Automated Market MakerA decentralized exchange design that uses a math formula instead of an order book to price trades. Uniswap V2 is the canonical constant-product AMM.
Constant Product FormulaThe core pricing equation x * y = k used by Uniswap V2 and many AMMs. Ensures the product of token reserves stays constant across every trade, adjusted only by fees.
Liquidity PoolA smart contract holding two tokens that can be swapped against each other. In Uniswap V2, each Pair contract is a liquidity pool.
LP TokenA token representing a share of a liquidity pool. Minted when you add liquidity, burned when you remove it. Grows in value as the pool earns fees.
Flash SwapA feature of Uniswap V2 where the Pair sends tokens to the recipient before checking the K-invariant, letting the recipient use them temporarily if they return them in the same transaction.
K-InvariantThe core invariant reserve0 * reserve1 >= k that Uniswap V2 preserves across every swap. Violations would let attackers drain the pool.
Uniswap V2The second version of the Uniswap AMM protocol, released in 2020. The most-forked DeFi contract ever shipped.

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx