├── .github └── workflows │ └── pages.yml ├── .gitignore ├── .gitmodules ├── README.md ├── book.toml ├── public └── CNAME ├── screenshot.png └── src ├── README.md ├── SUMMARY.md ├── highlight.js ├── images ├── book.jpg ├── cover.png ├── uf_logo.png └── uf_logo_big.png ├── milestone_0 ├── constant-function-market-maker.md ├── dev-environment.md ├── images │ ├── amm_simplified.png │ ├── curve_finite.png │ ├── curve_infinite.png │ ├── desmos.png │ ├── orderbook.png │ ├── the_curve.png │ ├── ticks_and_ranges.png │ └── usdceth_liquidity.png ├── introduction-to-markets.md ├── uniswap-v3.md └── what-we-will-build.md ├── milestone_1 ├── calculating-liquidity.md ├── deployment.md ├── first-swap.md ├── images │ ├── buy_eth_model.png │ ├── curve_liquidity.png │ ├── metamask.png │ ├── range_depleted.png │ ├── ui.png │ └── ui_metamask_connected.png ├── introduction.md ├── manager-contract.md ├── providing-liquidity.md └── user-interface.md ├── milestone_2 ├── generalize-minting.md ├── generalize-swapping.md ├── images │ ├── find_next_tick.png │ └── tick_bitmap.png ├── introduction.md ├── math-in-solidity.md ├── output-amount-calculation.md ├── quoter-contract.md ├── tick-bitmap-index.md └── user-interface.md ├── milestone_3 ├── cross-tick-swaps.md ├── different-ranges.md ├── flash-loans.md ├── images │ ├── add_liquidity_dialog.png │ ├── price_range_dynamics.png │ ├── ranges_outside_current_price.png │ ├── sandwich_attack.png │ ├── slippage_tolerance.png │ ├── swap_consecutive_price_ranges.png │ ├── swap_partially_overlapping_price_ranges.png │ ├── swap_within_overlapping_price_ranges.png │ └── swap_within_price_range.png ├── introduction.md ├── liquidity-calculation.md ├── more-on-fixed-point-numbers.md ├── slippage-protection.md └── user-interface.md ├── milestone_4 ├── factory-contract.md ├── images │ ├── pools_graph.png │ └── pools_scattered.png ├── introduction.md ├── multi-pool-swaps.md ├── path.md ├── tick-rounding.md └── user-interface.md ├── milestone_5 ├── flash-loan-fees.md ├── images │ ├── fees_inside_and_outside_price_range.png │ ├── interpolated_prices.png │ ├── liquidity_range_engaged.png │ ├── liquidity_ranges_fees.png │ ├── observations.png │ └── observations_wrapping.png ├── introduction.md ├── price-oracle.md ├── protocol-fees.md ├── swap-fees.md └── user-interface.md └── milestone_6 ├── erc721-overview.md ├── images ├── nft_example.png ├── nft_example_2.png ├── nft_example_3.png └── nft_template.png ├── introduction.md ├── nft-manager.md └── nft-renderer.md /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | Deploy: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: true # Fetch Hugo themes (true OR recursive) 17 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 18 | 19 | - name: Set up mdBook 20 | uses: peaceiris/actions-mdbook@v1 21 | with: 22 | mdbook-version: '0.4.36' 23 | 24 | - name: Install KaTex 25 | run: cargo install mdbook-katex 26 | 27 | - name: Build 28 | run: mdbook build --dest-dir public 29 | 30 | - name: Deploy 31 | uses: peaceiris/actions-gh-pages@v3 32 | if: ${{ github.ref == 'refs/heads/main' }} 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./public 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hugo_build.lock 2 | .DS_Store 3 | .vscode 4 | book 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/.gitmodules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap V3 Development Book 2 | 3 |

4 | Uniswap V3 Development Book cover 5 |

6 | 7 | 8 |

9 | 👉 READ ONLINE  |  PRINT OR SAVE AS PDF 👈 10 |

11 | 12 | This book will teach how to develop an advanced decentralized application! Specifically, we'll be building a clone of 13 | [Uniswap V3](https://uniswap.org/), which is a decentralized exchange. 14 | 15 | ## Why Uniswap? 16 | - It implements a very simple mathematical concept, `x * y = k`, which still makes it very powerful. 17 | - It's an advanced application that has a thick layer of engineering on top of the simple formula. 18 | - It's permissionless and battle-tested. Learning from an application that's been running in production for 19 | several years and handling billions of dollars will make you a better developer. 20 | 21 | ## What we'll build 22 | 23 | ![Front-end application screenshot](/screenshot.png) 24 | 25 | We'll build a full clone of Uniswap V3. It **won't be an exact copy** and it **won't be production-ready** because we'll 26 | do something in our own way and we'll **definitely** introduce multiple bugs. So, don't deploy this to the mainnet! 27 | 28 | While our focus will primarily be on smart contracts, we'll also build a front-end application as a side hustle. 🙂 29 | I'm not a front-end developer and I cannot make a front-end application better than you, but I can show you how a 30 | decentralized exchange can be integrated into a front-end application. 31 | 32 | The full code of what we'll build is stored in a separate repository: 33 | 34 | https://github.com/Jeiwan/uniswapv3-code 35 | 36 | You can read this book at: 37 | 38 | https://uniswapv3book.com/ 39 | 40 | ### Questions? 41 | 42 | Each milestone has its own section in [the GitHub Discussions](https://github.com/Jeiwan/uniswapv3-book/discussions). 43 | Don't hesitate to ask questions about anything that's not clear in the book! 44 | 45 | ## Table of Contents 46 | 47 | - Milestone 0. Introduction 48 | 1. Introduction to markets 49 | 1. Constant Function Market Makers 50 | 1. Uniswap V3 51 | 1. Development Environment 52 | 1. What We'll Build 53 | - Milestone 1. First Swap 54 | 1. Introduction 55 | 1. Calculating Liquidity 56 | 1. Providing Liquidity 57 | 1. First Swap 58 | 1. Manager Contract 59 | 1. Deployment 60 | 1. User Interface 61 | - Milestone 2. Second Swap 62 | 1. Introduction 63 | 1. Output Amount Calculation 64 | 1. Math in Solidity 65 | 1. Tick Bitmap Index 66 | 1. Generalize Minting 67 | 1. Generalize Swapping 68 | 1. Quoter Contract 69 | 1. User Interface 70 | - Milestone 3. Cross-tick Swaps 71 | 1. Introduction 72 | 1. Different Price Ranges 73 | 1. Cross-Tick Swaps 74 | 1. Slippage Protection 75 | 1. Liquidity Calculation 76 | 1. A Little Bit More on Fixed-point Numbers 77 | 1. Flash Loans 78 | 1. User Interface 79 | 80 | - Milestone 4. Multi-pool Swaps 81 | 1. Introduction 82 | 1. Factory Contract 83 | 1. Swap Path 84 | 1. Multi-pool Swaps 85 | 1. User Interface 86 | 1. Tick Rounding 87 | - Milestone 5. Fees and Price Oracle 88 | 1. Introduction 89 | 1. Swap Fees 90 | 1. Flash Loan Fees 91 | 1. Protocol Fees 92 | 1. Price Oracle 93 | 1. User Interface 94 | - Milestone 6: NFT positions 95 | 1. Introduction 96 | 1. ERC721 Overview 97 | 1. NFT Manager 98 | 1. NFT Renderer 99 | 100 | ## Running locally 101 | 102 | To run the book locally: 103 | 1. Install [Rust](https://www.rust-lang.org/). 104 | 1. Install [mdBook](https://github.com/rust-lang/mdBook): 105 | ```shell 106 | $ cargo install mdbook 107 | $ cargo install mdbook-katex 108 | ``` 109 | 1. Clone the repo: 110 | ```shell 111 | $ git clone https://github.com/Jeiwan/uniswapv3-book 112 | $ cd uniswapv3-book 113 | ``` 114 | 1. Run: 115 | ```shell 116 | $ mdbook serve --open 117 | ``` 118 | 1. Visit http://localhost:3000/ (or whatever URL the previous command outputs!) -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Jeiwan"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Uniswap V3 Development Book" 7 | description = "A book that teaches how to build a clone of Uniswap V3 in Solidity from scratch." 8 | 9 | [output.html] 10 | curly-quotes = true 11 | git-repository-url = "https://github.com/Jeiwan/uniswapv3-book" 12 | git-repository-icon = "fa-github" 13 | edit-url-template = "https://github.com/Jeiwan/uniswapv3-book/edit/main/{path}" 14 | cname = "uniswapv3book.com" 15 | 16 | [preprocessor.katex] 17 | after = ["links"] -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | uniswapv3book.com -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/screenshot.png -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Uniswap V3 Development Book 2 | 3 |

4 | Uniswap V3 Development Book cover 5 |

6 | 7 | Welcome to the world of decentralized finances and automated market makers! This book will be your guide in this mysterious and amusing world! Together, we'll build one of the most interesting and important applications, which serves as a pillar of today's decentralized finances–**Uniswap V3**! 8 | 9 | This book will guide you through the development of a decentralized application, including: 10 | - smart-contract development (in [Solidity](https://docs.soliditylang.org/en/latest/index.html)); 11 | - contracts testing and deployment (using Forge and Anvil from [Foundry](https://github.com/foundry-rs/foundry)); 12 | - design and mathematics of a decentralized exchange; 13 | - development of a front-end application for the exchange ([React](https://reactjs.org/) and [MetaMask](https://metamask.io/)). 14 | 15 | **This book is not for complete beginners.** 16 | 17 | I expect you to be an experienced developer, who has ever programmed in any programming language. It'll also be helpful if you know [the syntax of Solidity](https://docs.soliditylang.org/en/v0.8.17/introduction-to-smart-contracts.html), the main programming language of this book. If not, it's not a big problem: we'll learn a lot about Solidity and Ethereum Virtual Machine during our journey. 18 | 19 | **However, this book is for blockchain beginners.** 20 | 21 | If you only heard about blockchains and were interested but haven't had a chance to dive deeper, this book is for you! Yes, for you! You'll learn how to develop for blockchains (specifically, Ethereum), how blockchains work, how to program and deploy smart contracts, and how to run and test them on your computer. 22 | 23 | Alright, let's get started! 24 | 25 | ## Useful Links 26 | 27 | 1. This book is available at: 28 | 1. This book is hosted on GitHub: 29 | 1. All source codes are hosted in a separate repo: 30 | 1. If you think you can help Uniswap, they have [a grants program](https://www.notion.so/unigrants/Welcome-to-UNI-Grants-6e3e84967a984a5fb127ae749649ddc9). 31 | 1. If you're interested in DeFi and blockchains, [follow me on Twitter](https://twitter.com/jeiwan7). 32 | 33 | ## Questions? 34 | 35 | Each milestone has its section in [the GitHub Discussions](https://github.com/Jeiwan/uniswapv3-book/discussions). Don't hesitate to ask questions about anything that's not clear in the book! 36 | 37 | --- 38 | 39 | ## Where to Start for a Complete Beginner? 40 | 41 | This book will be easy for those who know something about constant-function market makers and Uniswap. If you're a complete beginner in decentralized exchanges, here's how I'd recommend starting: 42 | 1. Read my Uniswap V1 series. It covers the very basics of Uniswap, and the code is much simpler. If you have some experience with Solidity, skip the code since it's very basic and Uniswap V2 does it better. 43 | 1. [Programming DeFi: Uniswap. Part 1](https://jeiwan.net/posts/programming-defi-uniswap-1/) 44 | 1. [Programming DeFi: Uniswap. Part 2](https://jeiwan.net/posts/programming-defi-uniswap-2/) 45 | 1. [Programming DeFi: Uniswap. Part 3](https://jeiwan.net/posts/programming-defi-uniswap-3/) 46 | 1. Read my Uniswap V2 series. I don't go too deep into the math and underlying concepts here since they're covered in the V1 series, but the code of V2 is worth getting familiar with–it'll hopefully teach you a different way of thinking about smart contracts programming (it's not how we usually write programs). 47 | 1. [Programming DeFi: Uniswap V2. Part 1](https://jeiwan.net/posts/programming-defi-uniswapv2-1/) 48 | 1. [Programming DeFi: Uniswap V2. Part 2](https://jeiwan.net/posts/programming-defi-uniswapv2-2/) 49 | 1. [Programming DeFi: Uniswap V2. Part 3](https://jeiwan.net/posts/programming-defi-uniswapv2-3/) 50 | 1. [Programming DeFi: Uniswap V2. Part 4](https://jeiwan.net/posts/programming-defi-uniswapv2-4/) 51 | 52 | If math is an issue, consider going through [Algebra 1](https://www.khanacademy.org/math/algebra) and [Algebra 2](https://www.khanacademy.org/math/algebra2) courses on Khan Academy. The math of Uniswap is not hard, but it requires the skill of basic algebraic manipulations. 53 | 54 | ## Uniswap Grants Program 55 | 56 |

57 | Uniswap Foundation logo 58 |

59 | 60 | To write this book, I received a grant from [Uniswap Foundation](https://uniswapfoundation.mirror.xyz/). Without the grant, I wouldn't probably have had enough motivation and patience to dig Uniswap into its deepest depths and finish the book. The grant is also the main reason why the book is open-source and free for anyone. You can [learn more about the Uniswap Grants Program](https://www.unigrants.org/) (and maybe apply!). -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Uniswap V3 Development Book](README.md) 4 | 5 | # Milestone 0. Background 6 | - [Introduction to Markets](milestone_0/introduction-to-markets.md) 7 | - [Constant Function Market Maker](milestone_0/constant-function-market-maker.md) 8 | - [Uniswap V3](milestone_0/uniswap-v3.md) 9 | - [Development Environment](milestone_0/dev-environment.md) 10 | - [What We Will Build](milestone_0/what-we-will-build.md) 11 | 12 | # Milestone 1. First Swap 13 | - [Introduction](milestone_1/introduction.md) 14 | - [Calculating Liquidity](milestone_1/calculating-liquidity.md) 15 | - [Providing Liquidity](milestone_1/providing-liquidity.md) 16 | - [First Swap](milestone_1/first-swap.md) 17 | - [Manager Contract](milestone_1/manager-contract.md) 18 | - [Deployment](milestone_1/deployment.md) 19 | - [User Interface](milestone_1/user-interface.md) 20 | 21 | # Milestone 2. Second Swap 22 | - [Introduction](milestone_2/introduction.md) 23 | - [Output Amount Calculation](milestone_2/output-amount-calculation.md) 24 | - [Math in Solidity](milestone_2/math-in-solidity.md) 25 | - [Tick Bitmap Index](milestone_2/tick-bitmap-index.md) 26 | - [Generalized Minting](milestone_2/generalize-minting.md) 27 | - [Generalized Swapping](milestone_2/generalize-swapping.md) 28 | - [Quoter Contract](milestone_2/quoter-contract.md) 29 | - [User Interface](milestone_2/user-interface.md) 30 | 31 | # Milestone 3. Cross-Tick Swaps 32 | - [Introduction](milestone_3/introduction.md) 33 | - [Different Price Ranges](milestone_3/different-ranges.md) 34 | - [Cross-Tick Swaps](milestone_3/cross-tick-swaps.md) 35 | - [Slippage Protection](milestone_3/slippage-protection.md) 36 | - [Liquidity Calculation](milestone_3/liquidity-calculation.md) 37 | - [A Little Bit More on Fixed-Point Numbers](milestone_3/more-on-fixed-point-numbers.md) 38 | - [Flash Loans](milestone_3/flash-loans.md) 39 | - [User Interface](milestone_3/user-interface.md) 40 | 41 | # Milestone 4. Multi-pool Swaps 42 | - [Introduction](milestone_4/introduction.md) 43 | - [Factory Contract](milestone_4/factory-contract.md) 44 | - [Swap Path](milestone_4/path.md) 45 | - [Multi-Pool Swaps](milestone_4/multi-pool-swaps.md) 46 | - [User Interface](milestone_4/user-interface.md) 47 | - [Tick Rounding](milestone_4/tick-rounding.md) 48 | 49 | # Milestone 5. Fees and Price Oracle 50 | - [Introduction](milestone_5/introduction.md) 51 | - [Swap Fees](milestone_5/swap-fees.md) 52 | - [Flash Loan Fees](milestone_5/flash-loan-fees.md) 53 | - [Protocol Fees](milestone_5/protocol-fees.md) 54 | - [Price Oracle](milestone_5/price-oracle.md) 55 | - [User Interface](milestone_5/user-interface.md) 56 | 57 | # Milestone 6: NFT Positions 58 | - [Introduction](milestone_6/introduction.md) 59 | - [Overview of ERC721](milestone_6/erc721-overview.md) 60 | - [NFT Manager](milestone_6/nft-manager.md) 61 | - [NFT Renderer](milestone_6/nft-renderer.md) -------------------------------------------------------------------------------- /src/images/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/images/book.jpg -------------------------------------------------------------------------------- /src/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/images/cover.png -------------------------------------------------------------------------------- /src/images/uf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/images/uf_logo.png -------------------------------------------------------------------------------- /src/images/uf_logo_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/images/uf_logo_big.png -------------------------------------------------------------------------------- /src/milestone_0/constant-function-market-maker.md: -------------------------------------------------------------------------------- 1 | # Constant Function Market Makers 2 | 3 | > This chapter retells [the whitepaper of Uniswap V2](https://uniswap.org/whitepaper.pdf). Understanding this math is crucial to build a Uniswap-like DEX, but it's totally fine if you don't understand everything at this stage. 4 | 5 | As I mentioned in the previous section, there are different approaches to building AMM. We'll be focusing on and building one specific type of AMM–Constant Function Market Maker. Don't be scared by the long name! At its core is a very simple mathematical formula: 6 | 7 | $$x * y = k$$ 8 | 9 | That's it, this is the AMM. 10 | 11 | $x$ and $y$ are pool contract reserves–the amounts of tokens it currently holds. *k* is just their product, actual value doesn't matter. 12 | 13 | > **Why are there only two reserves, *x* and *y*?** 14 | Each Uniswap pool can hold only two tokens. We use *x* and *y* to refer to reserves of one pool, where *x* is the reserve of the first token and *y* is the reserve of the other token, and the order doesn't matter. 15 | 16 | The constant function formula says: **after each trade, *k* must remain unchanged**. When traders make trades, they put some amount of one token into a pool (the token they want to sell) and remove some amount of the other token from the pool (the token they want to buy). This changes the reserves of the pool, and the constant function formula says that **the product** of reserves must not change. As we will see many times in this book, this simple requirement is the core algorithm of how Uniswap works. 17 | 18 | ## The Trade Function 19 | Now that we know what pools are, let's write the formula of how trading happens in a pool: 20 | 21 | $$(x + r\Delta x)(y - \Delta y) = k$$ 22 | 23 | 1. There's a pool with some amount of token 0 ($x$) and some amount of token 1 ($y$) 24 | 1. When we buy token 1 for token 0, we give some amount of token 0 to the pool ($\Delta x$). 25 | 1. The pool gives us some amount of token 1 in exchange ($\Delta y$). 26 | 1. The pool also takes a small fee ($r = 1 - \text{swap fee}$) from the amount of token 0 we gave. 27 | 1. The reserve of token 0 changes ($x + r \Delta x$), and the reserve of token 1 changes as well ($y - \Delta y$). 28 | 1. The product of updated reserves must still equal $k$. 29 | 30 | > We'll use token 0 and token 1 notation for the tokens because this is how they're referenced in the code. At this point, it doesn't matter which of them is 0 and which is 1. 31 | 32 | We're basically giving a pool some amount of token 0 and getting some amount of token 1. The job of the pool is to give us a correct amount of token 1 calculated at a fair price. This leads us to the following conclusion: **pools decide what trade prices are**. 33 | 34 | ## Pricing 35 | 36 | How do we calculate the prices of tokens in a pool? 37 | 38 | Since Uniswap pools are separate smart contracts, **tokens in a pool are priced in terms of each other**. For example: in a ETH/USDC pool, ETH is priced in terms of USDC, and USDC is priced in terms of ETH. If 1 ETH costs 1000 USDC, then 1 USDC costs 0.001 ETH. The same is true for any other pool, whether it's a stablecoin pair or not (e.g. ETH/BTC). 39 | 40 | In the real world, everything is priced based on [the law of supply and demand](https://www.investopedia.com/terms/l/law-of-supply-demand.asp). This also holds true for AMMs. We'll put the demand part aside for now and focus on supply. 41 | 42 | The prices of tokens in a pool are determined by the supply of the tokens, that is by **the amounts of reserves of the tokens** that the pool is holding. Token prices are simply relations of reserves: 43 | 44 | $$P_x = \frac{y}{x}, \quad P_y=\frac{x}{y}$$ 45 | 46 | Where $P_x$ and $P_y$ are prices of tokens in terms of the other token. 47 | 48 | Such prices are called *spot prices* and they only reflect current market prices. However, the actual price of a trade is calculated differently. And this is where we need to bring the demand part back. 49 | 50 | Concluding from the law of supply and demand, **high demand increases the price**–and this is a property we need to have in a permissionless system. We want the price to be high when demand is high, and we can use pool reserves to measure the demand: the more tokens you want to remove from a pool (relative to the pool's reserves), the higher the impact of demand is. 51 | 52 | Let's return to the trade formula and look at it closer: 53 | 54 | $$(x + r\Delta x)(y - \Delta y) = xy$$ 55 | 56 | As you can see, we can derive $\Delta_x$ and $\Delta y$ from it, which means we can calculate the output amount of a trade based on the input amount and vice versa: 57 | 58 | $$\Delta y = \frac{yr\Delta x}{x + r\Delta x}$$ 59 | $$\Delta x = \frac{x \Delta y}{r(y - \Delta y)}$$ 60 | 61 | In fact, these formulas free us from calculating prices! We can always find the output amount using the $\Delta y$ formula (when we want to sell a known amount of tokens) and we can always find the input amount using the $\Delta x$ formula (when we want to buy a known amount of tokens). Notice that each of these formulas is a relation of reserves ($x/y$ or $y/x$) and they also take the trade amount ($\Delta x$ in the former and $\Delta y$ in the latter) into consideration. **These are the pricing functions that respect both supply and demand**. And we don't even need to calculate the prices! 62 | 63 | > Here's how you can derive the above formulas from the trade function: 64 | $$(x + r\Delta x)(y - \Delta y) = xy$$ 65 | $$y - \Delta y = \frac{xy}{x + r\Delta x}$$ 66 | $$-\Delta y = \frac{xy}{x + r\Delta x} - y$$ 67 | $$-\Delta y = \frac{xy - y({x + r\Delta x})}{x + r\Delta x}$$ 68 | $$-\Delta y = \frac{xy - xy - y r \Delta x}{x + r\Delta x}$$ 69 | $$-\Delta y = \frac{- y r \Delta x}{x + r\Delta x}$$ 70 | $$\Delta y = \frac{y r \Delta x}{x + r\Delta x}$$ 71 | And: 72 | $$(x + r\Delta x)(y - \Delta y) = xy$$ 73 | $$x + r\Delta x = \frac{xy}{y - \Delta y}$$ 74 | $$r\Delta x = \frac{xy}{y - \Delta y} - x$$ 75 | $$r\Delta x = \frac{xy - x(y - \Delta y)}{y - \Delta y}$$ 76 | $$r\Delta x = \frac{xy - xy + x \Delta y}{y - \Delta y}$$ 77 | $$r\Delta x = \frac{x \Delta y}{y - \Delta y}$$ 78 | $$\Delta x = \frac{x \Delta y}{r(y - \Delta y)}$$ 79 | 80 | ## The Curve 81 | 82 | The above calculations might seem too abstract and dry. Let's visualize the constant product function to better understand how it works. 83 | 84 | When plotted, the constant product function is a quadratic hyperbola: 85 | 86 | ![The shape of the constant product formula curve](images/the_curve.png) 87 | 88 | Where axes are the pool reserves. Every trade starts at the point on the curve that corresponds to the current ratio of reserves. To calculate the output amount, we need to find a new point on the curve, which has the $x$ coordinate of $x+\Delta x$, i.e. current reserve of token 0 + the amount we're selling. The change in $y$ is the amount of token 1 we'll get. 89 | 90 | Let's look at a concrete example: 91 | 92 | ![Desmos chart example](images/desmos.png) 93 | 94 | 1. The purple line is the curve, and the axes are the reserves of a pool (notice that they're equal at the start price). 95 | 1. The start price is 1. 96 | 1. We're selling 200 of token 0. If we use only the start price, we expect to get 200 of token 1. 97 | 1. However, the execution price is 0.666, so we get only 133.333 of token 1! 98 | 99 | This example is from [the Desmos chart](https://www.desmos.com/calculator/7wbvkts2jf) made by [Dan Robinson](https://twitter.com/danrobinson), one of the creators of Uniswap. To build a better intuition of how it works, try making up different scenarios and plotting them on the graph. Try different reserves, and see how the output amount changes when $\Delta x$ is small relative to $x$. 100 | 101 | > As the legend goes, Uniswap was invented in Desmos. 102 | 103 | I bet you're wondering why using such a curve. It might seem like it punishes you for trading big amounts. This is true, and this is a desirable property! The law of supply and demand tells us that when demand is high (and supply is constant) the price is also high. And when demand is low, the price is also lower. This is how markets work. And, magically, the constant product function implements this mechanism! Demand is defined by the amount you want to buy, and supply is the pool reserves. When you want to buy a big amount relative to pool reserves the price is higher than when you want to buy a smaller amount. Such a simple formula guarantees such a powerful mechanism! 104 | 105 | Even though Uniswap doesn't calculate trade prices, we can still see them on the curve. Surprisingly, there are multiple prices when making a trade: 106 | 107 | 1. Before a trade, there's *a spot price*. It's equal to the relation of reserves, $\frac{y}{x}$ or $\frac{x}{y}$ depending on the direction of the trade. This price is also *the slope of the tangent line* at the starting point. 108 | 1. After a trade, there's a new spot price, at a different point on the curve. And it's the slope of the tangent line at this new point. 109 | 1. The actual price of the trade is the slope of the line connecting the two points! 110 | 111 | **And that's the whole math of Uniswap! Phew!** 112 | 113 | Well, this is the math of Uniswap V2, and we're studying Uniswap V3. So in the next part, we'll see how the mathematics of Uniswap V3 is different. -------------------------------------------------------------------------------- /src/milestone_0/dev-environment.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | 3 | We're going to build two applications: 4 | 5 | 1. An on-chain one: a set of smart contracts deployed on Ethereum. 6 | 1. An off-chain one: a front-end application that will interact with the smart contracts. 7 | 8 | While the front-end application development is part of this book, it won't be our main focus. We will build it solely to demonstrate how smart contracts are integrated with front-end applications. Thus, the front-end application is optional, but I'll still provide the code. 9 | 10 | ## Quick Introduction to Ethereum 11 | 12 | Ethereum is a blockchain that allows anyone to run applications on it. It might look like a cloud provider, but there are multiple differences: 13 | 1. You don't pay for hosting your application. But you pay for deployment. 14 | 1. Your application is immutable. That is: you won't be able to modify it after it's deployed. 15 | 1. Users will pay to use your application. 16 | 17 | To better understand these moments, let's see what Ethereum is made of. 18 | 19 | At the core of Ethereum (and any other blockchain) is a database. The most valuable data in Ethereum's database is *the state of accounts*. An account is an Ethereum address with associated data: 20 | 21 | 1. Balance: account's ether balance. 22 | 1. Code: bytecode of the smart contract deployed at this address. 23 | 1. Storage: space used by smart contracts to store data. 24 | 1. Nonce: a serial integer that's used to protect against replay attacks. 25 | 26 | Ethereum's main job is building and maintaining this data in a secure way that doesn't allow unauthorized access. 27 | 28 | Ethereum is also a network, a network of computers that build and maintain the state independently of each other. The main goal of the network is to **decentralize access to the database**: there must be no single authority that's allowed to modify anything in the database unilaterally. This is achieved through *consensus*, which is a set of rules all the nodes in the network follow. If one party decides to abuse a rule, it'll be excluded from the network. 29 | 30 | > Fun fact: blockchain can use MySQL! Nothing prevents this besides performance. In its turn, Ethereum uses [LevelDB](https://github.com/google/leveldb), a fast key-value database. 31 | 32 | Every Ethereum node also runs EVM, Ethereum Virtual Machine. A virtual machine is a program that can run other programs, and EVM is a program that executes smart contracts. Users interact with contracts through transactions: besides simply sending ether, transactions can contain smart contract call data. It includes: 33 | 34 | 1. An encoded contract function name. 35 | 2. Function parameters. 36 | 37 | Transactions are packed in blocks and blocks are then mined by miners. Each participant in the network can validate any transaction and any block. 38 | 39 | In a sense, smart contracts are similar to JSON APIs but instead of endpoints you call smart contract functions and you provide function arguments. Similar to API backends, smart contracts execute programmed logic, which can optionally modify smart contract storage. Unlike JSON API, you need to send a transaction to mutate the blockchain state, and you'll need to pay for each transaction you're sending. 40 | 41 | Finally, Ethereum nodes expose a JSON-RPC API. Through this API we can interact with a node to: get account balance, estimate gas costs, get blocks and transactions, send transactions, and execute contract calls without sending transactions (this is used to read data from smart contracts). [Here](https://eth.wiki/json-rpc/API) you can find the full list of available endpoints. 42 | 43 | > Transactions are also sent through the JSON-RPC API, see [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction). 44 | 45 | ## Local Development Environment 46 | 47 | Multiple smart contract development environments are used today: 48 | 1. [Truffle](https://trufflesuite.com) 49 | 1. [Hardhat](https://hardhat.org) 50 | 1. [Foundry](https://github.com/foundry-rs/foundry) 51 | 52 | Truffle is the oldest of the three and is the least popular of them. Hardhat is its improved descendant and is the most widely used tool. Foundry is the new kid on the block, which brings a different view on testing. 53 | 54 | While HardHat is still a popular solution, more and more projects are switching to Foundry. And there are multiple reasons for that: 55 | 1. With Foundry, we can write tests in Solidity. This is much more convenient because we don't need to jump between JavaScript (Truffle and HardHat use JS for tests and automation) and Solidity during development. Writing tests in Solidity is much more convenient because you have all the native features (e.g. you don't need a special type for big numbers and you don't need to convert between strings and [BigNumber](https://docs.ethers.io/v5/api/utils/bignumber/)). 56 | 1. Foundry doesn't run a node during testing. This makes testing and iterating on features much faster! Truffle and HardHat start a node whenever you run tests; Foundry executes tests on an internal EVM. 57 | 58 | That being said, we'll use Foundry as our main smart contract development and testing tool. 59 | 60 | ### Foundry 61 | 62 | [Foundry](https://github.com/foundry-rs/foundry) is a set of tools for Ethereum applications development. Specifically, we're going to use: 63 | 1. [Forge](https://github.com/foundry-rs/foundry/tree/master/forge), a testing framework for Solidity. 64 | 1. [Anvil](https://github.com/foundry-rs/foundry/tree/master/anvil), a local Ethereum node designed for development with Forge. We'll use it to deploy our contracts to a local node and connect to it through the front-end app. 65 | 1. [Cast](https://github.com/foundry-rs/foundry/tree/master/cast), a CLI tool with a ton of helpful features. 66 | 67 | Forge makes smart contracts developer's life so much easier. With Forge, we don't need to run a local node to test contracts. Instead, Forge runs tests on its internal EVM, which is much faster and doesn't require sending transactions and mining blocks. 68 | 69 | Forge lets us write tests in Solidity! Forge also makes it easier to simulate blockchain state: we can easily fake our ether or token balance, execute contracts from other addresses, deploy any contracts at any address, etc. 70 | 71 | However, we'll still need a local node to deploy our contract to. For that, we'll use Anvil. Front-end applications use JavaScript Web3 libraries to interact with Ethereum nodes (to send transactions, query state, estimate transaction gas cost, etc.)–this is why we'll need to run a local node. 72 | 73 | ### Ethers.js 74 | 75 | [Ethers.js](https://github.com/ethers-io/ethers.js/) is a set of Ethereum utilities written in JavaScript. This is one of the two (the other one is [web3.js](https://github.com/ChainSafe/web3.js)) most popular JavaScript libraries used in decentralized applications development. These libraries allow us to interact with an Ethereum node via the JSON-API, and they come with multiple utility functions that make the developer's life easier. 76 | 77 | ### MetaMask 78 | 79 | [MetaMask](https://metamask.io/) is an Ethereum wallet in your browser. It's a browser extension that creates and securely stores private keys. MetaMask is the main Ethereum wallet application used by millions of users. We'll use it to sign transactions that we'll send to our local node. 80 | 81 | ### React 82 | 83 | [React](https://reactjs.org/) is a well-known JavaScript library for building front-end applications. You don't need to know React, I'll provide a template application. 84 | 85 | ## Setting up the Project 86 | 87 | To set up the project, create a new folder and run `forge init` in it: 88 | ```shell 89 | $ mkdir uniswapv3clone 90 | $ cd uniswapv3clone 91 | $ forge init 92 | ``` 93 | 94 | > If you're using Visual Studio Code, add `--vscode` flag to `forge init`: `forge init --vscode`. Forge will initialize the project with VSCode-specific settings. 95 | 96 | Forge will create sample contracts in the `src`, `test`, and `script` folders–these can be removed. 97 | 98 | To set up the front-end application: 99 | ```shell 100 | $ npx create-react-app ui 101 | ``` 102 | 103 | It's located in a subfolder so there's no conflict between folder names. 104 | -------------------------------------------------------------------------------- /src/milestone_0/images/amm_simplified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/amm_simplified.png -------------------------------------------------------------------------------- /src/milestone_0/images/curve_finite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/curve_finite.png -------------------------------------------------------------------------------- /src/milestone_0/images/curve_infinite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/curve_infinite.png -------------------------------------------------------------------------------- /src/milestone_0/images/desmos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/desmos.png -------------------------------------------------------------------------------- /src/milestone_0/images/orderbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/orderbook.png -------------------------------------------------------------------------------- /src/milestone_0/images/the_curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/the_curve.png -------------------------------------------------------------------------------- /src/milestone_0/images/ticks_and_ranges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/ticks_and_ranges.png -------------------------------------------------------------------------------- /src/milestone_0/images/usdceth_liquidity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_0/images/usdceth_liquidity.png -------------------------------------------------------------------------------- /src/milestone_0/introduction-to-markets.md: -------------------------------------------------------------------------------- 1 | # Introduction to Markets 2 | 3 | ## How Centralized Exchanges Work 4 | In this book, we'll build a decentralized exchange (DEX) that will run on Ethereum. There are multiple approaches to how an exchange can be designed. All centralized exchanges have *an order book* at their core. An order book is just a journal that stores all the sell and buy orders that traders want to make. Each order in this book contains a price the order must be executed at and the amount that must be bought or sold. 5 | 6 | ![Order book example](images/orderbook.png) 7 | 8 | For trading to happen, there must exist *liquidity*, which is simply the availability of assets on a market. If you want to buy a wardrobe but no one is selling one, there's no liquidity. If you want to sell a wardrobe but no one wants to buy it, there's liquidity but no buyers. If there's no liquidity, there's nothing to buy or sell. 9 | 10 | On centralized exchanges, the order book is where liquidity is accumulated. If someone places a sell order, they provide liquidity to the market. If someone places a buy order, they expect the market to have liquidity, otherwise, no trade is possible. 11 | 12 | When there's no liquidity, but markets are still interested in trades, *market makers* come into play. A market maker is a firm or an individual who provides liquidity to markets, that is someone who has a lot of money and who buys different assets to sell them on exchanges. For this job market makers are paid by exchanges. **Market makers make money by providing liquidity to exchanges**. 13 | 14 | ## How Decentralized Exchanges Work 15 | 16 | Don't be surprised, decentralized exchanges also need liquidity. And they also need someone who provides it to traders of a wide variety of assets. However, this process cannot be handled in a centralized way. **A decentralized solution must be found.** There are multiple decentralized solutions and some of them are implemented differently. Our focus will be on how Uniswap solves this problem. 17 | 18 | ## Automated Market Makers 19 | 20 | [The evolution of on-chain markets](https://bennyattar.substack.com/p/the-evolution-of-amms) brought us to the idea of Automated Market Makers (AMM). As the name implies, this algorithm works exactly like market makers but in an automated way. Moreover, it's decentralized and permissionless, that is: 21 | - it's not governed by a single entity; 22 | - all assets are not stored in one place; 23 | - anyone can use it from anywhere. 24 | 25 | ### What Is an AMM? 26 | 27 | An AMM is a set of smart contracts that define how liquidity is managed. Each trading pair (e.g. ETH/USDC) is a separate contract that stores both ETH and USDC and that's programmed to mediate trades: exchanging ETH for USDC and vice versa. 28 | 29 | The core idea is **pooling**: each contract is a *pool* that stores liquidity and lets different users (including other smart contracts) trade in a permissionless way. There are two roles, *liquidity providers* and traders, and these roles interact with each other through pools of liquidity, and the way they can interact with pools is programmed and immutable. 30 | 31 | ![Automated Market Maker simplified](images/amm_simplified.png) 32 | 33 | What makes this approach different from centralized exchanges is that **the smart contracts are fully automated and not managed by anyone**. There are no managers, admins, privileged users, etc. There are only liquidity providers and traders (they can be the same people), and all the algorithms are programmed, immutable, and public. 34 | 35 | Let's now look closer at how Uniswap implements an AMM. 36 | 37 | > Please note that I use *pool* and *pair* terms interchangeably throughout the book because a Uniswap pool is a pair of two tokens. 38 | 39 | > If you have any questions, feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-0-introduction)! -------------------------------------------------------------------------------- /src/milestone_0/uniswap-v3.md: -------------------------------------------------------------------------------- 1 | # Introduction to Uniswap V3 2 | 3 | > This chapter retells [the whitepaper of Uniswap V3](https://uniswap.org/whitepaper-v3.pdf). Again, it's totally ok if you don't understand all the concepts. They will be clearer when converted to code. 4 | 5 | To better understand the innovations Uniswap V3 brings, let's first look at the imperfections of Uniswap V2. 6 | 7 | Uniswap V2 is a general exchange that implements one AMM algorithm. However, not all trading pairs are equal. Pairs can be grouped by price volatility: 8 | 9 | 1. Tokens with medium and high price volatility. This group includes most tokens since most tokens don't have their prices pegged to something and are subject to market fluctuations. 10 | 1. Tokens with low volatility. This group includes pegged tokens, mainly stablecoins: USDC/USDT, USDC/DAI, USDT/DAI, etc. Also: ETH/stETH, ETH/rETH (variants of wrapped ETH). 11 | 12 | These groups require different, let's call them, pool configurations. The main difference is that pegged tokens require high liquidity to reduce the demand effect (we learned about it in the previous chapter) on big trades. The prices of USDC and USDT must stay close to 1, no matter how big the number of tokens we want to buy and sell. Since Uniswap V2's general AMM algorithm is not very well suited for stablecoin trading, alternative AMMs (mainly [Curve](https://curve.fi)) were more popular for stablecoin trading. 13 | 14 | What caused this problem is that liquidity in Uniswap V2 pools is distributed infinitely–pool liquidity allows trades at any price, from 0 to infinity: 15 | 16 | ![The curve is infinite](images/curve_infinite.png) 17 | 18 | This might not seem like a bad thing, but this makes capital inefficient. Historical prices of an asset stay within some defined range, whether it's narrow or wide. For example, the historical price range of ETH is from $0.75 to $4,800 (according to [CoinMarketCap](https://coinmarketcap.com/currencies/ethereum/)). Today (June 2022, 1 ETH costs \$1,800), no one would buy 1 ether at \$5000, so it makes no sense to provide liquidity at this price. Thus, it doesn't make sense to provide liquidity in a price range that's far away from the current price or that will never be reached. 19 | 20 | > However, we all believe in ETH reaching \$10,000 one day. 21 | 22 | ## Concentrated Liquidity 23 | 24 | Uniswap V3 introduces *concentrated liquidity*: liquidity providers can now choose the price range they want to provide liquidity into. This improves capital efficiency by allowing to put more liquidity into a narrow price range, which makes Uniswap more diverse: it can now have pools configured for pairs with different volatility. This is how V3 improves V2. 25 | 26 | In a nutshell, a Uniswap V3 pair is many small Uniswap V2 pairs. The main difference between V2 and V3 is that, in V3, there are **many price ranges** in one pair. And each of these shorter price ranges has **finite reserves**. The entire price range from 0 to infinite is split into shorter price ranges, with each of them having its own amount of liquidity. But, what's crucial is that within that shorter price range, **it works exactly as Uniswap V2**. This is why I say that a V3 pair is many small V2 pairs. 27 | 28 | Now, let's try to visualize it. What we're saying is that we don't want the curve to be infinite. We cut it at the points $a$ and $b$ and say that these are the boundaries of the curve. Moreover, we shift the curve so the boundaries lay on the axes. This is what we get: 29 | 30 | ![Uniswap V3 price range](images/curve_finite.png) 31 | 32 | > It looks lonely, doesn't it? This is why there are many price ranges in Uniswap V3–so they don't feel lonely 🙂 33 | 34 | As we saw in the previous chapter, buying or selling tokens moves the price along the curve. A price range limits the movement of the price. When the price moves to either of the points, the pool becomes **depleted**: one of the token reserves will be 0, and buying this token won't be possible. 35 | 36 | On the chart above, let's assume that the start price is at the middle of the curve. To get to the point $a$, we need to buy all available $y$ and maximize $x$ in the range; to get to the point $b$, we need to buy all available $x$ and maximize $y$ in the range. At these points, there's only one token in the range! 37 | 38 | > Fun fact: this allows using Uniswap V3 price ranges as limit orders! 39 | 40 | What happens when the current price range gets depleted during a trade? The price slips into the next price range. If the next price range doesn't exist, the trade ends up partially fulfilled-we'll see how this works later in the book. 41 | 42 | This is how liquidity is spread in [the USDC/ETH pool in production](https://info.uniswap.org/#/pools/0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8): 43 | 44 | ![Liquidity in the real USDC/ETH pool](images/usdceth_liquidity.png) 45 | 46 | You can see that there's a lot of liquidity around the current price but the further away from it the less liquidity there is–this is because liquidity providers strive to have higher efficiency of their capital. Also, the whole range is not infinite, its upper boundary is shown in the image. 47 | 48 | ## The Mathematics of Uniswap V3 49 | 50 | Mathematically, Uniswap V3 is based on V2: it uses the same formulas, but they're... let's call it *augmented*. 51 | 52 | To handle transitioning between price ranges, simplify liquidity management, and avoid rounding errors, Uniswap V3 uses these new concepts: 53 | 54 | $$L = \sqrt{xy}$$ 55 | 56 | $$\sqrt{P} = \sqrt{\frac{y}{x}}$$ 57 | 58 | $L$ is *the amount of liquidity*. Liquidity in a pool is the combination of token reserves (that is, two numbers). We know that their product is $k$, and we can use this to derive the measure of liquidity, which is $\sqrt{xy}$–a number that, when multiplied by itself, equals $k$. $L$ is the geometric mean of $x$ and $y$. 59 | 60 | $y/x$ is the price of token 0 in terms of 1. Since token prices in a pool are reciprocals of each other, we can use only one of them in calculations (and by convention Uniswap V3 uses $y/x$). The price of token 1 in terms of token 0 is simply $\frac{1}{y/x}=\frac{x}{y}$. Similarly, $\frac{1}{\sqrt{P}} = \frac{1}{\sqrt{y/x}} = \sqrt{\frac{x}{y}}$. 61 | 62 | Why using $\sqrt{p}$ instead of $p$? There are two reasons: 63 | 64 | 1. Square root calculation is not precise and causes rounding errors. Thus, it's easier to store the square root without calculating it in the contracts (we will not store $x$ and $y$ in the contracts). 65 | 1. $\sqrt{P}$ has an interesting connection to $L$: $L$ is also the relation between the change in output amount and the change in $\sqrt{P}$. 66 | 67 | $$L = \frac{\Delta y}{\Delta\sqrt{P}}$$ 68 | 69 | > Proof: 70 | $$L = \frac{\Delta y}{\Delta\sqrt{P}}$$ 71 | $$\sqrt{xy} = \frac{y_1 - y_0}{\sqrt{P_1} - \sqrt{P_0}}$$ 72 | $$\sqrt{xy} (\sqrt{P_1} - \sqrt{P_0}) = y_1 - y_0$$ 73 | $$\sqrt{xy} (\sqrt{\frac{y_1}{x_1}} - \sqrt{\frac{y_0}{x_0}}) = y_1 - y_0$$ 74 | $$\textrm{Since } \sqrt{x_1y_1} = \sqrt{x_0y_0} = \sqrt{xy} = L,$$ 75 | $$\sqrt{\frac{x_1y_1y_1}{x_1}} - \sqrt{\frac{x_0y_0y_0}{x_0}} = y_1 - y_0$$ 76 | $$\sqrt{y_1^2} - \sqrt{y_0^2} = y_1 - y_0$$ 77 | $$y_1 - y_0 = y_1 - y_0$$ 78 | 79 | ## Pricing 80 | 81 | Again, we don't need to calculate actual prices–we can calculate the output amount right away. Also, since we're not going to track and store $x$ and $y$, our calculation will be based only on $L$ and $\sqrt{P}$. 82 | 83 | From the above formula, we can find $\Delta y$: 84 | 85 | $$\Delta y = \Delta \sqrt{P} L$$ 86 | 87 | > See the third step in the proof above. 88 | 89 | As we discussed above, prices in a pool are reciprocals of each other. Thus, $\Delta x$ is: 90 | 91 | $$\Delta x = \Delta \frac{1}{\sqrt{P}} L$$ 92 | 93 | $L$ and $\sqrt{P}$ allow us to not store and update pool reserves. Also, we don't need to calculate $\sqrt{P}$ each time because we can always find $\Delta \sqrt{P}$ and its reciprocal. 94 | 95 | ## Ticks 96 | 97 | As we learned in this chapter, the infinite price range of V2 is split into shorter price ranges in V3. Each of these shorter price ranges is limited by boundaries–upper and lower points. To track the coordinates of these boundaries, Uniswap V3 uses *ticks*. 98 | 99 | ![Price ranges and ticks](images/ticks_and_ranges.png) 100 | 101 | In V3, the entire price range is demarcated by evenly distributed discrete ticks. Each tick has an index and corresponds to a certain price: 102 | 103 | $$p(i) = 1.0001^i$$ 104 | 105 | Where $p(i)$ is the price at tick $i$. Taking powers of 1.0001 has a desirable property: the difference between two adjacent ticks is 0.01% or *1 basis point*. 106 | 107 | > Basis point (1/100th of 1%, or 0.01%, or 0.0001) is a unit of measure of percentages in finance. You could've heard about the basis point when central banks announced changes in interest rates. 108 | 109 | As we discussed above, Uniswap V3 stores $\sqrt{P}$, not $P$. Thus, the formula is in fact: 110 | 111 | $$\sqrt{p(i)} = \sqrt{1.0001}^i = 1.0001 ^{\frac{i}{2}}$$ 112 | 113 | So, we get values like: $\sqrt{p(0)} = 1$, $\sqrt{p(1)} = \sqrt{1.0001} \approx 1.00005$, $\sqrt{p(-1)} \approx 0.99995$. 114 | 115 | Ticks are integers that can be positive and negative and, of course, they're not infinite. Uniswap V3 stores $\sqrt{P}$ as a fixed point Q64.96 number, which is a rational number that uses 64 bits for the integer part and 96 bits for the fractional part. Thus, prices (equal to the square of $\sqrt{P}$) are within the range: $[2^{-128}, 2^{128}]$. And ticks are within the range: 116 | 117 | $$[log_{1.0001}2^{-128}, log_{1.0001}{2^{128}}] = [-887272, 887272]$$ 118 | 119 | > For deeper dive into the math of Uniswap V3, I cannot but recommend [this technical note](https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf) by [Atis Elsts](https://twitter.com/atiselsts). -------------------------------------------------------------------------------- /src/milestone_0/what-we-will-build.md: -------------------------------------------------------------------------------- 1 | # What We Will Build 2 | 3 | The goal of the book is to build a clone of Uniswap V3. However, we won't build an exact copy. The main reason is that Uniswap is a big project with many nuances and auxiliary mechanics–breaking down all of them would bloat the book and make it harder for readers to finish it. Instead, we'll build the core of Uniswap, its hardest and most important mechanisms. This includes liquidity management, swapping, fees, a periphery contract, a quoting contract, and an NFT contract. After that, I'm sure, you'll be able to read the source code of Uniswap V3 and understand all the mechanics that were left outside of the scope of this book. 4 | 5 | 6 | ## Smart Contracts 7 | 8 | After finishing the book, you'll have these contracts implemented: 9 | 1. `UniswapV3Pool`–the core pool contract that implements liquidity management and swapping. This contract is very close to [the original one](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol), however, some implementation details are different and something is missed for simplicity. For example, our implementation will only handle "exact input" swaps, that is swaps with known input amounts. The original implementation also supports swaps with known *output* amounts (i.e. when you want to buy a certain amount of tokens). 10 | 1. `UniswapV3Factory`–the registry contract that deploys new pools and keeps a record of all deployed pools. This one is mostly identical to [the original one](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol) besides the ability to change owner and fees. 11 | 1. `UniswapV3Manager`–a periphery contract that makes it easier to interact with the pool contract. This is a very simplified implementation of [SwapRouter](https://github.com/Uniswap/v3-periphery/blob/main/contracts/SwapRouter.sol). Again, as you can see, I don't distinguish "exact input" and "exact output" swaps and implement only the former ones. 12 | 1. `UniswapV3Quoter` is a cool contract that allows calculating swap prices on-chain. This is a minimal copy of both [Quoter](https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol) and [QuoterV2](https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol). Again, only "exact input" swaps are supported. 13 | 1. `UniswapV3NFTManager` allows turning liquidity positions into NFTs. This is a simplified implementation of [NonfungiblePositionManager](https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol). 14 | 15 | 16 | ## Front-end Application 17 | 18 | For this book, I also built a simplified clone of [the Uniswap UI](https://app.uniswap.org/). This is a very dumb clone, and my React and front-end skills are very poor, but it demonstrates how a front-end application can interact with smart contracts using Ethers.js and MetaMask. -------------------------------------------------------------------------------- /src/milestone_1/calculating-liquidity.md: -------------------------------------------------------------------------------- 1 | # Calculating liquidity 2 | 3 | Trading is not possible without liquidity, and to make our first swap we need to put some liquidity into the pool contract. Here's what we need to know to add liquidity to the pool contract: 4 | 5 | 1. A price range. As a liquidity provider, we want to provide liquidity at a specific price range, and it'll only be used in this range. 6 | 1. Amount of liquidity, which is the amounts of two tokens. We'll need to transfer these amounts to the pool contract. 7 | 8 | Here, we're going to calculate these manually, but, in a later chapter, a contract will do this for us. Let's begin with a price range. 9 | 10 | ## Price Range Calculation 11 | 12 | Recall that, in Uniswap V3, the entire price range is demarcated into ticks: each tick corresponds to a price and has an index. In our first pool implementation, we're going to buy ETH for USDC at the price of \$5000 per 1 ETH. Buying ETH will remove some amount of it from the pool and will push the price slightly above \$5000. We want to provide liquidity at a range that includes this price. And we want to be sure that the final price will stay **within this range** (we'll do multi-range swaps in a later milestone). 13 | 14 | We'll need to find three ticks: 15 | 1. The current tick will correspond to the current price (5000 USDC for 1 ETH). 16 | 1. The lower and upper bounds of the price range we're providing liquidity into. Let the lower price be \$4545 and the upper price be \$5500. 17 | 18 | From the theoretical introduction, we know that: 19 | 20 | $$\sqrt{P} = \sqrt{\frac{y}{x}}$$ 21 | 22 | Since we've agreed to use ETH as the $x$ reserve and USDC as the $y$ reserve, the prices at each of the ticks are: 23 | 24 | $$\sqrt{P_c} = \sqrt{\frac{5000}{1}} = \sqrt{5000} \approx 70.71$$ 25 | 26 | $$\sqrt{P_l} = \sqrt{\frac{4545}{1}} \approx 67.42$$ 27 | 28 | $$\sqrt{P_u} = \sqrt{\frac{5500}{1}} \approx 74.16$$ 29 | 30 | Where $P_c$ is the current price, $P_l$ is the lower bound of the range, and $P_u$ is the upper bound of the range. 31 | 32 | Now, we can find corresponding ticks. We know that prices and ticks are connected via this formula: 33 | 34 | $$\sqrt{P(i)}=1.0001^{\frac{i}{2}}$$ 35 | 36 | Thus, we can find tick $i$ via: 37 | 38 | $$i = log_{\sqrt{1.0001}} \sqrt{P(i)}$$ 39 | 40 | > The square roots in this formula cancel out, but since we're working with $\sqrt{p}$ we need to preserve them. 41 | 42 | Let's find the ticks: 43 | 1. Current tick: $i_c = log_{\sqrt{1.0001}} 70.71 = 85176$ 44 | 1. Lower tick: $i_l = log_{\sqrt{1.0001}} 67.42 = 84222$ 45 | 1. Upper tick: $i_u = log_{\sqrt{1.0001}} 74.16 = 86129$ 46 | 47 | > To calculate these, I used Python: 48 | > ```python 49 | > import math 50 | > 51 | > def price_to_tick(p): 52 | > return math.floor(math.log(p, 1.0001)) 53 | > 54 | > price_to_tick(5000) 55 | > > 85176 56 | >``` 57 | 58 | That's it for price range calculation! 59 | 60 | Last thing to note here is that Uniswap uses [Q64.96 number](https://en.wikipedia.org/wiki/Q_%28number_format%29) to store $\sqrt{P}$. This is a fixed-point number that has 64 bits for the integer part and 96 bits for the fractional part. In our above calculations, prices are floating point numbers: `70.71`, `67.42`, and `74.16`. We need to convert them to Q64.96. Luckily, this is simple: we need to multiply the numbers by $2^{96}$ (Q-number is a binary fixed-point number, so we need to multiply our decimals numbers by the base of Q64.96, which is $2^{96}$). We'll get: 61 | 62 | $$\sqrt{P_c} = 5602277097478614198912276234240$$ 63 | 64 | $$\sqrt{P_l} = 5314786713428871004159001755648$$ 65 | 66 | $$\sqrt{P_u} = 5875717789736564987741329162240$$ 67 | 68 | > In Python: 69 | > ```python 70 | > q96 = 2**96 71 | > def price_to_sqrtp(p): 72 | > return int(math.sqrt(p) * q96) 73 | > 74 | > price_to_sqrtp(5000) 75 | > > 5602277097478614198912276234240 76 | > ``` 77 | > Notice that we're multiplying before converting to an integer. Otherwise, we'll lose precision. 78 | 79 | ## Token Amounts Calculation 80 | 81 | The next step is to decide how many tokens we want to deposit into the pool. The answer is as many as we want. The amounts are not strictly defined, we can deposit as much as it is enough to buy a small amount of ETH without making the current price leave the price range we put liquidity into. During development and testing we'll be able to mint any amount of tokens, so getting the amounts we want is not a problem. 82 | 83 | For our first swap, let's deposit 1 ETH and 5000 USDC. 84 | 85 | > Recall that the proportion of current pool reserves tells the current spot price. So if we want to put more tokens into the pool and keep the same price, the amounts must be proportional, e.g.: 2 ETH and 10,000 USDC; 10 ETH and 50,000 USDC, etc. 86 | 87 | ## Liquidity Amount Calculation 88 | 89 | Next, we need to calculate $L$ based on the amounts we'll deposit. This is a tricky part, so hold tight! 90 | 91 | From the theoretical introduction, you remember that: 92 | $$L = \sqrt{xy}$$ 93 | 94 | However, this formula is for the infinite curve 🙂 But we want to put liquidity into a limited price range, which is just a segment of that infinite curve. We need to calculate $L$ specifically for the price range we're going to deposit liquidity into. We need some more advanced calculations. 95 | 96 | To calculate $L$ for a price range, let's look at one interesting fact we have discussed earlier: price ranges can be depleted. It's possible to buy the entire amount of one token from a price range and leave the pool with only the other token. 97 | 98 | ![Range depletion example](images/range_depleted.png) 99 | 100 | At the points $a$ and $b$, there's only one token in the range: ETH at the point $a$ and USDC at the point $b$. 101 | 102 | That being said, we want to find an $L$ that will allow the price to move to either of the points. We want enough liquidity for the price to reach either of the boundaries of a price range. Thus, we want $L$ to be calculated based on the maximum amounts of $\Delta x$ and $\Delta y$. 103 | 104 | Now, let's see what the prices are at the edges. When ETH is bought from a pool, the price is growing; when USDC is bought, the price is falling. Recall that the price is $\frac{y}{x}$. So, at point $a$, the price is the lowest of the range; at point $b$, the price is the highest. 105 | 106 | >In fact, prices are not defined at these points because there's only one reserve in the pool, but what we need to understand here is that the price around the point $b$ is higher than the start price, and the price at the point $a$ is lower than the start price. 107 | 108 | Now, break the curve from the image above into two segments: one to the left of the start point and one to the right of the start point. We're going to calculate **two** $L$'s, one for each of the segments. Why? Because each of the two tokens of a pool contributes to **either of the segments**: the left segment is made entirely of token $x$, and the right segment is made entirely of token $y$. This comes from the fact that, during swapping, the price moves in either direction: it's either growing or falling. For the price to move, only either of the tokens is needed: 109 | 1. when the price is growing, only token $x$ is needed for the swap (we're buying token $x$, so we want to take only token $x$ from the pool); 110 | 1. when the price is falling, only token $y$ is needed for the swap. 111 | 112 | Thus, the liquidity in the segment of the curve to the left of the current price consists only of token $x$ and is calculated only from the amount of token $x$ provided. Similarly, the liquidity in the segment of the curve to the right of the current price consists only of token $y$ and is calculated only from the amount of token $y$ provided. 113 | 114 | ![Liquidity on the curve](images/curve_liquidity.png) 115 | 116 | This is why, when providing liquidity, we calculate two $L$'s and pick one of them. Which one? The smaller one. Why? Because the bigger one already includes the smaller one! We want the new liquidity to be distributed **evenly** along the curve, thus we want to add the same $L$ to the left and to the right of the current price. If we pick the bigger one, the user would need to provide more liquidity to compensate for the shortage in the smaller one. This is doable, of course, but this would make the smart contract more complex. 117 | 118 | > What happens with the remainder of the bigger $L$? Well, nothing. After picking the smaller $L$ we can simply convert it to a smaller amount of the token that resulted in the bigger $L$–this will adjust it down. After that, we'll have token amounts that will result in the same $L$. 119 | 120 | The final detail I need to focus your attention on here is: **new liquidity must not change the current price**. That is, it must be proportional to the current proportion of the reserves. And this is why the two $L$'s can be different–when the proportion is not preserved. And we pick the small $L$ to reestablish the proportion. 121 | 122 | I hope this will make more sense after we implement this in code! Now, let's look at the formulas. 123 | 124 | Let's recall how $\Delta x$ and $\Delta y$ are calculated: 125 | 126 | $$\Delta x = \Delta \frac{1}{\sqrt{P}} L$$ 127 | $$\Delta y = \Delta \sqrt{P} L$$ 128 | 129 | We can expand these formulas by replacing the delta P's with actual prices (we know them from the above): 130 | 131 | $$\Delta x = (\frac{1}{\sqrt{P_c}} - \frac{1}{\sqrt{P_b}}) L$$ 132 | $$\Delta y = (\sqrt{P_c} - \sqrt{P_a}) L$$ 133 | 134 | $P_a$ is the price at the point $a$, $P_b$ is the price at the point $b$, and $P_c$ is the current price (see the above chart). Notice that, since the price is calculated as $\frac{y}{x}$ (i.e. it's the price of $x$ in terms of $y$), the price at point $b$ is higher than the current price and the price at $a$. The price at $a$ is the lowest of the three. 135 | 136 | Let's find the $L$ from the first formula: 137 | 138 | $$\Delta x = (\frac{1}{\sqrt{P_c}} - \frac{1}{\sqrt{P_b}}) L$$ 139 | $$\Delta x = \frac{L}{\sqrt{P_c}} - \frac{L}{\sqrt{P_b}}$$ 140 | $$\Delta x = \frac{L(\sqrt{P_b} - \sqrt{P_c})}{\sqrt{P_b} \sqrt{P_c}}$$ 141 | $$L = \Delta x \frac{\sqrt{P_b} \sqrt{P_c}}{\sqrt{P_b} - \sqrt{P_c}}$$ 142 | 143 | And from the second formula: 144 | $$\Delta y = (\sqrt{P_c} - \sqrt{P_a}) L$$ 145 | $$L = \frac{\Delta y}{\sqrt{P_c} - \sqrt{P_a}}$$ 146 | 147 | So, these are our two $L$'s, one for each of the segments: 148 | 149 | $$L = \Delta x \frac{\sqrt{P_b} \sqrt{P_c}}{\sqrt{P_b} - \sqrt{P_c}}$$ 150 | $$L = \frac{\Delta y}{\sqrt{P_c} - \sqrt{P_a}}$$ 151 | 152 | Now, let's plug the prices we calculated earlier into them: 153 | 154 | $$L = \Delta x \frac{\sqrt{P_b}\sqrt{P_c}}{\sqrt{P_b}-\sqrt{P_c}} = 1 ETH * \frac{5875... * 5602...}{5875... - 5602...}$$ 155 | 156 | After converting to Q64.96, we get: 157 | 158 | $$L = 1519437308014769733632$$ 159 | 160 | And for the other $L$: 161 | $$L = \frac{\Delta y}{\sqrt{P_c}-\sqrt{P_a}} = \frac{5000USDC}{5602... - 5314...}$$ 162 | $$L = 1517882343751509868544$$ 163 | 164 | Of these two, we'll pick the smaller one. 165 | 166 | > In Python: 167 | > ```python 168 | > sqrtp_low = price_to_sqrtp(4545) 169 | > sqrtp_cur = price_to_sqrtp(5000) 170 | > sqrtp_upp = price_to_sqrtp(5500) 171 | > 172 | > def liquidity0(amount, pa, pb): 173 | > if pa > pb: 174 | > pa, pb = pb, pa 175 | > return (amount * (pa * pb) / q96) / (pb - pa) 176 | > 177 | > def liquidity1(amount, pa, pb): 178 | > if pa > pb: 179 | > pa, pb = pb, pa 180 | > return amount * q96 / (pb - pa) 181 | > 182 | > eth = 10**18 183 | > amount_eth = 1 * eth 184 | > amount_usdc = 5000 * eth 185 | > 186 | > liq0 = liquidity0(amount_eth, sqrtp_cur, sqrtp_upp) 187 | > liq1 = liquidity1(amount_usdc, sqrtp_cur, sqrtp_low) 188 | > liq = int(min(liq0, liq1)) 189 | > > 1517882343751509868544 190 | > ``` 191 | 192 | ## Token Amounts Calculation, Again 193 | 194 | Since we choose the amounts we're going to deposit, the amounts can be wrong. We cannot deposit any amounts at any price range; the liquidity amount needs to be distributed evenly along the curve of the price range we're depositing into. Thus, even though users choose amounts, the contract needs to re-calculate them, and actual amounts will be slightly different (at least because of rounding). 195 | 196 | Luckily, we already know the formulas: 197 | 198 | $$\Delta x = \frac{L(\sqrt{P_b} - \sqrt{P_c})}{\sqrt{P_b} \sqrt{P_c}}$$ 199 | $$\Delta y = L(\sqrt{P_c} - \sqrt{P_a})$$ 200 | 201 | > In Python: 202 | > ```python 203 | > def calc_amount0(liq, pa, pb): 204 | > if pa > pb: 205 | > pa, pb = pb, pa 206 | > return int(liq * q96 * (pb - pa) / pa / pb) 207 | > 208 | > 209 | > def calc_amount1(liq, pa, pb): 210 | > if pa > pb: 211 | > pa, pb = pb, pa 212 | > return int(liq * (pb - pa) / q96) 213 | > 214 | > amount0 = calc_amount0(liq, sqrtp_upp, sqrtp_cur) 215 | > amount1 = calc_amount1(liq, sqrtp_low, sqrtp_cur) 216 | > (amount0, amount1) 217 | > > (998976618347425408, 5000000000000000000000) 218 | > ``` 219 | > As you can see, the numbers are close to the amounts we want to provide, but ETH is slightly smaller. 220 | 221 | > **Hint**: use `cast --from-wei AMOUNT` to convert from wei to ether, e.g.: 222 | > `cast --from-wei 998976618347425280` will give you `0.998976618347425280`. -------------------------------------------------------------------------------- /src/milestone_1/first-swap.md: -------------------------------------------------------------------------------- 1 | # First Swap 2 | 3 | Now that we have liquidity, we can make our first swap! 4 | 5 | ## Calculating Swap Amounts 6 | 7 | The first step, of course, is to figure out how to calculate swap amounts. And, again, let's pick and hardcode some amount of USDC we're going to trade in for ETH. Let it be 42! We're going to buy ETH for 42 USDC. 8 | 9 | After deciding how many tokens we want to sell, we need to calculate how many tokens we'll get in exchange. In Uniswap V2, we would've used current pool reserves, but in Uniswap V3 we have $L$ and $\sqrt{P}$ and we know the fact that when swapping within a price range, only $\sqrt{P}$ changes and $L$ remains unchanged (Uniswap V3 acts exactly as V2 when swapping is done only within one price range). We also know that: 10 | 11 | $$L = \frac{\Delta y}{\Delta \sqrt{P}}$$ 12 | 13 | And... we know $\Delta y$! This is the 42 USDC we're going to trade in! Thus, we can find how selling 42 USDC will affect the current $\sqrt{P}$ given the $L$: 14 | 15 | $$\Delta \sqrt{P} = \frac{\Delta y}{L}$$ 16 | 17 | In Uniswap V3, we choose **the price we want our trade to lead to** (recall that swapping changes the current price, i.e. it moves the current price along the curve). Knowing the target price, the contract will calculate the amount of input token it needs to take from us and the respective amount of output token it'll give us. 18 | 19 | Let's plug our numbers into the above formula: 20 | 21 | $$\Delta \sqrt{P} = \frac{42 \enspace USDC}{1517882343751509868544} = 2192253463713690532467206957$$ 22 | 23 | After adding this to the current $\sqrt{P}$, we'll get the target price: 24 | 25 | $$\sqrt{P_{target}} = \sqrt{P_{current}} + \Delta \sqrt{P}$$ 26 | 27 | $$\sqrt{P_{target}} = 5604469350942327889444743441197$$ 28 | 29 | > To calculate the target price in Python: 30 | > ```python 31 | > amount_in = 42 * eth 32 | > price_diff = (amount_in * q96) // liq 33 | > price_next = sqrtp_cur + price_diff 34 | > print("New price:", (price_next / q96) ** 2) 35 | > print("New sqrtP:", price_next) 36 | > print("New tick:", price_to_tick((price_next / q96) ** 2)) 37 | > # New price: 5003.913912782393 38 | > # New sqrtP: 5604469350942327889444743441197 39 | > # New tick: 85184 40 | > ``` 41 | 42 | After finding the target price, we can calculate token amounts using the amounts calculation functions from a previous chapter: 43 | 44 | $$ x = \frac{L(\sqrt{p_b}-\sqrt{p_a})}{\sqrt{p_b}\sqrt{p_a}}$$ 45 | $$ y = L(\sqrt{p_b}-\sqrt{p_a}) $$ 46 | 47 | > In Python: 48 | > ```python 49 | > amount_in = calc_amount1(liq, price_next, sqrtp_cur) 50 | > amount_out = calc_amount0(liq, price_next, sqrtp_cur) 51 | > 52 | > print("USDC in:", amount_in / eth) 53 | > print("ETH out:", amount_out / eth) 54 | > # USDC in: 42.0 55 | > # ETH out: 0.008396714242162444 56 | > ``` 57 | 58 | To verify the amounts, let's recall another formula: 59 | 60 | $$\Delta x = \Delta \frac{1}{\sqrt{P}} L$$ 61 | 62 | Using this formula, we can find the amount of ETH we're buying, $\Delta x$, knowing the price change, $\Delta\frac{1}{\sqrt{P}}$, and liquidity $L$. Be careful though: $\Delta \frac{1}{\sqrt{P}}$ is not $\frac{1}{\Delta \sqrt{P}}$! The former is the change in the price of ETH, and it can be found using this expression: 63 | 64 | $$\Delta \frac{1}{\sqrt{P}} = \frac{1}{\sqrt{P_{target}}} - \frac{1}{\sqrt{P_{current}}}$$ 65 | 66 | Luckily, we already know all the values, so we can plug them in right away (this might not fit on your screen!): 67 | 68 | $$\Delta \frac{1}{\sqrt{P}} = \frac{1}{5604469350942327889444743441197} - \frac{1}{5602277097478614198912276234240}$$ 69 | 70 | $$= -6.982190286589445\text{e-}35 * 2^{96} $$ 71 | $$= -0.00000553186106731426$$ 72 | 73 | Now, let's find $\Delta x$: 74 | 75 | $$\Delta x = -0.00000553186106731426 * 1517882343751509868544 = -8396714242162698 $$ 76 | 77 | Which is 0.008396714242162698 ETH, and it's very close to the amount we found above! Notice that this amount is negative since we're removing it from the pool. 78 | 79 | ## Implementing a Swap 80 | 81 | Swapping is implemented in the `swap` function: 82 | ```solidity 83 | function swap(address recipient) 84 | public 85 | returns (int256 amount0, int256 amount1) 86 | { 87 | ... 88 | ``` 89 | At this moment, it only takes a recipient, who is a receiver of tokens. 90 | 91 | First, we need to find the target price and tick, as well as calculate the token amounts. Again, we'll simply hard-code the values we calculated earlier to keep things as simple as possible: 92 | ```solidity 93 | ... 94 | int24 nextTick = 85184; 95 | uint160 nextPrice = 5604469350942327889444743441197; 96 | 97 | amount0 = -0.008396714242162444 ether; 98 | amount1 = 42 ether; 99 | ... 100 | ``` 101 | 102 | Next, we need to update the current tick and `sqrtP` since trading affects the current price: 103 | ```solidity 104 | ... 105 | (slot0.tick, slot0.sqrtPriceX96) = (nextTick, nextPrice); 106 | ... 107 | ``` 108 | 109 | Next, the contract sends tokens to the recipient and lets the caller transfer the input amount into the contract: 110 | ```solidity 111 | ... 112 | IERC20(token0).transfer(recipient, uint256(-amount0)); 113 | 114 | uint256 balance1Before = balance1(); 115 | IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( 116 | amount0, 117 | amount1 118 | ); 119 | if (balance1Before + uint256(amount1) < balance1()) 120 | revert InsufficientInputAmount(); 121 | ... 122 | ``` 123 | 124 | Again, we're using a callback to pass the control to the caller and let it transfer the tokens. After that, we check that the pool's balance is correct and includes the input amount. 125 | 126 | Finally, the contract emits a `Swap` event to make the swap discoverable. The event includes all the information about the swap: 127 | ```solidity 128 | ... 129 | emit Swap( 130 | msg.sender, 131 | recipient, 132 | amount0, 133 | amount1, 134 | slot0.sqrtPriceX96, 135 | liquidity, 136 | slot0.tick 137 | ); 138 | ``` 139 | 140 | And that's it! The function simply sends some amount of tokens to the specified recipient address and expects a certain number of the other tokens in exchange. Throughout this book, the function will get much more complicated. 141 | 142 | ## Testing Swapping 143 | 144 | Now, we can test the swap function. In the same test file, create the `testSwapBuyEth` function and set up the test case. This test case uses the same parameters as `testMintSuccess`: 145 | ```solidity 146 | function testSwapBuyEth() public { 147 | TestCaseParams memory params = TestCaseParams({ 148 | wethBalance: 1 ether, 149 | usdcBalance: 5000 ether, 150 | currentTick: 85176, 151 | lowerTick: 84222, 152 | upperTick: 86129, 153 | liquidity: 1517882343751509868544, 154 | currentSqrtP: 5602277097478614198912276234240, 155 | shouldTransferInCallback: true, 156 | mintLiqudity: true 157 | }); 158 | (uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params); 159 | 160 | ... 161 | ``` 162 | 163 | The next steps will be different, however. 164 | 165 | > We're not going to test that liquidity has been correctly added to the pool since we tested this functionality in the other test cases. 166 | 167 | To make the test swap, we need 42 USDC: 168 | ```solidity 169 | token1.mint(address(this), 42 ether); 170 | ``` 171 | 172 | Before making the swap, we need to ensure we can transfer tokens to the pool contract when it requests them: 173 | ```solidity 174 | function uniswapV3SwapCallback(int256 amount0, int256 amount1) public { 175 | if (amount0 > 0) { 176 | token0.transfer(msg.sender, uint256(amount0)); 177 | } 178 | 179 | if (amount1 > 0) { 180 | token1.transfer(msg.sender, uint256(amount1)); 181 | } 182 | } 183 | ``` 184 | Since amounts during a swap can be positive (the amount that's sent to the pool) and negative (the amount that's taken from the pool), in the callback, we only want to send the positive amount, i.e. the amount we're trading in. 185 | 186 | Now, we can call `swap`: 187 | ```solidity 188 | (int256 amount0Delta, int256 amount1Delta) = pool.swap(address(this)); 189 | ``` 190 | 191 | The function returns token amounts used in the swap, and we can check them right away: 192 | ```solidity 193 | assertEq(amount0Delta, -0.008396714242162444 ether, "invalid ETH out"); 194 | assertEq(amount1Delta, 42 ether, "invalid USDC in"); 195 | ``` 196 | 197 | Then, we need to ensure that tokens were transferred from the caller: 198 | ```solidity 199 | assertEq( 200 | token0.balanceOf(address(this)), 201 | uint256(userBalance0Before - amount0Delta), 202 | "invalid user ETH balance" 203 | ); 204 | assertEq( 205 | token1.balanceOf(address(this)), 206 | 0, 207 | "invalid user USDC balance" 208 | ); 209 | ``` 210 | 211 | And sent to the pool contract: 212 | ```solidity 213 | assertEq( 214 | token0.balanceOf(address(pool)), 215 | uint256(int256(poolBalance0) + amount0Delta), 216 | "invalid pool ETH balance" 217 | ); 218 | assertEq( 219 | token1.balanceOf(address(pool)), 220 | uint256(int256(poolBalance1) + amount1Delta), 221 | "invalid pool USDC balance" 222 | ); 223 | ``` 224 | 225 | Finally, we're checking that the pool state was updated correctly: 226 | ```solidity 227 | (uint160 sqrtPriceX96, int24 tick) = pool.slot0(); 228 | assertEq( 229 | sqrtPriceX96, 230 | 5604469350942327889444743441197, 231 | "invalid current sqrtP" 232 | ); 233 | assertEq(tick, 85184, "invalid current tick"); 234 | assertEq( 235 | pool.liquidity(), 236 | 1517882343751509868544, 237 | "invalid current liquidity" 238 | ); 239 | ``` 240 | 241 | Notice that swapping doesn't change the current liquidity–in a later chapter, we'll see when it does change it. 242 | 243 | ## Homework 244 | 245 | Write a test that fails with an `InsufficientInputAmount` error. Keep in mind that there's a hidden bug 🙂 -------------------------------------------------------------------------------- /src/milestone_1/images/buy_eth_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/buy_eth_model.png -------------------------------------------------------------------------------- /src/milestone_1/images/curve_liquidity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/curve_liquidity.png -------------------------------------------------------------------------------- /src/milestone_1/images/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/metamask.png -------------------------------------------------------------------------------- /src/milestone_1/images/range_depleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/range_depleted.png -------------------------------------------------------------------------------- /src/milestone_1/images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/ui.png -------------------------------------------------------------------------------- /src/milestone_1/images/ui_metamask_connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_1/images/ui_metamask_connected.png -------------------------------------------------------------------------------- /src/milestone_1/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | In this milestone, we'll build a pool contract that can receive liquidity from users and make swaps within a price range. To keep it as simple as possible, we'll provide liquidity only in one price range and we'll allow to make swaps only in one direction. Also, we'll calculate all the required math manually to get better intuition before starting to use mathematical libs in Solidity. 4 | 5 | Let's model the situation we'll build: 6 | 1. There will be an ETH/USDC pool contract. ETH will be the \\(x\\) reserve, and USDC will be the \\(y\\) reserve. 7 | 1. We'll set the current price to 5000 USDC per 1 ETH. 8 | 1. The range we'll provide liquidity into is 4545-5500 USDC per 1 ETH. 9 | 1. We'll buy some ETH from the pool. At this point, since we have only one price range, we want the price of the trade 10 | to stay within the price range. 11 | 12 | Visually, this model looks like this: 13 | 14 | ![Buy ETH for USDC visualization](images/buy_eth_model.png) 15 | 16 | Before getting to the code, let's figure out the math and calculate all the parameters of the model. To keep things simple, I'll do math calculations in Python before implementing them in Solidity. This will allow us to focus on the math without diving into the nuances of math in Solidity. This also means that, in smart contracts, we'll hardcode all the amounts. This will allow us to start with a simple minimal viable product. 17 | 18 | For your convenience, I put all the Python calculations in [unimath.py](https://github.com/Jeiwan/uniswapv3-code/blob/main/unimath.py). 19 | 20 | > You'll find the complete code of this milestone in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_1). 21 | 22 | > If you have any questions, feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-1-first-swap)! -------------------------------------------------------------------------------- /src/milestone_1/manager-contract.md: -------------------------------------------------------------------------------- 1 | # Manager Contract 2 | 3 | Before deploying our pool contract, we need to solve one problem. As you remember, Uniswap V3 contracts are split into two categories: 4 | 1. Core contracts that implement the core functions and don't provide user-friendly interfaces. 5 | 2. Periphery contracts that implement user-friendly interfaces for the core contracts. 6 | 7 | The pool contract is a core contract, it's not supposed to be user-friendly and flexible. It expects the caller to do all the calculations (prices, amounts) and to provide proper call parameters. It also doesn't use ERC20's `transferFrom` to transfer tokens from the caller. Instead, it uses two callbacks: 8 | 1. `uniswapV3MintCallback`, which is called when minting liquidity; 9 | 1. `uniswapV3SwapCallback`, which is called when swapping tokens. 10 | 11 | In our tests, we implemented these callbacks in the test contract. Since it's only a contract that can implement them, the pool contract cannot be called by regular users (non-contract addresses). This is fine. But not anymore 🙂. 12 | 13 | Our next step in the book is deploying the pool contract to a local blockchain and interacting with it from a front-end app. Thus, we need to build a contract that will let non-contract addresses interact with the pool. Let's do this now! 14 | 15 | ## Workflow 16 | 17 | This is how the manager contract will work: 18 | 1. To mint liquidity, we'll approve the spending of tokens to the manager contract. 19 | 1. We'll then call the `mint` function of the manager contract and pass it minting parameters, as well as the address of the pool we want to provide liquidity into. 20 | 1. The manager contract will call the pool's `mint` function and will implement `uniswapV3MintCallback`. It'll have permission to send our tokens to the pool contract. 21 | 1. To swap tokens, we'll also approve the spending of tokens to the manager contract. 22 | 1. We'll then call the `swap` function of the manager contract and, similarly to minting, it'll pass the call to the pool. 23 | The manager contract will send our tokens to the pool contract, and the pool contract will swap them and send the output amount to us. 24 | 25 | Thus, the manager contract will act as an intermediary between users and pools. 26 | 27 | ## Passing Data to Callbacks 28 | 29 | Before implementing the manager contract, we need to upgrade the pool contract. 30 | 31 | The manager contract will work with any pool and it'll allow any address to call it. To achieve this, we need to upgrade the callbacks: we want to pass different pool addresses and user addresses to them. Let's look at our current implementation of `uniswapV3MintCallback` (in the test contract): 32 | ```solidity 33 | function uniswapV3MintCallback(uint256 amount0, uint256 amount1) public { 34 | if (transferInMintCallback) { 35 | token0.transfer(msg.sender, amount0); 36 | token1.transfer(msg.sender, amount1); 37 | } 38 | } 39 | ``` 40 | 41 | Key points here: 42 | 1. The function transfers tokens belonging to the test contract–we want it to transfer tokens from the caller by using `transferFrom`. 43 | 1. The function knows `token0` and `token1`, which will be different for every pool. 44 | 45 | Idea: we need to change the arguments of the callback so we can pass user and pool addresses. 46 | 47 | Now, let's look at the swap callback: 48 | ```solidity 49 | function uniswapV3SwapCallback(int256 amount0, int256 amount1) public { 50 | if (amount0 > 0 && transferInSwapCallback) { 51 | token0.transfer(msg.sender, uint256(amount0)); 52 | } 53 | 54 | if (amount1 > 0 && transferInSwapCallback) { 55 | token1.transfer(msg.sender, uint256(amount1)); 56 | } 57 | } 58 | ``` 59 | 60 | Identically, it transfers tokens from the test contract and it knows `token0` and `token1`. 61 | 62 | To pass the extra data to the callbacks, we need to pass it to `mint` and `swap` first (since callbacks are called from these functions). However, since this extra data is not used in the functions and to not make their arguments messier, we'll encode the extra data using [abi.encode()](https://docs.soliditylang.org/en/latest/units-and-global-variables.html?highlight=abi.encode#abi-encoding-and-decoding-functions). 63 | 64 | Let's define the extra data as a structure: 65 | ```solidity 66 | // src/UniswapV3Pool.sol 67 | ... 68 | struct CallbackData { 69 | address token0; 70 | address token1; 71 | address payer; 72 | } 73 | ... 74 | ``` 75 | 76 | And then pass encoded data to the callbacks: 77 | ```solidity 78 | function mint( 79 | address owner, 80 | int24 lowerTick, 81 | int24 upperTick, 82 | uint128 amount, 83 | bytes calldata data // <--- New line 84 | ) external returns (uint256 amount0, uint256 amount1) { 85 | ... 86 | IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback( 87 | amount0, 88 | amount1, 89 | data // <--- New line 90 | ); 91 | ... 92 | } 93 | 94 | function swap(address recipient, bytes calldata data) // <--- `data` added 95 | public 96 | returns (int256 amount0, int256 amount1) 97 | { 98 | ... 99 | IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( 100 | amount0, 101 | amount1, 102 | data // <--- New line 103 | ); 104 | ... 105 | } 106 | ``` 107 | 108 | Now, we can read the extra data in the callbacks in the test contract. 109 | ```solidity 110 | function uniswapV3MintCallback( 111 | uint256 amount0, 112 | uint256 amount1, 113 | bytes calldata data 114 | ) public { 115 | if (transferInMintCallback) { 116 | UniswapV3Pool.CallbackData memory extra = abi.decode( 117 | data, 118 | (UniswapV3Pool.CallbackData) 119 | ); 120 | 121 | IERC20(extra.token0).transferFrom(extra.payer, msg.sender, amount0); 122 | IERC20(extra.token1).transferFrom(extra.payer, msg.sender, amount1); 123 | } 124 | } 125 | ``` 126 | 127 | Try updating the rest of the code yourself, and if it gets too difficult, feel free to peek [at this commit](https://github.com/Jeiwan/uniswapv3-code/commit/cda23134fd12a190aaeebe718786545621e16c0e). 128 | 129 | ## Implementing Manager Contract 130 | 131 | Besides implementing the callbacks, the manager contract won't do much: it'll simply redirect calls to a pool contract. This is a very minimalistic contract at this moment: 132 | ```solidity 133 | pragma solidity ^0.8.14; 134 | 135 | import "../src/UniswapV3Pool.sol"; 136 | import "../src/interfaces/IERC20.sol"; 137 | 138 | contract UniswapV3Manager { 139 | function mint( 140 | address poolAddress_, 141 | int24 lowerTick, 142 | int24 upperTick, 143 | uint128 liquidity, 144 | bytes calldata data 145 | ) public { 146 | UniswapV3Pool(poolAddress_).mint( 147 | msg.sender, 148 | lowerTick, 149 | upperTick, 150 | liquidity, 151 | data 152 | ); 153 | } 154 | 155 | function swap(address poolAddress_, bytes calldata data) public { 156 | UniswapV3Pool(poolAddress_).swap(msg.sender, data); 157 | } 158 | 159 | function uniswapV3MintCallback(...) {...} 160 | function uniswapV3SwapCallback(...) {...} 161 | } 162 | ``` 163 | 164 | The callbacks are identical to those in the test contract, with the exception that there are no `transferInMintCallback` and `transferInSwapCallback` flags since the manager contract always transfers tokens. 165 | 166 | Well, we're now fully prepared to deploy and integrate with a front-end app! -------------------------------------------------------------------------------- /src/milestone_1/user-interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | Finally, we made it to the final stop of this milestone–building a user interface! 4 | 5 | ![Interface of the UI app](images/ui.png) 6 | 7 | Since building a front-end app is not the main goal of this book, I won't show how to build such an app from scratch. Instead, I'll show how to use MetaMask to interact with smart contracts. 8 | 9 | >If you want to experiment with the app and run it locally, you can fund it in the [ui](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_1/ui) folder in the code repo. This is a simple React app, to run it locally set contract addresses in `App.js` and run `yarn start`. 10 | 11 | 12 | ## Overview of Tools 13 | 14 | ### What is MetaMask? 15 | 16 | MetaMask is an Ethereum wallet implemented as a browser extension. It creates and stores private keys, shows token balances, allows to connect to different networks, and sends and receives ether and tokens–everything a wallet has to do. 17 | 18 | Besides that, MetaMask acts as a signer and a provider. As a provider, it connects to an Ethereum node and provides an interface to use its JSON-RPC API. As a signer, it provides an interface for secure transaction signing, thus it can be used to sign any transaction using a private key from the wallet. 19 | 20 | ![How MetaMask works](images/metamask.png) 21 | 22 | ### Convenience Libraries 23 | 24 | MetaMask, however, doesn't provide much functionality: it can only manage accounts and send raw transactions. We need another library that will make interaction with contracts easy. We also want a set of utilities that will make our life easier when handling EVM-specific data (ABI encoding/decoding, big numbers handling, etc.). 25 | 26 | There are multiple such libraries. The two most popular ones are: [web3.js](https://github.com/ChainSafe/web3.js) and [ethers.js](https://github.com/ethers-io/ethers.js/). Picking either of them is a matter of personal preference. To me, Ethers.js seems to have a cleaner contract interaction interface, so I'll pick it. 27 | 28 | ## Workflows 29 | 30 | Let's now see how we can implement interaction scenarios using MetaMask + Ethers.js. 31 | 32 | ### Connecting to Local Node 33 | 34 | To send transactions and fetch blockchain data, MetaMask connects to an Ethereum node. To interact with our contracts, we need to connect to the local Anvil node. To do this, open MetaMask, click on the list of networks, click "Add Network", and add a network with RPC URL `http://localhost:8545`. It'll automatically detect the chain ID (31337 in the case of Anvil). 35 | 36 | After connecting to the local node, we need to import our private key. In MetaMask, click on the list of addresses, click "Import Account", and paste the private key of the address you picked before deploying the contracts. After that, go to the assets list and import the addresses of the two tokens. Now you should see balances of the tokens in MetaMask. 37 | 38 | > MetaMask is still somewhat bugged. One problem I struggled with is that it caches the blockchain state when connected to `localhost`. Because of this, when restarting the node, you might see old token balances and states. To fix this, go to the advanced settings and click "Reset Account". You'll need to do this each time after restarting the node. 39 | 40 | ### Connecting to MetaMask 41 | 42 | Not every website is allowed to get access to your address in MetaMask. A website first needs to connect to MetaMask. When a new website is connecting to MetaMask, you'll see a window that asks for permissions. 43 | 44 | Here's how to connect to MetaMask from a front-end app: 45 | ```js 46 | // ui/src/contexts/MetaMask.js 47 | const connect = () => { 48 | if (typeof (window.ethereum) === 'undefined') { 49 | return setStatus('not_installed'); 50 | } 51 | 52 | Promise.all([ 53 | window.ethereum.request({ method: 'eth_requestAccounts' }), 54 | window.ethereum.request({ method: 'eth_chainId' }), 55 | ]).then(function ([accounts, chainId]) { 56 | setAccount(accounts[0]); 57 | setChain(chainId); 58 | setStatus('connected'); 59 | }) 60 | .catch(function (error) { 61 | console.error(error) 62 | }); 63 | } 64 | ``` 65 | 66 | `window.ethereum` is an object provided by MetaMask, it's the interface to communicate with MetaMask. If it's undefined, MetaMask is not installed. If it's defined, we can send two requests to MetaMask: `eth_requestAccounts` and `eth_chainId`. In fact, `eth_requestAccounts` connects a website to MetaMask. It queries an address from MetaMask, and MetaMask asks for permission from the user. The user will be able to choose which addresses to give access to. 67 | 68 | `eth_chainId` will ask for the chain ID of the node MetaMask is connected to. After obtaining an address and chain ID, it's a good practice to display them in the interface: 69 | 70 | ![MetaMask is connected](images/ui_metamask_connected.png) 71 | 72 | ### Providing Liquidity 73 | 74 | To provide liquidity into the pool, we need to build a form that asks the user to type the amounts they want to deposit. After clicking "Submit", the app will build a transaction that calls `mint` in the manager contract and provides the amounts chosen by users. Let's see how to do this. 75 | 76 | Ether.js provides the `Contract` interface to interact with contracts. It makes our life much easier, since it takes on the job of encoding function parameters, creating a valid transaction, and handing it over to MetaMask. For us, calling contracts looks like calling asynchronous methods on a JS object. 77 | 78 | Let's see how to create an instance of `Contracts`: 79 | 80 | ```js 81 | token0 = new ethers.Contract( 82 | props.config.token0Address, 83 | props.config.ABIs.ERC20, 84 | new ethers.providers.Web3Provider(window.ethereum).getSigner() 85 | ); 86 | ``` 87 | 88 | A `Contract` instance is an address and the ABI of the contract deployed at this address. The ABI is needed to interact with the contract. The third parameter is the signer interface provided by MetaMask–it's used by the JS contract instance to sign transactions via MetaMask. 89 | 90 | Now, let's add a function for adding liquidity to the pool: 91 | ```js 92 | const addLiquidity = (account, { token0, token1, manager }, { managerAddress, poolAddress }) => { 93 | const amount0 = ethers.utils.parseEther("0.998976618347425280"); 94 | const amount1 = ethers.utils.parseEther("5000"); // 5000 USDC 95 | const lowerTick = 84222; 96 | const upperTick = 86129; 97 | const liquidity = ethers.BigNumber.from("1517882343751509868544"); 98 | const extra = ethers.utils.defaultAbiCoder.encode( 99 | ["address", "address", "address"], 100 | [token0.address, token1.address, account] 101 | ); 102 | ... 103 | ``` 104 | 105 | The first thing to do is to prepare the parameters. We use the same values we calculated earlier. 106 | 107 | Next, we allow the manager contract to take our tokens. First, we check the current allowances: 108 | ```js 109 | Promise.all( 110 | [ 111 | token0.allowance(account, managerAddress), 112 | token1.allowance(account, managerAddress) 113 | ] 114 | ) 115 | ``` 116 | 117 | Then, we check if either of them is enough to transfer a corresponding amount of tokens. If not, we're sending an `approve` transaction, which asks the user to approve spending of a specific amount to the manager contract. After ensuring that the user has approved full amounts, we call `manager.mint` to add liquidity: 118 | ```js 119 | .then(([allowance0, allowance1]) => { 120 | return Promise.resolve() 121 | .then(() => { 122 | if (allowance0.lt(amount0)) { 123 | return token0.approve(managerAddress, amount0).then(tx => tx.wait()) 124 | } 125 | }) 126 | .then(() => { 127 | if (allowance1.lt(amount1)) { 128 | return token1.approve(managerAddress, amount1).then(tx => tx.wait()) 129 | } 130 | }) 131 | .then(() => { 132 | return manager.mint(poolAddress, lowerTick, upperTick, liquidity, extra) 133 | .then(tx => tx.wait()) 134 | }) 135 | .then(() => { 136 | alert('Liquidity added!'); 137 | }); 138 | }) 139 | ``` 140 | 141 | > `lt` is a method of [BigNumber](https://docs.ethers.io/v5/api/utils/bignumber/). Ethers.js uses BigNumber to represent the `uint256` type, for which JavaScript [doesn't have enough precision](https://docs.ethers.io/v5/api/utils/bignumber/#BigNumber--notes-safenumbers). This is one of the reasons why we want a convenient library. 142 | 143 | This is pretty much similar to the test contract, besides the allowances part. 144 | 145 | `token0`, `token1`, and `manager` in the above code are instances of `Contract`. `approve` and `mint` are contract functions, which were generated dynamically from the ABIs we provided when instantiated the contracts. When calling these methods, Ethers.js: 146 | 1. encodes function parameters; 147 | 1. builds a transaction; 148 | 1. passes the transaction to MetaMask and asks to sign it; the user sees a MetaMask window and presses "Confirm"; 149 | 1. sends the transaction to the node MetaMask is connected to; 150 | 1. returns a transaction object with full information about the sent transaction. 151 | 152 | The transaction object also contains the `wait` function, which we call to wait for a transaction to be mined–this allows us to wait for a transaction to be successfully executed before sending another. 153 | 154 | > Ethereum requires a strict order of transactions. Remember the nonce? It's an account-wide index of transactions, sent by this account. Every new transaction increases this index, and Ethereum won't mine a transaction until a previous transaction (one with a smaller nonce) is mined. 155 | 156 | ### Swapping Tokens 157 | 158 | To swap tokens, we use the same pattern: get parameters from the user, check allowance, and call `swap` on the manager. 159 | 160 | ```js 161 | const swap = (amountIn, account, { tokenIn, manager, token0, token1 }, { managerAddress, poolAddress }) => { 162 | const amountInWei = ethers.utils.parseEther(amountIn); 163 | const extra = ethers.utils.defaultAbiCoder.encode( 164 | ["address", "address", "address"], 165 | [token0.address, token1.address, account] 166 | ); 167 | 168 | tokenIn.allowance(account, managerAddress) 169 | .then((allowance) => { 170 | if (allowance.lt(amountInWei)) { 171 | return tokenIn.approve(managerAddress, amountInWei).then(tx => tx.wait()) 172 | } 173 | }) 174 | .then(() => { 175 | return manager.swap(poolAddress, extra).then(tx => tx.wait()) 176 | }) 177 | .then(() => { 178 | alert('Swap succeeded!'); 179 | }).catch((err) => { 180 | console.error(err); 181 | alert('Failed!'); 182 | }); 183 | } 184 | ``` 185 | 186 | The only new thing here is the `ethers.utils.parseEther()` function, which we use to convert numbers to wei, the smallest unit in Ethereum. 187 | 188 | ### Subscribing to Changes 189 | 190 | For a decentralized application, it's important to reflect the current blockchain state. For example, in the case of a decentralized exchange, it's critical to properly calculate swap prices based on current pool reserves; outdated data can cause slippage and make a swap transaction fail. 191 | 192 | While developing the pool contract, we learned about events, that act as blockchain data indexes: whenever a smart contract state is modified, it's a good practice to emit an event since events are indexed for quick search. What we're going to do now, is to subscribe to contract events to keep our front-end app updated. Let's build an event feed! 193 | 194 | If you checked the ABI file as I recommended earlier, you saw that it also contains the description of events: event name and its fields. Well, [Ether.js parses them](https://docs.ethers.io/v5/api/contract/contract/#Contract--events) and provides an interface to subscribe to new events. Let's see how this works. 195 | 196 | To subscribe to events, we'll use the `on(EVENT_NAME, handler)` function. The callback receives all the fields of the event and the event itself as parameters: 197 | ```js 198 | const subscribeToEvents = (pool, callback) => { 199 | pool.on("Mint", (sender, owner, tickLower, tickUpper, amount, amount0, amount1, event) => callback(event)); 200 | pool.on("Swap", (sender, recipient, amount0, amount1, sqrtPriceX96, liquidity, tick, event) => callback(event)); 201 | } 202 | ``` 203 | 204 | To filter and fetch previous events, we can use `queryFilter`: 205 | ```js 206 | Promise.all([ 207 | pool.queryFilter("Mint", "earliest", "latest"), 208 | pool.queryFilter("Swap", "earliest", "latest"), 209 | ]).then(([mints, swaps]) => { 210 | ... 211 | }); 212 | ``` 213 | 214 | You probably noticed that some event fields are marked as `indexed`–such fields are indexed by Ethereum nodes, which lets search events by specific values in such fields. For example, the `Swap` event has `sender` and `recipient` fields indexed, so we can search by swap sender and recipient. And again, Ethere.js makes this easier: 215 | ```js 216 | const swapFilter = pool.filters.Swap(sender, recipient); 217 | const swaps = await pool.queryFilter(swapFilter, fromBlock, toBlock); 218 | ``` 219 | 220 | --- 221 | 222 | And that's it! We're done with Milestone 1! 223 | 224 |

225 | 🎉🍾🍾🍾🎉 226 |

-------------------------------------------------------------------------------- /src/milestone_2/generalize-minting.md: -------------------------------------------------------------------------------- 1 | # Generalized Minting 2 | 3 | Now, we're ready to update the `mint` function so we don't need to hard code values anymore and can calculate them instead. 4 | 5 | 6 | ## Indexing Initialized Ticks 7 | 8 | Recall that, in the `mint` function, we update the TickInfo mapping to store information about available liquidity at ticks. Now, we also need to index newly initialized ticks in the bitmap index–we'll later use this index to find the next initialized tick during swapping. 9 | 10 | First, we need to update the `Tick.update` function: 11 | ```solidity 12 | // src/lib/Tick.sol 13 | function update( 14 | mapping(int24 => Tick.Info) storage self, 15 | int24 tick, 16 | uint128 liquidityDelta 17 | ) internal returns (bool flipped) { 18 | ... 19 | flipped = (liquidityAfter == 0) != (liquidityBefore == 0); 20 | ... 21 | } 22 | ``` 23 | 24 | It now returns a `flipped` flag, which is set to true when liquidity is added to an empty tick or when entire liquidity is removed from a tick. 25 | 26 | Then, in the `mint` function, we update the bitmap index: 27 | ```solidity 28 | // src/UniswapV3Pool.sol 29 | ... 30 | bool flippedLower = ticks.update(lowerTick, amount); 31 | bool flippedUpper = ticks.update(upperTick, amount); 32 | 33 | if (flippedLower) { 34 | tickBitmap.flipTick(lowerTick, 1); 35 | } 36 | 37 | if (flippedUpper) { 38 | tickBitmap.flipTick(upperTick, 1); 39 | } 40 | ... 41 | ``` 42 | 43 | > Again, we're setting tick spacing to 1 until we introduce different values in Milestone 4. 44 | 45 | ## Token Amounts Calculation 46 | 47 | The biggest change in the `mint` function is switching to tokens amount calculation. In Milestone 1, we hard-coded these values: 48 | ```solidity 49 | amount0 = 0.998976618347425280 ether; 50 | amount1 = 5000 ether; 51 | ``` 52 | 53 | And now we're going to calculate them in Solidity using formulas from Milestone 1. Let's recall those formulas: 54 | 55 | $$\Delta x = \frac{L(\sqrt{p(i_u)} - \sqrt{p(i_c)})}{\sqrt{p(i_u)}\sqrt{p(i_c)}}$$ 56 | $$\Delta y = L(\sqrt{p(i_c)} - \sqrt{p(i_l)})$$ 57 | 58 | $\Delta x$ is the amount of `token0`, or token $x$. Let's implement it in Solidity: 59 | ```solidity 60 | // src/lib/Math.sol 61 | function calcAmount0Delta( 62 | uint160 sqrtPriceAX96, 63 | uint160 sqrtPriceBX96, 64 | uint128 liquidity 65 | ) internal pure returns (uint256 amount0) { 66 | if (sqrtPriceAX96 > sqrtPriceBX96) 67 | (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); 68 | 69 | require(sqrtPriceAX96 > 0); 70 | 71 | amount0 = divRoundingUp( 72 | mulDivRoundingUp( 73 | (uint256(liquidity) << FixedPoint96.RESOLUTION), 74 | (sqrtPriceBX96 - sqrtPriceAX96), 75 | sqrtPriceBX96 76 | ), 77 | sqrtPriceAX96 78 | ); 79 | } 80 | ``` 81 | 82 | > This function is identical to `calc_amount0` in our Python script. 83 | 84 | The first step is to sort the prices to ensure we don't underflow when subtracting. Next, we convert `liquidity` to a Q96.64 number by multiplying it by 2**96. Next, according to the formula, we multiply it by the difference of the prices and divide it by the bigger price. Then, we divide by the smaller price. The order of division doesn't matter, but we want to have two divisions because the multiplication of prices can overflow. 85 | 86 | We're using `mulDivRoundingUp` to multiply and divide in one operation. This function is based on `mulDiv` from `PRBMath`: 87 | ```solidity 88 | function mulDivRoundingUp( 89 | uint256 a, 90 | uint256 b, 91 | uint256 denominator 92 | ) internal pure returns (uint256 result) { 93 | result = PRBMath.mulDiv(a, b, denominator); 94 | if (mulmod(a, b, denominator) > 0) { 95 | require(result < type(uint256).max); 96 | result++; 97 | } 98 | } 99 | ``` 100 | 101 | `mulmod` is a Solidity function that multiplies two numbers (`a` and `b`), divides the result by `denominator`, and returns the remainder. If the remainder is positive, we round the result up. 102 | 103 | Next, $\Delta y$: 104 | ```solidity 105 | function calcAmount1Delta( 106 | uint160 sqrtPriceAX96, 107 | uint160 sqrtPriceBX96, 108 | uint128 liquidity 109 | ) internal pure returns (uint256 amount1) { 110 | if (sqrtPriceAX96 > sqrtPriceBX96) 111 | (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); 112 | 113 | amount1 = mulDivRoundingUp( 114 | liquidity, 115 | (sqrtPriceBX96 - sqrtPriceAX96), 116 | FixedPoint96.Q96 117 | ); 118 | } 119 | ``` 120 | 121 | > This function is identical to `calc_amount1` in our Python script. 122 | 123 | Again, we're using `mulDivRoundingUp` to avoid overflows during multiplication. 124 | 125 | And that's it! We can now use the functions to calculate token amounts: 126 | ```solidity 127 | // src/UniswapV3Pool.sol 128 | function mint(...) { 129 | ... 130 | Slot0 memory slot0_ = slot0; 131 | 132 | amount0 = Math.calcAmount0Delta( 133 | slot0_.sqrtPriceX96, 134 | TickMath.getSqrtRatioAtTick(upperTick), 135 | amount 136 | ); 137 | 138 | amount1 = Math.calcAmount1Delta( 139 | slot0_.sqrtPriceX96, 140 | TickMath.getSqrtRatioAtTick(lowerTick), 141 | amount 142 | ); 143 | ... 144 | } 145 | ``` 146 | 147 | Everything else remains the same. You'll need to update the amounts in the pool tests, they'll be slightly different due to rounding. -------------------------------------------------------------------------------- /src/milestone_2/images/find_next_tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_2/images/find_next_tick.png -------------------------------------------------------------------------------- /src/milestone_2/images/tick_bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_2/images/tick_bitmap.png -------------------------------------------------------------------------------- /src/milestone_2/introduction.md: -------------------------------------------------------------------------------- 1 | # Second Swap 2 | 3 | Alright, this is where it gets real. So far, our implementation has been looking too synthetic and static. We have calculated and hard-coded all the amounts to make the learning curve less steep, and now we're ready to make it dynamic. We're going to implement the second swap, which is a swap in the opposite direction: sell ETH to buy USDC. To do this, we're going to improve our smart contracts significantly: 4 | 1. We need to implement math calculations in Solidity. However, since implementing math in Solidity is tricky due to Solidity supporting only integer division, we'll use third-party libraries. 5 | 1. We'll need to let users choose swap direction, and the pool contract will need to support swapping in both directions. We'll improve the contract and will bring it closer to multi-range swaps, which we'll implement in the next milestone. 6 | 1. Finally, we'll update the UI to support swaps in both directions AND output amount calculation! This will require us to implement another contract, Quoter. 7 | 8 | At the end of this milestone, we'll have an app that works almost like a real DEX! 9 | 10 | Let's begin! 11 | 12 | > You'll find the complete code of this chapter in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_2). 13 | > 14 | > This milestone introduces a lot of code changes in existing contracts. [Here you can see all changes since the last milestone](https://github.com/Jeiwan/uniswapv3-code/compare/milestone_1...milestone_2) 15 | 16 | > If you have any questions, feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-2-second-swap)! -------------------------------------------------------------------------------- /src/milestone_2/math-in-solidity.md: -------------------------------------------------------------------------------- 1 | # Math in Solidity 2 | 3 | Due to Solidity not supporting numbers with the fractional part, math in Solidity is somewhat complicated. Solidity gives us integer and unsigned integer types, which are not enough for more or less complex math calculations. 4 | 5 | Another difficulty is gas consumption: the more complex an algorithm, the more gas it consumes. Thus, if we need to have advanced math operations (like `exp`, `ln`, and `sqrt`), we want them to be as gas efficient as possible. 6 | 7 | Another big problem is the possibility of under/overflow. When multiplying `uint256` numbers, there's a risk of an overflow: the resulting number might be so big that it won't fit into 256 bits. 8 | 9 | All these difficulties force us to use third-party math libraries that implement advanced math operations and, ideally, optimize their gas consumption. In the case when there's no library for an algorithm we need, we'll have to implement it ourselves, which is a difficult task if we need to implement a unique computation. 10 | 11 | ## Re-Using Math Contracts 12 | 13 | In our Uniswap V3 implementation, we're going to use two third-party math contracts: 14 | 1. [PRBMath](https://github.com/paulrberg/prb-math), which is a great library of advanced fixed-point math algorithms. We'll use the `mulDiv` function to handle overflows when multiplying and then dividing integer numbers. 15 | 1. [TickMath](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickMath.sol) from the original Uniswap V3 repo. This contract implements two functions, `getSqrtRatioAtTick` and `getTickAtSqrtRatio`, which convert $\sqrt{P}$'s 16 | to ticks and back. 17 | 18 | Let's focus on the latter. 19 | 20 | In our contracts, we'll need to convert ticks to corresponding $\sqrt{P}$ and back. The formulas are: 21 | 22 | $$\sqrt{P(i)} = \sqrt{1.0001^i} = 1.0001^{\frac{i}{2}}$$ 23 | 24 | $$i = log_{\sqrt{1.0001}}\sqrt{P(i)}$$ 25 | 26 | These are complex mathematical operations (for Solidity, at least) and they require high precision because we don't want to allow rounding errors when calculating prices. To have better precision and optimization we'll need a unique implementation. 27 | 28 | If you look at the original code of [getSqrtRatioAtTick](https://github.com/Uniswap/v3-core/blob/8f3e4645a08850d2335ead3d1a8d0c64fa44f222/contracts/libraries/TickMath.sol#L23-L54) and [getTickAtSqrtRatio](https://github.com/Uniswap/v3-core/blob/8f3e4645a08850d2335ead3d1a8d0c64fa44f222/contracts/libraries/TickMath.sol#L61-L204) you'll see that they're quite complex: there're a lot of magic numbers (like `0xfffcb933bd6fad37aa2d162d1a594001`), multiplication, and bitwise operations. At this point, we're not going to analyze the code or re-implement it since this is a very advanced and somewhat different topic. We'll use the contract as is. And, in a later milestone, we'll break down the computations. -------------------------------------------------------------------------------- /src/milestone_2/output-amount-calculation.md: -------------------------------------------------------------------------------- 1 | # Output Amount Calculation 2 | 3 | Our collection of Uniswap math formulas lacks a final piece: the formula for calculating the output amount when selling ETH (that is: selling token $x$). In the previous milestone, we had an analogous formula for the scenario when ETH is bought (buying token $x$): 4 | 5 | $$\Delta \sqrt{P} = \frac{\Delta y}{L}$$ 6 | 7 | This formula finds the change in the price when selling token $y$. We then added this change to the current price to find the target price: 8 | 9 | $$\sqrt{P_{target}} = \sqrt{P_{current}} + \Delta \sqrt{P}$$ 10 | 11 | Now, we need a similar formula to find the target price when selling token $x$ (ETH in our case) and buying token $y$ (USDC in our case). 12 | 13 | Recall that the change in token $x$ can be calculated as: 14 | 15 | $$\Delta x = \Delta \frac{1}{\sqrt{P}}L$$ 16 | 17 | From this formula, we can find the target price: 18 | 19 | $$\Delta x = (\frac{1}{\sqrt{P_{target}}} - \frac{1}{\sqrt{P_{current}}}) L$$ 20 | $$= \frac{L}{\sqrt{P_{target}}} - \frac{L}{\sqrt{P_{current}}}$$ 21 | 22 | From this, we can find $\sqrt{P_{target}}$ using basic algebraic transformations: 23 | 24 | $$\sqrt{P_{target}} = \frac{\sqrt{P}L}{\Delta x \sqrt{P} + L}$$ 25 | 26 | Knowing the target price, we can find the output amount similarly to how we found it in the previous milestone. 27 | 28 | Let's update our Python script with the new formula: 29 | ```python 30 | # Swap ETH for USDC 31 | amount_in = 0.01337 * eth 32 | 33 | print(f"\nSelling {amount_in/eth} ETH") 34 | 35 | price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur)) 36 | 37 | print("New price:", (price_next / q96) ** 2) 38 | print("New sqrtP:", price_next) 39 | print("New tick:", price_to_tick((price_next / q96) ** 2)) 40 | 41 | amount_in = calc_amount0(liq, price_next, sqrtp_cur) 42 | amount_out = calc_amount1(liq, price_next, sqrtp_cur) 43 | 44 | print("ETH in:", amount_in / eth) 45 | print("USDC out:", amount_out / eth) 46 | ``` 47 | 48 | Its output: 49 | ```shell 50 | Selling 0.01337 ETH 51 | New price: 4993.777388290041 52 | New sqrtP: 5598789932670289186088059666432 53 | New tick: 85163 54 | ETH in: 0.013369999999998142 55 | USDC out: 66.80838889019013 56 | ``` 57 | 58 | This means that we'll get 66.8 USDC when selling 0.01337 ETH using the liquidity we provided in the previous step. 59 | 60 | This looks good, but enough of Python! We're going to implement all the math calculations in Solidity. -------------------------------------------------------------------------------- /src/milestone_2/quoter-contract.md: -------------------------------------------------------------------------------- 1 | # Quoter Contract 2 | 3 | To integrate our updated Pool contract into the front-end app, we need a way to calculate swap amounts without making a swap. Users will type in the amount they want to sell, and we want to calculate and show them the amount they'll get in exchange. We'll do this through the Quoter contract. 4 | 5 | Since liquidity in Uniswap V3 is scattered over multiple price ranges, we cannot calculate swap amounts with a formula (which was possible in Uniswap V2). The design of Uniswap V3 forces us to use a different approach: to calculate swap amounts, we'll initiate a real swap and will interrupt it in the callback function, grabbing the amounts calculated by the Pool contract. That is, we have to simulate a real swap to calculate the output amount! 6 | 7 | Again, we'll make a helper contract for that: 8 | 9 | ```solidity 10 | contract UniswapV3Quoter { 11 | struct QuoteParams { 12 | address pool; 13 | uint256 amountIn; 14 | bool zeroForOne; 15 | } 16 | 17 | function quote(QuoteParams memory params) 18 | public 19 | returns ( 20 | uint256 amountOut, 21 | uint160 sqrtPriceX96After, 22 | int24 tickAfter 23 | ) 24 | { 25 | ... 26 | ``` 27 | 28 | Quoter is a contract that implements only one public function–`quote`. Quoter is a universal contract that works with any pool so it takes pool address as a parameter. The other parameters (`amountIn` and `zeroForOne`) are required to simulate a swap. 29 | 30 | ```solidity 31 | try 32 | IUniswapV3Pool(params.pool).swap( 33 | address(this), 34 | params.zeroForOne, 35 | params.amountIn, 36 | abi.encode(params.pool) 37 | ) 38 | {} catch (bytes memory reason) { 39 | return abi.decode(reason, (uint256, uint160, int24)); 40 | } 41 | ``` 42 | 43 | The only thing that the contract does is calling the `swap` function of a pool. The call is expected to revert (i.e. throw an error)–we'll do this in the swap callback. In the case of a revert, the revert reason is decoded and returned; `quote` will never revert. Notice that, in the extra data, we're passing only the pool address–in the swap callback, we'll use it to get the pool's `slot0` after a swap. 44 | 45 | ```solidity 46 | function uniswapV3SwapCallback( 47 | int256 amount0Delta, 48 | int256 amount1Delta, 49 | bytes memory data 50 | ) external view { 51 | address pool = abi.decode(data, (address)); 52 | 53 | uint256 amountOut = amount0Delta > 0 54 | ? uint256(-amount1Delta) 55 | : uint256(-amount0Delta); 56 | 57 | (uint160 sqrtPriceX96After, int24 tickAfter) = IUniswapV3Pool(pool) 58 | .slot0(); 59 | ``` 60 | 61 | In the swap callback, we're collecting values that we need: output amount, new price, and corresponding tick. Next, we need to save these values and revert: 62 | 63 | ```solidity 64 | assembly { 65 | let ptr := mload(0x40) 66 | mstore(ptr, amountOut) 67 | mstore(add(ptr, 0x20), sqrtPriceX96After) 68 | mstore(add(ptr, 0x40), tickAfter) 69 | revert(ptr, 96) 70 | } 71 | ``` 72 | 73 | For gas optimization, this piece is implemented in [Yul](https://docs.soliditylang.org/en/latest/assembly.html), the language used for inline assembly in Solidity. Let's break it down: 74 | 1. `mload(0x40)` reads the pointer of the next available memory slot (memory in EVM is organized in 32-byte slots); 75 | 1. at that memory slot, `mstore(ptr, amountOut)` writes `amountOut`; 76 | 1. `mstore(add(ptr, 0x20), sqrtPriceX96After)` writes `sqrtPriceX96After` right after `amountOut`; 77 | 1. `mstore(add(ptr, 0x40), tickAfter)` writes `tickAfter` after `sqrtPriceX96After`; 78 | 1. `revert(ptr, 96)` reverts the call and returns 96 bytes (total length of the values we wrote to memory) of data at address `ptr` (start of the data we wrote above). 79 | 80 | So, we're concatenating the bytes representations of the values we need (exactly what `abi.encode()` does). Notice that the offsets are always 32 bytes, even though `sqrtPriceX96After` takes 20 bytes (`uint160`) and `tickAfter` takes 3 bytes (`int24`). This is so we could use `abi.decode()` to decode the data: its counterpart, `abi.encode()`, encodes all integers as 32-byte words. 81 | 82 | Aaaand... ~it's gone~ done. 83 | 84 | ## Recap 85 | 86 | Let's recap to better understand the algorithm: 87 | 1. `quote` calls `swap` of a pool with input amount and swap direction; 88 | 1. `swap` performs a real swap, it runs the loop to fill the input amount specified by the user; 89 | 1. to get tokens from the user, `swap` calls the swap callback on the caller; 90 | 1. the caller (Quote contract) implements the callback, in which it reverts with output amount, new price, and new tick; 91 | 1. the revert bubbles up to the initial `quote` call; 92 | 1. in `quote`, the revert is caught, revert reason is decoded and returned as the result of calling `quote`. 93 | 94 | I hope this is clear! 95 | 96 | ## Quoter Limitation 97 | 98 | This design has one significant limitation: since `quote` calls the `swap` function of the Pool contract, and the `swap` function is not a pure or view function (because it modifies contract state), `quote` cannot also be pure or view. `swap` modifies state and so does `quote`, even if not in Quoter contract. But we treat `quote` as a getter, a function that only reads contract data. This inconsistency means that EVM will use [CALL](https://www.evm.codes/#f1) opcode instead of [STATICCALL](https://www.evm.codes/#fa) when `quote` is called. This is not a big problem since Quoter reverts in the swap callback, and reverting resets the state modified during a call–this guarantees that `quote` won't modify the state of the Pool contract (no actual trade will happen). 99 | 100 | Another inconvenience that comes from this issue is that calling `quote` from a client library (Ethers.js, Web3.js, etc.) will trigger a transaction. To fix this, we'll need to force the library to make a static call. We'll see how to do this in Ethers.js later in this milestone. 101 | -------------------------------------------------------------------------------- /src/milestone_2/tick-bitmap-index.md: -------------------------------------------------------------------------------- 1 | # Tick Bitmap Index 2 | 3 | As the first step towards dynamic swaps, we need to implement an index of ticks. In the previous milestone, we used to calculate the target tick when making a swap: 4 | ```solidity 5 | function swap(address recipient, bytes calldata data) 6 | public 7 | returns (int256 amount0, int256 amount1) 8 | { 9 | int24 nextTick = 85184; 10 | ... 11 | } 12 | ``` 13 | 14 | When there's liquidity provided in different price ranges, we cannot simply calculate the target tick. We need to **find it**. Thus, we need to index all ticks that have liquidity and then use the index to find ticks to "inject" enough liquidity for a swap. In this step, we're going to implement such an index. 15 | 16 | ## Bitmap 17 | 18 | Bitmap is a popular technique of indexing data in a compact way. A bitmap is simply a number represented in the binary system, e.g. 31337 is `111101001101001`. We can look at it as an array of zeros and ones, with each digit having an index. We then say that 0 means a flag is not set and 1 means it's set. So what we get is a very compact array of indexed flags: each byte can fit 8 flags. In Solidity, we can have integers up to 256 bits, which means one `uint256` can hold 256 flags. 19 | 20 | Uniswap V3 uses this technique to store the information about initialized ticks, that is ticks with some liquidity. When a flag is set (1), the tick has liquidity; when a flag is not set (0), the tick is not initialized. Let's look at the implementation. 21 | 22 | ## TickBitmap Contract 23 | 24 | In the pool contract, the tick index is stored in a state variable: 25 | ```solidity 26 | contract UniswapV3Pool { 27 | using TickBitmap for mapping(int16 => uint256); 28 | mapping(int16 => uint256) public tickBitmap; 29 | ... 30 | } 31 | ``` 32 | 33 | This is mapping where keys are `int16`'s and values are words (`uint256`). Imagine an infinite continuous array of ones and zeros: 34 | 35 | ![Tick indexes in tick bitmap](images/tick_bitmap.png) 36 | 37 | Each element in this array corresponds to a tick. To navigate in this array, we break it into words: sub-arrays of length 256 bits. To find the tick's position in this array, we do: 38 | 39 | ```solidity 40 | function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { 41 | wordPos = int16(tick >> 8); 42 | bitPos = uint8(uint24(tick % 256)); 43 | } 44 | ``` 45 | 46 | That is: we find its word position and then its bit in this word. `>> 8` is identical to integer division by 256. So, word position is the integer part of a tick index divided by 256, and bit position is the remainder. 47 | 48 | As an example, let's calculate word and bit positions for one of our ticks: 49 | ```python 50 | tick = 85176 51 | word_pos = tick >> 8 # or tick // 2**8 52 | bit_pos = tick % 256 53 | print(f"Word {word_pos}, bit {bit_pos}") 54 | # Word 332, bit 184 55 | ``` 56 | 57 | ### Flipping Flags 58 | 59 | When adding liquidity into a pool, we need to set a couple of tick flags in the bitmap: one for the lower tick and one for the upper tick. We do this in the `flipTick` method of the bitmap mapping: 60 | ```solidity 61 | function flipTick( 62 | mapping(int16 => uint256) storage self, 63 | int24 tick, 64 | int24 tickSpacing 65 | ) internal { 66 | require(tick % tickSpacing == 0); // ensure that the tick is spaced 67 | (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); 68 | uint256 mask = 1 << bitPos; 69 | self[wordPos] ^= mask; 70 | } 71 | ``` 72 | 73 | > Until later in the book, `tickSpacing` is always 1. Please keep in mind that this value affects which ticks can be initialized: when it equals 1, all ticks can be flipped; when it's set to a different value, only ticks divisible by the value can be flipped. 74 | 75 | After finding word and bit positions, we need to make a mask. A mask is a number that has a single 1 flag set at the bit position of the tick. To find the mask, we simply calculate `2**bit_pos` (equivalent of `1 << bit_pos`): 76 | ```python 77 | mask = 2**bit_pos # or 1 << bit_pos 78 | print(format(mask, '#0258b')) ↓ here 79 | #0b0000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 80 | ``` 81 | 82 | Next, to flip a flag, we apply the mask to the tick's word via bitwise XOR: 83 | ```python 84 | word = (2**256) - 1 # set word to all ones 85 | print(format(word ^ mask, '#0258b')) ↓ here 86 | #0b1111111111111111111111111111111111111111111111111111111111111111111111101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 87 | ``` 88 | 89 | You'll see that the 184th bit (counting from the right starting at 0) has flipped to 0. 90 | 91 | If a bit is zero, it'll set it to 1: 92 | ```python 93 | word = 0 94 | print(format(word ^ mask, '#0258b')) ↓ here 95 | #0b0000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 96 | ``` 97 | 98 | ### Finding Next Tick 99 | 100 | The next step is finding ticks with liquidity using the bitmap index. 101 | 102 | During swapping, we need to find a tick with liquidity that's before or after the current tick (that is: to the left or the right of it). In the previous milestone, we used to [calculate and hard code it](https://github.com/Jeiwan/uniswapv3-code/blob/85b8605c37a9065c141a234ee2c18d9507eeba22/src/UniswapV3Pool.sol#L142), but now we need to find such tick using the bitmap index. We'll do this in the `TickBitmap.nextInitializedTickWithinOneWord` function. In this function, we'll need to implement two scenarios: 103 | 104 | 1. When selling token $x$ (ETH in our case), find the next initialized tick in the current tick's word and **to the right** of the current tick. 105 | 1. When selling token $y$ (USDC in our case), find the next initialized tick in the next (current + 1) tick's word and **to the left** of the current tick. 106 | 107 | This corresponds to the price movement when making swaps in either direction: 108 | 109 | ![Finding next initialized tick during a swap](images/find_next_tick.png) 110 | 111 | > Be aware that, in the code, the direction is flipped: when buying token $x$, we search for initialized ticks **to the left** of the current; when selling token $x$, we search ticks **to the right**. But this is only true within a word; words are ordered from left to right. 112 | 113 | When there's no initialized tick in the current word, we'll continue searching in an adjacent word in the next loop cycle. 114 | 115 | Now, let's look at the implementation: 116 | ```solidity 117 | function nextInitializedTickWithinOneWord( 118 | mapping(int16 => uint256) storage self, 119 | int24 tick, 120 | int24 tickSpacing, 121 | bool lte 122 | ) internal view returns (int24 next, bool initialized) { 123 | int24 compressed = tick / tickSpacing; 124 | ... 125 | ``` 126 | 127 | 1. The first argument makes this function a method of `mapping(int16 => uint256)`. 128 | 1. `tick` is the current tick. 129 | 1. `tickSpacing` is always 1 until we start using it in Milestone 4. 130 | 1. `lte` is the flag that sets the direction. When `true`, we're selling token $x$ and searching for the next initialized tick to the right of the current one. When `false,` it's the other way around. `lte` equals the swap direction: `true` when selling token $x$, `false` otherwise. 131 | 132 | ```solidity 133 | if (lte) { 134 | (int16 wordPos, uint8 bitPos) = position(compressed); 135 | uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); 136 | uint256 masked = self[wordPos] & mask; 137 | ... 138 | ``` 139 | 140 | When selling $x$, we're: 141 | 1. taking the current tick's word and bit positions; 142 | 1. making a mask where all bits to the right of the current bit position, including it, are ones (`mask` is all ones, its length = `bitPos`); 143 | 1. applying the mask to the current tick's word. 144 | 145 | ```solidity 146 | ... 147 | initialized = masked != 0; 148 | next = initialized 149 | ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing 150 | : (compressed - int24(uint24(bitPos))) * tickSpacing; 151 | ... 152 | ``` 153 | 154 | Next, `masked` won't equal 0 if at least one bit of it is set to 1. If so, there's an initialized tick; if not, there isn't (not in the current word). Depending on the result, we either return the index of the next initialized tick or the leftmost bit in the next word–this will allow us to search for initialized ticks in the word during another loop cycle. 155 | 156 | ```solidity 157 | ... 158 | } else { 159 | (int16 wordPos, uint8 bitPos) = position(compressed + 1); 160 | uint256 mask = ~((1 << bitPos) - 1); 161 | uint256 masked = self[wordPos] & mask; 162 | ... 163 | ``` 164 | 165 | Similarly, when selling $y$, we: 166 | 1. take the current tick's word and bit positions; 167 | 1. make a different mask, where all bits to the left of the current tick bit position are ones and all the bits to the right are zeros; 168 | 1. apply the mask to the current tick's word. 169 | 170 | Again, if there are no initialized ticks to the left, the rightmost bit of the previous word is returned: 171 | ```solidity 172 | ... 173 | initialized = masked != 0; 174 | // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 175 | next = initialized 176 | ? (compressed + 1 + int24(uint24((BitMath.leastSignificantBit(masked) - bitPos)))) * tickSpacing 177 | : (compressed + 1 + int24(uint24((type(uint8).max - bitPos)))) * tickSpacing; 178 | } 179 | ``` 180 | 181 | And that's it! 182 | 183 | As you can see, `nextInitializedTickWithinOneWord` doesn't find the exact tick if it's far away–its scope of search is current or next tick's word. Indeed, we don't want to iterate over the infinite bitmap index. -------------------------------------------------------------------------------- /src/milestone_2/user-interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | Let's make our web app work more like a real DEX. We can now remove hardcoded swap amounts and let users type arbitrary amounts. Moreover, we can now let users swap in both directions, so we also need a button to swap the token inputs. After updating, the swap form will look like: 4 | 5 | ```jsx 6 |
7 | 13 | 14 | 19 | 20 | 21 | ``` 22 | 23 | Each input has an amount assigned to it depending on the swap direction controlled by the `zeroForOne` state variable. The lower input field is always read-only because its value is calculated by the Quoter contract. 24 | 25 | The `setAmount_` function does two things: it updates the value of the top input and calls the Quoter contract to calculate the value of the lower input: 26 | 27 | ```js 28 | const updateAmountOut = debounce((amount) => { 29 | if (amount === 0 || amount === "0") { 30 | return; 31 | } 32 | 33 | setLoading(true); 34 | 35 | quoter.callStatic 36 | .quote({ pool: config.poolAddress, amountIn: ethers.utils.parseEther(amount), zeroForOne: zeroForOne }) 37 | .then(({ amountOut }) => { 38 | zeroForOne ? setAmount1(ethers.utils.formatEther(amountOut)) : setAmount0(ethers.utils.formatEther(amountOut)); 39 | setLoading(false); 40 | }) 41 | .catch((err) => { 42 | zeroForOne ? setAmount1(0) : setAmount0(0); 43 | setLoading(false); 44 | console.error(err); 45 | }) 46 | }) 47 | 48 | const setAmount_ = (setAmountFn) => { 49 | return (amount) => { 50 | amount = amount || 0; 51 | setAmountFn(amount); 52 | updateAmountOut(amount) 53 | } 54 | } 55 | ``` 56 | 57 | Notice the `callStatic` called on `quoter`–this is what we discussed in the previous chapter: we need to force Ethers.js to make a static call. Since `quote` is not a `pure` or `view` function, Ethers.js will try to call `quote` in a transaction. 58 | 59 | And that's it! The UI now allows us to specify arbitrary amounts and swap in either direction! -------------------------------------------------------------------------------- /src/milestone_3/different-ranges.md: -------------------------------------------------------------------------------- 1 | # Different Price Ranges 2 | 3 | The way we implemented it, our Pool contract creates only price ranges that include the current price: 4 | ```solidity 5 | // src/UniswapV3Pool.sol 6 | function mint() { 7 | ... 8 | amount0 = Math.calcAmount0Delta( 9 | slot0_.sqrtPriceX96, 10 | TickMath.getSqrtRatioAtTick(upperTick), 11 | amount 12 | ); 13 | 14 | amount1 = Math.calcAmount1Delta( 15 | slot0_.sqrtPriceX96, 16 | TickMath.getSqrtRatioAtTick(lowerTick), 17 | amount 18 | ); 19 | 20 | liquidity += uint128(amount); 21 | ... 22 | } 23 | ``` 24 | 25 | From this piece, you can also see that we always update the liquidity tracker (which tracks only currently available liquidity, i.e. liquidity available at the current price). 26 | 27 | However, in reality, price ranges can also be created **below or above** the current price. That's it: the design of Uniswap V3 allows liquidity providers to provide liquidity that doesn't get immediately used. Such liquidity gets "injected" when the current price gets into such "sleeping" price ranges. 28 | 29 | These are kinds of price ranges that can exist: 30 | 1. Active price range, i.e. one that includes the current price. 31 | 1. Price range placed below the current price. The upper tick of this range is below the current tick. 32 | 1. Price range placed above the current price. The lower tick of this range is above the current tick. 33 | 34 | ## Limit Orders 35 | 36 | An interesting fact about inactive liquidity (i.e. liquidity not provided at the current price) is that it acts as *limit orders*. 37 | 38 | In trading, limit orders are orders that get executed when the price crosses a level chosen by the trader. For example, you can place a limit order that buys 1 ETH when its price drops to \$1000. Similarly, you can use limit order to sell assets. With Uniswap V3, you can get similar behavior by placing liquidity at ranges that are below or above the current price. Let's see how this works: 39 | 40 | ![Liquidity ranges outside of the current price](images/ranges_outside_current_price.png) 41 | 42 | If you provide liquidity below the current price (i.e. the price range you chose lays entirely below the current price) or above it, then your whole liquidity will be composed of **only one asset**–the asset will be the cheaper one of the two. In our example, we're building a pool with ETH being token $x$ and USDC being token $y$, and we define the price as: 43 | 44 | $$P = \frac{y}{x}$$ 45 | 46 | If we put liquidity below the current price, then the liquidity will be composed of USDC solely because, where we added the liquidity, the price of USDC is lower than the current price. Likewise, when we put liquidity above the current price, then the liquidity will be composed of ETH because ETH is cheaper in that range. 47 | 48 | Recall this illustration from the introduction: 49 | 50 | ![Price range depletion](../milestone_1/images/range_depleted.png) 51 | 52 | If we buy all available amounts of ETH from this range, the range will contain only the other token, USDC, and the price will move to the right of the curve. The price, as we defined it ($\frac{y}{x}$), will **increase**. If there's a price range to the right of this one, it needs to have ETH liquidity, and only ETH, not USDC: it needs to provide ETH for the next swaps. If we keep buying and raising the price, we might "drain" the next price range as well, which means buying all its ETH and selling USDC. Again, the price range ends up having only USDC, and the current price moves outside of it. 53 | 54 | Similarly, if we're buying USDC tokens, we move the price to the left and remove USDC tokens from the pool. The next price range will only contain USDC tokens to satisfy our demand, and, similarly to the above scenario, will end up containing only ETH tokens if we buy all USDC from it. 55 | 56 | Note the interesting fact: when crossing an entire price range, its liquidity is swapped from one token to another. And if we set a very narrow price range, one that gets crossed quickly during a price move, we get a limit order! For example, if you want to buy ETH at a lower price, you need to place a price range containing only USDC at the lower price and wait for the current price to cross it. After that, you'll need to remove your liquidity and get it converted to ETH! 57 | 58 | I hope this example didn't confuse you! I think this is a good way to explain the dynamics of price ranges. 59 | 60 | ## Updating the `mint` Function 61 | 62 | To support all kinds of price ranges, we need to know whether the current price is below, inside, or above the price range specified by the user and calculate token amounts accordingly. If the price range is above the current price, we want the liquidity to be composed of token $x$: 63 | 64 | ```solidity 65 | // src/UniswapV3Pool.sol 66 | function mint() { 67 | ... 68 | if (slot0_.tick < lowerTick) { 69 | amount0 = Math.calcAmount0Delta( 70 | TickMath.getSqrtRatioAtTick(lowerTick), 71 | TickMath.getSqrtRatioAtTick(upperTick), 72 | amount 73 | ); 74 | ... 75 | ``` 76 | 77 | When the price range includes the current price, we want both tokens in amounts proportional to the price (this is the scenario we implemented earlier): 78 | ```solidity 79 | } else if (slot0_.tick < upperTick) { 80 | amount0 = Math.calcAmount0Delta( 81 | slot0_.sqrtPriceX96, 82 | TickMath.getSqrtRatioAtTick(upperTick), 83 | amount 84 | ); 85 | 86 | amount1 = Math.calcAmount1Delta( 87 | slot0_.sqrtPriceX96, 88 | TickMath.getSqrtRatioAtTick(lowerTick), 89 | amount 90 | ); 91 | 92 | liquidity = LiquidityMath.addLiquidity(liquidity, int128(amount)); 93 | ``` 94 | 95 | Notice that this is the only scenario where we want to update `liquidity` since the variable tracks liquidity that's available immediately. 96 | 97 | In all other cases, when the price range is below the current price, we want the range to contain only token $y$: 98 | ```solidity 99 | } else { 100 | amount1 = Math.calcAmount1Delta( 101 | TickMath.getSqrtRatioAtTick(lowerTick), 102 | TickMath.getSqrtRatioAtTick(upperTick), 103 | amount 104 | ); 105 | } 106 | ``` 107 | 108 | And that's it! -------------------------------------------------------------------------------- /src/milestone_3/flash-loans.md: -------------------------------------------------------------------------------- 1 | ## Flash Loans 2 | 3 | Both Uniswap V2 and V3 implement flash loans: unlimited and uncollateralized loans that must be repaid in the same transaction. Pools give users arbitrary amounts of tokens that they request, but, by the end of the call, the amounts must be repaid, with a small fee on top. 4 | 5 | The fact that flash loans must be repaid in the same transaction means that flash loans cannot be taken by regular users: as a user, you cannot program custom logic in transactions. Flash loans can only be taken and repaid by smart contracts. 6 | 7 | Flash loans are a powerful financial instrument in DeFi. While it's often used to exploit vulnerabilities in DeFi protocols (by inflating pool balances and abusing flawed state management), it's many good applications (e.g. leveraged positions management on lending protocols)–this is why DeFi applications that store liquidity provide permissionless flash loans. 8 | 9 | ### Implementing Flash Loans 10 | 11 | In Uniswap V2 flash loans were part of the swapping functionality: it was possible to borrow tokens during a swap, but you had to return them or an equal amount of the other pool token, in the same transaction. In V3, flash loans are separated from swapping–it's simply a function that gives the caller a number of tokens they requested, calls a callback on the caller, and ensures a flash loan was repaid: 12 | 13 | ```solidity 14 | function flash( 15 | uint256 amount0, 16 | uint256 amount1, 17 | bytes calldata data 18 | ) public { 19 | uint256 balance0Before = IERC20(token0).balanceOf(address(this)); 20 | uint256 balance1Before = IERC20(token1).balanceOf(address(this)); 21 | 22 | if (amount0 > 0) IERC20(token0).transfer(msg.sender, amount0); 23 | if (amount1 > 0) IERC20(token1).transfer(msg.sender, amount1); 24 | 25 | IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(data); 26 | 27 | require(IERC20(token0).balanceOf(address(this)) >= balance0Before); 28 | require(IERC20(token1).balanceOf(address(this)) >= balance1Before); 29 | 30 | emit Flash(msg.sender, amount0, amount1); 31 | } 32 | ``` 33 | 34 | The function sends tokens to the caller and then calls `uniswapV3FlashCallback` on it–this is where the caller is expected to repay the loan. Then the function ensures that its balances haven't decreased. Notice that custom data is allowed to be passed to the callback. 35 | 36 | Here's an example of the callback implementation: 37 | 38 | ```solidity 39 | function uniswapV3FlashCallback(bytes calldata data) public { 40 | (uint256 amount0, uint256 amount1) = abi.decode( 41 | data, 42 | (uint256, uint256) 43 | ); 44 | 45 | if (amount0 > 0) token0.transfer(msg.sender, amount0); 46 | if (amount1 > 0) token1.transfer(msg.sender, amount1); 47 | } 48 | ``` 49 | 50 | In this implementation, we're simply sending tokens back to the pool (I used this callback in `flash` function tests). In reality, it can use the loaned amounts to perform some operations on other DeFi protocols. But it always must repay the loan in this callback. 51 | 52 | And that's it! -------------------------------------------------------------------------------- /src/milestone_3/images/add_liquidity_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/add_liquidity_dialog.png -------------------------------------------------------------------------------- /src/milestone_3/images/price_range_dynamics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/price_range_dynamics.png -------------------------------------------------------------------------------- /src/milestone_3/images/ranges_outside_current_price.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/ranges_outside_current_price.png -------------------------------------------------------------------------------- /src/milestone_3/images/sandwich_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/sandwich_attack.png -------------------------------------------------------------------------------- /src/milestone_3/images/slippage_tolerance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/slippage_tolerance.png -------------------------------------------------------------------------------- /src/milestone_3/images/swap_consecutive_price_ranges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/swap_consecutive_price_ranges.png -------------------------------------------------------------------------------- /src/milestone_3/images/swap_partially_overlapping_price_ranges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/swap_partially_overlapping_price_ranges.png -------------------------------------------------------------------------------- /src/milestone_3/images/swap_within_overlapping_price_ranges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/swap_within_overlapping_price_ranges.png -------------------------------------------------------------------------------- /src/milestone_3/images/swap_within_price_range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_3/images/swap_within_price_range.png -------------------------------------------------------------------------------- /src/milestone_3/introduction.md: -------------------------------------------------------------------------------- 1 | # Cross-Tick Swaps 2 | 3 | We have made great progress so far and our Uniswap V3 implementation is quite close to the original one! However, our implementation only supports swaps within a price range–and this is what we're going to improve in this milestone. 4 | 5 | In this milestone, we'll: 6 | 1. update the `mint` function to provide liquidity in different price ranges; 7 | 1. update the `swap` function to cross price ranges when there's not enough liquidity in the current price range; 8 | 1. learn how to calculate liquidity in smart contracts; 9 | 1. implement slippage protection in the `mint` and `swap` functions; 10 | 1. update the UI application to allow to add liquidity at different price ranges; 11 | 1. learn a little bit more about fixed-point numbers. 12 | 13 | In this milestone, we'll complete swapping, the core functionality of Uniswap! 14 | 15 | Let's begin! 16 | 17 | > You'll find the complete code of this chapter in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_3). 18 | > 19 | > This milestone introduces a lot of code changes in existing contracts. [Here you can see all changes since the last milestone](https://github.com/Jeiwan/uniswapv3-code/compare/milestone_2...milestone_3) 20 | 21 | > If you have any questions feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-3-cross-tick-swaps)! -------------------------------------------------------------------------------- /src/milestone_3/liquidity-calculation.md: -------------------------------------------------------------------------------- 1 | # Liquidity Calculation 2 | 3 | Of the whole math of Uniswap V3, what we haven't yet implemented in Solidity is liquidity calculation. In the Python script, we have these functions: 4 | 5 | ```python 6 | def liquidity0(amount, pa, pb): 7 | if pa > pb: 8 | pa, pb = pb, pa 9 | return (amount * (pa * pb) / q96) / (pb - pa) 10 | 11 | 12 | def liquidity1(amount, pa, pb): 13 | if pa > pb: 14 | pa, pb = pb, pa 15 | return amount * q96 / (pb - pa) 16 | ``` 17 | 18 | Let's implement them in Solidity so we can calculate liquidity in the `Manager.mint()` function. 19 | 20 | ## Implementing Liquidity Calculation for Token X 21 | 22 | The functions we're going to implement allow us to calculate liquidity ($L = \sqrt{xy}$) when token amounts and price ranges are known. Luckily, we already know all the formulas. Let's recall this one: 23 | 24 | $$\Delta x = \Delta \frac{1}{\sqrt{P}}L$$ 25 | 26 | In a previous chapter, we used this formula to calculate swap amounts ($\Delta x$ in this case) and now we're going to use it to find $L$: 27 | 28 | $$L = \frac{\Delta x}{\Delta \frac{1}{\sqrt{P}}}$$ 29 | 30 | Or, after simplifying it: 31 | $$L = \frac{\Delta x \sqrt{P_u} \sqrt{P_l}}{\sqrt{P_u} - \sqrt{P_l}}$$ 32 | 33 | > We derived this formula in [Liquidity Amount Calculation](https://uniswapv3book.com/docs/milestone_1/calculating-liquidity/#liquidity-amount-calculation). 34 | 35 | In Solidity, we'll again use `PRBMath` to handle overflows when multiplying and then dividing: 36 | 37 | ```solidity 38 | function getLiquidityForAmount0( 39 | uint160 sqrtPriceAX96, 40 | uint160 sqrtPriceBX96, 41 | uint256 amount0 42 | ) internal pure returns (uint128 liquidity) { 43 | if (sqrtPriceAX96 > sqrtPriceBX96) 44 | (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); 45 | 46 | uint256 intermediate = PRBMath.mulDiv( 47 | sqrtPriceAX96, 48 | sqrtPriceBX96, 49 | FixedPoint96.Q96 50 | ); 51 | liquidity = uint128( 52 | PRBMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96) 53 | ); 54 | } 55 | ``` 56 | 57 | ## Implementing Liquidity Calculation for Token Y 58 | 59 | Similarly, we'll use the other formula from [Liquidity Amount Calculation](https://uniswapv3book.com/docs/milestone_1/calculating-liquidity/#liquidity-amount-calculation) to find $L$ when the amount of $y$ and the price range is known: 60 | 61 | $$\Delta y = \Delta\sqrt{P} L$$ 62 | $$L = \frac{\Delta y}{\sqrt{P_u}-\sqrt{P_l}}$$ 63 | 64 | ```solidity 65 | function getLiquidityForAmount1( 66 | uint160 sqrtPriceAX96, 67 | uint160 sqrtPriceBX96, 68 | uint256 amount1 69 | ) internal pure returns (uint128 liquidity) { 70 | if (sqrtPriceAX96 > sqrtPriceBX96) 71 | (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); 72 | 73 | liquidity = uint128( 74 | PRBMath.mulDiv( 75 | amount1, 76 | FixedPoint96.Q96, 77 | sqrtPriceBX96 - sqrtPriceAX96 78 | ) 79 | ); 80 | } 81 | ``` 82 | 83 | I hope this is clear! 84 | 85 | ## Finding Fair Liquidity 86 | 87 | You might be wondering why there are two ways of calculating $L$ while we have always had only one $L$, which is calculated as $L = \sqrt{xy}$, and which of these ways is correct? The answer is: they're both correct. 88 | 89 | In the above formulas, we calculate $L$ based on different parameters: price range and the amount of either token. Different price ranges and different token amounts will result in different values of $L$. And there's a scenario where we need to calculate both of the $L$'s and pick one of them. Recall this piece from the `mint` function: 90 | 91 | ```solidity 92 | if (slot0_.tick < lowerTick) { 93 | amount0 = Math.calcAmount0Delta(...); 94 | } else if (slot0_.tick < upperTick) { 95 | amount0 = Math.calcAmount0Delta(...); 96 | 97 | amount1 = Math.calcAmount1Delta(...); 98 | 99 | liquidity = LiquidityMath.addLiquidity(liquidity, int128(amount)); 100 | } else { 101 | amount1 = Math.calcAmount1Delta(...); 102 | } 103 | ``` 104 | 105 | It turns out, we also need to follow this logic when calculating liquidity: 106 | 1. if we're calculating liquidity for a range that's above the current price, we use the $\Delta x$ version on the formula; 107 | 1. when calculating liquidity for a range that's below the current price, we use the $\Delta y$ one; 108 | 1. when a price range includes the current price, we calculate **both** and pick the smaller of them. 109 | 110 | > Again, we discussed these ideas in [Liquidity Amount Calculation](https://uniswapv3book.com/docs/milestone_1/calculating-liquidity/#liquidity-amount-calculation). 111 | 112 | Let's implement this logic now. 113 | 114 | When the current price is below the lower bound of a price range: 115 | ```solidity 116 | function getLiquidityForAmounts( 117 | uint160 sqrtPriceX96, 118 | uint160 sqrtPriceAX96, 119 | uint160 sqrtPriceBX96, 120 | uint256 amount0, 121 | uint256 amount1 122 | ) internal pure returns (uint128 liquidity) { 123 | if (sqrtPriceAX96 > sqrtPriceBX96) 124 | (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); 125 | 126 | if (sqrtPriceX96 <= sqrtPriceAX96) { 127 | liquidity = getLiquidityForAmount0( 128 | sqrtPriceAX96, 129 | sqrtPriceBX96, 130 | amount0 131 | ); 132 | ``` 133 | 134 | When the current price is within a range, we're picking the smaller $L$: 135 | ```solidity 136 | } else if (sqrtPriceX96 <= sqrtPriceBX96) { 137 | uint128 liquidity0 = getLiquidityForAmount0( 138 | sqrtPriceX96, 139 | sqrtPriceBX96, 140 | amount0 141 | ); 142 | uint128 liquidity1 = getLiquidityForAmount1( 143 | sqrtPriceAX96, 144 | sqrtPriceX96, 145 | amount1 146 | ); 147 | 148 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; 149 | ``` 150 | 151 | And finally: 152 | ```solidity 153 | } else { 154 | liquidity = getLiquidityForAmount1( 155 | sqrtPriceAX96, 156 | sqrtPriceBX96, 157 | amount1 158 | ); 159 | } 160 | ``` 161 | 162 | Done. -------------------------------------------------------------------------------- /src/milestone_3/more-on-fixed-point-numbers.md: -------------------------------------------------------------------------------- 1 | # A Little Bit More on Fixed-point Numbers 2 | 3 | In this bonus chapter, I'd like to show you how to convert prices to ticks in Solidity. We don't need to do this in the main contracts, but it's helpful to have such function in tests so we don't hardcode ticks and could write something like `tick(5000)`–this makes code easier to read because it's more convenient for us to think in prices, not tick indexes. 4 | 5 | Recall that, to find ticks, we use the `TickMath.getTickAtSqrtRatio` function, which takes $\sqrt{P}$ as its argument, and the $\sqrt{P}$ is a Q64.96 fixed-point number. In smart contract tests, we need to check $\sqrt{P}$ many times in many different test cases: mostly after mints and swaps. Instead of hard-coding actual values, it might be cleaner to use a helper function like `sqrtP(5000)` that converts prices to $\sqrt{P}$. 6 | 7 | So, what's the problem? 8 | 9 | The problem is that Solidity doesn't natively support the square root operation, which means we need a third-party library. Another problem is that prices are often relatively small numbers, like 10, 5000, 0.01, etc., and we don't want to lose precision when taking square root. 10 | 11 | You probably remember that we used `PRBMath` earlier in the book to implement a multiply-then-divide operation that doesn't overflow during multiplication. If you check the `PRBMath.sol` contract, you'll notice the `sqrt` function. However, the function doesn't support fixed-point numbers, as the function description says. You can give it a try and see that `PRBMath.sqrt(5000)` results in `70`, which is an integer number with lost precision (without the fractional part). 12 | 13 | If you check [prb-math](https://github.com/paulrberg/prb-math) repo, you'll see these contracts: `PRBMathSD59x18.sol` and `PRBMathUD60x18.sol`. Aha! These are fixed-point number implementations. Let's pick the latter and see how it goes: `PRBMathUD60x18.sqrt(5000 * PRBMathUD60x18.SCALE)` returns `70710678118654752440`. This looks interesting! `PRBMathUD60x18` is a library that implements fixed numbers with 18 decimal places in the fractional part. So the number we got is 70.710678118654752440 (use `cast --from-wei 70710678118654752440`). 14 | 15 | However, we cannot use this number! 16 | 17 | There are fixed-point numbers and fixed-point numbers. The Q64.96 fixed-point number used by Uniswap V3 is a **binary** number–64 and 96 signify *binary places*. But `PRBMathUD60x18` implements a *decimal* fixed-point number (UD in the contract name means "unsigned, decimal"), where 60 and 18 signify *decimal places*. This difference is quite significant. 18 | 19 | Let's see how to convert an arbitrary number (42) to either of the above fixed-point numbers: 20 | 1. Q64.96: $42 * 2^{96}$ or, using bitwise left shift, `2 << 96`. The result is 3327582825599102178928845914112. 21 | 1. UD60.18: $42 * 10^{18}$. The result is 42000000000000000000. 22 | 23 | Let's now see how to convert numbers with the fractional part (42.1337): 24 | 1. Q64.96: $421337 * 2^{92}$ or `421337 << 92`. The result is 2086359769329537075540689212669952. 25 | 1. UD60.18: $421337 * 10^{14}$. The result is 42133700000000000000. 26 | 27 | The second variant makes more sense to us because it uses the decimal system, which we learned in our childhood. The first variant uses the binary system and it's much harder for us to read. 28 | 29 | But the biggest problem with different variants is that it's hard to convert between them. 30 | 31 | This all means that we need a different library, one that implements a binary fixed-point number and a `sqrt` function for it. Luckily, there's such a library: [abdk-libraries-solidity](https://github.com/abdk-consulting/abdk-libraries-solidity). The library implemented Q64.64, not exactly what we need (not 96 bits in the fractional part) but this is not a problem. 32 | 33 | Here's how we can implement the price-to-tick function using the new library: 34 | ```solidity 35 | function tick(uint256 price) internal pure returns (int24 tick_) { 36 | tick_ = TickMath.getTickAtSqrtRatio( 37 | uint160( 38 | int160( 39 | ABDKMath64x64.sqrt(int128(int256(price << 64))) << 40 | (FixedPoint96.RESOLUTION - 64) 41 | ) 42 | ) 43 | ); 44 | } 45 | ``` 46 | 47 | `ABDKMath64x64.sqrt` takes Q64.64 numbers so we need to convert `price` to such number. The price is expected to not have the fractional part, so we're shifting it by 64 bits. The `sqrt` function also returns a Q64.64 number but `TickMath.getTickAtSqrtRatio` takes a Q64.96 number–this is why we need to shift the result of the square root operation by `96 - 64` bits to the left. -------------------------------------------------------------------------------- /src/milestone_3/slippage-protection.md: -------------------------------------------------------------------------------- 1 | # Slippage Protection 2 | 3 | Slippage is a very important issue in decentralized exchanges. Slippage simply means the difference between the price that you see on the screen when initialing a transaction and the actual price when the swap is executed. This difference appears because there's a short (and sometimes long, depending on network congestion and gas costs) delay between when you send a transaction and when it gets mined. In more technical terms, blockchain state changes every block and there's no guarantee that your transaction will be applied at a specific block. 4 | 5 | Another important problem that slippage protection fixes is *sandwich attacks*–this is a common type of attack on decentralized exchange users. During sandwiching, attackers "wrap" your swap transactions in their two transactions: one goes before your transaction and the other goes after it. In the first transaction, an attacker modifies the state of a pool so that your swap becomes very unprofitable for you and somewhat profitable for the attacker. This is achieved by adjusting pool liquidity so that your trade happens at a lower price. In the second transaction, the attacker reestablishes pool liquidity and the price. As a result, you get much fewer tokens than expected due to manipulated prices, and the attacker gets some profit. 6 | 7 | ![Sandwich attack](images/sandwich_attack.png) 8 | 9 | The way slippage protection is implemented in decentralized exchanges is by letting users choose how far the actual price is allowed to drop. By default, Uniswap V3 sets slippage tolerance to 0.1%, which means a swap is executed only if the price at the moment of execution is not smaller than 99.9% of the price the user saw in the browser. This is a very tight range and users are allowed to adjust this number, which is useful when volatility is high. 10 | 11 | Let's add slippage protection to our implementation! 12 | 13 | ## Slippage Protection in Swaps 14 | 15 | To protect swaps, we need to add one more parameter to the `swap` function–we want to let the user choose a stop price, a price at which swapping will stop. We'll call the parameter `sqrtPriceLimitX96`: 16 | 17 | ```solidity 18 | function swap( 19 | address recipient, 20 | bool zeroForOne, 21 | uint256 amountSpecified, 22 | uint160 sqrtPriceLimitX96, 23 | bytes calldata data 24 | ) public returns (int256 amount0, int256 amount1) { 25 | ... 26 | if ( 27 | zeroForOne 28 | ? sqrtPriceLimitX96 > slot0_.sqrtPriceX96 || 29 | sqrtPriceLimitX96 < TickMath.MIN_SQRT_RATIO 30 | : sqrtPriceLimitX96 < slot0_.sqrtPriceX96 && 31 | sqrtPriceLimitX96 > TickMath.MAX_SQRT_RATIO 32 | ) revert InvalidPriceLimit(); 33 | ... 34 | ``` 35 | 36 | When selling token $x$ (`zeroForOne` is true), `sqrtPriceLimitX96` must be between the current price and the minimal $\sqrt{P}$ since selling token $x$ moves the price down. Likewise, when selling token $y$, `sqrtPriceLimitX96` must be between the current price and the maximal $\sqrt{P}$ because the price moves up. 37 | 38 | In the while loop, we want to satisfy two conditions: the full swap amount has not been filled and the current price isn't equal to `sqrtPriceLimitX96`: 39 | ```solidity 40 | .. 41 | while ( 42 | state.amountSpecifiedRemaining > 0 && 43 | state.sqrtPriceX96 != sqrtPriceLimitX96 44 | ) { 45 | ... 46 | ``` 47 | 48 | This means that Uniswap V3 pools don't fail when slippage tolerance gets hit and simply execute the swap partially. 49 | 50 | Another place where we need to consider `sqrtPriceLimitX96` is when calling `SwapMath.computeSwapStep`: 51 | 52 | ```solidity 53 | (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath 54 | .computeSwapStep( 55 | state.sqrtPriceX96, 56 | ( 57 | zeroForOne 58 | ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 59 | : step.sqrtPriceNextX96 > sqrtPriceLimitX96 60 | ) 61 | ? sqrtPriceLimitX96 62 | : step.sqrtPriceNextX96, 63 | state.liquidity, 64 | state.amountSpecifiedRemaining 65 | ); 66 | ``` 67 | 68 | Here, we want to ensure that `computeSwapStep` never calculates swap amounts outside of `sqrtPriceLimitX96`–this guarantees that the current price will never cross the limiting price. 69 | 70 | ## Slippage Protection in Minting 71 | 72 | Adding liquidity also requires slippage protection. This comes from the fact that price cannot be changed when adding liquidity (liquidity must be proportional to the current price), thus liquidity providers also suffer from slippage. Unlike the `swap` function, however, we're not forced to implement slippage protection in the Pool contract–recall that the Pool contract is a core contract and we don't want to put unnecessary logic into it. This is why we made the Manager contract, and it's in the Manager contract where we'll implement slippage protection. 73 | 74 | The Manager contract is a wrapper contract that makes calls to the Pool contract more convenient. To implement slippage protection in the `mint` function, we can simply check the amounts of tokens taken by Pool and compare them to some minimal amounts chosen the by user. Additionally, we can free users from calculating $\sqrt{P_{lower}}$ and $\sqrt{P_{upper}}$, as well as liquidity, and calculate these in `Manager.mint()`. 75 | 76 | Our updated `mint` function will now take more parameters, so let's group them in a struct: 77 | ```solidity 78 | // src/UniswapV3Manager.sol 79 | contract UniswapV3Manager { 80 | struct MintParams { 81 | address poolAddress; 82 | int24 lowerTick; 83 | int24 upperTick; 84 | uint256 amount0Desired; 85 | uint256 amount1Desired; 86 | uint256 amount0Min; 87 | uint256 amount1Min; 88 | } 89 | 90 | function mint(MintParams calldata params) 91 | public 92 | returns (uint256 amount0, uint256 amount1) 93 | { 94 | ... 95 | ``` 96 | 97 | `amount0Min` and `amount1Min` are the amounts that are calculated based on slippage tolerance. They must be smaller than the desired amounts, with the gap controlled by the slippage tolerance setting. The liquidity provider expects to provide amounts not smaller than `amount0Min` and `amount1Min`. 98 | 99 | Next, we calculate $\sqrt{P_{lower}}$, $\sqrt{P_{upper}}$, and liquidity: 100 | ```solidity 101 | ... 102 | IUniswapV3Pool pool = IUniswapV3Pool(params.poolAddress); 103 | 104 | (uint160 sqrtPriceX96, ) = pool.slot0(); 105 | uint160 sqrtPriceLowerX96 = TickMath.getSqrtRatioAtTick( 106 | params.lowerTick 107 | ); 108 | uint160 sqrtPriceUpperX96 = TickMath.getSqrtRatioAtTick( 109 | params.upperTick 110 | ); 111 | 112 | uint128 liquidity = LiquidityMath.getLiquidityForAmounts( 113 | sqrtPriceX96, 114 | sqrtPriceLowerX96, 115 | sqrtPriceUpperX96, 116 | params.amount0Desired, 117 | params.amount1Desired 118 | ); 119 | ... 120 | ``` 121 | 122 | `LiquidityMath.getLiquidityForAmounts` is a new function, we'll discuss it in the next chapter. 123 | 124 | The next step is to provide liquidity to the pool and check the amounts returned by the pool: if they're too low, we revert. 125 | ```solidity 126 | (amount0, amount1) = pool.mint( 127 | msg.sender, 128 | params.lowerTick, 129 | params.upperTick, 130 | liquidity, 131 | abi.encode( 132 | IUniswapV3Pool.CallbackData({ 133 | token0: pool.token0(), 134 | token1: pool.token1(), 135 | payer: msg.sender 136 | }) 137 | ) 138 | ); 139 | 140 | if (amount0 < params.amount0Min || amount1 < params.amount1Min) 141 | revert SlippageCheckFailed(amount0, amount1); 142 | ``` 143 | 144 | That's it! -------------------------------------------------------------------------------- /src/milestone_3/user-interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | We're now ready to update the UI with the changes we made in this milestone. We'll add two new features: 4 | 1. Add Liquidity dialog window; 5 | 1. slippage tolerance in swapping. 6 | 7 | 8 | ## Add Liquidity Dialog 9 | 10 | ![Add Liquidity dialog window](images/add_liquidity_dialog.png) 11 | 12 | This change will finally remove hard-coded liquidity amounts from our code and will allow us to add liquidity at arbitrary ranges. 13 | 14 | The dialog is a simple component with a couple of inputs. We can even re-use the `addLiquidity` function from the previous implementation. However, now we need to convert prices to tick indices in JavaScript: we want users to type in prices but the contracts expect ticks. To make our job easier, we'll use [the official Uniswap V3 SDK](https://github.com/Uniswap/v3-sdk/) for that. 15 | 16 | To convert price to $\sqrt{P}$, we can use [encodeSqrtRatioX96](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/utils/encodeSqrtRatioX96.ts) function. The function takes two amounts as input and calculates a price by dividing one by the other. Since we only want to convert price to $\sqrt{P}$, we can pass 1 as `amount0`: 17 | ```javascript 18 | const priceToSqrtP = (price) => encodeSqrtRatioX96(price, 1); 19 | ``` 20 | 21 | To convert price to tick index, we can use [TickMath.getTickAtSqrtRatio](https://github.com/Uniswap/v3-sdk/blob/08a7c050cba00377843497030f502c05982b1c43/src/utils/tickMath.ts#L82) function. This is an implementation of the Solidity TickMath library in JavaScript: 22 | 23 | ```javascript 24 | const priceToTick = (price) => TickMath.getTickAtSqrtRatio(priceToSqrtP(price)); 25 | ``` 26 | 27 | So we can now convert prices typed in by users to ticks: 28 | 29 | ```javascript 30 | const lowerTick = priceToTick(lowerPrice); 31 | const upperTick = priceToTick(upperPrice); 32 | ``` 33 | 34 | Another thing we need to add here is slippage protection. For simplicity, I made it a hard-coded value and set it to 0.5%. Here's how to use slippage tolerance to calculate minimal amounts: 35 | 36 | ```javascript 37 | const slippage = 0.5; 38 | const amount0Desired = ethers.utils.parseEther(amount0); 39 | const amount1Desired = ethers.utils.parseEther(amount1); 40 | const amount0Min = amount0Desired.mul((100 - slippage) * 100).div(10000); 41 | const amount1Min = amount1Desired.mul((100 - slippage) * 100).div(10000); 42 | ``` 43 | 44 | ## Slippage Tolerance in Swapping 45 | 46 | Even though we're the only users of the application and thus will never have problems with slippage during development, let's add an input to control slippage tolerance during swaps. 47 | 48 | ![Main screen of the web app](images/slippage_tolerance.png) 49 | 50 | When swapping, slippage protection is implemented via limiting price–a price we don't to go above or below during a swap. This means that we need to know this price before sending a swap transaction. However, we don't need to calculate it on the front end because the Quoter contract does this for us: 51 | 52 | ```solidity 53 | function quote(QuoteParams memory params) 54 | public 55 | returns ( 56 | uint256 amountOut, 57 | uint160 sqrtPriceX96After, 58 | int24 tickAfter 59 | ) { ... } 60 | ``` 61 | 62 | And we're calling Quoter to calculate swap amounts. 63 | 64 | So, to calculate the limiting price we need to take `sqrtPriceX96After` and subtract slippage tolerance from it–this will be the price we don't want to go below during a swap. 65 | 66 | ```solidity 67 | const limitPrice = priceAfter.mul((100 - parseFloat(slippage)) * 100).div(10000); 68 | ``` 69 | 70 | And that's it! -------------------------------------------------------------------------------- /src/milestone_4/images/pools_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_4/images/pools_graph.png -------------------------------------------------------------------------------- /src/milestone_4/images/pools_scattered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_4/images/pools_scattered.png -------------------------------------------------------------------------------- /src/milestone_4/introduction.md: -------------------------------------------------------------------------------- 1 | # Multi-Pool Swaps 2 | 3 | After implementing cross-tick swaps, we've got close to real Uniswap V3 swaps. One significant limitation of our implementation is that it allows only swaps within a pool–if there's no pool for a pair of tokens, then swapping between these tokens is not possible. This is not so in Uniswap since it allows multi-pool swaps. In this chapter, we're going to add multi-pool swaps to our implementation. 4 | 5 | Here's the plan: 6 | 7 | 1. first, we'll learn about and implement the Factory contract; 8 | 1. then, we'll see how chained or multi-pool swaps work and implement the Path library; 9 | 1. then, we'll update the front-end app to support multi-pool swaps; 10 | 1. we'll implement a basic router that finds a path between two tokens; 11 | 1. along the way, we'll also learn about tick spacing which is a way of optimizing swaps. 12 | 13 | 14 | After finishing this chapter, our implementation will be able to handle multi-pool swaps, for example, swapping WBTC for WETH via different stablecoins: WETH → USDC → USDT → WBTC. 15 | 16 | Let's begin! 17 | 18 | > You'll find the complete code of this chapter in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_4). 19 | > 20 | > This milestone introduces a lot of code changes in existing contracts. [Here you can see all changes since the last milestone](https://github.com/Jeiwan/uniswapv3-code/compare/milestone_3...milestone_4) 21 | 22 | > If you have any questions feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-4-multi-pool-swaps)! -------------------------------------------------------------------------------- /src/milestone_4/multi-pool-swaps.md: -------------------------------------------------------------------------------- 1 | # Multi-Pool Swaps 2 | 3 | We're now proceeding to the core of this milestone–implementing multi-pool swaps in our contracts. We won't touch the Pool contract in this milestone because it's a core contract that should implement only core features. Multi-pool swaps are a utility feature, and we'll implement it in the Manager and Quoter contracts. 4 | 5 | ## Updating the Manager Contract 6 | 7 | ### Single-Pool and Multi-Pool Swaps 8 | In our current implementation, the `swap` function in the Manager contract supports only single-pool swaps and takes pool address in parameters: 9 | 10 | ```solidity 11 | function swap( 12 | address poolAddress_, 13 | bool zeroForOne, 14 | uint256 amountSpecified, 15 | uint160 sqrtPriceLimitX96, 16 | bytes calldata data 17 | ) public returns (int256, int256) { ... } 18 | ``` 19 | 20 | We're going to split it into two functions: single-pool swap and multi-pool swap. These functions will have different set of parameters: 21 | 22 | ```solidity 23 | struct SwapSingleParams { 24 | address tokenIn; 25 | address tokenOut; 26 | uint24 tickSpacing; 27 | uint256 amountIn; 28 | uint160 sqrtPriceLimitX96; 29 | } 30 | 31 | struct SwapParams { 32 | bytes path; 33 | address recipient; 34 | uint256 amountIn; 35 | uint256 minAmountOut; 36 | } 37 | ``` 38 | 39 | 1. `SwapSingleParams` takes pool parameters, input amount, and a limiting price–this is pretty much identical to what we had before. Notice, that `data` is no longer required. 40 | 1. `SwapParams` takes path, output amount recipient, input amount, and minimal output amount. The latter parameter replaces `sqrtPriceLimitX96` because, when doing multi-pool swaps, we cannot use the slippage protection from the Pool contract (which uses a limiting price). We need to implement another slippage protection, which checks the final output amount and compares it with `minAmountOut`: the slippage protection fails when the final output amount is smaller than `minAmountOut`. 41 | 42 | ### Core Swapping Logic 43 | 44 | Let's implement an internal `_swap` function that will be called by both single- and multi-pool swap functions. It'll prepare parameters and call `Pool.swap`. 45 | 46 | ```solidity 47 | function _swap( 48 | uint256 amountIn, 49 | address recipient, 50 | uint160 sqrtPriceLimitX96, 51 | SwapCallbackData memory data 52 | ) internal returns (uint256 amountOut) { 53 | ... 54 | ``` 55 | 56 | `SwapCallbackData` is a new data structure that contains data we pass between swap functions and `uniswapV3SwapCallback`: 57 | ```solidity 58 | struct SwapCallbackData { 59 | bytes path; 60 | address payer; 61 | } 62 | ``` 63 | 64 | `path` is a swap path and `payer` is the address that provides input tokens in swaps–we'll have different payers during multi-pool swaps. 65 | 66 | The first thing we do in `_swap`, is to extract pool parameters using the `Path` library: 67 | 68 | ```solidity 69 | // function _swap(...) { 70 | (address tokenIn, address tokenOut, uint24 tickSpacing) = data 71 | .path 72 | .decodeFirstPool(); 73 | ``` 74 | 75 | Then we identify swap direction: 76 | 77 | ```solidity 78 | bool zeroForOne = tokenIn < tokenOut; 79 | ``` 80 | 81 | Then we make the actual swap: 82 | ```solidity 83 | // function _swap(...) { 84 | (int256 amount0, int256 amount1) = getPool( 85 | tokenIn, 86 | tokenOut, 87 | tickSpacing 88 | ).swap( 89 | recipient, 90 | zeroForOne, 91 | amountIn, 92 | sqrtPriceLimitX96 == 0 93 | ? ( 94 | zeroForOne 95 | ? TickMath.MIN_SQRT_RATIO + 1 96 | : TickMath.MAX_SQRT_RATIO - 1 97 | ) 98 | : sqrtPriceLimitX96, 99 | abi.encode(data) 100 | ); 101 | ``` 102 | 103 | This piece is identical to what we had before but this time we're calling `getPool` to find the pool. `getPool` is a function that sorts tokens and calls `PoolAddress.computeAddress`: 104 | 105 | ```solidity 106 | function getPool( 107 | address token0, 108 | address token1, 109 | uint24 tickSpacing 110 | ) internal view returns (IUniswapV3Pool pool) { 111 | (token0, token1) = token0 < token1 112 | ? (token0, token1) 113 | : (token1, token0); 114 | pool = IUniswapV3Pool( 115 | PoolAddress.computeAddress(factory, token0, token1, tickSpacing) 116 | ); 117 | } 118 | ``` 119 | 120 | After making a swap, we need to figure out which of the amounts is the output one: 121 | ```solidity 122 | // function _swap(...) { 123 | amountOut = uint256(-(zeroForOne ? amount1 : amount0)); 124 | ``` 125 | 126 | And that's it. Let's now look at how a single-pool swap works. 127 | 128 | ### Single-Pool Swapping 129 | 130 | `swapSingle` acts simply as a wrapper of `_swap`: 131 | 132 | ```solidity 133 | function swapSingle(SwapSingleParams calldata params) 134 | public 135 | returns (uint256 amountOut) 136 | { 137 | amountOut = _swap( 138 | params.amountIn, 139 | msg.sender, 140 | params.sqrtPriceLimitX96, 141 | SwapCallbackData({ 142 | path: abi.encodePacked( 143 | params.tokenIn, 144 | params.tickSpacing, 145 | params.tokenOut 146 | ), 147 | payer: msg.sender 148 | }) 149 | ); 150 | } 151 | ``` 152 | 153 | Notice that we're building a one-pool path here: single-pool swap is a multi-pool swap with one pool 🙂. 154 | 155 | ### Multi-Pool Swapping 156 | 157 | Multi-pool swapping is only slightly more difficult than single-pool swapping. Let's look at it: 158 | 159 | ```solidity 160 | function swap(SwapParams memory params) public returns (uint256 amountOut) { 161 | address payer = msg.sender; 162 | bool hasMultiplePools; 163 | ... 164 | ``` 165 | 166 | The first swap is paid by the user because it's the user who provides input tokens. 167 | 168 | Then, we start iterating over pools in the path: 169 | 170 | ```solidity 171 | ... 172 | while (true) { 173 | hasMultiplePools = params.path.hasMultiplePools(); 174 | 175 | params.amountIn = _swap( 176 | params.amountIn, 177 | hasMultiplePools ? address(this) : params.recipient, 178 | 0, 179 | SwapCallbackData({ 180 | path: params.path.getFirstPool(), 181 | payer: payer 182 | }) 183 | ); 184 | ... 185 | ``` 186 | 187 | In each iteration, we're calling `_swap` with these parameters: 188 | 1. `params.amountIn` tracks input amounts. During the first swap, it's the amount provided by the user. During the next swaps, it's the amounts returned from previous swaps. 189 | 1. `hasMultiplePools ? address(this) : params.recipient`–if there are multiple pools in the path, the recipient is the Manager contract, it'll store tokens between swaps. If there's only one pool (the last one) in the path, the recipient is the one specified in the parameters (usually the same user that initiates the swap). 190 | 1. `sqrtPriceLimitX96` is set to 0 to disable slippage protection in the Pool contract. 191 | 1. The last parameter is what we pass to `uniswapV3SwapCallback`–we'll look at it shortly. 192 | 193 | After making one swap, we need to proceed to the next pool in a path or return: 194 | ```solidity 195 | ... 196 | 197 | if (hasMultiplePools) { 198 | payer = address(this); 199 | params.path = params.path.skipToken(); 200 | } else { 201 | amountOut = params.amountIn; 202 | break; 203 | } 204 | } 205 | ``` 206 | 207 | This is where we're changing payer and removing a processed pool from the path. 208 | 209 | Finally, the new slippage protection: 210 | 211 | ```solidity 212 | if (amountOut < params.minAmountOut) 213 | revert TooLittleReceived(amountOut); 214 | ``` 215 | 216 | ### Swap Callback 217 | 218 | Let's look at the updated swap callback: 219 | 220 | ```solidity 221 | function uniswapV3SwapCallback( 222 | int256 amount0, 223 | int256 amount1, 224 | bytes calldata data_ 225 | ) public { 226 | SwapCallbackData memory data = abi.decode(data_, (SwapCallbackData)); 227 | (address tokenIn, address tokenOut, ) = data.path.decodeFirstPool(); 228 | 229 | bool zeroForOne = tokenIn < tokenOut; 230 | 231 | int256 amount = zeroForOne ? amount0 : amount1; 232 | 233 | if (data.payer == address(this)) { 234 | IERC20(tokenIn).transfer(msg.sender, uint256(amount)); 235 | } else { 236 | IERC20(tokenIn).transferFrom( 237 | data.payer, 238 | msg.sender, 239 | uint256(amount) 240 | ); 241 | } 242 | } 243 | ``` 244 | 245 | The callback expects encoded `SwapCallbackData` with path and payer address. It extracts pool tokens from the path, figures out the swap direction (`zeroForOne`), and the amount the contract needs to transfer out. Then, it acts differently depending on the payer address: 246 | 1. If the payer is the current contract (this is so when making consecutive swaps), it transfers tokens to the next pool (the one that called this callback) from the current contract's balance. 247 | 1. If the payer is a different address (the user that initiated the swap), it transfers tokens from the user's balance. 248 | 249 | ## Updating the Quoter Contract 250 | 251 | Quoter is another contract that needs to be updated because we want to use it to also find output amounts in multi-pool swaps. Similarly to Manager, we'll have two variants of the `quote` function: single-pool and multi-pool one. Let's look at the former first. 252 | 253 | ### Single-pool Quoting 254 | We need to make only a couple of changes in our current `quote` implementation: 255 | 1. rename it to `quoteSingle`; 256 | 1. extract parameters into a struct (this is mostly a cosmetic change); 257 | 1. instead of a pool address, take two token addresses and a tick spacing in the parameters. 258 | 259 | ```solidity 260 | // src/UniswapV3Quoter.sol 261 | struct QuoteSingleParams { 262 | address tokenIn; 263 | address tokenOut; 264 | uint24 tickSpacing; 265 | uint256 amountIn; 266 | uint160 sqrtPriceLimitX96; 267 | } 268 | 269 | function quoteSingle(QuoteSingleParams memory params) 270 | public 271 | returns ( 272 | uint256 amountOut, 273 | uint160 sqrtPriceX96After, 274 | int24 tickAfter 275 | ) 276 | { 277 | ... 278 | ``` 279 | 280 | The only change we have in the body of the function is the usage of `getPool` to find the pool address: 281 | ```solidity 282 | ... 283 | IUniswapV3Pool pool = getPool( 284 | params.tokenIn, 285 | params.tokenOut, 286 | params.tickSpacing 287 | ); 288 | 289 | bool zeroForOne = params.tokenIn < params.tokenOut; 290 | ... 291 | ``` 292 | 293 | ### Multi-pool Quoting 294 | 295 | Multi-pool quoting implementation is similar to the multi-pool swapping one, but it uses fewer parameters. 296 | 297 | ```solidity 298 | function quote(bytes memory path, uint256 amountIn) 299 | public 300 | returns ( 301 | uint256 amountOut, 302 | uint160[] memory sqrtPriceX96AfterList, 303 | int24[] memory tickAfterList 304 | ) 305 | { 306 | sqrtPriceX96AfterList = new uint160[](path.numPools()); 307 | tickAfterList = new int24[](path.numPools()); 308 | ... 309 | ``` 310 | 311 | As parameters, we only need an input amount and a swap path. The function returns similar values as `quoteSingle`, but "price after" and "tick after" are collected after each swap, thus we need to return arrays. 312 | 313 | ```solidity 314 | uint256 i = 0; 315 | while (true) { 316 | (address tokenIn, address tokenOut, uint24 tickSpacing) = path 317 | .decodeFirstPool(); 318 | 319 | ( 320 | uint256 amountOut_, 321 | uint160 sqrtPriceX96After, 322 | int24 tickAfter 323 | ) = quoteSingle( 324 | QuoteSingleParams({ 325 | tokenIn: tokenIn, 326 | tokenOut: tokenOut, 327 | tickSpacing: tickSpacing, 328 | amountIn: amountIn, 329 | sqrtPriceLimitX96: 0 330 | }) 331 | ); 332 | 333 | sqrtPriceX96AfterList[i] = sqrtPriceX96After; 334 | tickAfterList[i] = tickAfter; 335 | amountIn = amountOut_; 336 | i++; 337 | 338 | if (path.hasMultiplePools()) { 339 | path = path.skipToken(); 340 | } else { 341 | amountOut = amountIn; 342 | break; 343 | } 344 | } 345 | ``` 346 | 347 | The logic of the loop is identical to the one in the updated `swap` function: 348 | 1. get the current pool's parameters; 349 | 1. call `quoteSingle` on the current pool; 350 | 1. save returned values; 351 | 1. repeat if there are more pools in the path, or return otherwise. 352 | -------------------------------------------------------------------------------- /src/milestone_4/path.md: -------------------------------------------------------------------------------- 1 | # Swap Path 2 | 3 | Let's imagine that we have only these pools: WETH/USDC, USDC/USDT, and WBTC/USDT. If we want to swap WETH for WBTC, we'll need to make multiple swaps (WETH→USDC→USDT→WBTC) since there's no WETH/WBTC pool. We can do this manually or we can improve our contracts to handle such chained, or multi-pool, swaps. Of course, we'll do the latter! 4 | 5 | When doing multi-pool swaps, we send the output of the previous swap to the input of the next one. For example: 6 | 7 | 1. in the WETH/USDC pool, we're selling WETH and buying USDC; 8 | 1. in the USDC/USDT pool, we're selling USDC from the previous swap and buying USDT; 9 | 1. in the WBTC/USDT pool, we're selling USDT from the previous pool and buying WBTC. 10 | 11 | We can turn this series into a path: 12 | 13 | ``` 14 | WETH/USDC,USDC/USDT,WBTC/USDT 15 | ``` 16 | 17 | And iterate over such a path in our contracts to perform multiple swaps in one transaction. However, recall from the previous chapter that we don't need to know pool addresses and, instead, we can derive them from pool parameters. Thus, the above path can be turned into a series of tokens: 18 | 19 | ``` 20 | WETH, USDC, USDT, WBTC 21 | ``` 22 | 23 | Recall that tick spacing is another parameter (besides tokens) that identifies a pool. Thus, the above path becomes: 24 | 25 | ``` 26 | WETH, 60, USDC, 10, USDT, 60, WBTC 27 | ``` 28 | 29 | Where 60 and 10 are tick spacings. We're using 60 in volatile pairs (e.g. ETH/USDC, WBTC/USDT) and 10 in stablecoin pairs (USDC/USDT). 30 | 31 | Now, having such a path, we can iterate over it to build pool parameters for each of the pools: 32 | 33 | 1. `WETH, 60, USDC`; 34 | 1. `USDC, 10, USDT`; 35 | 1. `USDT, 60, WBTC`. 36 | 37 | Knowing these parameters, we can derive pool addresses using `PoolAddress.computeAddress`, which we implemented in the previous chapter. 38 | 39 | > We also can use this concept when doing swaps within one pool: the path would simply contain the parameters of one pool. And, thus, we can use swap paths in all swaps, universally. 40 | 41 | Let's build a library to work with swap paths. 42 | 43 | ## Path Library 44 | 45 | In code, a swap path is a sequence of bytes. In Solidity, a path can be built like this: 46 | ```solidity 47 | bytes.concat( 48 | bytes20(address(weth)), 49 | bytes3(uint24(60)), 50 | bytes20(address(usdc)), 51 | bytes3(uint24(10)), 52 | bytes20(address(usdt)), 53 | bytes3(uint24(60)), 54 | bytes20(address(wbtc)) 55 | ); 56 | ``` 57 | 58 | It looks like this: 59 | ```shell 60 | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 # weth address 61 | 00003c # 60 62 | A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # usdc address 63 | 00000a # 10 64 | dAC17F958D2ee523a2206206994597C13D831ec7 # usdt address 65 | 00003c # 60 66 | 2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 # wbtc address 67 | ``` 68 | 69 | These are the functions that we'll need to implement: 70 | 1. calculating the number of pools in a path; 71 | 1. figuring out if a path has multiple pools; 72 | 1. extracting first pool parameters from a path; 73 | 1. proceeding to the next pair in a path; 74 | 1. and decoding first pool parameters. 75 | 76 | ### Calculating the Number of Pools in a Path 77 | Let's begin with calculating the number of pools in a path: 78 | ```solidity 79 | // src/lib/Path.sol 80 | library Path { 81 | /// @dev The length the bytes encoded address 82 | uint256 private constant ADDR_SIZE = 20; 83 | /// @dev The length the bytes encoded tick spacing 84 | uint256 private constant TICKSPACING_SIZE = 3; 85 | 86 | /// @dev The offset of a single token address + tick spacing 87 | uint256 private constant NEXT_OFFSET = ADDR_SIZE + TICKSPACING_SIZE; 88 | /// @dev The offset of an encoded pool key (tokenIn + tick spacing + tokenOut) 89 | uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; 90 | /// @dev The minimum length of a path that contains 2 or more pools; 91 | uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = 92 | POP_OFFSET + NEXT_OFFSET; 93 | 94 | ... 95 | ``` 96 | 97 | We first define a few constants: 98 | 1. `ADDR_SIZE` is the size of an address, 20 bytes; 99 | 1. `TICKSPACING_SIZE` is the size of a tick spacing, 3 bytes (`uint24`); 100 | 1. `NEXT_OFFSET` is the offset of a next token address–to get it, we skip an address and a tick spacing; 101 | 1. `POP_OFFSET` is the offset of a pool key (token address + tick spacing + token address); 102 | 1. `MULTIPLE_POOLS_MIN_LENGTH` is the minimum length of a path that contains 2 or more pools (one set of pool parameters + tick spacing + token address). 103 | 104 | To count the number of pools in a path, we subtract the size of an address (first or last token in a path) and divide the remaining part by `NEXT_OFFSET` (address + tick spacing): 105 | 106 | ```solidity 107 | function numPools(bytes memory path) internal pure returns (uint256) { 108 | return (path.length - ADDR_SIZE) / NEXT_OFFSET; 109 | } 110 | ``` 111 | 112 | ### Figuring Out if a Path Has Multiple Pools 113 | To check if there are multiple pools in a path, we need to compare the length of a path with `MULTIPLE_POOLS_MIN_LENGTH`: 114 | 115 | ```solidity 116 | function hasMultiplePools(bytes memory path) internal pure returns (bool) { 117 | return path.length >= MULTIPLE_POOLS_MIN_LENGTH; 118 | } 119 | ``` 120 | 121 | ### Extracting First Pool Parameters From a Path 122 | 123 | To implement other functions, we'll need a helper library because Solidity doesn't have native bytes manipulation functions. Specifically, we'll need a function to extract a sub-array from an array of bytes, and a couple of functions to convert bytes to `address` and `uint24`. 124 | 125 | Luckily, there's a great open-source library called [solidity-bytes-utils](https://github.com/GNSPS/solidity-bytes-utils). To use the library, we need to extend the `bytes` type in the `Path` library: 126 | ```solidity 127 | library Path { 128 | using BytesLib for bytes; 129 | ... 130 | } 131 | ``` 132 | 133 | We can implement `getFirstPool` now: 134 | ```solidity 135 | function getFirstPool(bytes memory path) 136 | internal 137 | pure 138 | returns (bytes memory) 139 | { 140 | return path.slice(0, POP_OFFSET); 141 | } 142 | ``` 143 | 144 | The function simply returns the first "token address + tick spacing + token address" segment encoded as bytes. 145 | 146 | ### Proceeding to a Next Pair in a Path 147 | 148 | 149 | We'll use the next function when iterating over a path and throwing away processed pools. Notice that we're removing "token address + tick spacing", not full pool parameters, because we need the other token address to calculate the next pool address. 150 | 151 | ```solidity 152 | function skipToken(bytes memory path) internal pure returns (bytes memory) { 153 | return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); 154 | } 155 | ``` 156 | 157 | ### Decoding First Pool Parameters 158 | 159 | And, finally, we need to decode the parameters of the first pool in a path: 160 | 161 | ```solidity 162 | function decodeFirstPool(bytes memory path) 163 | internal 164 | pure 165 | returns ( 166 | address tokenIn, 167 | address tokenOut, 168 | uint24 tickSpacing 169 | ) 170 | { 171 | tokenIn = path.toAddress(0); 172 | tickSpacing = path.toUint24(ADDR_SIZE); 173 | tokenOut = path.toAddress(NEXT_OFFSET); 174 | } 175 | ``` 176 | 177 | Unfortunately, `BytesLib` doesn't implement `toUint24` function but we can implement it ourselves! `BytesLib` has multiple `toUintXX` functions, so we can take one of them and convert it to a `uint24` one: 178 | ```solidity 179 | library BytesLibExt { 180 | function toUint24(bytes memory _bytes, uint256 _start) 181 | internal 182 | pure 183 | returns (uint24) 184 | { 185 | require(_bytes.length >= _start + 3, "toUint24_outOfBounds"); 186 | uint24 tempUint; 187 | 188 | assembly { 189 | tempUint := mload(add(add(_bytes, 0x3), _start)) 190 | } 191 | 192 | return tempUint; 193 | } 194 | } 195 | ``` 196 | 197 | We're doing this in a new library contract, which we can then use in our Path library alongside `BytesLib`: 198 | 199 | ```solidity 200 | library Path { 201 | using BytesLib for bytes; 202 | using BytesLibExt for bytes; 203 | ... 204 | } 205 | ``` 206 | -------------------------------------------------------------------------------- /src/milestone_4/tick-rounding.md: -------------------------------------------------------------------------------- 1 | # Tick Rounding 2 | 3 | Let's review some other changes we need to make to support different tick spacings. 4 | 5 | Tick spacing greater than 1 won't allow users to select arbitrary price ranges: tick indexes must be multiples of a tick spacing. For example, for tick spacing 60 we can have ticks: 0, 60, 120, 180, etc. Thus, when the user picks a range, we need to "round" it so its boundaries are multiples of the pool's tick spacing. 6 | 7 | ## `nearestUsableTick` in JavaScript 8 | 9 | In [the Uniswap V3 SDK](https://github.com/Uniswap/v3-sdk), the function that does that is called [nearestUsableTick](https://github.com/Uniswap/v3-sdk/blob/b6cd73a71f8f8ec6c40c130564d3aff12c38e693/src/utils/nearestUsableTick.ts): 10 | ```javascript 11 | /** 12 | * Returns the closest tick that is nearest a given tick and usable for the given tick spacing 13 | * @param tick the target tick 14 | * @param tickSpacing the spacing of the pool 15 | */ 16 | export function nearestUsableTick(tick: number, tickSpacing: number) { 17 | invariant(Number.isInteger(tick) && Number.isInteger(tickSpacing), 'INTEGERS') 18 | invariant(tickSpacing > 0, 'TICK_SPACING') 19 | invariant(tick >= TickMath.MIN_TICK && tick <= TickMath.MAX_TICK, 'TICK_BOUND') 20 | const rounded = Math.round(tick / tickSpacing) * tickSpacing 21 | if (rounded < TickMath.MIN_TICK) return rounded + tickSpacing 22 | else if (rounded > TickMath.MAX_TICK) return rounded - tickSpacing 23 | else return rounded 24 | } 25 | ``` 26 | 27 | At its core, it's just: 28 | ```javascript 29 | Math.round(tick / tickSpacing) * tickSpacing 30 | ``` 31 | 32 | Where `Math.round` is rounding to the nearest integer: when the fractional part is less than 0.5, it rounds to the lower integer; when it's greater than 0.5 it rounds to the greater integer; and when it's 0.5, it rounds to the greater integer as well. 33 | 34 | So, in the web app, we'll use `nearestUsableTick` when building `mint` parameters: 35 | ```javascript 36 | const mintParams = { 37 | tokenA: pair.token0.address, 38 | tokenB: pair.token1.address, 39 | tickSpacing: pair.tickSpacing, 40 | lowerTick: nearestUsableTick(lowerTick, pair.tickSpacing), 41 | upperTick: nearestUsableTick(upperTick, pair.tickSpacing), 42 | amount0Desired, amount1Desired, amount0Min, amount1Min 43 | } 44 | ``` 45 | 46 | > In reality, it should be called whenever the user adjusts a price range because we want the user to see the actual price that will be created. In our simplified app, we make it less user-friendly. 47 | 48 | However, we also want to have a similar function in Solidity tests, but neither of the math libraries we're using implements it. 49 | 50 | ## `nearestUsableTick` in Solidity 51 | 52 | In our smart contract tests, we need a way to round ticks and convert rounded prices to $\sqrt{P}$. In a previous chapter, we chose to use [ABDKMath64x64](https://github.com/abdk-consulting/abdk-libraries-solidity) to handle fixed-point numbers math in tests. The library, however, doesn't implement the rounding function we need to port `nearestUsableTick`, so we'll need to implement it ourselves: 53 | 54 | ```solidity 55 | function divRound(int128 x, int128 y) 56 | internal 57 | pure 58 | returns (int128 result) 59 | { 60 | int128 quot = ABDKMath64x64.div(x, y); 61 | result = quot >> 64; 62 | 63 | // Check if remainder is greater than 0.5 64 | if (quot % 2**64 >= 0x8000000000000000) { 65 | result += 1; 66 | } 67 | } 68 | ``` 69 | 70 | The function does multiple things: 71 | 1. it divides two Q64.64 numbers; 72 | 1. it then rounds the result to the decimal one (`result = quot >> 64`), the fractional part is lost at this point (i.e. the result is rounded down); 73 | 1. it then divides the quotient by $2^{64}$, takes the remainder, and compares it with `0x8000000000000000` (which is 0.5 in Q64.64); 74 | 1. if the remainder is greater or equal to 0.5, it rounds the result to the greater integer. 75 | 76 | What we get is an integer rounded according to the rules of `Math.round` from JavaScript. We can then re-implement `nearestUsableTick`: 77 | 78 | ```solidity 79 | function nearestUsableTick(int24 tick_, uint24 tickSpacing) 80 | internal 81 | pure 82 | returns (int24 result) 83 | { 84 | result = 85 | int24(divRound(int128(tick_), int128(int24(tickSpacing)))) * 86 | int24(tickSpacing); 87 | 88 | if (result < TickMath.MIN_TICK) { 89 | result += int24(tickSpacing); 90 | } else if (result > TickMath.MAX_TICK) { 91 | result -= int24(tickSpacing); 92 | } 93 | } 94 | ``` 95 | 96 | That's it! -------------------------------------------------------------------------------- /src/milestone_4/user-interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | After introducing swap paths, we can significantly simplify the internals of our web app. First of all, every swap now uses a path since a path doesn't have to contain multiple pools. Second, it's now easier to change the direction of a swap: we can simply reverse the path. And, thanks to the unified pool address generation via `CREATE2` and unique salts, we no longer need to store pool addresses and care about token orders. 4 | 5 | However, we cannot integrate multi-pool swaps in the web app without adding one crucial algorithm. Ask yourself the question: "How to find a path between two tokens that don't have a pool?" 6 | 7 | ## AutoRouter 8 | 9 | Uniswap implements what's called *AutoRouter*, an algorithm that finds the shortest path between two tokens. Moreover, it also splits one payment into multiple smaller payments to find the best average exchange rate. The profit can be as big as [36.84% compared to trades that are not split](https://uniswap.org/blog/auto-router-v2). This sounds great, however, we're not going to build such an advanced algorithm. Instead, we'll build something simpler. 10 | 11 | ## A Simple Router Design 12 | 13 | Suppose we have a whole bunch of pools: 14 | 15 | ![Scattered pools](images/pools_scattered.png) 16 | 17 | How do we find the shortest path between two tokens in such a mess? 18 | 19 | The most suitable solution for such kinds of tasks is based on a *graph*. A graph is a data structure that consists of nodes (objects representing something) and edges (links connecting nodes). We can turn that mess of pools into a graph where each node is a token (that has a pool) and each edge is a pool this token belongs to. So a pool represented as a graph is two nodes connected with an edge. The above pools become this graph: 20 | 21 | ![Pools graph](images/pools_graph.png) 22 | 23 | The biggest advantage graphs give us is the ability to traverse its nodes, from one node to another, to find paths. Specifically, we'll use [A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). Feel free to learn about how the algorithm works, but, in our app, we'll use a library to make our life easier. The set of libraries we'll use is [ngraph.ngraph](https://github.com/anvaka/ngraph.graph) for building graphs and [ngraph.path](https://github.com/anvaka/ngraph.path) for finding paths (it's the latter that implements A* search algorithm, as well as some others). 24 | 25 | In the UI app, let's create a pathfinder. This will be a class that, when instantiated, turns a list of pairs into a graph to later use the graph to find the shortest path between two tokens. 26 | ```javascript 27 | import createGraph from 'ngraph.graph'; 28 | import path from 'ngraph.path'; 29 | 30 | class PathFinder { 31 | constructor(pairs) { 32 | this.graph = createGraph(); 33 | 34 | pairs.forEach((pair) => { 35 | this.graph.addNode(pair.token0.address); 36 | this.graph.addNode(pair.token1.address); 37 | this.graph.addLink(pair.token0.address, pair.token1.address, pair.tickSpacing); 38 | this.graph.addLink(pair.token1.address, pair.token0.address, pair.tickSpacing); 39 | }); 40 | 41 | this.finder = path.aStar(this.graph); 42 | } 43 | 44 | ... 45 | ``` 46 | 47 | In the constructor, we're creating an empty graph and fill it with linked nodes. Each node is a token address and links have associated data, which is tick spacings–we'll be able to extract this information from paths found by A*. After initializing a graph, we instantiate the A* algorithm implementation. 48 | 49 | Next, we need to implement a function that will find a path between tokens and turn it into an array of token addresses and tick spacings: 50 | 51 | ```javascript 52 | findPath(fromToken, toToken) { 53 | return this.finder.find(fromToken, toToken).reduce((acc, node, i, orig) => { 54 | if (acc.length > 0) { 55 | acc.push(this.graph.getLink(orig[i - 1].id, node.id).data); 56 | } 57 | 58 | acc.push(node.id); 59 | 60 | return acc; 61 | }, []).reverse(); 62 | } 63 | ``` 64 | 65 | `this.finder.find(fromToken, toToken)` returns a list of nodes and, unfortunately, doesn't contain the information about edges between them (we store tick spacings in edges). Thus, we're calling `this.graph.getLink(previousNode, currentNode)` to find edges. 66 | 67 | Now, whenever the user changes the input or output token, we can call `pathFinder.findPath(token0, token1)` to build a new path. -------------------------------------------------------------------------------- /src/milestone_5/flash-loan-fees.md: -------------------------------------------------------------------------------- 1 | # Flash Loan Fees 2 | 3 | In a previous chapter, we implemented flash loans and made them free. However, Uniswap collects swap fees on flash loans, and we're going to add this to our implementation: the amounts repaid by flash loan borrowers must include a fee. 4 | 5 | Here's what the updated `flash` function looks like: 6 | ```solidity 7 | function flash( 8 | uint256 amount0, 9 | uint256 amount1, 10 | bytes calldata data 11 | ) public { 12 | uint256 fee0 = Math.mulDivRoundingUp(amount0, fee, 1e6); 13 | uint256 fee1 = Math.mulDivRoundingUp(amount1, fee, 1e6); 14 | 15 | uint256 balance0Before = IERC20(token0).balanceOf(address(this)); 16 | uint256 balance1Before = IERC20(token1).balanceOf(address(this)); 17 | 18 | if (amount0 > 0) IERC20(token0).transfer(msg.sender, amount0); 19 | if (amount1 > 0) IERC20(token1).transfer(msg.sender, amount1); 20 | 21 | IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback( 22 | fee0, 23 | fee1, 24 | data 25 | ); 26 | 27 | if (IERC20(token0).balanceOf(address(this)) < balance0Before + fee0) 28 | revert FlashLoanNotPaid(); 29 | if (IERC20(token1).balanceOf(address(this)) < balance1Before + fee1) 30 | revert FlashLoanNotPaid(); 31 | 32 | emit Flash(msg.sender, amount0, amount1); 33 | } 34 | ``` 35 | 36 | What's changed is that we're now calculating fees on the amounts requested by the caller and then expect pool balances to have grown by the fee amounts. -------------------------------------------------------------------------------- /src/milestone_5/images/fees_inside_and_outside_price_range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/fees_inside_and_outside_price_range.png -------------------------------------------------------------------------------- /src/milestone_5/images/interpolated_prices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/interpolated_prices.png -------------------------------------------------------------------------------- /src/milestone_5/images/liquidity_range_engaged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/liquidity_range_engaged.png -------------------------------------------------------------------------------- /src/milestone_5/images/liquidity_ranges_fees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/liquidity_ranges_fees.png -------------------------------------------------------------------------------- /src/milestone_5/images/observations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/observations.png -------------------------------------------------------------------------------- /src/milestone_5/images/observations_wrapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_5/images/observations_wrapping.png -------------------------------------------------------------------------------- /src/milestone_5/introduction.md: -------------------------------------------------------------------------------- 1 | # Fees and Price Oracle 2 | 3 | In this milestone, we're going to add two new features to our Uniswap implementation. They share one similarity: they work on top of what we have already built–that's why we've delayed them until this milestone. However, they're not equally important. 4 | 5 | We're going to add swap fees and a price oracle: 6 | - Swap fees are a crucial mechanism of the DEX design we're implementing. They're the glue that makes things stick together. Swap fees incentivize liquidity providers to provide liquidity, and no trades are possible without liquidity, as we have already learned. 7 | - A price oracle, on the other hand, is an optional utility function of a DEX. A DEX, while conducting trades, can also function as a price oracle–that is, provide token prices to other services. This doesn't affect actual swaps but provides a useful service to other on-chain applications. 8 | 9 | Alright, let's get building! 10 | 11 | > You'll find the complete code of this chapter in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_5). 12 | > 13 | > This milestone introduces a lot of code changes in existing contracts. [Here you can see all changes since the last milestone](https://github.com/Jeiwan/uniswapv3-code/compare/milestone_4...milestone_5) 14 | 15 | > If you have any questions feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-5-fees-and-price-oracle)! -------------------------------------------------------------------------------- /src/milestone_5/protocol-fees.md: -------------------------------------------------------------------------------- 1 | # Protocol Fees 2 | 3 | While working on the Uniswap implementation, you've probably asked yourself, "How does Uniswap make money?" Well, it doesn't (at least as of September 2022). 4 | 5 | In the implementation we've built so far, traders pay liquidity providers for providing liquidity, and Uniswap Labs, as the company that developed the DEX, is not part of this process. Neither traders nor liquidity providers pay Uniswap Labs for using the Uniswap DEX. How come? 6 | 7 | There's a way for Uniswap Labs to start making money on the DEX. However, the mechanism hasn't been enabled yet (again, as of September 2022). Each Uniswap pool has a *protocol fees* collection mechanism. Protocol fees are collected from swap fees: a small portion of swap fees is subtracted and saved as protocol fees to later be collected by the Factory contract owner (Uniswap Labs). The size of protocol fees is expected to be determined by UNI token holders, but it must be between $1/4$ and $1/10$ (inclusive) of swap fees. 8 | 9 | For brevity, we're not going to add protocol fees to our implementation, but let's see how they're implemented in Uniswap. 10 | 11 | Protocol fee size is stored in `slot0`: 12 | 13 | ```solidity 14 | // UniswapV3Pool.sol 15 | struct Slot0 { 16 | ... 17 | // the current protocol fee as a percentage of the swap fee taken on withdrawal 18 | // represented as an integer denominator (1/x)% 19 | uint8 feeProtocol; 20 | ... 21 | } 22 | ``` 23 | 24 | A global accumulator is needed to track accrued fees: 25 | ```solidity 26 | // accumulated protocol fees in token0/token1 units 27 | struct ProtocolFees { 28 | uint128 token0; 29 | uint128 token1; 30 | } 31 | ProtocolFees public override protocolFees; 32 | ``` 33 | 34 | Protocol fees are set in the `setFeeProtocol` function: 35 | 36 | ```solidity 37 | function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner { 38 | require( 39 | (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10)) && 40 | (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10)) 41 | ); 42 | uint8 feeProtocolOld = slot0.feeProtocol; 43 | slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4); 44 | emit SetFeeProtocol(feeProtocolOld % 16, feeProtocolOld >> 4, feeProtocol0, feeProtocol1); 45 | } 46 | ``` 47 | 48 | As you can see, it's allowed to set protocol fees separately for each of the tokens. The values are two `uint8` that are packed to be stored in one `uint8`: `feeProtocol1` is shifted to the left by 4 bits (this is identical to multiplying it by 16) and added to `feeProtocol0`. To unpack `feeProtocol0`, a remainder of division `slot0.feeProtocol` by 16 is taken; `feeProtocol1` is simply shifting `slot0.feeProtocol` to the right by 4 bits. Such packing works because neither `feeProtocol0`, nor `feeProtocol1` can be greater than 10. 49 | 50 | Before beginning a swap, we need to choose one of the protocol fees depending on the swap direction (swap and protocol fees are collected on input tokens): 51 | 52 | ```solidity 53 | function swap(...) { 54 | ... 55 | uint8 feeProtocol = zeroForOne ? (slot0_.feeProtocol % 16) : (slot0_.feeProtocol >> 4); 56 | ... 57 | ``` 58 | 59 | To accrue protocol fees, we subtract them from swap fees right after computing swap step amounts: 60 | 61 | ```solidity 62 | ... 63 | while (...) { 64 | (..., step.feeAmount) = SwapMath.computeSwapStep(...); 65 | 66 | if (cache.feeProtocol > 0) { 67 | uint256 delta = step.feeAmount / cache.feeProtocol; 68 | step.feeAmount -= delta; 69 | state.protocolFee += uint128(delta); 70 | } 71 | 72 | ... 73 | } 74 | ... 75 | ``` 76 | 77 | After a swap is done, the global protocol fees accumulator needs to be updated: 78 | ```solidity 79 | if (zeroForOne) { 80 | if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee; 81 | } else { 82 | if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee; 83 | } 84 | ``` 85 | 86 | Finally, the Factory contract owner can collect accrued protocol fees by calling `collectProtocol`: 87 | 88 | ```solidity 89 | function collectProtocol( 90 | address recipient, 91 | uint128 amount0Requested, 92 | uint128 amount1Requested 93 | ) external override lock onlyFactoryOwner returns (uint128 amount0, uint128 amount1) { 94 | amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested; 95 | amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested; 96 | 97 | if (amount0 > 0) { 98 | if (amount0 == protocolFees.token0) amount0--; 99 | protocolFees.token0 -= amount0; 100 | TransferHelper.safeTransfer(token0, recipient, amount0); 101 | } 102 | if (amount1 > 0) { 103 | if (amount1 == protocolFees.token1) amount1--; 104 | protocolFees.token1 -= amount1; 105 | TransferHelper.safeTransfer(token1, recipient, amount1); 106 | } 107 | 108 | emit CollectProtocol(msg.sender, recipient, amount0, amount1); 109 | } 110 | ``` -------------------------------------------------------------------------------- /src/milestone_5/user-interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | In this milestone, we've added the ability to remove liquidity from a pool and collect accumulated fees. Thus, we need to reflect these changes in the user interface to allow users to remove liquidity. 4 | 5 | ## Fetching Positions 6 | 7 | To let the user choose how much liquidity to remove, we first need to fetch the user's positions from a pool. To make this easier, we can add a helper function to the Manager contract, which will return the user position in a specific pool: 8 | ```solidity 9 | function getPosition(GetPositionParams calldata params) 10 | public 11 | view 12 | returns ( 13 | uint128 liquidity, 14 | uint256 feeGrowthInside0LastX128, 15 | uint256 feeGrowthInside1LastX128, 16 | uint128 tokensOwed0, 17 | uint128 tokensOwed1 18 | ) 19 | { 20 | IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); 21 | 22 | ( 23 | liquidity, 24 | feeGrowthInside0LastX128, 25 | feeGrowthInside1LastX128, 26 | tokensOwed0, 27 | tokensOwed1 28 | ) = pool.positions( 29 | keccak256( 30 | abi.encodePacked( 31 | params.owner, 32 | params.lowerTick, 33 | params.upperTick 34 | ) 35 | ) 36 | ); 37 | } 38 | ``` 39 | 40 | This will free us from calculating a pool address and a position key on the front end. 41 | 42 | Then, after the user has typed in a position range, we can try fetching a position: 43 | ```js 44 | const getAvailableLiquidity = debounce((amount, isLower) => { 45 | const lowerTick = priceToTick(isLower ? amount : lowerPrice); 46 | const upperTick = priceToTick(isLower ? upperPrice : amount); 47 | 48 | const params = { 49 | tokenA: token0.address, 50 | tokenB: token1.address, 51 | fee: fee, 52 | owner: account, 53 | lowerTick: nearestUsableTick(lowerTick, feeToSpacing[fee]), 54 | upperTick: nearestUsableTick(upperTick, feeToSpacing[fee]), 55 | } 56 | 57 | manager.getPosition(params) 58 | .then(position => setAvailableAmount(position.liquidity.toString())) 59 | .catch(err => console.error(err)); 60 | }, 500); 61 | ``` 62 | 63 | ## Getting Pool Address 64 | 65 | Since we need to call `burn` and `collect` on a pool, we still need to compute the pool's address on the front end. Recall that pool addresses are computed using the `CREATE2` opcode, which requires a salt and the hash of the contract's code. Luckily, Ether.js has the `getCreate2Address` function that allows to compute `CREATE2` in JavaScript: 66 | 67 | ```js 68 | const sortTokens = (tokenA, tokenB) => { 69 | return tokenA.toLowerCase() < tokenB.toLowerCase ? [tokenA, tokenB] : [tokenB, tokenA]; 70 | } 71 | 72 | const computePoolAddress = (factory, tokenA, tokenB, fee) => { 73 | [tokenA, tokenB] = sortTokens(tokenA, tokenB); 74 | 75 | return ethers.utils.getCreate2Address( 76 | factory, 77 | ethers.utils.keccak256( 78 | ethers.utils.solidityPack( 79 | ['address', 'address', 'uint24'], 80 | [tokenA, tokenB, fee] 81 | )), 82 | poolCodeHash 83 | ); 84 | } 85 | ``` 86 | 87 | However, the pool's codehash has to be hard coded because we don't want to store its code on the front end to calculate the hash. So, we'll use Forge to get the hash: 88 | 89 | ```shell 90 | $ forge inspect UniswapV3Pool bytecode| xargs cast keccak 91 | 0x... 92 | ``` 93 | 94 | And then use the output value in a JS constant: 95 | ```js 96 | const poolCodeHash = "0x9dc805423bd1664a6a73b31955de538c338bac1f5c61beb8f4635be5032076a2"; 97 | ``` 98 | 99 | ## Removing Liquidity 100 | 101 | After obtaining the liquidity amount and the pool address, we're ready to call `burn`: 102 | 103 | ```js 104 | const removeLiquidity = (e) => { 105 | e.preventDefault(); 106 | 107 | if (!token0 || !token1) { 108 | return; 109 | } 110 | 111 | setLoading(true); 112 | 113 | const lowerTick = nearestUsableTick(priceToTick(lowerPrice), feeToSpacing[fee]); 114 | const upperTick = nearestUsableTick(priceToTick(upperPrice), feeToSpacing[fee]); 115 | 116 | pool.burn(lowerTick, upperTick, amount) 117 | .then(tx => tx.wait()) 118 | .then(receipt => { 119 | if (!receipt.events[0] || receipt.events[0].event !== "Burn") { 120 | throw Error("Missing Burn event after burning!"); 121 | } 122 | 123 | const amount0Burned = receipt.events[0].args.amount0; 124 | const amount1Burned = receipt.events[0].args.amount1; 125 | 126 | return pool.collect(account, lowerTick, upperTick, amount0Burned, amount1Burned) 127 | }) 128 | .then(tx => tx.wait()) 129 | .then(() => toggle()) 130 | .catch(err => console.error(err)); 131 | } 132 | ``` 133 | 134 | If burning was successful, we immediately call `collect` to collect the token amounts that were freed during burning. -------------------------------------------------------------------------------- /src/milestone_6/erc721-overview.md: -------------------------------------------------------------------------------- 1 | # Overview of ERC721 2 | 3 | Let's begin with an overview of [EIP-721](https://eips.ethereum.org/EIPS/eip-721), the standard that defines NFT contracts. 4 | 5 | ERC721 is a variant of ERC20. The main difference between them is that ERC721 tokens are *non-fungible*, that is: one token is not identical to another. To distinguish ERC721 tokens, each of them has a unique ID, which is almost always the counter at which a token was minted. ERC721 tokens also have an extended concept of ownership: the owner of each token is tracked and stored in the contract. This means that only distinct tokens, identified by token IDs, can be transferred (or approved for transfer). 6 | 7 | What Uniswap V3 liquidity positions and NFTs have in common is this non-fungibility: NFTs and liquidity positions are not interchangeable and are identified by unique IDs. It's this similarity that will allow us to merge the two concepts. 8 | 9 | The biggest difference between ERC20 and ERC721 is the `tokenURI` function in the latter. NFT tokens, which are implemented as ERC721 smart contracts, have linked assets that are stored externally, not on the blockchain. To link token IDs to images (or sounds, or anything else) stored outside of the blockchain, ERC721 defines the `tokenURI` function. The function is expected to return a link to a JSON file that defines NFT token metadata, e.g.: 10 | ```json 11 | { 12 | "name": "Thor's hammer", 13 | "description": "Mjölnir, the legendary hammer of the Norse god of thunder.", 14 | "image": "https://game.example/item-id-8u5h2m.png", 15 | "strength": 20 16 | } 17 | ``` 18 | (This example is taken from the [ERC721 documentation on OpenZeppelin](https://docs.openzeppelin.com/contracts/4.x/erc721)) 19 | 20 | Such JSON file defines the name of a token, the description of a collection, the link to the image of a token, and the properties of a token. 21 | 22 | Alternatively, we may store JSON metadata and token images on-chain. This is very expensive of course (saving data on-chain is the most expensive operation in Ethereum), but we can make it cheaper if we store templates. All tokens within a collection have similar metadata (mostly identical but image links and properties are different for each token) and visuals. For the latter, we can use SVG, which is an HTML-like format, and HTML is a good templating language. 23 | 24 | When storing JSON metadata and SVG on-chain, the `tokenURI` function, instead of returning a link, would return JSON metadata directly, using the [data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax) to encode it. SVG images would also be inlined, it won't be necessary to make external requests to download token metadata and images. -------------------------------------------------------------------------------- /src/milestone_6/images/nft_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_6/images/nft_example.png -------------------------------------------------------------------------------- /src/milestone_6/images/nft_example_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_6/images/nft_example_2.png -------------------------------------------------------------------------------- /src/milestone_6/images/nft_example_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_6/images/nft_example_3.png -------------------------------------------------------------------------------- /src/milestone_6/images/nft_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeiwan/uniswapv3-book/79587424d702f82f9fb9ccedcf3bc28287adb4c0/src/milestone_6/images/nft_template.png -------------------------------------------------------------------------------- /src/milestone_6/introduction.md: -------------------------------------------------------------------------------- 1 | # NFT Positions 2 | 3 | This is the cherry on the cake of this book. In this milestone, we're going to learn how Uniswap contracts can be extended and integrated into third-party protocols. This possibility is a direct consequence of having core contracts with only crucial functions, which allows for integration into other contracts without the need to add new features to core contracts. 4 | 5 | A bonus feature of Uniswap V3 was the ability to turn liquidity positions into NFT tokens. Here's an example of one such token: 6 | 7 |

8 | Uniswap V3 NFT example 9 |

10 | 11 | It shows token symbols, pool fees, position ID, lower and upper ticks, token addresses, and the segment of the curve where the position is provided. 12 | 13 | > You can see all Uniswap V3 NFT positions in [this OpenSea collection](https://opensea.io/collection/uniswap-v3-positions). 14 | 15 | In this milestone, we're going to add NFT tokenization of liquidity positions! 16 | 17 | Let's go! 18 | 19 | > You'll find the complete code of this chapter in [this Github branch](https://github.com/Jeiwan/uniswapv3-code/tree/milestone_6). 20 | > 21 | > This milestone introduces a lot of code changes in existing contracts. [Here you can see all changes since the last milestone](https://github.com/Jeiwan/uniswapv3-code/compare/milestone_5...milestone_6) 22 | 23 | > If you have any questions feel free to ask them in [the GitHub Discussion of this milestone](https://github.com/Jeiwan/uniswapv3-book/discussions/categories/milestone-6-nft-positions)! -------------------------------------------------------------------------------- /src/milestone_6/nft-manager.md: -------------------------------------------------------------------------------- 1 | # NFT ManagerContract 2 | 3 | We're not going to add NFT-related functionality to the pool contract–we need a separate contract that will merge NFTs and liquidity positions. Recall that, while working on our implementation, we built the `UniswapV3Manager` contract to facilitate interaction with pool contracts (to make some calculations simpler and to enable multi-pool swaps). This contract was a good demonstration of how core Uniswap contracts can be extended. And we're going to push this idea a little bit further. 4 | 5 | We'll need a manager contract that will implement the ERC721 standard and will manage liquidity positions. The contract will have the standard NFT functionality (minting, burning, transferring, balances and ownership tracking, etc.) and will allow to provide and remove liquidity to pools. The contract will need to be the actual owner of liquidity in pools because we don't want to let users add liquidity without minting a token and removing the entire liquidity without burning one. We want every liquidity position to be linked to an NFT token, and we want them to be synchronized. 6 | 7 | Let's see what functions we'll have in the new contract: 8 | 1. since it'll be an NFT contract, it'll have all the ERC721 functions, including `tokenURI`, which returns the URI of the image of an NFT token; 9 | 1. `mint` and `burn` to mint and burn liquidity and NFT tokens at the same time; 10 | 1. `addLiquidity` and `removeLiquidity` to add and remove liquidity in existing positions; 11 | 1. `collect`, to collect tokens after removing liquidity. 12 | 13 | Alright, let's get to code. 14 | 15 | ## The Minimal Contract 16 | 17 | Since we don't want to implement the ERC721 standard from scratch, we're going to use a library. We already have [Solmate](https://github.com/transmissions11/solmate) in the dependencies, so we're going to use [its ERC721 implementation](https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol). 18 | 19 | > Using [the ERC721 implementation from OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721) is also an option, but I prefer the gas-optimized contracts from Solmate. 20 | 21 | This will be the bare minimum of the NFT manager contract: 22 | 23 | ```solidity 24 | contract UniswapV3NFTManager is ERC721 { 25 | address public immutable factory; 26 | 27 | constructor(address factoryAddress) 28 | ERC721("UniswapV3 NFT Positions", "UNIV3") 29 | { 30 | factory = factoryAddress; 31 | } 32 | 33 | function tokenURI(uint256 tokenId) 34 | public 35 | view 36 | override 37 | returns (string memory) 38 | { 39 | return ""; 40 | } 41 | } 42 | ``` 43 | 44 | `tokenURI` will return an empty string until we implement a metadata and SVG renderer. We've added the stub so that the Solidity compiler doesn't fail while we're working on the rest of the contract (the `tokenURI` function in the Solmate ERC721 contract is virtual, so we must implement it). 45 | 46 | ## Minting 47 | 48 | Minting, as we discussed earlier, will involve two operations: adding liquidity to a pool and minting an NFT. 49 | 50 | To keep the links between pool liquidity positions and NFTs, we'll need a mapping and a structure: 51 | 52 | ```solidity 53 | struct TokenPosition { 54 | address pool; 55 | int24 lowerTick; 56 | int24 upperTick; 57 | } 58 | mapping(uint256 => TokenPosition) public positions; 59 | ``` 60 | 61 | To find a position we need: 62 | 1. a pool address; 63 | 1. an owner address; 64 | 1. the boundaries of a position (lower and upper ticks). 65 | 66 | Since the NFT manager contract will be the owner of all positions created via it, we don't need to store the position's owner address and we can only store the rest data. The keys in the `positions` mapping are token IDs; the mapping links NFT IDs to the position data that is required to find a liquidity position. 67 | 68 | Let's implement minting: 69 | 70 | ```solidity 71 | struct MintParams { 72 | address recipient; 73 | address tokenA; 74 | address tokenB; 75 | uint24 fee; 76 | int24 lowerTick; 77 | int24 upperTick; 78 | uint256 amount0Desired; 79 | uint256 amount1Desired; 80 | uint256 amount0Min; 81 | uint256 amount1Min; 82 | } 83 | 84 | function mint(MintParams calldata params) public returns (uint256 tokenId) { 85 | ... 86 | } 87 | ``` 88 | 89 | The minting parameters are identical to those of `UniswapV3Manager`, with the addition of `recipient`, which will allow minting NFT to another address. 90 | 91 | In the `mint` function, we first add liquidity to a pool: 92 | 93 | ```solidity 94 | IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); 95 | 96 | (uint128 liquidity, uint256 amount0, uint256 amount1) = _addLiquidity( 97 | AddLiquidityInternalParams({ 98 | pool: pool, 99 | lowerTick: params.lowerTick, 100 | upperTick: params.upperTick, 101 | amount0Desired: params.amount0Desired, 102 | amount1Desired: params.amount1Desired, 103 | amount0Min: params.amount0Min, 104 | amount1Min: params.amount1Min 105 | }) 106 | ); 107 | ``` 108 | 109 | `_addLiquidity` is identical to the body of the `mint` function in the `UniswapV3Manager` contract: it converts ticks to $\sqrt(P)$, computes liquidity amount, and calls `pool.mint()`. 110 | 111 | Next, we mint an NFT: 112 | 113 | ```solidity 114 | tokenId = nextTokenId++; 115 | _mint(params.recipient, tokenId); 116 | totalSupply++; 117 | ``` 118 | 119 | `tokenId` is set to the current `nextTokenId` and the latter is then incremented. The `_mint` function is provided by the ERC721 contract from Solmate. After minting a new token, we update `totalSupply`. 120 | 121 | Finally, we need to store the information about the new token and the new position: 122 | 123 | ```solidity 124 | TokenPosition memory tokenPosition = TokenPosition({ 125 | pool: address(pool), 126 | lowerTick: params.lowerTick, 127 | upperTick: params.upperTick 128 | }); 129 | 130 | positions[tokenId] = tokenPosition; 131 | ``` 132 | 133 | This will later help us find liquidity position by token ID. 134 | 135 | ## Adding Liquidity 136 | 137 | Next, we'll implement a function to add liquidity to an existing position, in the case when we want to add more liquidity in a position that already has some. In such cases, we don't want to mint an NFT, but only to increase the amount of liquidity in an existing position. For that, we'll only need to provide a token ID and token amounts: 138 | 139 | ```solidity 140 | function addLiquidity(AddLiquidityParams calldata params) 141 | public 142 | returns ( 143 | uint128 liquidity, 144 | uint256 amount0, 145 | uint256 amount1 146 | ) 147 | { 148 | TokenPosition memory tokenPosition = positions[params.tokenId]; 149 | if (tokenPosition.pool == address(0x00)) revert WrongToken(); 150 | 151 | (liquidity, amount0, amount1) = _addLiquidity( 152 | AddLiquidityInternalParams({ 153 | pool: IUniswapV3Pool(tokenPosition.pool), 154 | lowerTick: tokenPosition.lowerTick, 155 | upperTick: tokenPosition.upperTick, 156 | amount0Desired: params.amount0Desired, 157 | amount1Desired: params.amount1Desired, 158 | amount0Min: params.amount0Min, 159 | amount1Min: params.amount1Min 160 | }) 161 | ); 162 | } 163 | ``` 164 | 165 | This function ensures there's an existing token and calls `pool.mint()` with parameters of an existing position. 166 | 167 | ## Remove Liquidity 168 | 169 | Recall that in the `UniswapV3Manager` contract we didn't implement a `burn` function because we wanted users to be owners of liquidity positions. Now, we want the NFT manager to be the owner. And we can have liquidity burning implemented in it: 170 | 171 | ```solidity 172 | struct RemoveLiquidityParams { 173 | uint256 tokenId; 174 | uint128 liquidity; 175 | } 176 | 177 | function removeLiquidity(RemoveLiquidityParams memory params) 178 | public 179 | isApprovedOrOwner(params.tokenId) 180 | returns (uint256 amount0, uint256 amount1) 181 | { 182 | TokenPosition memory tokenPosition = positions[params.tokenId]; 183 | if (tokenPosition.pool == address(0x00)) revert WrongToken(); 184 | 185 | IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); 186 | 187 | (uint128 availableLiquidity, , , , ) = pool.positions( 188 | poolPositionKey(tokenPosition) 189 | ); 190 | if (params.liquidity > availableLiquidity) revert NotEnoughLiquidity(); 191 | 192 | (amount0, amount1) = pool.burn( 193 | tokenPosition.lowerTick, 194 | tokenPosition.upperTick, 195 | params.liquidity 196 | ); 197 | } 198 | ``` 199 | 200 | We're again checking that the provided token ID is valid. And we also need to ensure that a position has enough liquidity to burn. 201 | 202 | ## Collecting Tokens 203 | 204 | The NFT manager contract can also collect tokens after burning liquidity. Notice that collected tokens are sent to `msg.sender` since the contract manages liquidity on behalf of the caller: 205 | 206 | ```solidity 207 | struct CollectParams { 208 | uint256 tokenId; 209 | uint128 amount0; 210 | uint128 amount1; 211 | } 212 | 213 | function collect(CollectParams memory params) 214 | public 215 | isApprovedOrOwner(params.tokenId) 216 | returns (uint128 amount0, uint128 amount1) 217 | { 218 | TokenPosition memory tokenPosition = positions[params.tokenId]; 219 | if (tokenPosition.pool == address(0x00)) revert WrongToken(); 220 | 221 | IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); 222 | 223 | (amount0, amount1) = pool.collect( 224 | msg.sender, 225 | tokenPosition.lowerTick, 226 | tokenPosition.upperTick, 227 | params.amount0, 228 | params.amount1 229 | ); 230 | } 231 | ``` 232 | 233 | ## Burning 234 | 235 | Finally, burning. Unlike the other functions of the contract, this function doesn't do anything with a pool: it only burns an NFT. To burn an NFT, the underlying position must be empty and tokens must be collected. So, if we want to burn an NFT, we need to: 236 | 1. call `removeLiquidity` and remove the entire position liquidity; 237 | 1. call `collect` to collect the tokens after burning the position; 238 | 1. call `burn` to burn the token. 239 | 240 | ```solidity 241 | function burn(uint256 tokenId) public isApprovedOrOwner(tokenId) { 242 | TokenPosition memory tokenPosition = positions[tokenId]; 243 | if (tokenPosition.pool == address(0x00)) revert WrongToken(); 244 | 245 | IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); 246 | (uint128 liquidity, , , uint128 tokensOwed0, uint128 tokensOwed1) = pool 247 | .positions(poolPositionKey(tokenPosition)); 248 | 249 | if (liquidity > 0 || tokensOwed0 > 0 || tokensOwed1 > 0) 250 | revert PositionNotCleared(); 251 | 252 | delete positions[tokenId]; 253 | _burn(tokenId); 254 | totalSupply--; 255 | } 256 | ``` 257 | 258 | That's it! --------------------------------------------------------------------------------