-
Notifications
You must be signed in to change notification settings - Fork 622
Added guide for single-sided liquidity in v4 #940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
--- | ||
title: Single-sided Liquidity | ||
--- | ||
|
||
In this example we will create a single-side liquidity position a Buy Limit style range order that swaps token1 for token0 as the market price falls into your defined tick range. | ||
|
||
Fetch the current pool price (sqrtPriceX96) | ||
|
||
Compute a -5% target price and align it with a usable tick | ||
|
||
Calculate the required liquidity using only token1 | ||
|
||
Approve the correct token and use mint() to create a position | ||
|
||
Monitor and fine-tune the amount1Max buffer for slippage tolerance | ||
|
||
## Introduction | ||
|
||
This guide will cover how single-side liquidity provisioning can be used to execute **Limit Orders** on Uniswap V4 Pools. | ||
|
||
|
||
This guide will **cover**: | ||
|
||
1. Understanding Range Orders | ||
2. Calculating our Tick Range | ||
3. Creating a single-side liquidity position | ||
5. Closing the Limit Order | ||
|
||
Before working through this guide, consider checking out the Range Orders [concept page](/concepts/protocol/range-orders) to understand how Limit orders can be executed with Uniswap v4. | ||
|
||
:::info | ||
This guide is built using the [v4-template repository](https://github.com/uniswapfoundation/v4-template) and utilises the Config and Constants files as imports for contract addresses. | ||
::: | ||
|
||
|
||
## Understanding Range Orders | ||
|
||
:::info | ||
If you have read the [Range Order Concept page](../../../../concepts/protocol/range-orders.md), you can skip this section. | ||
::: | ||
|
||
If both of a position's ticks are less than (or greater than) the current tick of the pool, the position is entirely made up of a single token. | ||
|
||
We will call this a **Single Side Liquidity Position**. | ||
|
||
Single-sided positions are useful because they allow liquidity providers to: | ||
- Provide liquidity using only one token they already hold | ||
- Avoid having to acquire the other token | ||
- Set price targets for automatic token swaps | ||
- Manage exposure to price movements in a specific direction | ||
|
||
If we look at the structure of the [mint function](../reference/periphery/PositionManager#_mint) in the Position Manager contract, the actual **amount** of `token0` and `token1` that a Pool owes the Position owner is calculated from the parts of the liquidity position that are to the left and right of the current Tick. | ||
|
||
Liquidity right of the current Tick is denominated in `token0` and liquidity left of the current Tick is denominated in `token1`. | ||
|
||
<img src={require('../images/range-order.png').default} alt="RangeOrder" box-shadow="none"/> | ||
|
||
When the current Tick of the Pool moves across the Position, the ratio of token0 and token1 will change, and if the pool's tick moves entirely through a position, the underlying assets flip from one token to the other. | ||
|
||
We will utilise this behaviour to provide liquidity with `token1` and withdraw the position when it has been converted to `token0`. | ||
|
||
## Calculating the Tick Range | ||
|
||
Our goal for this guide is to create a [Buy Limit Order](/concepts/protocol/range-orders#buy-limit-orders) that trades `token1` for `token0` when the Price of `token1` decreases by 5%. | ||
|
||
To create our Position, we need to first decide the Tick Range that we want to provide liquidity in. | ||
|
||
### Upper Tick | ||
|
||
We [create a Pool](/contracts/v4/quickstart/create-pool) that represents the v4 Pool we are interacting with and get the `token1Price`. | ||
We won't need full tick data in this example. | ||
|
||
```solidity | ||
import {TickMath} from "v4-core/src/libraries/TickMath.sol"; | ||
import {Constants} from "./base/Constants.sol"; | ||
import {Config} from "./base/Config.sol"; | ||
import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; | ||
|
||
uint24 public constant LP_FEE = 3000; | ||
int24 public constant TICK_SPACING = 60; | ||
uint256 public constant token1Amount = 1e6; //USDC | ||
|
||
PoolKey memory pool = PoolKey({ | ||
currency0: currency0, // imported from Config.sol | ||
currency1: currency1, // imported from Config.sol | ||
fee: LP_FEE, | ||
tickSpacing: TICK_SPACING, | ||
hooks: hookContract | ||
}); | ||
|
||
(uint160 sqrtPriceX96,,,) = POOLMANAGER.getSlot0(pool.toId()); | ||
int24 currentTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96); | ||
``` | ||
|
||
Next we decrease the `Price` by 5%. We create a new Price with a numerator 5% lower than our current Price: | ||
|
||
```solidity | ||
// calculating sqrt(price * 0.95e18/1e18) is the same as | ||
// (sqrt(price) * Q96) * (sqrt(0.95e18/1e18)) | ||
// (sqrt(price) * Q96) * (sqrt(0.95e18) / sqrt(1e18)) | ||
uint160 targetSqrtPriceX96 = uint160( | ||
FixedPointMathLib.mulDivDown( | ||
uint256(sqrtPriceX96), FixedPointMathLib.sqrt(0.95e18), FixedPointMathLib.sqrt(1e18) | ||
) | ||
); | ||
``` | ||
|
||
Now we need to find the **nearest usable tick** because a position's ticks should align with the pool's tick spacing. If a pool's tick spacing is 60, the position's tickLower and tickUpper should be a multiple of 60, i.e. [-300, 180] | ||
|
||
The TickMath library has functions for converting between ticks and sqrtPriceX96 values. | ||
|
||
```solidity | ||
import {TickMath} from "v4-core/src/libraries/TickMath.sol"; | ||
|
||
// we are aligning the targetTick to an useable tick. | ||
int24 targetTick = TickMath.getTickAtSqrtPrice(targetSqrtPriceX96); | ||
// This arithmetic is round-to-zero | ||
int24 tickUpper = (targetTick / TICK_SPACING) * TICK_SPACING; | ||
``` | ||
|
||
This nearest Tick will most likely not **exactly** match our Price target. | ||
|
||
Depending on our personal preferences we can either err on the higher or lower side of our target by adding or subtracting the `tickSpacing` if the initializable Tick is lower or higher than the theoretically closest Tick. | ||
|
||
### Lower Tick | ||
|
||
We now find the lower Tick by subtracting the tickSpacing from the upper Tick: | ||
|
||
```solidity | ||
int24 tickLower = tickUpper - TICK_SPACING; | ||
``` | ||
|
||
If the price difference is too low, the lower tick may be left of the current Tick of the Pool. | ||
In that case we would not be able to provide single side liquidity. | ||
We can either throw an Error or increase our Position by one Tick. | ||
|
||
```solidity | ||
if (tickLower <= currentTick) { | ||
tickLower += TICK_SPACING; | ||
tickUpper += TICK_SPACING; | ||
} | ||
``` | ||
|
||
We now have a lower and upper Tick for our Position, next we need to construct and mint it. | ||
|
||
## Creating the Single Side Liquidity Position | ||
|
||
We will calculate the liquidity corresponding to our token1, tickLower and tickUpper and the requiredToken1 necessary for that liquidity using the [LiquidityAmounts library.](../../v3/reference/periphery/libraries/LiquidityAmounts.md) | ||
|
||
Finally, we have to set up upper limits for Amount1 and Amount0 accounting for slippage. | ||
|
||
```solidity | ||
uint256 amount1Max = requiredToken1 + 1_000; // 0.001 USDC buffer | ||
uint256 amount0Max = 1e14; // small ETH buffer | ||
bytes memory hookData = new bytes(0); | ||
``` | ||
|
||
### Minting the Position | ||
|
||
You can refer to the [Mint guide](../quickstart/manage-liquidity/mint-position) for a detailed explanation on how to mint a position. | ||
|
||
:::note | ||
We can find the Contract address on the official [Uniswap GitHub](/contracts/v4/deployments). | ||
For local development, the contract address is the same as the network we are forking from. | ||
So if we are using a local fork of mainnet like described in the [Local development guide](/sdk/v3/guides/local-development), the contract address would be the same as on mainnet. | ||
::: | ||
|
||
Here is the full code example for these code snippets: | ||
|
||
```solidity | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import "forge-std/Script.sol"; | ||
import "forge-std/console.sol"; | ||
|
||
//Other necessary imports | ||
|
||
contract AddSingleSidedLiquidity is Script, Constants, Config { | ||
using CurrencyLibrary for Currency; | ||
using StateLibrary for IPoolManager; | ||
using EasyPosm for IPositionManager; | ||
|
||
//constants | ||
|
||
function run() external { | ||
PoolKey memory pool = PoolKey({ | ||
currency0: currency0, | ||
currency1: currency1, | ||
fee: LP_FEE, | ||
tickSpacing: TICK_SPACING, | ||
hooks: hookContract | ||
}); | ||
|
||
// Get current pool sqrtPrice and tick | ||
(uint160 sqrtPriceX96,,,) = POOLMANAGER.getSlot0(pool.toId()); | ||
int24 currentTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96); | ||
|
||
//Calculating new target price | ||
uint160 targetSqrtPriceX96 = uint160( | ||
FixedPointMathLib.mulDivDown( | ||
uint256(sqrtPriceX96), FixedPointMathLib.sqrt(0.95e18), FixedPointMathLib.sqrt(1e18) | ||
) | ||
); | ||
int24 targetTick = TickMath.getTickAtSqrtPrice(targetSqrtPriceX96); | ||
int24 alignedTick = (targetTick / TICK_SPACING) * TICK_SPACING; | ||
|
||
int24 tickUpper = alignedTick; | ||
int24 tickLower = tickUpper - TICK_SPACING; | ||
|
||
if (tickLower <= currentTick) { | ||
tickLower += TICK_SPACING; | ||
tickUpper += TICK_SPACING; | ||
} | ||
|
||
// Calculate liquidity from token1 (USDC) | ||
uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1( | ||
//sqrtPrice at tickLower, sqrtPrice at tickUpper, token1Amount | ||
); | ||
|
||
// Calculate required token1 amount from the liquidity | ||
uint256 requiredToken1 = LiquidityAmounts.getAmount1ForLiquidity( | ||
//sqrtPrice at tickLower, sqrtPrice at tickUpper, liquidity | ||
); | ||
|
||
//Token Approval and Minting a Position | ||
|
||
} | ||
} | ||
``` | ||
|
||
### Getting the tokenId | ||
|
||
Now that we have minted a new position, we want to read the response to our `Mint` function call to get **the token id**. | ||
|
||
We will need the tokenId to fetch the Position Info and use it to burn and take out position and can use the nextTokenId function from Position Manager contract. | ||
|
||
```solidity | ||
uint256 tokenId = posm.nextTokenId() - 1; | ||
``` | ||
|
||
We have created our Range Order Position, now we need to monitor it and close the position when desired tick range is crossed and token1 increases in price value compare to token0. | ||
|
||
## Closing the Limit Order | ||
|
||
When your limit order has been executed (when the price moves across your position's range), you'll want to withdraw your funds. You can refer to the [Burn guide](../quickstart/manage-liquidity/burn-position) for details on how to close positions. | ||
|
||
The key steps involve: | ||
1. Calling the `modifyLiquidities` function of the Position Manager contract | ||
2. Using the BURN_POSITION and TAKE actions to remove liquidity and collect tokens | ||
|
||
## Caveats | ||
|
||
Executing a range order has certain limitations that may have become obvious during the course of this guide. | ||
|
||
- If the price of the Pool drops below `tickUpper` while we already decided to withdraw our liquidity our order may fail and we either receive `token0`, `token0` and `token1` or our transaction fails depending on our exact implementation. | ||
- Range Orders can only be created between initializable ticks and may not exactly represent our limit order Price-Target. | ||
- Depending on the price ratio of the tokens in the Pool the minimum price difference to the current price may be significant. | ||
- The tokens received are the average between the Price of `tickUpper` and `tickLower` of the Range order. This can be a significant difference for Pools with a tickCurrent far from 0, for example tokens with different decimals (WETH/ USDT, WETH/USDC). The example showcases this behaviour well with the default configuration. | ||
|
||
## Next Steps | ||
|
||
This guide showcases everything you need to implement Single-sided Liquidity on your own, but only demonstrates creating a `token1` only position. | ||
|
||
Try implementing a `token0` only position in v4. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.