diff --git a/x/concentrated-liquidity/README.md b/x/concentrated-liquidity/README.md index a57139df934..f575097e93c 100644 --- a/x/concentrated-liquidity/README.md +++ b/x/concentrated-liquidity/README.md @@ -228,13 +228,13 @@ b) Having the front end round the tick's actual price to the nearest human reada One draw back of this implementation is the requirement to create many ticks that will likely never be used. For example, in order to create ticks at 10 cent increments for spot prices greater than _$10000_, a $exponentAtPriceOne$ value of -5 must be set, requiring us to traverse ticks 1-3600000 before reaching _$10,000_. This should simply be an inconvenience and should not present any valid DOS vector for the chain. -### Scope of Concentrated Liquidity +## Scope of Concentrated Liquidity -#### Concentrated Liquidity Module +### Concentrated Liquidity Module > As an engineer, I would like the concentrated liquidity logic to exist in its own module so that I can easily reason about the concentrated liquidity abstraction that is different from the existing pools. -##### `MsgCreatePosition` +#### `MsgCreatePosition` - **Request** @@ -282,7 +282,7 @@ type MsgCreatePositionResponse struct { This message should call the `createPosition` keeper method that is introduced in the `"Liquidity Provision"` section of this document. -##### `MsgWithdrawPosition` +#### `MsgWithdrawPosition` - **Request** @@ -314,7 +314,7 @@ type MsgWithdrawPositionResponse struct { This message should call the `withdrawPosition` keeper method that is introduced in the `"Liquidity Provision"` section of this document. -##### `MsgCreatePool` +#### `MsgCreatePool` This message is responsible for creating a concentrated-liquidity pool. It propagates the execution flow to the `x/poolmanager` module for pool id @@ -341,7 +341,7 @@ type MsgCreateConcentratedPoolResponse struct { } ``` -##### `MsgCollectFees` +#### `MsgCollectFees` This message allows collecting fee from a position that is defined by the given pool id, sender's address, lower tick and upper tick. @@ -369,9 +369,9 @@ type MsgCollectFeesResponse struct { } ``` -#### Relationship to Pool Manager Module +## Relationship to Pool Manager Module -##### Pool Creation +### Pool Creation As previously mentioned, the `x/poolmanager` is responsible for creating the pool upon being called from the `x/concentrated-liquidity` module's message server. @@ -390,7 +390,7 @@ Note, that `InitializePool` is a method defined on the `SwapI` interface that is implemented by all swap modules. For example, `x/gamm` also implements it so that `x/pool-manager` can route pool initialization there as well. -##### Swaps +### Swaps We rely on the swap messages located in `x/poolmanager`: @@ -402,7 +402,7 @@ is associated with the `concentrated-liquidity` pool, the swap is routed into the relevant module. The routing is done via the mapping from state that was discussed in the "Pool Creation" section. -#### Liquidity Provision +### Liquidity Provision > As an LP, I want to provide liquidity in ranges so that I can achieve greater capital efficiency @@ -411,7 +411,7 @@ to a pool. A pool's liquidity is consisted of two assets: asset0 and asset1. In all pools, asset0 will be the lexicographically smaller of the two assets. At the current tick, the bucket at this tick consists of a mix of both asset0 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 while positions above the current price only contain asset1. -##### Adding Liquidity +### Adding Liquidity We can either provide liquidity above or below the current price, which would act as a range order, or decide to provide liquidity at the current price. @@ -457,7 +457,7 @@ func createPosition( } ``` -##### Removing Liquidity +### Removing Liquidity Removing liquidity is achieved via method `withdrawPosition` which is the inverse of previously discussed `createPosition`. In fact, the two methods share the same underlying logic, having the only difference being the sign of the liquidity. Plus signifying addition @@ -479,7 +479,7 @@ func (k Keeper) withdrawPosition( } ``` -#### Swapping +### Swapping > As a trader, I want to be able to swap over a concentrated liquidity pool so that my trades incur lower slippage @@ -519,7 +519,7 @@ State updates only occur upon successful execution of the swap inside the calc m We ensure that calc does not update state by injecting `sdk.CacheContext` as its context parameter. The cache context is dropped on failure and committed on success. -##### Calculating Swap Amounts +### 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 `calcInAmtGivenOut` @@ -660,7 +660,55 @@ Then, we either proceed to the next swap step or finalize the swap. 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. -##### Swapping. Appendix A: Example +## Migration + +Users can migrate their Balancer positions to a Concentrated Liquidity full range position provided the underlying Balancer pool has a governance-selected +canonical Concentrated Liquidity pool. The migration follows two distinct flows depending on the state of the underlying Balancer position: + +1. Balancer position is: + * Superfluid delegated + * Superfluid undelegating + * Locked + * Unlocked + +2. Balancer position has no underlying lock whatsoever + +Regardless of the path taken, a single message executes all of the below logic: + +`UnlockAndMigrateSharesToFullRangeConcentratedPosition` in superfluid for path 1, and `MigrateSharesToFullRangeConcentratedPosition` in gamm for path 2. + +### Superfluid Delegated Balancer to Concentrated + +The following diagram illustrates the migration flow for a Superfluid delegated Balancer position to a Superfluid delegated Concentrated Liquidity position. + +![Migrate Superfluid Delegate Balancer to Concentrated](./img/MigrateSuperfluidDelegated.png) + +The migration process starts by removing the connection between the GAMM lock and the GAMM intermediary account. The synthetic OSMO that was previously minted by the GAMM intermediary account is immediately undelegated (skipping the two-week unbonding period) and sent to the Superfluid module account where it is burned. + +Next, the Lockup module account holding the original GAMM shares sends them back to the user, deleting the GAMM lock in the process. These shares are used to claim the underlying two assets from the GAMM pool, which are then immediately put into a full range Concentrated Liquidity position in the canonical Concentrated Liquidity pool. + +The underlying liquidity this creates is tokenized (similar to GAMM shares) and is put into a new lock, which is then routed to the Lockup module account. A new intermediary account is created based on this new CL share denom. The new intermediary account mints synthetic OSMO and delegates it to the validator the user originally delegated to. Finally, a new synthetic lock in a bonded status is created based on the new CL lock ID, the new CL intermediary account, and the new CL synthetic denom. + +### Superfluid Undelegating Balancer to Concentrated + +The following diagram illustrates the migration flow for a superfluid undelegating balancer position to a superfluid undelegating concentrated liquidity position. The reason we must account for this situation is to respect the two week unbonding period that is required for superfluid undelegating, and be capable of slashing a position that was migrated. + +![Migrate Superfluid Undelegating Balancer to Concentrated](./img/MigrateSuperfluidUndelegating.png) + +The process is identical to the Superfluid delegated migration, with three exceptions. First, the connection between the GAMM intermediary account and the GAMM lock is already removed when a user started undelegation, so it does not need 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 create a new CL synthetic lock in an unlocking status. This lock will be unlocked once the two-week unbonding period is over. + +### Locked and Unlocked Balancer to Concentrated + +The locked<>locked and unlocked<>unlocked migration utilizes a subset of actions that were taken in the superfluid migration. The Lockup module account that was holding the original GAMM shares sends them back to the user, deleting the GAMM lock in the process. These shares are used to claim the underlying two assets from the GAMM pool, which are then immediately put into a full range Concentrated Liquidity position in the canonical Concentrated Liquidity pool. + +If it was previously locked, we keep the concentrated locked for the same period of time. If it was previously unlocking, we begin unlocking the concentrated lock from 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 two assets, which are then immediately put into a full range concentrated liquidity position in the canonical concentrated liquidity pool. No locks are involved in this migration. + + +## Swapping. Appendix A: Example Note, that the numbers used in this example are not realistic. They are used to illustrate the concepts on the high level. @@ -756,13 +804,13 @@ See more details about the fee growth in the "Fees" section. TODO: Swapping, Appendix B: Compute Swap Step Internals and Math -#### Range Orders +## Range Orders > As a trader, I want to be able to execute ranger orders so that I have better control of the price at which I trade TODO -#### Fees +## Fees > As a an LP, I want to earn fees on my capital so that I am incentivized to participate in the market making actively. @@ -951,7 +999,7 @@ if !isPositionNew { position.FeeGrowthInsideLast.Token0 = feeGrowthInside.Token0 ``` -##### Collecting Fees +## Collecting Fees Collecting fees is as simple as transferring the requested amount from the pool address to the position's owner. @@ -975,7 +1023,7 @@ func (k Keeper) collectFees( } ``` -##### Swaps +## Swaps Swapping within a single tick works as the regular `xy = k` curve. For swaps across ticks to work, we simply apply the same fee calculation logic for every swap step. @@ -1016,7 +1064,7 @@ Here, `tokenInAmtAfterFee` is delta x. Once we have the updated square root price, we can calculate the amount of `tokenOut` to be returned. The returned `tokenOut` is computed with fees accounted for given that we used `tokenInAmtAfterFee`. -##### Swap Step Fees +## Swap Step Fees We have a notion of `swapState.amountSpecifiedRemaining` which is the amount of token in remaining over all swap steps. @@ -1049,11 +1097,11 @@ protection got trigerred. feeChargeTotal = amountIn.Mul(swapFee) ``` -#### Liquidity Rewards +## Liquidity Rewards TODO -#### TWAP Integration +## TWAP Integration In the context of twap, concentrated liquidity pools function differently from CFMM pools. @@ -1107,9 +1155,9 @@ x/twap module). Lastly, see the "Listeners" section for more details on how twap is enabled by the use of these hooks. -#### Listeners +## Listeners -##### `AfterConcentratedPoolCreated` +### `AfterConcentratedPoolCreated` This listener executes after the pool is created. @@ -1118,28 +1166,28 @@ The twap module is expected to create twap records where the last error time is set to the block time of when the pool was created. This is because there is no liquidity in the pool at creation time. -##### `AfterInitialPoolPositionCreated` +### `AfterInitialPoolPositionCreated` This listener executes after the first position is created in a concentrated liquidity pool. At the time of this writing, it is only utilized by the `x/twap` module. -##### `AfterLastPoolPositionRemoved` +### `AfterLastPoolPositionRemoved` This listener executes after the last position is removed in a concentrated liquidity pool. At the time of this writing, it is only utilized by the `x/twap` module. -##### `AfterConcentratedPoolSwap` +### `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 +### State - global (per-pool) @@ -1147,9 +1195,9 @@ At the time of this writing, it is only utilized by the `x/twap` module. - per-position -#### Placeholder +### Placeholder -### Terminology +## Terminology We will use the following terms throughout the document: @@ -1168,7 +1216,7 @@ We will use the following terms throughout the document: - `Range` - TODO -### External Sources +## External Sources - [Uniswap V3 Whitepaper](https://uniswap.org/whitepaper-v3.pdf) - [Technical Note on Liquidity Math](https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf) diff --git a/x/concentrated-liquidity/img/MigrateSuperfluidDelegated.png b/x/concentrated-liquidity/img/MigrateSuperfluidDelegated.png new file mode 100644 index 00000000000..8d0fd38e316 Binary files /dev/null and b/x/concentrated-liquidity/img/MigrateSuperfluidDelegated.png differ diff --git a/x/concentrated-liquidity/img/MigrateSuperfluidUndelegating.png b/x/concentrated-liquidity/img/MigrateSuperfluidUndelegating.png new file mode 100644 index 00000000000..e29c9e2d83d Binary files /dev/null and b/x/concentrated-liquidity/img/MigrateSuperfluidUndelegating.png differ diff --git a/x/lockup/README.md b/x/lockup/README.md index 1e91375de5d..d4601bd0579 100644 --- a/x/lockup/README.md +++ b/x/lockup/README.md @@ -454,6 +454,8 @@ coins are sent from lockup `ModuleAccount`. - Transfer the tokens from lockup `ModuleAccount` to the `MsgUnlockTokens.Owner`. +Note: If the token in the lock contains the ClTokenPrefix ("cl/pool/{poolId}), this token is burned instead of being sent to the owner. + ### Remove synthetic locks after removal time mature For synthetic lockups, no coin movement is made, but lockup record and diff --git a/x/superfluid/README.md b/x/superfluid/README.md index 657b3c88e99..cbdcccc7a03 100644 --- a/x/superfluid/README.md +++ b/x/superfluid/README.md @@ -876,6 +876,13 @@ We do this by: - It removes these from the underlying lock and the synthetic lock. - These coins are moved to the community pool. +Slashing a concentrated liquidity superfluid lockup happens in the same way, however +instead of sending the concentrated full range position shares from the lockup +module account to the community pool, we determine the underlying assets +that the slashed shares represent and send those from the respective pool +account to the community pool. The shares residing in the lockup module +account that represented the funds that got sent to the community pool are then burned. + ### Nuances - Slashed tokens go to the community pool, rather than being burned as