Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions docs/contracts/v4/guides/15-single-sided-liquidity.mdx
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.
Binary file added docs/contracts/v4/images/range-order.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.