Table of Contents
- About The Project
- Getting Started
- Usage
- Constant Product AMM
- Test Single and Multi hop swap
- Uniswap V2 Single Hop Swap
- Uniswap V3 Multi Hop Swap
- Uniswap V3 Curve of real reserves
- Test Mint new position
- Uniswap V3 Liquidity of a single position
- Test Collect fees
- Test Increase liquidity
- Test Decrease liquidity
- Uniswap V3 Liquidity Delta
- Uniswap V3 How many tokens to add
- Uniswap V2 Add and Remove Liquidity
- Uniswap V3 Price of ETH from sqrtPriceX96
- Uniswap V3 Tick and sqrtPriceX96
- Uniswap V3 Price Change from a Swap
- Uniswap V3 Liquidity Price Graph
- Uniswap V3 Liquidity Price Graph Example
- Uniswap V3 Flash Swap
- Uniswap V3 Capital Efficiency
- Uniswap V3 Just In Time Liquidity
- Forking mainnet
- Note
- Roadmap
- Contributing
- License
- Contact
- Acknowledgments
This project shows how to interact with the main functions of Uniswap V3 and derive the equations used in the protocol
To get a local copy up and running follow these simple example steps.
-
npm
npm init
-
hardhat
npm install --save-dev hardhat
run:
npx hardhat
verify:
npx hardhat verify --network goerli "contract address" "pair address"
-
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/contracts@3.4.2
To fix compiler errors, we'll need to change the compilation settings
If you need testnet funds, use the Alchemy testnet faucet.
This project shows how to swap, add and remove liquidity
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
npx hardhat test test/unlock-account.test.js
npx hardhat test test/swapV3.test.js
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
- Address of tokens (2 or 3) and the address of the router
- Set interfaces for tokens and router
- Transfer
amountIn
frommsg.sender
- Approve
amountIn
torouter
- Set the
params
by preparing struct ISwapRouter.ExactInputSingleParams - Call
exactInputSingle
on ISwapRouter (router interface)
- 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
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
- Address of tokens and the address of the router
- Set interfaces for tokens and router
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
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
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
Let's now derive the equation, the curve for the real reserve
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)
Let's solve for Xv
Let's solve for Yv
The final step to derive the curve for the real reserve is to combine all of the equations that we have derived so far
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
npx hardhat test test/unlock-account.test.js
npx hardhat test test/liquidityV3.test.js
npx hardhat test test/liquidityV3.test.js
No fees in this case
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):
Let's start with the first case
Let's derive the equation for liquidity when the current price >= Pb
Let's apply the same technique to find Ly
Find Lx and Ly
Finally let's put the two equations that we derived earlier
npx hardhat test test/liquidityV3.test.js
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
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()
Note that after calling decreaseLiquidity
, DAI balance and WETH balance remain the same
let's derive these two equations
First, preliminary math:
Let's define liquidity delta
Four steps to calculate liquidity delta:
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?
How do we compute delta Y? We'll use the previous equation (Uniswap V3 Liquidity Delta)
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
If we compare with the UI, the result is very close
Manage liquidity in Uniswap V3
Mint new position Increase liquidity Decrease liquidity Collect fees and withdraw tokens
Contract inherits from IERC721Receiver
- Address of tokens. Set MIN_TICK, MAX_TICK, TICK_SPACING
- Set interfaces for tokens and manager with the
INonfungiblePositionManager
interface
This function is called when safeTransferFrom is called on INonFungiblePositionManager.
- 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
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.
Let's find out where the 10**12 comes from
First, let's find P through an example
But we are interested in the price of ETH in terms of USDC
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
Find sqrtPriceX96
variable on Uniswap USDC / ETH pool contract
Let's consider sqrtPriceX96 = 2200755647817846498385002322429664
. The same can be done with the actual sqrtPriceX95
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
We're gonna need these two equations to calculate from tick
to sqrtPriceX96
and from sqrtPriceX96
back to tick
For the first example, we'll say tht we know what the tick
is and we'll try to compute the sqrtPriceX96
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)
First, let's explain what we mean by "we are assuming there is enough liquidity to swap on the curve"
- 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
=> 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
We'll do the same trick to find delta y
Finally, let's derive the equations for sqrt(P1) and sqrt(P2)
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
)
Let's see why this is:
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
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
Let's see why
Notice that in the graph below:
- X is on the left and Y is on the right
- ticks are negative
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
Here is the link to the ETH / USDC pool on Uniswap v3
npx hardhat test test/unlock-account.test.js
npx hardhat test test/flashSwapV3.test.js
npx hardhat test test/flashSwapArbV3.test.js
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.
- 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
- 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
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;
}
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.
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
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.
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)
)
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
Let's see how just-in-time
liquidity will change how the liquidity providers earn fees
In this case, Justin
sees Bob's transaction in mempool
and front-runs Bob
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
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
This contract assumes that token0 and token1 both have same decimals
Consider Uniswap trading fee = 0.3%
You can find Uniswap pools referenced below. Select a pool with the highest TVL
You can find official Uniswap documentation below:
- [-] 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).
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
Distributed under the MIT License. See LICENSE.txt
for more information.
Reda Aboutika - @twitter - reda.aboutika@gmail.com
Project Link: https://github.com/Aboudoc/Uniswap-v3.git