Concentrated Liquidity | Osmosis Docs (2024)

Table of Contents
Background​ Architecture​ Ticks​ Context​ Geometric Tick Spacing with Additive Ranges​ Formulas​ Tick Spacing Example: Tick to Price​ Tick Spacing Example: Price to Tick​ Choosing an Exponent At Price One Value​ Example 1​ Example 2​ Consequences​ Concentrated Liquidity Module Messages​ MsgCreatePosition​ MsgWithdrawPosition​ MsgCreatePool​ MsgCollectSpreadRewards​ MsgFungifyChargedPositions​ Relationship to Pool Manager Module​ Pool Creation​ Swaps​ Liquidity Provision​ Adding Liquidity​ Removing Liquidity​ Swapping​ Calculating Swap Amounts​ Migration​ Superfluid Delegated Balancer to Concentrated​ Superfluid Undelegating Balancer to Concentrated​ Locked and Unlocked Balancer to Concentrated​ Balancer to Concentrated with No Lock​ Position Fungification​ Swapping. Appendix A: Example​ Range Orders​ Spread Rewards​ Collecting Spread Rewards​ Swaps​ Swap Step Spread Factors​ Incentive/Liquidity Mining Mechanism​ Overview​ Target Properties​ Liquidity Depth​ Liquidity Breadth​ Liquidity Uptime​ Current Standard: Pro-rata in Active Tick​ Our Implementation​ Note on supported and authorized uptimes​ Incentive Creation and Querying​ Reward Splitting Between Classic and CL pools​ TWAP Integration​ Parameters​ Listeners​ AfterConcentratedPoolCreated​ AfterInitialPoolPositionCreated​ AfterLastPoolPositionRemoved​ AfterConcentratedPoolSwap​ State entries and KV store management​ State and Keys​ Incentive Records​ Precision Issues With Price​ Solution​ Terminology​ External Sources​ FAQs

Background

Concentrated liquidity is a novel Automated Market Maker (AMM) design introducedby Uniswap that allows for more efficient use of capital. The improvement isachieved by providing liquidity in specific price ranges chosen by the user.

For instance, a pool with stablecoin pairs like USDC/USDT has a spot price thatshould always be trading near 1. As a result, Liquidity Providers (LPs) canfocus their capital in a small range around 1, rather than the full range from 0to infinity. This approach leads to an average of 200-300x higher capitalefficiency. Moreover, traders benefit from lower price impact because the poolincentivizes greater depth around the current price.

Concentrated liquidity also opens up new opportunities for providing liquidityrewards to desired strategies. For example, it's possible to incentivize LPsbased on their position's proximity to the current price and the time spentwithin that position. This design also allows for a new "range order" type,similar to a limit order with order-books.

Architecture

The traditional Balancer AMM relies on the following curve that tracks current reserves:

Concentrated Liquidity | Osmosis Docs (1)

This formula allows for distributing liquidity along the $xy=k$ curve and acrossthe entire price range of (0, ∞).

With the new architecture, we introduce the concept of a position that allowsusers to concentrate liquidity within a fixed range. A position only needs tomaintain enough reserves to satisfy trading within this range. Consequently,it functions as the traditional xy = k within that range.

In the new architecture, real reserves are described by the following formula:

Concentrated Liquidity | Osmosis Docs (2)

Where P_l is the lower tick, P_u is the upper tick, and L is the amountof liquidity provided,Concentrated Liquidity | Osmosis Docs (3)

This formula stems from the original $xy = k$ but with a limited range. In thetraditional design, a pool's x and y tokens are tracked directly. However,with the concentrated design, we only track $L$ and $\sqrt P$, which can becalculated with:

Concentrated Liquidity | Osmosis Docs (4)

By rearranging the above, we obtain the following formulas to track virtual reserves:Concentrated Liquidity | Osmosis Docs (5)

Note the square root around price. By tracking it this way, we can utilize thefollowing core property of the architecture:

Concentrated Liquidity | Osmosis Docs (6)

Since only one of the following changes at a time:

  • $L$: When an LP adds or removes liquidity
  • sqrt P: When a trader swaps

We can use the above relationship to calculate the outcome of swaps as well aspool joins that mint shares.

Conversely, we calculate liquidity from the other token in the pool:

Concentrated Liquidity | Osmosis Docs (7)

Overall, the architecture's goal is to enable LPs to provide concentratedliquidity within a specific range while maintaining high capital efficiency.

Ticks

Context

In Uniswap V3, discrete points (called ticks) are used when providing liquidityin a concentrated liquidity pool.

The price [p] corresponding to a tick [t] is defined by the equation:

Concentrated Liquidity | Osmosis Docs (8)

This results in a .01% difference between adjacent tick prices. This does not,however, allow for control over the specific prices that the ticks correspondto. For example, if a user wants to make a limit order at the $17,100.50 price point,they would have to interact with either tick 97473 (corresponding to price$17,099.60) or tick 97474 (price $17101.30).

Since we know what range a pair will generally trade in, how can we provide moregranularity at that range and provide a more optimal price range between ticksinstead of the "one-size-fits-all" approach explained above?

Geometric Tick Spacing with Additive Ranges

In Osmosis' implementation of concentrated liquidity, we will instead make useof geometric tick spacing with additive ranges.

We start by defining an exponent for the precision factor of each incrementaltick starting at the spot price of one. This is referred to as $exponentAtPriceOne$.

In the current design, we hardcode $exponentAtPriceOne$ as -6. When used with atick spacing of 100, this effectively acts as an $exponentAtPriceOne$ of -4,since only every 100 ticks are able to be initialized.

When $exponentAtPriceOne = -6$ (and tick spacing is 100), each tick starting at0 and ending at the first factor of 10 will represents a spot price increase of 0.0001:

  • tick_{0} = 1
  • tick_{100} = 1.0001
  • tick_{200} = 1.0002
  • tick_{300} = 1.0003

This continues until the pool reaches a spot price of 10. At this point, sincethe pool has increased by a factor of 10, the $exponentAtCurrentTick$ increasesfrom -4 to -3 (decreasing the incremental precision), and the ticks willincrease as follows:

  • tick_{8999900} = 9.9999
  • tick_{9000000} = 10.000
  • tick_{9000100} = 10.001
  • tick_{9000200} = 10.002

For spot prices less than a dollar, the precision factor decreases(increasing the incremental precision) at every factor of 10:

  • tick_{-100} = 0.99999
  • tick_{-200} = 0.99998
  • tick_{-500100} = 0.94999
  • tick_{-500200} = 0.94998
  • tick_{-9000100} = 0.099999
  • tick_{-9000200} = 0.099998

This goes on in the negative direction until it reaches a spot price of0.000000000000000001 or in the positive direction until it reaches a spotprice of 100000000000000000000000000000000000000.

The minimum spot price was chosen as this is the smallest possible numbersupported by the sdk.Dec type. As for the maximum spot price, the above numberwas based on gamm's max spot price of 340282366920938463463374607431768211455.While these numbers are not the same, the max spot price used in concentratedliquidity utilizes the same number of significant figures as gamm's max spotprice and is less than gamm's max spot price which satisfies the initial design requirements.

Formulas

After we define tick spacing (which effectively defines the $exponentAtPriceOne$,since $exponentAtPriceOne$ is fixed), we can then calculate how many ticks mustbe crossed in order for $k$ to be incremented( $geometricExponentIncrementDistanceInTicks$ ).

Concentrated Liquidity | Osmosis Docs (9)

Since we define exponentAtPriceOne and utilize this as the increment startingpoint instead of price zero, we must multiply the result by 9 as shown above.In other words, starting at 1, it takes 9 ticks to get to the first power of 10.Then, starting at 10, it takes 9*10 ticks to get to the next power of 10, etc.

Now that we know how many ticks must be crossed in order for ourexponentAtPriceOne to be incremented, we can then figure out what our changein exponentAtPriceOne will be based on what tick is being traded at:

Concentrated Liquidity | Osmosis Docs (10)

With geometricExponentDelta and exponentAtPriceOne, we can figure out whatthe exponentAtPriceOne value we will be at when we reach the provided tick:

Concentrated Liquidity | Osmosis Docs (11)

Knowing what our exponentAtCurrentTick is, we must then figure out what powerof 10 this $exponentAtPriceOne$ corresponds to (by what number does the pricegets incremented with each new tick):

Concentrated Liquidity | Osmosis Docs (12)

Lastly, we must determine how many ticks above the current increment we are at:

Concentrated Liquidity | Osmosis Docs (13)

With this, we can determine the price:

Concentrated Liquidity | Osmosis Docs (14)

where (10^{geometricExponentDelta}) is the price after $geometricExponentDelta$increments of exponentAtPriceOne (which is basically the number of decrementsof difference in price between two adjacent ticks by the power of 10)

Tick Spacing Example: Tick to Price

Bob sets a limit order on the USD<>BTC pool at tick 36650010. This pool's$exponentAtPriceOne$ is -6. What price did Bob set his limit order at?

Concentrated Liquidity | Osmosis Docs (15)

Bob set his limit order at price $16,500.10

Tick Spacing Example: Price to Tick

Bob sets a limit order on the USD<>BTC pool at price $16,500.10. This pool's$exponentAtPriceOne$ is -6. What tick did Bob set his limit order at?

Concentrated Liquidity | Osmosis Docs (16)

We must loop through increasing exponents until we find the first exponent thatis greater than or equal to the desired price

Concentrated Liquidity | Osmosis Docs (17)

10 is less than 16,500.10, so we must increase our exponent and try again

Concentrated Liquidity | Osmosis Docs (18)

100 is less than 16,500.10, so we must increase our exponent and try again.This goes on until...

Concentrated Liquidity | Osmosis Docs (19)

100000 is greater than 16,500.10. This means we must now find out how manyadditive tick in the currentAdditiveIncrementInTicks of -2 we must pass inorder to reach 16,500.10.

Concentrated Liquidity | Osmosis Docs (20)

Bob set his limit order at tick 36650010

Choosing an Exponent At Price One Value

The creator of a pool cannot choose an exponenetAtPriceOne as one of the inputparameters since it is hard coded to -6. The number can be psedo-controlled bychoosing the tick spacing a pool is initialized with. For example, if a poolis desired to have an exponentAtPriceOne of -6, the pool creator can choose atick spacing of 1. If a pool is desired to have an exponentAtPriceOne of -4,this is two factors of 10 greater than -6, so the pool creator can choose atick spacing of 100 to achieve this level of precision.

As explained previously, the exponent at price one determines how much the spotprice increases or decreases when traversing ticks. The following equation willassist in selecting this value:

Concentrated Liquidity | Osmosis Docs (21)

Example 1

SHIB is trading at $0.00001070 per SHIBBTC is trading at $28,000 per BTC

We want to create a SHIB/BTC concentrated liquidity pool where SHIB is thebaseAsset (asset0) and BTC is the quoteAsset (asset1). In terms of the quoteAsset,we want to increment in 10 cent values.Concentrated Liquidity | Osmosis Docs (22)

We can therefore conclude that we can use an exponent at price one of -5(slightly under precise) or -6 (slightly over precise) for this base/quote pairand desired price granularity. This means we would either want a tick spacing of 1(to have an exponent at price one of -6) or 10 (to have an exponent at price one of -5).

Example 2

Flipping the quoteAsset/baseAsset, for BTC/SHIB, lets determine what theexponentAtPriceOne should be. For SHIB as a quote, centralized exchangeslist prices at the 10^-8, so we will set our desired increment to this value.

Concentrated Liquidity | Osmosis Docs (23)

We can therefore conclude that we can use an exponent at price one of -3for this base/quote pair and desired price granularity. This means we wouldwant a tick spacing of 1000 (to have an exponent at price one of -3).

Consequences

This decision allows us to define ticks at spot prices that users actuallydesire to trade on, rather than arbitrarily defining ticks at .01% distancebetween each other. This will also make integration with UX seamless,instead of either:

a) Preventing trade at a desirable spot price orb) Having the front end round the tick's actual price to the nearesthuman readable/desirable spot price

One side effect of increasing precision as we get closer to the minimum tickis that multiple ticks can represent the same price. For example, tick-161795100 (along with the ticks surrounding it) correlate to a priceof 0.000000000000000002. To get around any issues this may cause, when aposition is created with a user defined lower and upper tick, we determineif a larger tick exists that represents the same price. If so, we use that tickinstead of the user defined tick. In the above example, the tick would bechanged to -161000000, which is the first tick that represents the same price.

Concentrated Liquidity Module Messages

MsgCreatePosition

  • Request

This message allows LPs to provide liquidity between LowerTick and UpperTickin a given PoolId. The user provides the amount of each token desired. SinceLPs are only allowed to provide liquidity proportional to the existing reserves,the actual amount of tokens used might differ from requested. As a result, LPsmay also provide the minimum amount of each token to be used so that the system failsto create position if the desired amounts cannot be satisfied.

Three KV stores are initialized when a position is created:

  1. Position ID -> Position - This is a mapping from a unique position ID to aposition object. The position ID is a monotonically increasing integer that isincremented every time a new position is created.
  2. Owner | Pool ID | Position ID -> Position ID - This is a mapping from acomposite key of the owner address, pool ID, and position ID to the position ID.This is used to keep track of all positions owned by a given owner in a given pool.
  3. Pool ID -> Position ID - This is a mapping from a pool ID to a position ID.This is used to keep track of all positions in a given pool.
type MsgCreatePosition struct {
PoolId uint64
Sender string
LowerTick int64
UpperTick int64
TokenDesired0 types.Coin
TokenDesired1 types.Coin
TokenMinAmount0 github_com_cosmos_cosmos_sdk_types.Int
TokenMinAmount1 github_com_cosmos_cosmos_sdk_types.Int
}
  • Response

On successful response, we receive the actual amounts of each token used tocreate the liquidityCreated number of shares in the given range.

type MsgCreatePositionResponse struct {
PositionId uint64
Amount0 github_com_cosmos_cosmos_sdk_types.Int
Amount1 github_com_cosmos_cosmos_sdk_types.Int
JoinTime google.protobuf.Timestamp
LiquidityCreated github_com_cosmos_cosmos_sdk_types.Dec

}

This message should call the createPosition keeper method that is introducedin the "Liquidity Provision" section of this document.

MsgWithdrawPosition

  • Request

This message allows LPs to withdraw their position via their position ID,potentially in partial amount of liquidity. It should fail if the position IDdoes not exist or if attempting to withdraw an amount higher than originallyprovided. If an LP withdraws all of their liquidity from a position, then theposition is deleted from state along with the three KV stores that wereinitialized in the MsgCreatePosition section. However, the spread factor accumulatorsassociated with the position are still retained until a user claims them manually.

type MsgWithdrawPosition struct {
PositionId uint64
Sender string
LiquidityAmount github_com_cosmos_cosmos_sdk_types.Dec
}
  • Response

On successful response, we receive the amounts of each token withdrawnfor the provided share liquidity amount.

type MsgWithdrawPositionResponse struct {
Amount0 github_com_cosmos_cosmos_sdk_types.Int
Amount1 github_com_cosmos_cosmos_sdk_types.Int
}

This message should call the withdrawPosition keeper method that is introducedin the "Liquidity Provision" section of this document.

MsgCreatePool

This message is responsible for creating a concentrated-liquidity pool.It propagates the execution flow to the x/poolmanager module for pool idmanagement and for routing swaps.

type MsgCreateConcentratedPool struct {
Sender string
Denom0 string
Denom1 string
TickSpacing uint64
SpreadFactor github_com_cosmos_cosmos_sdk_types.Dec
}
  • Response

On successful response, the pool id is returned.

type MsgCreateConcentratedPoolResponse struct {
PoolID uint64
}

MsgCollectSpreadRewards

This message allows collecting rewards from spreads for multiple position IDs from asingle owner.

The spread factor collection is discussed in more detail in the "Spread Rewards" section of this document.

  • Response

On successful response, the collected tokens are returned.The sender should also see their balance increase by the returnedamounts.

type MsgCollectSpreadRewardsResponse struct {
CollectedSpreadRewards []types.Coin
}

MsgFungifyChargedPositions

This message allows fungifying the fully charged unlocked positions belonging to the same ownerand located in the same tick range.MsgFungifyChargedPosition takes in a list of positionIds and combines them into a single position.It validates that all positions belong to the same owner, are in the same ticks and are fully charged.Fails if not. Otherwise, it creates a completely new position P. P's liquidity equals to the sum of allliquidities of positions given by positionIds. The uptime of the join time of the new position equalsto current block time - max authorized uptime duration (to signify that it is fully charged).The previous positions are deleted from state. Prior to deleting, the rewards are claimed.The old position's unclaimed rewards are transferred to the new position.The new position ID is returned.

type MsgFungifyChargedPositions struct {
PositionIds []uint64
Sender string
}
  • Response

On successful response, the new position id is returned.

type MsgFungifyChargedPositionsResponse struct {
NewPositionId uint64
}

Relationship to Pool Manager Module

Pool Creation

As previously mentioned, the x/poolmanager is responsible for creating thepool upon being called from the x/concentrated-liquidity module's message server.

It does so to store the mapping from pool id to concentrated-liquidity module sothat it knows where to route swaps.

Upon successful pool creation and pool id assignment, the x/poolmanager modulereturns the execution to x/concentrated-liquidity module by calling InitializePoolon the x/concentrated-liquidity keeper.

The InitializePool method is responsible for doing concentrated-liquidity specificinitialization and storing the pool in state.

Note, that InitializePool is a method defined on the SwapI interface that isimplemented by all swap modules. For example, x/gamm also implements it so thatx/pool-manager can route pool initialization there as well.

Swaps

We rely on the swap messages located in x/poolmanager:

  • MsgSwapExactAmountIn
  • MsgSwapExactAmountOut

The x/poolmanager received the swap messages and, as long as the swap's pool idis associated with the concentrated-liquidity pool, the swap is routedinto the relevant module. The routing is done via the mapping from state that wasdiscussed in the "Pool Creation" section.

Liquidity Provision

As an LP, I want to provide liquidity in ranges so that I can achieve greatercapital efficiency

This is a basic function that should allow LPs to provide liquidity in specific rangesto a pool.

A pool's liquidity is consisted of two assets: asset0 and asset1. In all pools,asset1 will be the quote asset and must be an approved denom listed in the moduleparameters. At the current tick, the bucket at this tick consists of a mix of bothasset0 and asset1 and is called the virtual liquidity of the pool (or "L" for short).Any positions set below the current price are consisted solely of asset0 whilepositions above the current price only contain asset1.

Adding Liquidity

We can either provide liquidity above or below the current price, which wouldact as a range order, or decide to provide liquidity at the current price.

As declared in the API for createPosition, users provide the upper and lowertick to denote the range they want to provide the liquidity in. The users arealso prompted to provide the amount of token0 and token1 they desire to receive.The liquidity that needs to be provided for the given token0 and token1 amountswould be then calculated by the following methods:

Liquidity needed for token0:$$L = \frac{\Delta x \sqrt{P_u} \sqrt{P_l}}{\sqrt{P_u} - \sqrt{P_l}}$$

Liquidity needed for token1:$$L = \frac{\Delta y}{\sqrt{P_u}-\sqrt{P_l}}$$

Then, we pick the smallest of the two values for choosing the final L. Thereason we do that is because the new liquidity must be proportional to the oldone. By choosing the smaller value, we distribute the liqudity evenly betweenthe two tokens. In the future steps, we will re-calculate the amount of token0and token1 as a result the one that had higher liquidity will end up smallerthan originally given by the user.

Note that the liquidity used here does not represent an amount of a specifictoken, but the liquidity of the pool itself, represented in sdk.Dec.

Using the provided liquidity, now we calculate the delta amount of both token0and token1, using the following equations, where L is the liquidity calculated above:

$$\Delta x = \frac{L(\sqrt{p(i_u)} - \sqrt{p(i_c)})}{\sqrt{p(i_u)}\sqrt{p(i_c)}}$$$$\Delta y = L(\sqrt{p(i_c)} - \sqrt{p(i_l)})$$

Again, by recalculating the delta amount of both tokens, we make sure that thenew liquidity is proportional to the old one and the excess amount of the tokenthat originally computed a larger liquidity is given back to the user.

The delta X and the delta Y are the actual amounts of tokens joined for therequested position.

Given the parameters needed for calculating the tokens needed for creating aposition for a given tick, the API in the keeper layer would look like the following:

ctx sdk.Context, poolId uint64, owner sdk.AccAddress, amount0Desired,
amount1Desired, amount0Min, amount1Min sdk.Int,
lowerTick, upperTick int64, frozenUntil time.Time
func createPosition(
ctx sdk.Context,
poolId uint64,
owner sdk.AccAddress,
amount0Desired,
amount1Desired,
amount0Min,
amount1Min sdk.Int
lowerTick,
upperTick int64) (amount0, amount1 sdk.Int, sdk.Dec, error) {
...
}

Removing Liquidity

Removing liquidity is achieved via method withdrawPosition which is the inverseof previously discussed createPosition. In fact, the two methods share the sameunderlying logic, having the only difference being the sign of the liquidity.Plus signifying addition while minus signifying subtraction.

Withdraw position also takes an additional parameter which represents the liquditya user wants to remove. It must be less than or equal to the available liquidityin the position to be successful.

func (k Keeper) withdrawPosition(
ctx sdk.Context,
poolId uint64,
owner sdk.AccAddress,
lowerTick,
upperTick int64,
frozenUntil time.Time,
requestedLiquidityAmountToWithdraw sdk.Dec)
(amtDenom0, amtDenom1 sdk.Int, err error) {
...
}

Swapping

As a trader, I want to be able to swap over a concentrated liquidity pool sothat my trades incur lower slippage

Unlike balancer pools where liquidity is spread out over an infinite range,concentrated liquidity pools allow for LPs to provide deeper liquidity forspecific price ranges, which in turn allows traders to incur less slippage ontheir trades.

Despite this improvement, the liquidity at the current price is still finite,and large single trades in times of high volume, as well as trades againstvolatile assets, are eventually bound to incur some slippage.

In order to determine the depth of liquidity and subsequent amountIn/amountOutvalues for a given pool, we track the swap's state across multiple swap "steps".You can think of each of these steps as the current price following the originalxy=k curve, with the far left bound being the next initialized tick below thecurrent price and the far right bound being the next initialized tick above thecurrent price. It is also important to note that we always view prices of asset1in terms of asset0, and selling asset1 for asset0 would, in turn, increase itsspot price. The reciprocal is also true, where if we sell asset0 for asset1,we would decrease the pool's spot price.

When a user swaps asset0 for asset1 (can also be seen as "selling" asset0), wemove left along the curve until asset1 reserves in this tick are depleted.If the tick of the current price has enough liquidity to fulfill the order withoutstepping to the next tick, the order is complete. If we deplete all of asset1 inthe current tick, this then marks the end of the first swap "step". Since allliquidity in this tick has been depleted, we search for the next closest tickto the left of the current tick that has liquidity. Once we reach this tick, wedetermine how much more of asset1 is needed to complete the swap. This processcontinues until either the entire order is fulfilled or all liquidity is drainedfrom the pool.

The same logic is true for swapping asset1, which is analogous to buying asset0;however, instead of moving left along the set of curves, we instead search forliquidity to the right.

From the user perspective, there are two ways to swap:

  1. Swap given token in for token out.

    • E.g. I have 1 ETH that I swap for some computed amount of DAI.
  2. Swap given token out for token in

    • E.g. I want to get out 3000 DAI for some amount of ETH to compute.

Each case has a corresponding message discussed previously in the x/poolmanagersection.

  • MsgSwapExactIn
  • MsgSwapExactOut

Once a message is received by the x/poolmanager, it is propageted into acorresponding keeperin x/concentrated-liquidity.

The relevant keeper method then calls its non-mutative calc version which isone of:

  • calcOutAmtGivenIn
  • calcInAmtGivenOut

State updates only occur upon successful execution of the swap inside the calc method.We ensure that calc does not update state by injecting sdk.CacheContext as itscontext parameter. The cache context is dropped on failure and committed on success.

Calculating Swap Amounts

Let's now focus on the core logic of calculating swap amounts.We mainly focus on calcOutAmtGivenIn as the high-level steps of calcInAmtGivenOutare similar.

1. Determine Swap Strategy

The first step we need to determine is the swap strategy. The swap strategy determinesthe direction of the swap, and it is one of:

  • zeroForOne - swap token zero in for token one out.

  • oneForZero - swap token one in for token zero out.

Note that the first token in the strategy name always corresponds to the tokenbeing swapped in, while the second token corresponds to the token being swappedout. This is true for both calcOutAmtGivenIn and calcInAmtGivenOut calc methods.

Recall that, in our model, we fix the tokens axis at the time of pool creation.The token on the x-axis is token zero, while the token on the y-axis is token one.

Given that the sqrt price is defined as $$\sqrt (y / x)$$, as we swap token zero(x-axis) in for token one (y-axis), we decrease the sqrt price and move downalong the price/tick curve. Conversely, as we swap token one (y-axis) in for tokenzero (x-axis), we increase the sqrt price and move up along the price/tick curve.

The reason we call this a price/tick curve is because there is a relationshipbetween the price and the tick. As a result, when we perform the swap, we arelikely to end up crossing a tick boundary. As a tick is crossed, the swap stateinternals must be updated. We will discuss this in more detail later.

2. Initialize Swap State

The next step is to initialize the swap state. The swap state is a struct thatcontains all of the swap state to be done within the current active tick(before we across a tick boundary).

It contains the following fields:

// SwapState defines the state of a swap.
// It is initialized as the swap begins and is updated after every swap step.
// Once the swap is complete, this state is either returned to the estimate
// swap querier or committed to state.
type SwapState struct {
// Remaining amount of specified token.
// if out given in, amount of token being swapped in.
// if in given out, amount of token being swapped out.
// Initialized to the amount of the token specified by the user.
// Updated after every swap step.
amountSpecifiedRemaining sdk.Dec

// Amount of the other token that is calculated from the specified token.
// if out given in, amount of token swapped out.
// if in given out, amount of token swapped in.
// Initialized to zero.
// Updated after every swap step.
amountCalculated sdk.Dec

// Current sqrt price while calculating swap.
// Initialized to the pool's current sqrt price.
// Updated after every swap step.
sqrtPrice sdk.Dec
// Current tick while calculating swap.
// Initialized to the pool's current tick.
// Updated each time a tick is crossed.
tick sdk.Int
// Current liqudiity within the active tick.
// Initialized to the pool's current tick's liquidity.
// Updated each time a tick is crossed.
liquidity sdk.Dec

// Global spread reward growth per-current swap.
// Initialized to zero.
// Updated after every swap step.
spreadRewardGrowthGlobal sdk.Dec
}

3. Compute Swap

The next step is to compute the swap. Conceptually, it can be done in two wayslisted below.Before doing so, we find the next initialized tick. An initializedtick is the tick that is touched by the edges of at least one position. If noposition has an edge at a tick, then that tick is uninitialized.

a. Swap within the same initialized tick range.

See "Appendix A" for details on what "initialized" means.

This case occurs when swapState.amountSpecifiedRemaining is less than or equalto the amount needed to reach the next tick. We omit the math needed to determinehow much is enough until a later section.

b. Swap across multiple initialized tick ranges.

See "Appendix A" for details on what "initialized" means.

This case occurs when swapState.amountSpecifiedRemaining is greater than theamount needed to reach the next tick

In terms of the code implementation, we loop, calling a swapStrategy.ComputeSwapStepOutGivenInor swapStrategy.ComputeSwapStepInGivenOut method, depending on swap out givenin or in given out, respectively.

The swap strategy is already initialized to be either zeroForOne or oneForZerofrom step 1. Go dynamically determines the desired implementation via polymorphism.

We leave details of the ComputeSwapStepOutGivenIn and ComputeSwapStepInGivenOutmethods to the appendix of the "Swapping" section.

The iteration stops when swapState.amountSpecifiedRemaining runs out or whenswapState.sqrtPrice reaches the sqrt price limit specified by the user as a priceimpact protection.

4. Update Swap State

Upon computing the swap step, we update the swap state with the results of theswap step. Namely,

  • Subtract the consumed specified amount from swapState.amountSpecifiedRemaining.

  • Add the calculated amount to swapState.amountCalculated.

  • Update swapState.sqrtPrice to the new sqrt price. The new sqrt price is notnecessarily the sqrt price of the next tick. It is the sqrt price of the next tickif the swap step crosses a tick boundary. Otherwise, it is something in betweenthe original and the next tick sqrt price.

  • Update swapState.tick to the next initialized tick if it is reached;otherwise, update it to the new tick calculated from the new sqrt price.If the sqrt price is unchanged, the tick remains unchanged as well.

  • Update swapState.liquidity to the new liquidity only if the next initializedtick is crossed. The liquidity is updated by incorporating the liquidity_netamount associated with the next initialized tick being crossed.

  • Update swapState.spreadRewardGrowthGlobal to the value of the total spread factor charged withinthe swap step on the amount of token in per one unit of liquidity within thetick range being swapped in.

Then, we either proceed to the next swap step or finalize the swap.

5. Update Global State

Once the swap is completed, we persiste the swap state to the global state(if mutative action is performed) and return the amountCalculated to the user.

Migration

Users can migrate their Balancer positions to a Concentrated Liquidity full rangeposition provided the underlying Balancer pool has a governance-selected canonicalConcentrated Liquidity pool. The migration is routed depending on the state of theunderlying Balancer position:

Balancer position is:

  • Superfluid delegated
    • Locked
  • Superfluid undelegating
    • Locked
    • Unlocking
  • Normal lock
    • Locked
    • Unlocking
  • Unlocked

Regardless of the path taken, the UnlockAndMigrateSharesToFullRangeConcentratedPositionmessage executes all of the below logic:

Superfluid Delegated Balancer to Concentrated

The following diagram illustrates the migration flow for a Superfluid delegatedBalancer position to a Superfluid delegated Concentrated Liquidity position.

Concentrated Liquidity | Osmosis Docs (24)

The migration process starts by removing the connection between the GAMM lock andthe GAMM intermediary account. The synthetic OSMO that was previously minted bythe GAMM intermediary account is immediately undelegated (skipping the two-weekunbonding period) and sent to the Superfluid module account where it is burned.

Next, the Lockup module account holding the original GAMM shares sends them backto the user, deleting the GAMM lock in the process. These shares are used toclaim the underlying two assets from the GAMM pool, which are then immediatelyput into a full range Concentrated Liquidity position in the canonicalConcentrated Liquidity pool.

The underlying liquidity this creates is tokenized (similar to GAMM shares) andis put into a new lock, which is then routed to the Lockup module account. A newintermediary account is created based on this new CL share denom. The newintermediary account mints synthetic OSMO and delegates it to the validator theuser originally delegated to. Finally, a new synthetic lock in a bonded statusis created based on the new CL lock ID, the new CL intermediary account, and thenew CL synthetic denom.

Superfluid Undelegating Balancer to Concentrated

The following diagram illustrates the migration flow for a superfluid undelegatingbalancer position to a superfluid undelegating concentrated liquidity position.The reason we must account for this situation is to respect the two week unbondingperiod that is required for superfluid undelegating, and be capable of slashinga position that was migrated.

Concentrated Liquidity | Osmosis Docs (25)

The process is identical to the Superfluid delegated migration, with threeexceptions. First, the connection between the GAMM intermediary account and theGAMM lock is already removed when a user started undelegation, so it does notneed to be done again. Second, no synthetic OSMO needs to be burned or created.Lastly, instead of creating a new CL synthetic lock in a bonded status, we createa new CL synthetic lock in an unlocking status. This lock will be unlocked oncethe two-week unbonding period is over.

Locked and Unlocked Balancer to Concentrated

The locked<>locked and unlocked<>unlocked migration utilizes a subset of actionsthat were taken in the superfluid migration. The Lockup module account that washolding the original GAMM shares sends them back to the user, deleting the GAMMlock in the process. These shares are used to claim the underlying two assetsfrom the GAMM pool, which are then immediately put into a full range ConcentratedLiquidity position in the canonical Concentrated Liquidity pool.

If it was previously locked, we keep the concentrated locked for the same periodof time. If it was previously unlocking, we begin unlocking the concentrated lockfrom where the GAMM lock left off.

Balancer to Concentrated with No Lock

When GAMM shares are not locked, they are simply claimed for the underlying twoassets, which are then immediately put into a full range concentrated liquidityposition in the canonical concentrated liquidity pool. No locks are involved inthis migration.

Position Fungification

There is a possibility to fungify fully-charged positions within the same tick range.Assume that there are two positions in the same tick range and both are fully charged.

As a user, I might want to combine them into a single position so that I don't have to managepositions inside the same tick range separately.

Therefore, I execute MsgFungifyChargedPositions that takes a list of position ids to fungifyand merges them into one.

Besides being fully charged, all of the positions must be in the same tick range and have the sameowner (sender). All must belong to the same pool and be unlocked. As a result, none of the positionscan be superfluid staked if they are full-range.

Once the message finishes, the user will have a completely new position with spread factors and incentive rewardsmoved into the new position. The old positions will be deleted.

Swapping. Appendix A: Example

Note, that the numbers used in this example are not realistic. They are used toillustrate the concepts on the high level.

Imagine a tick range from min tick -1000 to max tick 1000 in a pool with a 1%spread factor.

Assume that user A created a full range position from ticks -1000 to 1000 for10_000 liquidity units.

Assume that user B created a narrow range position from ticks 0 to 100 for 1_000liquidity units.

Assume the current active tick is -34 and user perform a swap in the positivedirection of the tick range by swapping 5_000 tokens one in for some tokenszero out.

Our tick range and liquidity graph now looks like this:

 cur_sqrt_price ////////// <--- position by user B

///////////////////////////////////////////////////////// <---position by user A
-1000 -34 0 100 1000

The swap state is initialized as follows:

  • amountSpecifiedRemaining is set to 5_000 tokens one in specified by the user.
  • amountCalculated is set to zero.
  • sqrtPrice is set to the current sqrt price of the pool(computed from the tick -34)
  • tick is set to the current tick of the pool (-34)
  • liquidity is set to the current liquidity tracked by the pool at tick -34 (10_000)
  • spreadRewardGrowthGlobal is set to (0)

We proceed by getting the next initialized tick in the direction of the swap (0).

Each initialized tick has 2 fields:

  • liquidity_gross - this is the total liquidity referencing that tickat tick -1000: 10_000at tick 0: 1_000at tick 100: 1_000at tick 1000: 10_000

  • liquidity_net - liquidity that needs to be added to the active liquidity aswe cross the tick moving in the positive direction so that the active liquidityis always the sum of all liquidity_net amounts of initialized ticks below thecurrent one.at tick -1000: 10_000at tick 0: 1_000at tick 100: -1_000at tick 1000: -10_000

Next, we compute swap step from tick -34 to tick 0. Assume that 5_000 tokens onein is more than enough to cross tick 0 and it returns 10_000 of token zero outwhile consuming half of token one in (2500).

Now, we update the swap state as follows:

  • amountSpecifiedRemaining is set to 5000 - 2_500 = 2_500 tokens one in remaining.

  • amountCalculated is set to 10_000 tokens zero out calculated.

  • sqrtPrice is set to the sqrt price of the crossed initialized tick 0 (0).

  • tick is set to the tick of the crossed initialized tick 0 (0).

  • liquidity is set to the old liquidity value (10_000) + the liquidity_netof the crossed tick 0 (1_000) = 11_000.

  • spreadRewardGrowthGlobal is set to 2_500 * 0.01 / 10_000 = 0.0025 because we assumed1% spread factor.

Now, we proceed by getting the next initialized tick in the direction ofthe swap (100).

Next, we compute swap step from tick 0 to tick 100. Assume that 2_500 remainingtokens one in is not enough to reach the next initialized tick 100 and it returns12_500 of token zero out while only reaching tick 70. The reason why we now get agreater amount of token zero out for the same amount of token one in is because theliquidity in this tick range is greater than the liquidity in the previous tick range.

Now, we update the swap state as follows:

  • amountSpecifiedRemaining is set to 2_500 - 2_500 = 0 tokens one in remaining.

  • amountCalculated is set to 10_000 + 12_500 = 22_500 tokens zero out calculated.

  • sqrtPrice is set to the reached sqrt price.

  • tick is set to an uninitialized tick associated with the reached sqrt price (70).

  • liquidity is set kept the same as we did not cross any initialized tick.

  • spreadRewardGrowthGlobal is updated to 0.0025 + (2_500 * 0.01 / 10_000) = 0.005because we assumed 1% spread factor.

As a result, we complete the swap having swapped 5_000 tokens one in for 22_500tokens zero out. The tick is now at 70 and the current liquidity at the activetick tracked by the pool is 11_000. The global spread reward growth per unit of liquidityhas increased by 50 units of token one. See more details about the spread reward growthin the "Spread Rewards" section.

TODO: Swapping, Appendix B: Compute Swap Step Internals and Math

Range Orders

As a trader, I want to be able to execute ranger orders so that I have bettercontrol of the price at which I trade

TODO

Spread Rewards

As a an LP, I want to earn spread rewards on my capital so that I am incentivized toparticipate in active market making.

In Balancer-style pools, spread rewards go directly back into the pool to benefit all LPs pro-rata.For concentrated liquidity pools, this approach is no longer feasible due to thenon-fungible property of positions. As a result, we use a different accumulator-basedmechanism for tracking and storing spread rewards.

Reference the following papers for more information on the inspiration behind our accumulator package:

We define the following accumulator and spread-reward-related fields to be stored on variouslayers of state:

  • Per-pool
// Note that this is proto-generated.
type Pool struct {
...
SpreadFactor sdk.Dec
}

Each pool is initialized with a static spread factor value SpreadFactor to be paid by swappers.Additionally, each pool's spread reward accumulator tracks and stores the total rewards accrued from spreadsthroughout its lifespan, named SpreadRewardGrowthGlobal.

  • Per-tick
// Note that this is proto-generated.
type TickInfo struct {
...
SpreadRewardGrowthOppositeDirectionOfLastTraversal sdk.DecCoins
}

TickInfo keeps a record of spread rewards accumulated opposite the direction the tick was last traversed.In other words, when traversing the tick from right to left, SpreadRewardGrowthOppositeDirectionOfLastTraversalrepresents the spread rewards accumulated above that tick. When traversing the tick from left to right,SpreadRewardGrowthOppositeDirectionOfLastTraversal represents the spread rewards accumulated below that tick.

Concentrated Liquidity | Osmosis Docs (26)

This information is required for calculating the amount of spread rewards that accrue betweena range of two ticks.

Note that keeping track of the spread reward growth is only necessary for the ticks thathave been initialized. In other words, at least one position must be referencingthat tick to require tracking the spread reward growth occurring in that tick.

By convention, when a new tick is activated, it is set to the pool's SpreadRewardGrowthGlobalif the tick being initialized is above the current tick.

See the following code snippet:

if tickIndex <= currentTick {
accum, err := k.GetSpreadRewardAccumulator(ctx, poolId)
if err != nil {
return err
}

tickInfo.SpreadRewardGrowthBelow = accum.GetValue()
}

Essentially, setting the tick's tickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversalto the pools accum value represents the amount of spread rewards collected by the pool up untilthe tick was activated.

Once a tick is activated again (crossed in either direction),tickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversal is updated to add the differencebetween the pool's current accumulator value and the old value oftickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversal.

Tracking how many spread rewards are collected below, in the case of a lower tick, and above,in the case of an upper tick, allows us to calculate theamount of spread rewards inside a position (spread reward growth inside between two ticks) on demand.This is done by updating the activated tick with the amount of spread rewards collected forevery tick lower than the tick that is being crossed.

This has two benefits:

  • We avoid updating all ticks
  • We can calculate a range by subtracting the upper and lower ticks for the rangeusing the logic below.

We calculate the spread reward growth above the upper tick in the following way:

  • If calculating spread reward growth for an upper tick, we consider the following two cases:
    • currentTick >= upperTick: If the current tick is greater than or equal to theupper tick, the spread reward growth would be the pool's spread reward growth minus the upper tick's
    • currentTick < upperTick: If the current tick is smaller than the upper tick,the spread reward growth would be the upper tick's spread reward growth outside.

This process is vice versa for calculating spread reward growth below the lower tick.

Now, by having the spread reward growth below the lower and above the upper tick of a range,we can calculate the spread reward growth inside the range by subtracting the two from theglobal per-unit-of-liquidity spread reward growth.

Concentrated Liquidity | Osmosis Docs (27)

spreadRewardGrowthInsideRange := SpreadRewardGrowthGlobalOutside - spreadRewardGrowthBelowLowerTick - spreadRewardGrowthAboveUpperTick

Note that although tickInfo.SpreadRewardGrowthOutside may be initialized at different timesfor each tick, the comparison of these values between ticks is not meaningful, andthere is no guarantee that the values across ticks will follow any particular pattern.However, this does not affect the per-position calculations since all the positionneeds to know is the spread reward growth inside the position's range since the position waslast interacted with.

  • Per-position-accumulator

In a concentrated liquidity pool, unlike traditional pools, spread rewards do not get automaticallyre-added to pool. Instead, they are tracked by the unclaimedRewards fields of eachposition's accumulator.

The amount of uncollected spread rewards needs to be calculated every time a user modifiestheir position. This occurs when a position is created, and liquidity is removed(liquidity added is analogous to creating a new position).

We must recalculate the values for any modification, because with a change in liquidityfor the position, the amount of spread rewards allocated to the position must also change accordingly.

Collecting Spread Rewards

Once calculated, collecting spread rewards is a straightforward process of transferring thecalculated amount from the pool address to the position owner.

To collect spread rewards, users call MsgCollectSpreadRewards with the ID corresponding totheir position. The function collectSpreadRewards in the keeper is responsible forexecuting the spread reward collection and returning the amount collected, given the owner'saddress and the position ID:

func (k Keeper) collectSpreadRewards(
ctx sdk.Context,
owner sdk.AccAddress,
positionId uint64) (sdk.Coins, error) {
}

This returns the amount of spread rewards collected by the user.

Swaps

Swapping within a single tick works as the regular xy = k curve. For swapsacross ticks to work, we simply apply the same spread reward calculation logic for every swap step.

Consider data structures defined above. Let tokenInAmt be the amount of token beingswapped in.

Then, to calculate the spread reward within a single tick, we perform the following steps:

  1. Calculate an updated tokenInAmtAfterSpreadReward by charging the pool.SpreadFactor on tokenInAmt.
// Update global spread reward accumulator tracking spread rewards for denom of tokenInAmt.
// TODO: revisit to make sure if truncations need to happen.
pool.SpreadRewardGrowthGlobalOutside.TokenX = pool.SpreadRewardGrowthGlobalOutside.TokenX.Add(tokenInAmt.Mul(pool.SpreadFactor))

// Update tokenInAmt to account for spread factor.
spread_factor = tokenInAmt.Mul(pool.SpreadFactor).Ceil()
tokenInAmtAfterSpreadFactor = tokenInAmt.Sub(spread_factor)

k.bankKeeper.SendCoins(ctx, swapper, pool.GetAddress(), ...) // send tokenInAmtAfterSpreadFactor
  1. Proceed to calculating the next square root price by utilizing the updated `tokenInAmtAfterSpreadFactor.

Depending on which of the tokens in tokenIn,

If token1 is being swapped in:$$\Delta \sqrt P = \Delta y / L$$

Here, tokenInAmtAfterSpreadFactor is delta y.

If token0 is being swapped in:$$\Delta \sqrt P = L / \Delta x$$

Here, tokenInAmtAfterSpreadFactor is delta x.

Once we have the updated square root price, we can calculate the amount oftokenOut to be returned. The returned tokenOut is computed with spread rewardsaccounted for given that we used tokenInAmtAfterSpreadFactor.

Swap Step Spread Factors

We have a notion of swapState.amountSpecifiedRemaining which is the amount oftoken in remaining over all swap steps.

After performing the current swap step, the following cases are possible:

  1. All amount remaining is consumed

In that case, the spread factor is equal to the difference between the original amount remainingand the one actually consumed. The difference between them is the spread factor.

spreadRewardChargeTotal = amountSpecifiedRemaining.Sub(amountIn)
  1. Did not consume amount remaining in-full.

The spread factor is charged on the amount actually consumed during a swap step.

spreadRewardChargeTotal = amountIn.Mul(spreadFactor)
  1. Price impact protection makes it exit before consuming all amount remaining.

The spread factor is charged on the amount in actually consumed before price impactprotection got triggered.

spreadRewardChargeTotal = amountIn.Mul(spreadFactor)

Incentive/Liquidity Mining Mechanism

Overview

Due to the nonfungibility of positions and ticks, incentives for concentrated liquidity requires aslightly different mechanism for distributing incentives compared to Balancer and Stableswap pools.In general, the design space of incentive mechanisms for concentrated liquidity DEXs is extremelyunderexplored, so our implementation takes this as an opportunity to break some new ground in thebroader design space of order-book-style AMMs.

Below, we outline the approach for CL incentives that Osmosis will be implementing for its initialimplementation of concentrated liquidity, as well as our baseline reasoning for why we are pursuingthis design.

Target Properties

As a starting point, it's important to understand the properties of a healthy liquidity pool.These are all, of course, properties that become self-sustaining once the positive feedback cyclebetween liquidity and volume kicks off, but for the sake of understanding what exactly it is thatwe are trying to bootstrap with incentives it helps to be explicit with our goals.

Liquidity Depth

We want to ensure spread rewards and incentives are being used to maximize liquidity depth at the active tick(i.e. the tick the current spot price is in), as this gives the best execution price for trades onthe pool.

Liquidity Breadth

It is critical that as we roll out concentrated liquidity, there is an incentive for there to bewidth in the books for our major pools. This is to avoid the scenario where the liquidity in theactive tick gets filled and liquidity falls off a cliff (e.g. when there is a large price move andactive tick LPs get bulk arbed against). It is important for our liquidity base to be broad when itis low until our CL markets mature and active LPs begin participating.

Liquidity Uptime

We want to ensure that the active tick is not only liquid, but that it is consistently liquid,meaning that liquidity providers are incentivized to keep their liquidity on the books whilethey trade.

Specifically, we want to ensure that idle liquidity waiting for volume does not sit off thebooks with the goal of jumping in when a trade happens, as this makes Osmosis's liquiditylook thinner than it is and risks driving volume to other exchanges.

While just-in-time (JIT) liquidity technically benefits the trader on a first-degree basis(better price execution for that specific trade), it imposes a cost on the whole system bypushing LPs to an equilibrium that ultimately hurts the DEX (namely that liquidity stays ofthe books until a trade happens). This instance of Braess's paradoxcan be remedied with mechanisms designed around rewarding liquidity uptime.

Current Standard: Pro-rata in Active Tick

The current status quo for concentrated liquidity incentives is to distribute them pro-ratato all LPs providing liquidity in the active tick. Withsome clever accumulator tricks,this can be designed to ensure that each LP only receives incentives for liquidity they contributeto the active tick. This approach is incredible for liquidity depth, which is arguably the mostimportant property we need incentives to be able to accommodate. It is also a user flow thaton-chain market makers are already somewhat familiar with and has enough live examples wherewe roughly know that it functions as intended.

Our Implementation

At launch, Osmosis's CL incentives will primarily be in the format described above while weiron out a mechanism that achieves the remaining two properties predictably and effectively.As a piece of foreshadowing, the primary problem space we will be tackling is the following:status quo incentives advantage LPs who keep their liquidity off the books until a tradehappens, ultimately pushing liquidity off of the DEX and creating ambiguity around the "real"liquidity depth. This forces traders to make uninformed decisions about where to trade theirassets (or worse, accept worse execution on an inferior venue).

In other words, instead of having incentives go towards bootstrapping healthy liquidity pools,they risk going towards adversely pushing volume to other exchanges at the cost of the DEX,active LPs, and ultimately traders.

Note on supported and authorized uptimes

If you dig through our incentives logic, you might find code dealing with notions of Supported Uptimesand Authorized Uptimes. These are for an uptime incentivization mechanism we are keeping offat launch while we refine a more sophisticated version. We leave the state-related partsin core logic to ensure that if we do decide to turn the feature on (even if just toexperiment), it could be done by a simple governance proposal (to add more supporteduptimes to the list of authorized uptimes) and not require a state migration for pools.At launch, only the 1ns uptime will be authorized, which is roughly equivalent to statusquo CL incentives with the small difference that positions that are created and closed inthe same block are not eligible for any incentives.

For the sake of clarity, this mechanism functions very similarly to status quo incentives,but it has a separate accumulator for each supported uptime and ensures that only liquiditythat has been in the pool for the required amount of time qualifies for claiming incentives.

Incentive Creation and Querying

While it is technically possible for Osmosis to enable the creation of incentive records directly in the CL module, incentive creation is currently funneled through existing gauge infrastructure in the x/incentives module. This simplifies UX drastically for frontends, external incentive creators, and governance, while making CL incentives fully backwards-compatible with incentive creation and querying flows that everyone is already used to. As of the initial version of Osmosis's CL, all incentive creation and querying logic will be handled by respective gauge functions (e.g. the IncentivizedPools query in the x/incentives module will include CL pools that have internal incentives on them).

To create a gauge dedicated to the concentrated liquidity pool, run a MsgCreateGauge message in the x/incentives module with the following parameter constraints:

  • PoolId: The ID of the CL pool to create a gauge for.
  • DistrTo.LockQueryType must be set to locktypes.LockQueryType.NoLock
  • DistrTo.Denom must be an empty string.

The rest of the parameters can be set according to the desired configuration of the gauge. Please read the x/incentives module documentation for more information on how to configure gauges.

Note, that the created gauge will start emitting at the first epoch after the given StartTime. During the epoch, a x/concentrated-liquiditymodule IncentiveRecord will be created for every denom in the gauge. This incentive record will be configured to emit all given incentivesover the period of an epoch. If the gauge is non-perpetual (emits over several epochs), the distribution will be split evenly between the epochs.and a new IncentiveRecord will be created for each denom every epoch with the emission rate and token set to finish emitting at the end of the epoch.

Reward Splitting Between Classic and CL pools

While we want to nudge Classic pool LPs to transition to CL pools, we also want to ensure that we do not have a hard cutoff for incentives where past a certain point it is no longer worth it to provide liquidity to Classic pools. This is because we want to ensure that we have a healthy transition period where liquidity is not split between Classic and CL pools, but rather that liquidity is added to CL pools while Classic pools are slowly drained of liquidity.

To achieve this in a way that is difficult to game and efficient for the chain to process, we will be using a reward-splitting mechanism that treats bonded liquidity in a Classic pool that is paired by governance to a CL pool (e.g. for the purpose of migration) as a single full-range position on the CL pool for the purpose of calculating incentives. Note that this does not affect spread reward distribution and only applies to the flow of incentives through a CL pool.

One implication of this mechanism is that it moves the incentivization process to a higher level of abstraction (incentivizing pairs instead of pools). For internal incentives (which are governance managed), this is in line with the goal of continuing to push governance to require less frequent actions, which this change ultimately does.

To keep a small but meaningful incentive for LPs to still migrate their positions, we have added a discount rate to incentives that are redirected to Classic pools. This is initialized to 5% by default but is a governance-upgradable parameter that can be increased in the future. A discount rate of 100% is functionally equivalent to all the incentives staying in the CL pool.

TWAP Integration

In the context of twap, concentrated liquidity pools function differently fromCFMM pools.

There are 2 major differences that stem from how the liquidity is added andremoved in concentrated-liquidity.

The first one is given by the fact that a user does not provide liquidity atpool creation time. Instead, they have to issue a separate message post-poolcreation. As a result, there can be a time where there is no valid spot priceinitialized for a concentrated liquidity pool. When a concentrated liquidity poolis created, the x/twap module still initializes the twap records. However, theserecords are invalidated by setting the "last error time" field to the block timeat pool creation. Only adding liquidity to the pool will initialize the spot priceand twap records correctly. One technical detail to note is that adding liquidityin the same block as pool creation will still set the "last error time" field tothe block time despite spot price already being initialized. Although we fix anerror within that block, it still occurs. As a result, this is deemed acceptable.However, this is a technical trade-off for implementation simplicity and not anintentional design decision.

The second difference from balancer pools is focused around the fact thatliquidity can be completely removed from a concentrated liquidity pool,making its spot price be invalid.

To recap the basic LP functionality in concentrated liquidity, a user addsliqudity by creating a position. To remove liquidity, they withdraw theirposition. Contrary to CFMM pools, adding or removing liquidity does not affectthe price in 99% of the cases in concentrated liquidity. The only two exceptionsto this rule are:

Creating the first position in the pool.

In this case, we transition from invalid state where there is no liqudity, andthe spot price is uninitialized to the state where there is some liqudity, andas a result a valid spot price.

Note, that if there is a pool where liqudiity is completely drained and re-added,the TWAP's last error time will be pointing at the time when the liquidity was drained.This is different from how twap functions in CFMM pool where liquidity cannotbe removed in-full.

Removing the last position in the pool.

In this case, we transition from a valid state with liquidity and spot price toan invalid state where there is no liquidity and, as a result, no valid spotprice anymore. The last spot price error will be set to the block time of whenthe last position was removed.

To reiterate, the above two exceptions are the only cases where twap is updateddue to adding or removing liquidity.

The major source of updates with respect to twap is the swap logic. It functionssimilarly to CFMM pools where upon the completion of a swap, a listener AfterConcentratedPoolSwappropagates the execution to the twap module for the purposes of tracking state updatesnecessary to retrieve the spot price and update the twap accumulators(more details in x/twap module).

Lastly, see the "Listeners" section for more details on how twap is enabled bythe use of these hooks.

Parameters

  • AuthorizedQuoteDenoms []string

This is a list of quote denoms that can be used as token1 when creating a pool.We limit the quote assets to a small set for the purposes of having convenientprice increments stemming from tick to price conversion. These increments arein a human readable magnitude only for token1 as a quote. For limit orders inthe future, this will be a desirable property in terms of UX as to allow usersto set limit orders at prices in terms of token1 (quote asset) that are easyto reason about.

This goes in-hand with centralized exchanges that limit the quote asset setto only a few denoms.

Our list at launch is expected to consist of OSMO, DAI and USDC. These are setin the v16 upgrade handler.

  • IsPermisionlessPoolCreationEnabled bool

The flag indicating whether permissionless pool creation is enabled or not. Forlaunch, we have decided to disable permissionless pool creation. It will stillbe enabled via governance. This is because we want to limit the number of poolsfor risk management and want to avoid fragmenting liquidity for major denompairs with configurations of tick spacing that are not ideal.

Listeners

AfterConcentratedPoolCreated

This listener executes after the pool is created.

At the time of this writing, it is only utilized by the x/twap module.The twap module is expected to create twap records where the last error timeis set to the block time of when the pool was created. This is because thereis no liquidity in the pool at creation time.

AfterInitialPoolPositionCreated

This listener executes after the first position is created in a concentratedliquidity pool.

At the time of this writing, it is only utilized by the x/twap module.

AfterLastPoolPositionRemoved

This listener executes after the last position is removed in a concentratedliquidity pool.

At the time of this writing, it is only utilized by the x/twap module.

AfterConcentratedPoolSwap

This listener executes after a swap in a concentrated liquidity pool.

At the time of this writing, it is only utilized by the x/twap module.

State entries and KV store management

The following are the state entries (key and value pairs) stored for the concentrated liquidity module.

  • structs
    • TickPrefix + pool ID + tickIndex ➝ Tick Info struct
    • PoolPrefix + pool id ➝ pool struct
    • IncentivePrefix | pool id | min uptime index | denom | addr ➝ Incentive Record body struct
  • links
    • positionToLockPrefix | position id ➝ lock id
    • lockToPositionPrefix | lock id ➝ position id
    • PositionPrefix | addr bytes | pool id | position id ➝ boolean
    • PoolPositionPrefix | pool id | position id ➝ boolean

Note that for storing ticks, we use 9 bytes instead of directly using uint64, first byte being reserved for the Negative / Positive prefix, and the remaining 8 bytes being reserved for the tick itself, which is of uint64. Although we directly store signed integers as values, we use the first byte to indicate and re-arrange tick indexes from negative to positive.

State and Keys

Incentive Records

  • KeyIncentiveRecord

0x04| || string encoding of pool ID || | || string encoding of min uptime index || | || string encoding of incentive ID

Note that the reason for having pool ID and min uptime index is so that we can retrieveall incentive records for a given pool ID and min uptime index by performing prefix iteration.

Precision Issues With Price

There are precision issues that we must be considerate of in our design.

Consider the balancer pool between arb base unit and uosmo:

osmosisd q gamm pool 1011
pool:
'@type': /osmosis.gamm.v1beta1.Pool
address: osmo1pv6ffw8whyle2nyxhh8re44k4mu4smqd7fd66cu2y8gftw3473csxft8y5
future_pool_governor: 24h
id: "1011"
pool_assets:
- token:
amount: "101170077995723619690981"
denom: ibc/10E5E5B06D78FFBB61FD9F89209DEE5FD4446ED0550CBB8E3747DA79E10D9DC6
weight: "536870912000000"
- token:
amount: "218023341414"
denom: uosmo
weight: "536870912000000"
pool_params:
exit_fee: "0.000000000000000000"
smooth_weight_change_params: null
swap_fee: "0.002000000000000000"
total_shares:
amount: "18282469846754434906194"
denom: gamm/pool/1011
total_weight: "1073741824000000"

Let's say we want to migrate this into a CL pool where uosmo is the quoteasset and arb base unit is the base asset.

Note that quote asset is denom1 and base asset is denom0.We want quote asset to be uosmo so that limit orders on tickshave tick spacing in terms of uosmo as the quote.

Note:

  • OSMO has precision of 6. 1 OSMO = 10**6 uosmo
  • ARB has precision of 18. 1 ARB = 10**18 arb base unit

Therefore, the true price of the pool is:

>>> (218023341414 / 10**6) / (101170077995723619690981 / 10**18)
2.1550180224553714

However, in our core logic it is represented as:

218023341414 / 101170077995723619690981
2.1550180224553714e-12

or

osmosisd q gamm spot-price 1011 uosmo ibc/10E5E5B06D78FFBB61FD9F89209DEE5FD4446ED0550CBB8E3747DA79E10D9DC6
spot_price: "0.000000000002155018"

As a protocol, we need to accommodate prices that are very far apart.In the example above, the difference between 10**6 and 10**18

Most of the native precision is 106. However, most of the ETHprecision is 1018.

This starts to matter for assets such as upepe. That havea precision of 18 and a very low price level relative tothe quote asset that has precision of 6 (e.g uosmo or uusdc).

The true price of PEPE in USDC terms is 0.0000009749.

In the "on-chain representation", this would be:0.0000009749 * 10**6 / 10**18 = 9.749e-19

Note that this is below the minimum precision of sdk.Dec.

Additionally, there is a problem with tick to sqrt price conversionswhere at small price levels, two sqrt prices can map to the sametick.

As a workaround, we have decided to limit min spot price to 10^-12and min tick to -108000000. It has been shown at at price levelsbelow 10^-12, this issue is most apparent. See this issue for details:https://github.com/osmosis-labs/osmosis/issues/5550

Now, we have a problem that we cannot handle pairs wherethe quote asset has a precision of 6 and the base asset has aprecision of 18.

Note that this is not a problem for pairs where the quote assethas a precision of 18 and the base asset has a precision of 6.E.g. OSMO/DAI.

Solution

At launch, pool creation is permissioned. Therefore, we canensure correctness for the initial set of pools.

Long term, we will implement a wrapper contract around concentrated liquiditythat will handle the precision issues and scale the prices to all have a precision of at most 12.

The contract will have to handle truncation and rounding to determinehow to handle dust during this process. The truncated amount can be significant.That being said, this problem is out of scope for this document.

Terminology

We will use the following terms throughout the document and our codebase:

  • Tick - a unit that has a 1:1 mapping with price

  • Bucket - an area between two initialized ticks.

  • Tick Range - a general term to describe a concept with lower and upper bound.

    • Position is defined on a tick range.
    • Bucket is defined on a tick range.
    • A trader performs a swap over a tick range.
  • Tick Spacing - the distance between two ticks that can be initialized. This iswhat defines the minimum bucket size.

Note that ticks are defined inside buckets. Assume tick spacing is 100. A liquidity providercreates a position with amounts such that the current tick is 155 between ticks 100 and 200.

Note, that the current tick of 155 is defined inside the bucket over a range of 100 to 200.

  • Initialized Tick - a tick at which LPs can provide liquidity. Some ticks cannot beinitialized due to tick spacing. MinCurrentTick is an exception due to being 1 tick belowMinInitializedTick. Only initialized ticks are crossed during a swap (see "Crossing Tick")for details.

  • MinInitializedTick - the minimum tick at which a position can be initialized. When this tick iscrossed, all liquidity is consumed at the tick ends up on MinCurrentTick. At that point, thereis no liquidity and the pool is in no bucket. To enter the first bucket, a swap right must be doneto cross the next initialized tick and kick in the liquidity. If at least one full range position isdefined, MinInitializedTick will be the first such tick.

  • MinCurrentTick - is the minimum value that a current tick can take. If we consume all liquidity andcross the min initialized tick, our current tick will equal to MinInitializedTick - 1 (MinCurrentTick)with zero liquidity. However, note that this MinCurrentTick cannot be crossed. If current tick equalsto this tick, it is only possible to swap in the right (one for zero) direction.

  • MaxTick- is the maximum tick at which a position can be initialized. It is also the maximum value thata current tick can be. Note that this is different from theMinInitializedTickandMinCurrentTickdueto our definition of the full range (see below). The full range is inclusive of the lower tick but exclusiveof the upper tick. As a result, we do not need to differentiate between the two for the max. When the poolis on theMaxTick, there is no liquidity. To kick in the liquidity, a swap left must be done to crossthe MaxTick` and enter the last bucket (when sequencing from left to right).

  • Initialized Range - the range of ticks that can be initialized: [MinInitializedTick, MaxTick]

  • Full Range - the maximum range at which a position can be defined: [MinInitializedTick, MaxTick)

  • Crossing Tick - crossing a tick means leaving one bucket and entering another. Each tick has a liquiditynet value defined. This value measures "how much of liquidity needs to be added to the current when crossinga tick going left-to-right and entering a new bucket". This value is positive for lower ticks of a positionand negative for higher. When going left-to-right, instead of adding, we subtract this value from the current liquidity.There are two edge cases. First, when pool crosses a MinInitializedTick, the pool does not enter any bucket.since it is now outside of the Full Range. Second, when pool crossed a MaxTick, the pool does not enterany bucket since it is now outside of the Full Range. Instead, we treat this being directly on eitherthe MinCurrentTick or MaxTick.

External Sources

Concentrated Liquidity | Osmosis Docs (2024)

FAQs

What is concentrated liquidity? ›

Concentrated liquidity represents a significant evolution in the field of decentralized finance (DeFi) and liquidity provision. This concept allows liquidity providers (LPs) to allocate their capital to specific price ranges within a liquidity pool, rather than across the entire price curve.

What are ticks in concentrated liquidity? ›

To achieve concentrated liquidity, the once continuous spectrum of price space has been partitioned with ticks. Ticks are the boundaries between discrete areas in price space. Ticks are spaced such that an increase or decrease of 1 tick represents a 0.01% increase or decrease in price at any point in price space.

What is a concentrated liquidity amm? ›

Concentrated liquidity is a feature in AMMs that allows liquidity providers (LPs) to allocate their assets within specific price ranges, rather than across the entire spectrum of possible prices. This innovation was popularized by Uniswap V3, one of the leading AMM platforms.

How to manage concentrated liquidity? ›

The prevailing strategy with concentrated liquidity is to keep the range tight to maximize fees and actively manage it. This takes a ton of effort, forces us to realize a lot of impermanent loss, and has led to a majority of liquidity providers losing money.

What are the risks of concentrated liquidity? ›

Opening positions in Concentrated Liquidity Pools are associated with certain risks and should be understood before depositing!
  • Impermanent Loss / Divergence loss. ...
  • Price exposure to the underlying tokens. ...
  • Rebalancing risk.
Jan 5, 2024

What benefits come with concentrated liquidity? ›

Benefits. Price Efficiency: Concentrated liquidity strategies contribute to price efficiency by ensuring that liquidity is concentrated around the current market price, reducing slippage and improving execution quality for traders.

Is a AMM risky? ›

Another major risk involves the concentration of large deposits in AMM pools. High concentrations of 'whales' can lead to significant risky situations where the pricing dynamics of a pool could change substantially if a large depositor withdraws their assets.

How does AMM liquidity work? ›

AMMs operate using liquidity pools. Users deposit cryptocurrencies into these pools to provide liquidity. The pools then use algorithms to set token prices based on the ratio of assets in the pool. When a user wants to trade, they swap one token for another directly through the AMM.

What is the difference between AMM and CLMM? ›

Concentrated Liquidity Market Maker (CLMM) pools allow liquidity providers to select a specific price range at which liquidity is active for trades within a pool. This is in contrast to constant product Automated Market Maker (AMM) pools, where all liquidity is spread out on a price curve from 0 to ∞.

How do you mitigate liquidity? ›

Mitigation of liquidity risk can start with a complete understanding of the ratios you are monitoring, those you should be monitoring, an assessment of your financial planning and analysis efforts, and perhaps more frequent forecasting of cash flow.

How do you control liquidity? ›

Common ways to manage liquidity risk include maintaining a portfolio of high-quality liquid assets, employing rigorous cash flow forecasting, and diversifying funding sources.

What is a liquidity pool? ›

A liquidity pool is a collection of crypto held in a smart contract. The purpose of the pool is to facilitate transactions. Decentralized exchanges (DEXs) use liquidity pools so that traders can swap between different assets within the pool.

What are the two types of liquidity? ›

Liquidity refers to the ease with which an asset, or security, can be converted into ready cash without affecting its market price. Cash is the most liquid of assets, while tangible items are less liquid. The two main types of liquidity are market liquidity and accounting liquidity.

What are the different levels of liquidity? ›

High liquidity means that an asset can be easily converted to cash for the expected value or market price. Low liquidity means that markets have few opportunities to buy and sell, and assets become difficult to trade.

What does highly liquidity mean? ›

When there is a high demand for an asset, there is high liquidity, as it will be easier to find a buyer (or seller) for that asset. Cash is considered the most liquid asset because it is very stable, can be readily accessed and easily spent.

What does it mean when a stock has liquidity? ›

A stock's liquidity generally refers to how rapidly shares of a stock can be bought or sold without substantially impacting the stock price. Stocks with low liquidity may be difficult to sell and may cause you to take a bigger loss if you cannot sell the shares when you want to.

Top Articles
Walking tours and themed walks
Banned Dog Types | Breed Specific Legislation | RSPCA - RSPCA - rspca.org.uk
Faridpur Govt. Girls' High School, Faridpur Test Examination—2023; English : Paper II
Regal Amc Near Me
J & D E-Gitarre 905 HSS Bat Mark Goth Black bei uns günstig einkaufen
Repentance (2 Corinthians 7:10) – West Palm Beach church of Christ
Booknet.com Contract Marriage 2
COLA Takes Effect With Sept. 30 Benefit Payment
Melfme
How to Type German letters ä, ö, ü and the ß on your Keyboard
Pj Ferry Schedule
Best Cav Commanders Rok
Cube Combination Wiki Roblox
Red Heeler Dog Breed Info, Pictures, Facts, Puppy Price & FAQs
Yesteryear Autos Slang
Beau John Maloney Houston Tx
RBT Exam: What to Expect
Radio Aleluya Dialogo Pastoral
Echat Fr Review Pc Retailer In Qatar Prestige Pc Providers – Alpha Marine Group
Charter Spectrum Store
Mission Impossible 7 Showtimes Near Marcus Parkwood Cinema
Halo Worth Animal Jam
Aps Day Spa Evesham
Tu Pulga Online Utah
European city that's best to visit from the UK by train has amazing beer
E32 Ultipro Desktop Version
TJ Maxx‘s Top 12 Competitors: An Expert Analysis - Marketing Scoop
Kuttymovies. Com
Ipcam Telegram Group
Craigslist Albany Ny Garage Sales
Bridger Park Community Garden
AsROck Q1900B ITX und Ramverträglichkeit
Why Holly Gibney Is One of TV's Best Protagonists
Giantess Feet Deviantart
SF bay area cars & trucks "chevrolet 50" - craigslist
3302577704
Vision Source: Premier Network of Independent Optometrists
Kelley Blue Book Recalls
Topos De Bolos Engraçados
Vocabulary Workshop Level B Unit 13 Choosing The Right Word
Anguilla Forum Tripadvisor
Appraisalport Com Dashboard Orders
Vintage Stock Edmond Ok
Blue Beetle Showtimes Near Regal Evergreen Parkway & Rpx
Ohio Road Construction Map
Gonzalo Lira Net Worth
Wrentham Outlets Hours Sunday
Campaign Blacksmith Bench
Call2Recycle Sites At The Home Depot
Swissport Timecard
Inloggen bij AH Sam - E-Overheid
Latest Posts
Article information

Author: Prof. An Powlowski

Last Updated:

Views: 5858

Rating: 4.3 / 5 (64 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Prof. An Powlowski

Birthday: 1992-09-29

Address: Apt. 994 8891 Orval Hill, Brittnyburgh, AZ 41023-0398

Phone: +26417467956738

Job: District Marketing Strategist

Hobby: Embroidery, Bodybuilding, Motor sports, Amateur radio, Wood carving, Whittling, Air sports

Introduction: My name is Prof. An Powlowski, I am a charming, helpful, attractive, good, graceful, thoughtful, vast person who loves writing and wants to share my knowledge and understanding with you.