Uniswap-v3
Uniswap-v3 copied to clipboard
🦄🦄 🦄Single & multi hop swap, add and remove liquidity, flash swap ⚡
Table of Contents
-
About The Project
- Built With
-
Getting Started
- Prerequisites
- Installation
- Usage
- Constant Product AMM
- Test Single and Multi hop swap
- Uniswap V2 Single Hop Swap
- State variables
- Function swapExactInputSingleHop
- Function swapExactOutputSingleHop
- State variables
- Function swapExactInputMultiHop
- Function swapExactOutputMultiHop
- State variables
- Constructor
- Function addLiquidity
- Function removeLiquidiquiity
- State variables
- Constructor
- Function flashSwap
About The Project
This project shows how to interact with the main functions of Uniswap V3 and derive the equations used in the protocol
(back to top)
Built With
(back to top)
Getting Started
To get a local copy up and running follow these simple example steps.
Prerequisites
-
npm
npm init
-
hardhat
npm install --save-dev hardhat
run:
npx hardhat
verify:
npx hardhat verify --network goerli "contract address" "pair address"
Installation
-
Clone the repo
git clone https://github.com/Aboudoc/Uniswap-v3.git
-
Install NPM packages
npm install
-
Dependencies
npm add @uniswap/v3-periphery @uniswap/v3-core
For openzeppelin contract, we'll need to install first
solidity 0.7
npm i @openzeppelin/[email protected]
To fix compiler errors, we'll need to change the compilation settings
(back to top)
Usage
If you need testnet funds, use the Alchemy testnet faucet.
This project shows how to swap, add and remove liquidity
Constant Product AMM
Uniswap V3 is a Constant product AMM (automated market maker) <=> a decentralized exchange where 2 tokens are traded. You can find a deep overview of CPAMM in this repo
Test Single and Multi hop swap
npx hardhat test test/unlock-account.test.js
npx hardhat test test/swapV3.test.js
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/test.png)
(back to top)
Uniswap V3 Single Hop Swap
This contract introduces 2 functions to perform single hop swaps on Uniswap V3
exactInputSingle
- Sell all of input token.
exactOutputSingle
- Buy specific amount of output token.
We are interacting with the interface for Uniswap V3 router ISwapRouter
State variables
- Address of tokens (2 or 3) and the address of the router
- Set interfaces for tokens and router
Function swapExactInputSingleHop
- Transfer
amountIn
frommsg.sender
- Approve
amountIn
torouter
- Set the
params
by preparing struct ISwapRouter.ExactInputSingleParams - Call
exactInputSingle
on ISwapRouter (router interface)
Function swapExactOutputSingleHop
- Transfer
amountInMax
frommsg.sender
- Approve
amountInMax
torouter
- Set the
params
by preparing struct ISwapRouter.ExactOutputSingleParams - Call
exactOutputSingle
on ISwapRouter and store amount of WETH spent by Uniswap in amountIn (uint) - Refund WETH not spent back to msg.sender
- Reset approvals of WETH for router to 0
Uniswap V3 Multi Hop Swap
Swap WETH for USDC and then USDC for DAI.
exactInput
- Sell all of input token.
exactOutput
- Buy specific amount of output token.
We are interacting with the interface for Uniswap V3 router ISwapRouter
State variables
- Address of tokens and the address of the router
- Set interfaces for tokens and router
Function swapExactInputMultiHop
This function will swap WETH for maximum amount of DAI.
- Transfer
amountIn
frommsg.sender
- Approve
amountIn
torouter
- Setup the swapping
path
- Prepare struct ISwapRouter.ExactInputParams
- Execute the trade by calling
router.exactInput
with the parameters prepared above
Function swapExactOutputMultiHop
This function will swap minimum amount of WETH for a specific amount of DAI.
- Transfer
amountInMax
frommsg.sender
- Approve
amountInMax
torouter
- Setup the swapping
path
- Call
swapTokensForExactTokens
on IUniswapV2Router and store the actual amount of token in swapped for token out in amountSwap (uint) - Refund efund WETH not spent back to msg.sender
- Reset approvals of WETH for router to 0
(back to top)
Uniswap V3 Curve of real reserves
In Uniswap V3 the curve of the real reserve is giving by the formula below (orange square)
Let's derive this equation starting from the constant product equation XY = K
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths0bis.png)
Let's now derive the equation, the curve for the real reserve
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths1.png)
Now that we can rewrite x
and y
in terms of the liquidity L
and the current price P
, let's now derive the equation for the real reserves (solve for Xv and Yv)
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math02.png)
Let's solve for Xv
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths02bis.png)
Let's solve for Yv
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math02ter.png)
The final step to derive the curve for the real reserve is to combine all of the equations that we have derived so far
![Maths](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math03.png)
Later on, we will use this equation to derive the liquidity delta
: changing liquidity when we add some amount of token x and token y
(back to top)
Test Mint new position
npx hardhat test test/unlock-account.test.js
npx hardhat test test/liquidityV3.test.js
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/test1.png)
(back to top)
Test Collect fees
npx hardhat test test/liquidityV3.test.js
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/test2.png)
No fees in this case
(back to top)
Uniswap V3 Liquidity of a single position
We previously derived the curve for the real reserves.
Let's use this equation to derive the equation for the liquidity(this equation has 3 parts):
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths04.png)
Let's start with the first case
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths05.png)
Let's derive the equation for liquidity when the current price >= Pb
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths06.png)
Let's apply the same technique to find Ly
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths07.png)
Find Lx and Ly
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths08.png)
Finally let's put the two equations that we derived earlier
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths09.png)
(back to top)
Test Increase liquidity
npx hardhat test test/liquidityV3.test.js
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/test3.png)
(back to top)
5000 DAI and 2,77 WETH added
Amounts specified in liquidityV3.test.js
:
const daiAmount = 5000n * 10n ** 18n;
const wethAmount = 10n * 10n ** 18n;
Learn more about Add liquidity - How many dx, dy to add? in this repo about Constant Product AMM
Test Decrease liquidity
npx hardhat test test/liquidityV3.test.js
When we call the function decreaseLiquidity
, it doesn't transfer the tokens back to contract
To actually withdraw the tokens from Uniswap V3, after calling the function decreaseLiquidity()
, we'll have to call the function collect()
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/test4.png)
Note that after calling decreaseLiquidity
, DAI balance and WETH balance remain the same
(back to top)
Uniswap V3 Liquidity Delta
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math10.png)
let's derive these two equations
First, preliminary math:
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths11.png)
Let's define liquidity delta
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math12.png)
Four steps to calculate liquidity delta:
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths13.png)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths14.png)
(back to top)
Uniswap V3 How many tokens to add
Let's see a real example of how to calculate the amount of tokens needed when we add liquidity to Uniswap V3
How much USDC will we need to add with the following parameters?
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths15.png)
How do we compute delta Y? We'll use the previous equation (Uniswap V3 Liquidity Delta)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths16.png)
Comparing with user interface
You can access the uniswap app to compare the amount of USDC that we will need to put in when the price of ETH is $1754
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths17.png)
If we compare with the UI, the result is very close
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/ui.png)
(back to top)
Uniswap V3 Add and Remove Liquidity
Manage liquidity in Uniswap V3
Mint new position Increase liquidity Decrease liquidity Collect fees and withdraw tokens
State variables
Contract inherits from IERC721Receiver
- Address of tokens. Set MIN_TICK, MAX_TICK, TICK_SPACING
- Set interfaces for tokens and manager with the
INonfungiblePositionManager
interface
Function onERC721Received
This function is called when safeTransferFrom is called on INonFungiblePositionManager.
Function onERC721Received
- Transfer
wethAmountDesired
anddaiAmountDesired
frommsg.sender
- Approve
amountwethAmountDesired
anddaiAmountDesired
torouter
- Call
addLiqiuidity()
onrouter
and storewethAmount
,daiAmount
andliquidity
returned from the function call - Refund to msg.sender, excess WETH and DAI that were not added to liquidity
Function mint
This function removes liquidity from the Uniswap WETH - DAI pool.
- Transfer DAI and WETH from
msg.sender
into this contract.amount0ToAdd
is DAI amount,amount1ToAdd
is WETH - Approve
manager
to spend DAI and WETH from this contract - Set
tickLower
andtickUpper
, price range to add liquidity. Both ticks must be a multiple ofTICK_SPACING
. - Prepare parameter to add new liquidity and mint new position
- Add liquidity by calling
manager.mint
with the parameters prepared above - manager.mint returns 4 outputs. Refund tokens not added to liquidity back to msg.sender. We pulled in amount0ToAdd and amount1ToAdd. Actual amount added to Uniswap V3 are amount0 and amount1.
- Reset approvals of DAI and WETH for manager to 0
- Emit Mint with tokenId.
(back to top)
Uniswap V3 Price of ETH from sqrtPriceX96
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math18.png)
Let's find out where the 10**12 comes from
First, let's find P through an example
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths19.png)
But we are interested in the price of ETH in terms of USDC
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math20.png)
But how we calculate the price of ETH starting from sqrtPriceX96?
This is the variable related to the price of tokens, stored in Uniswap V3 contract
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths21.png)
Find sqrtPriceX96
variable on Uniswap USDC / ETH pool contract
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/etherscan.png)
Let's consider sqrtPriceX96 = 2200755647817846498385002322429664
. The same can be done with the actual sqrtPriceX95
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths22.png)
(back to top)
Uniswap V3 Tick and sqrtPriceX96
let's see how to convert from tick
to sqrtPriceX96
and from sqrtPriceX96
to tick
First, let's start by reviewing some of the variables
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/tick.png)
We're gonna need these two equations to calculate from tick
to sqrtPriceX96
and from sqrtPriceX96
back to tick
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/tick01.png)
For the first example, we'll say tht we know what the tick
is and we'll try to compute the sqrtPriceX96
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/etherscan.png)
Let's use python
to compute sqrtPriceX96
and tick
import math
Q96 = 2 ** 96
def tick_to_sqrt_price_x_96(tick):
return int(1.0001 ** (tick / 2) * Q96)
def sqrt_price_x_96_to_tick(sqrt_price_x_96):
base = math.sqrt(1.0001)
p = sqrt_price_x_96 / Q96
return math.floor(math.log(p, base))
tick = 204632
tick_to_sqrt_price_x_96(tick)
sqrt_price_x_96 = 1892484952596364096357191768742857
sqrt_price_x_96_to_tick(sqrt_price_x_96)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/tick02.png)
(back to top)
Uniswap V3 Price Change from a Swap
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths23.png)
First, let's explain what we mean by "we are assuming there is enough liquidity to swap on the curve"
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths24.png)
- Liquidity is only supported between the price range
Pa
andPb
, and the price after the swap falls outside of the price range range - In the second case, the price P1 after the swap remains in the range
Pa
andPb
Let's go over the math, let's derive the equations
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths25.png)
=> These equations can be applied to Y0, Y1, X0 and X1 since all of these points are on the curve XY = L2
This is a important fact to remember as we will derive the equations for delta x and delta y
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths26.png)
We'll do the same trick to find delta y
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths27.png)
Finally, let's derive the equations for sqrt(P1) and sqrt(P2)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths28.png)
(back to top)
Uniswap V3 Liquidity Price Graph
Let's see how to read liquidity price graph
The current liquidity on Uniswap v3 is L
and we are trading on the curve XY = L2
There is a single position supporting this liquidity from the price range [Pa - Pb]
From Pa
, Pb
and the current price P
, let's convert to ticks
(equations inside the square) and let's map it horizontally.
The vertical axis represents liquidity
=> To the left of the current tick t
, all the tokens will be in token 1 (Y
)
=> To the right of the current tick t
, all the tokens will be in token 0 (X
)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths29.png)
Let's see why this is:
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths30.png)
When the current price is equal to Pb, then the liquidity is fully in token Y
When the price P is equal to Pa, then the liquidity is fully in token X
=> Combinng these two observations, we know that liquidity is in Y for the left of the current tick t, and liquidity is X for the right of t
(back to top)
Uniswap V3 Liquidity Price Graph Example
In the following Liquidity Price Graph of the ETH / USDC pool, in pink you can see the current price
To the left of the current price all of the tokens are in USDC
And to the right of the current price, all of the liquidity are in ETH
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/liquidityGraph.png)
Let's see why
Notice that in the graph below:
- X is on the left and Y is on the right
- ticks are negative
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths31.png)
Instead of P increasing to the right, we want to convert this into a graph where 1 / P increases to the right
Inside the first purple rectangle below, we start by mapping the ticks to this new graph (we flip the inequality before mapping into the graph), then we convert 1 / P to ticks inside the second purple rectangle
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths32.png)
Here is the link to the ETH / USDC pool on Uniswap v3
(back to top)
Test Flash Swap
npx hardhat test test/unlock-account.test.js
npx hardhat test test/flashSwapV3.test.js
npx hardhat test test/flashSwapArbV3.test.js
Uniswap V3 Flash Swap
Borrow tokens from Uniswap V3 pool and then repay with fee in a single transaction. This is called flash loan.
We'll call flash
on Uniswap V3 pool to borrow WETH.
State Variables
- Address of tokens and the address of the factory
- Set WETH interface and declare pool (IUniswapV3Pool)
- Set
POOL_FEE
to 3000 because we'll call DAI / WETH pool with 0.3% fee - Declare struct
FlashData
withwethAmount
andcaller
Constructor
- The variable pool is immutable, so initialize it inside the constructor.
- Address of the pool can be obtained by calling
PoolAddress.computeAddress
. - PoolKey can be obtained by calling
PoolAddress.getPoolKey
Function flash
This function will initialize the flash loan on Uniswap V3 pool by calling pool.flash
Below is an overview of what will happen after pool.flash is called.
- The pool sends tokens to the borrower.
- The pool calls uniswapV3FlashCallback on the borrower.
- Inside the uniswapV3FlashCallback our customm code is executed. At the end of the code, we must pay back the borrowed amount plus fees.
- Prepare data to be passed to
pool.flash
. Any bytes can be passed, but we will encode our custom structFlashData
. - Call
pool.flash
interface IUniswapV3Pool {
function flash(
address recipient,
uint amount0,
uint amount1,
bytes calldata data
) external;
}
Function uniswapV3FlashCallback
This is the function called by pool, after we call pool.flash.
Here we have the requested borrow amount of WETH. Our custom code logic goes here. In this case, we will simply repay the borrowed amount plus fee.
- Require that msg.sender is the pool.
- Decode the data into FlashData
- Caller stored in FlashData will pay for the fee on borrow. Transfer WETH from decoded.caller into this contract for the amount fee1.
- Repay WETH back to the pool. Transfer borrowed amount + fee1.
(back to top)
Uniswap V3 Capital Efficiency
If we were to add the same amount of tokens to Uniswap v2
and Uniswap v3
, then how much more can we increase the liquidity in Uniswap v3 if we were to narrow the price range?
Let's start with these equations from Liquidity Delta section to derive the capital efficiency equation
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/math33.png)
As an example, we calculated the capital efficiency if we were to provide liquidity +/- 2%
of the current price P
In other words, if we were to provide the same amount of token to Uniswap v2 and Uniswap v3, the amount of tokens are deltaX
and deltaY
However, in Uniswap v3, let's say that we provide the liquidity for the price range +/- 2% of the current price, then the liquidity in Uniswap v3 is about 100 times greater than in Uniswap v2.
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths34.png)
We calculated the ratio of liquidity delta for Uniswap v2 and for Uniswap v3
Next, let's replace the current price P
with another number
- We're going to say that the current price
P
is the geometric mean ofPa
andPb
- We take the equation highlighted previously in green
- We replace the
sqrtP
highlighted below by the geometric mean of Pa and Pb (sqrt(PaPb)
)
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths35.png)
(back to top)
Uniswap V3 Just In Time Liquidity
Alice
is a liquidity provider and Bob
swaps tokens.
From this trade, Alice earns some fees :)
This is how normally how a liquidity provider will earn some fees
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths36.png)
Let's see how just-in-time
liquidity will change how the liquidity providers earn fees
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths36bis.png)
In this case, Justin
sees Bob's transaction in mempool
and front-runs Bob
![Test](https://github.com/Aboudoc/Uniswap-v3/raw/main/images/maths37.png)
In the example above we assume that Justin has a lot of liquidity in a narrow price range including the current (shown in red)
Close to the current price P0
, Aloce has provided liquidity as shown in the green area
Whereas in the same price range, Justin provided liquidity as shown in the red area, a lot more than Alice
This means that when a trade executes in this price range, Justin will earn the majority of the fees (after Bob's trade is processed)
Note that the price change from P0
to P*1
is less than the change from P0
to P1
This is beause there is a lot of liquidity arount the current price range in the second case => the price moves very little, this is called just-in-time liquidity
(back to top)
Forking mainnet
hardhat.config.js
networks: {
hardhat: {
forking: {
url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`,
},
},
}
Note: Replace the ${}
component of the URL with your personal Alchemy API key.
npx hardhat test test/swapV3.test.js
(back to top)
Note
This contract assumes that token0 and token1 both have same decimals
Consider Uniswap trading fee = 0.3%
Further reading
You can find Uniswap pools referenced below. Select a pool with the highest TVL
You can find official Uniswap documentation below:
Sources
(back to top)
Roadmap
- [-] Uniswap V3 TWAP
- [-] Further reading
- [-] Unit test
- [-] Flash Swap
- [ ] Flash Swap test
- [ ] Uniswap V3 Capital Efficiency (maths)
- [ ] Uniswap V3 Just In Time Liquidity (maths)
- [ ] Deploy on mainnet
See the open issues for a full list of proposed features (and known issues).
(back to top)
Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
(back to top)
License
Distributed under the MIT License. See LICENSE.txt
for more information.
(back to top)
Contact
Reda Aboutika - @twitter - [email protected]
Project Link: https://github.com/Aboudoc/Uniswap-v3.git
(back to top)
Acknowledgments
(back to top)