├── .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 |
5 |
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 |  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 |
5 |
57 |
58 |
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 |  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 |  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 | 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 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |
9 |