Position
To add or remove the tokens we call the function modifyPosition
which is defined in PoolManager.sol
file
struct ModifyPositionParams {
// the lower and upper tick of the position
int24 tickLower;
int24 tickUpper;
// how to modify the liquidity
int256 liquidityDelta;
}
/// @notice Modify the position for the given pool
function modifyPosition(PoolKey memory key, ModifyPositionParams memory params, bytes calldata hookData)
external
returns (BalanceDelta);
Important Concepts
Some of the important concepts to understand when working with Uniswap V3 or V4 positions are:
- Tick
- Tick Spacing
- SquareRoot Price X96
- Liquidity Delta
Tick
tick
is a measure used in this code to handle prices of two different assets (tokens) in a unique way. A tick
represents a specific price ratio between these two assets, calculated using a mathematical formula.
There are minimum and maximum tick
values defined within the code, ensuring that calculated prices are within a
reasonable and acceptable range.
In Uniswap V3 (and V4), liquidity providers can provide liquidity at specific price ranges (ticks), allowing them to concentrate their liquidity and potentially earn more fees.
Each tick corresponds to a specific price, and not all prices are represented due to the discrete nature of the ticks.
Tick Spacing
The tick spacing is a parameter that determines the separation between these usable ticks, making only every
Nth tick available for liquidity provision, where N
is the tick spacing. This is a form of quantization of
the price levels that liquidity can be provided at.
SquareRoot Price X96
In Uniswap V3 (and V4), the square root price (sqrtPriceX96
) is a key concept and a crucial part of the mathematical
calculations. It's utilized for various calculations, including determining the amount of tokens that should be moved
during a swap and the liquidity calculations within specific price ranges.
Here’s a breakdown of what sqrtPriceX96
represents:
1. Square Root Price:
The price is represented as the square root of the actual price ratio of the tokens. This representation simplifies the math, especially when it comes to calculating the amounts to be swapped, as well as the liquidity calculations within tick ranges.
2. X96:
The X96 suffix refers to the fixed-point format used. Uniswap V3 utilizes a 96-bit fixed-point number format. In this representation, there is a convention to maintain high precision in calculations. The fixed-point representation means that the actual floating-point number is multiplied by (2^{96}) and stored as an integer. When reading the value, it has to be interpreted properly by dividing it by (2^{96}) to get the actual value.
SqrtPriceX96 to Tick
Since both the tick
and sqrtPriceX96
are representations of the price, they can be converted from one to the other.
The Uniswap V3/V4 core library provides two functions to convert between the two representations:
https://github.com/Uniswap/v4-core/blob/main/src/libraries/TickMath.sol
The function
getSqrtRatioAtTick
takes atick
value as an input, and it computes the square root of the price ratio of the two assets at that specifictick
. The result represents the price relationship between two assets in a particular state or position.The
getTickAtSqrtRatio
function does the reverse—it takes a square root of a price ratio and calculates the correspondingtick
. Thistick
value represents a position or state where the assets have the given price relationship.
Liquidity Delta
The liquidityDelta
is the difference between the current liquidity and the desired liquidity in a position. It can
be positive (when you're adding liquidity) or negative (when you're removing liquidity).
Here is the code from Uniswap V3 contracts which calculates the liquidity (or liquidityDelta):
https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/LiquidityManagement.sol
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);
liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
params.amount0Desired,
params.amount1Desired
);
Example - Add Liquidity
Here is the code that adds liquidity to a position
int24 tickLower = -600;
int24 tickUpper = 600;
uint256 liquidity = 1e18;
PoolManager manager = new PoolManager(500000);
// Helpers for interacting with the pool
PoolModifyPositionTest modifyPositionRouter = new PoolModifyPositionTest(IPoolManager(address(manager)));
modifyPositionRouter.modifyPosition(
poolKey,
IPoolManager.ModifyPositionParams({
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(liquidity)
}),
ZERO_BYTES
);
Note: PoolModifyPositionTest
implements the ILockCallback
interface and adds the lockAcquired
function, which in turn calls the manager.modifyPosition
function.
Acquiring Lock
Full detail about the locking mechanism is explained in the Locking Mechanism section.
The contract that calls the modifyPosition
must implement ILockCallback interface.
PoolModifyPositionTest.sol has some examples of how to acquire lock and some basic checks in place. https://github.com/Uniswap/v4-core/blob/main/src/test/PoolModifyPositionTest.sol
In PoolModifyPositionTest
the lockAcquired
function is executed when the lock is acquired, handling
balance adjustments and interactions with external currencies and contracts. The function takes raw
encoded data as input, which is then decoded into structured data, specifically CallbackData
. Essential
validations and checks are performed, ensuring the caller of the function is the manager, and it
processes the modifications such as settling amounts and making necessary transfers based on
conditions like whether the amount being positive or negative.