Skip to content

Commit

Permalink
feat: uninitialize empty ticks (osmosis-labs#5883)
Browse files Browse the repository at this point in the history
* initial push uninitialize ticks

* check tick fully removed

* update CHANGELOG.md

* add comments / clean up

* Update x/concentrated-liquidity/lp.go

Co-authored-by: Sishir Giri <[email protected]>

---------

Co-authored-by: Sishir Giri <[email protected]>
  • Loading branch information
2 people authored and VitalyV1337 committed Jul 31, 2023
1 parent 6f7bea6 commit 325bf74
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### State Breaking

* [#5532](https://github.com/osmosis-labs/osmosis/pull/5532) fix: Fix x/tokenfactory genesis import denoms reset x/bank existing denom metadata
* [#5883](https://github.com/osmosis-labs/osmosis/pull/5883) feat: Uninitialize empty ticks
* [#5874](https://github.com/osmosis-labs/osmosis/pull/5874) Remove Partial Migration from superfluid migration to CL
* [#5901](https://github.com/osmosis-labs/osmosis/pull/5901) Adding support for CW pools in ProtoRev

Expand Down
2 changes: 1 addition & 1 deletion x/concentrated-liquidity/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (k Keeper) ComputeInAmtGivenOut(
return k.computeInAmtGivenOut(ctx, desiredTokenOut, tokenInDenom, spreadFactor, priceLimit, poolId)
}

func (k Keeper) InitOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int64, tickIndex int64, liquidityIn sdk.Dec, upper bool) (err error) {
func (k Keeper) InitOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int64, tickIndex int64, liquidityIn sdk.Dec, upper bool) (tickIsEmpty bool, err error) {
return k.initOrUpdateTick(ctx, poolId, currentTick, tickIndex, liquidityIn, upper)
}

Expand Down
2 changes: 1 addition & 1 deletion x/concentrated-liquidity/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (s *KeeperTestSuite) validateTickUpdates(poolId uint64, lowerTick int64, up
}

func (s *KeeperTestSuite) initializeTick(ctx sdk.Context, currentTick int64, tickIndex int64, initialLiquidity sdk.Dec, spreadRewardGrowthOppositeDirectionOfTraversal sdk.DecCoins, uptimeTrackers []model.UptimeTracker, isLower bool) {
err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(ctx, validPoolId, currentTick, tickIndex, initialLiquidity, isLower)
_, err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(ctx, validPoolId, currentTick, tickIndex, initialLiquidity, isLower)
s.Require().NoError(err)

tickInfo, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(ctx, validPoolId, tickIndex)
Expand Down
45 changes: 27 additions & 18 deletions x/concentrated-liquidity/lp.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr
}

// Initialize / update the position in the pool based on the provided tick range and liquidity delta.
actualAmount0, actualAmount1, err = k.UpdatePosition(ctx, poolId, owner, lowerTick, upperTick, liquidityDelta, joinTime, positionId)
actualAmount0, actualAmount1, _, _, err = k.UpdatePosition(ctx, poolId, owner, lowerTick, upperTick, liquidityDelta, joinTime, positionId)
if err != nil {
return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, 0, 0, err
}
Expand Down Expand Up @@ -160,9 +160,9 @@ func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr
// When the last position within a pool is removed, this function calls an AfterLastPoolPosistionRemoved listener
// Currently, it creates twap records. Assumming that pool had all liqudity drained and then re-initialized,
// the whole twap state is completely reset. This is because when there is no liquidity in pool, spot price
// is undefined.
// Additionally, when the last position is removed by calling this method, the current sqrt price and current
// tick of the pool are set to zero.
// is undefined. When the last position is removed by calling this method, the current sqrt price and current
// tick of the pool are set to zero. Lastly, if the tick being withdrawn from is now empty due to the withdrawal,
// it is deleted from state.
// Returns error if
// - the provided owner does not own the position being withdrawn
// - there is no position in the given tick ranges
Expand Down Expand Up @@ -219,7 +219,7 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
liquidityDelta := requestedLiquidityAmountToWithdraw.Neg()

// Update the position in the pool based on the provided tick range and liquidity delta.
actualAmount0, actualAmount1, err := k.UpdatePosition(ctx, position.PoolId, owner, position.LowerTick, position.UpperTick, liquidityDelta, position.JoinTime, positionId)
actualAmount0, actualAmount1, lowerTickIsEmpty, upperTickIsEmpty, err := k.UpdatePosition(ctx, position.PoolId, owner, position.LowerTick, position.UpperTick, liquidityDelta, position.JoinTime, positionId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
Expand Down Expand Up @@ -266,6 +266,14 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
}
}

// If lowertick/uppertick has no liquidity in it, delete it from state.
if lowerTickIsEmpty {
k.RemoveTickInfo(ctx, position.PoolId, position.LowerTick)
}
if upperTickIsEmpty {
k.RemoveTickInfo(ctx, position.PoolId, position.UpperTick)
}

tokensRemoved := sdk.Coins{}
if actualAmount0.IsPositive() {
tokensRemoved = tokensRemoved.Add(sdk.NewCoin(pool.GetToken0(), actualAmount0))
Expand Down Expand Up @@ -394,63 +402,64 @@ func (k Keeper) addToPosition(ctx sdk.Context, owner sdk.AccAddress, positionId
// Updates ticks and pool liquidity. Returns how much of each token is either added or removed.
// Negative returned amounts imply that tokens are removed from the pool.
// Positive returned amounts imply that tokens are added to the pool.
// If the lower and/or upper ticks are being updated to have zero liquidity, a boolean is returned to flag the tick as empty to be deleted at the end of the withdrawPosition method.
// WARNING: this method may mutate the pool, make sure to refetch the pool after calling this method.
func (k Keeper) UpdatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, lowerTick, upperTick int64, liquidityDelta sdk.Dec, joinTime time.Time, positionId uint64) (sdk.Int, sdk.Int, error) {
func (k Keeper) UpdatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, lowerTick, upperTick int64, liquidityDelta sdk.Dec, joinTime time.Time, positionId uint64) (sdk.Int, sdk.Int, bool, bool, error) {
if err := k.validatePositionUpdateById(ctx, positionId, owner, lowerTick, upperTick, liquidityDelta, joinTime, poolId); err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

pool, err := k.getPoolById(ctx, poolId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

currentTick := pool.GetCurrentTick()

// update lower tickInfo state
err = k.initOrUpdateTick(ctx, poolId, currentTick, lowerTick, liquidityDelta, false)
lowerTickIsEmpty, err := k.initOrUpdateTick(ctx, poolId, currentTick, lowerTick, liquidityDelta, false)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// update upper tickInfo state
err = k.initOrUpdateTick(ctx, poolId, currentTick, upperTick, liquidityDelta, true)
upperTickIsEmpty, err := k.initOrUpdateTick(ctx, poolId, currentTick, upperTick, liquidityDelta, true)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// update position state
err = k.initOrUpdatePosition(ctx, poolId, owner, lowerTick, upperTick, liquidityDelta, joinTime, positionId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// Refetch pool to get the updated pool.
// Note that updateUptimeAccumulatorsToNow may modify the pool state and rewrite it to the store.
pool, err = k.getPoolById(ctx, poolId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// calculate the actual amounts of tokens 0 and 1 that were added or removed from the pool.
actualAmount0, actualAmount1, err := pool.CalcActualAmounts(ctx, lowerTick, upperTick, liquidityDelta)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// the pool's liquidity value is only updated if this position is active
pool.UpdateLiquidityIfActivePosition(ctx, lowerTick, upperTick, liquidityDelta)

if err := k.setPool(ctx, pool); err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

if err := k.initOrUpdatePositionSpreadRewardAccumulator(ctx, poolId, lowerTick, upperTick, positionId, liquidityDelta); err != nil {
return sdk.Int{}, sdk.Int{}, err
return sdk.Int{}, sdk.Int{}, false, false, err
}

// The returned amounts are rounded down to avoid returning more to clients than they actually deposited.
return actualAmount0.TruncateInt(), actualAmount1.TruncateInt(), nil
return actualAmount0.TruncateInt(), actualAmount1.TruncateInt(), lowerTickIsEmpty, upperTickIsEmpty, nil
}

// sendCoinsBetweenPoolAndUser takes the amounts calculated from a join/exit position and executes the send between pool and user
Expand Down
35 changes: 29 additions & 6 deletions x/concentrated-liquidity/lp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
timeElapsed time.Duration
createLockState lockState
withdrawWithNonOwner bool
isFullLiquidityWithdraw bool
}{
"base case: withdraw full liquidity amount": {
setupConfig: baseCase,
Expand All @@ -422,7 +423,8 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
// Note: subtracting one due to truncations in favor of the pool when withdrawing.
amount1Expected: baseCase.amount1Expected.Sub(sdk.OneInt()), // 5000 usdc
},
timeElapsed: defaultTimeElapsed,
timeElapsed: defaultTimeElapsed,
isFullLiquidityWithdraw: true,
},
"withdraw full liquidity amount with underlying lock that has finished unlocking": {
setupConfig: baseCase,
Expand All @@ -445,8 +447,9 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
liquidityAmount: FullRangeLiquidityAmt,
underlyingLockId: 1,
},
createLockState: unlocked,
timeElapsed: defaultTimeElapsed,
createLockState: unlocked,
timeElapsed: defaultTimeElapsed,
isFullLiquidityWithdraw: true,
},
"error: withdraw full liquidity amount but still locked": {
setupConfig: baseCase,
Expand Down Expand Up @@ -484,7 +487,8 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
// Note: subtracting one due to truncations in favor of the pool when withdrawing.
amount1Expected: baseCase.amount1Expected.Sub(sdk.OneInt()), // 5000 usdc
},
timeElapsed: 0,
timeElapsed: 0,
isFullLiquidityWithdraw: true,
},
"error: no position created": {
setupConfig: baseCase,
Expand Down Expand Up @@ -698,6 +702,17 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
s.validatePositionUpdate(s.Ctx, config.positionId, expectedRemainingLiquidity)
}

// Check that ticks were removed if liquidity is fully withdrawn.
lowerTickValue := store.Get(types.KeyTick(defaultPoolId, config.lowerTick))
upperTickValue := store.Get(types.KeyTick(defaultPoolId, config.upperTick))
if tc.isFullLiquidityWithdraw {
s.Require().Nil(lowerTickValue)
s.Require().Nil(upperTickValue)
} else {
s.Require().NotNil(lowerTickValue)
s.Require().NotNil(upperTickValue)
}

// Check tick state.
s.validateTickUpdates(config.poolId, config.lowerTick, config.upperTick, expectedRemainingLiquidity, config.expectedSpreadRewardGrowthOutsideLower, config.expectedSpreadRewardGrowthOutsideUpper)

Expand Down Expand Up @@ -1601,12 +1616,12 @@ func (s *KeeperTestSuite) TestUpdatePosition() {
)
s.Require().NoError(err)

// explicitly make update time different to ensure that the pool is updated with last liqudity update.
// explicitly make update time different to ensure that the pool is updated with last liquidity update.
expectedUpdateTime := tc.joinTime.Add(time.Second)
s.Ctx = s.Ctx.WithBlockTime(expectedUpdateTime)

// system under test
actualAmount0, actualAmount1, err := s.App.ConcentratedLiquidityKeeper.UpdatePosition(
actualAmount0, actualAmount1, lowerTickIsEmpty, upperTickIsEmpty, err := s.App.ConcentratedLiquidityKeeper.UpdatePosition(
s.Ctx,
tc.poolId,
s.TestAccs[tc.ownerIndex],
Expand All @@ -1624,6 +1639,14 @@ func (s *KeeperTestSuite) TestUpdatePosition() {
} else {
s.Require().NoError(err)

if tc.liquidityDelta.Equal(DefaultLiquidityAmt.Neg()) {
s.Require().True(lowerTickIsEmpty)
s.Require().True(upperTickIsEmpty)
} else {
s.Require().False(lowerTickIsEmpty)
s.Require().False(upperTickIsEmpty)
}

var (
expectedAmount0 sdk.Dec
expectedAmount1 sdk.Dec
Expand Down
2 changes: 1 addition & 1 deletion x/concentrated-liquidity/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ func (k Keeper) fungifyChargedPosition(ctx sdk.Context, owner sdk.AccAddress, po

// Create the new position in the pool based on the provided tick range and liquidity delta.
// This also initializes the spread reward accumulator and the uptime accumulators for the new position.
_, _, err = k.UpdatePosition(ctx, poolId, owner, lowerTick, upperTick, combinedLiquidityOfAllPositions, joinTime, newPositionId)
_, _, _, _, err = k.UpdatePosition(ctx, poolId, owner, lowerTick, upperTick, combinedLiquidityOfAllPositions, joinTime, newPositionId)
if err != nil {
return 0, err
}
Expand Down
4 changes: 2 additions & 2 deletions x/concentrated-liquidity/spread_rewards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1245,10 +1245,10 @@ func (s *KeeperTestSuite) TestInitOrUpdateSpreadRewardAccumulatorPosition_Updati
s.crossTickAndChargeSpreadReward(poolId, DefaultLowerTick)
}

err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, poolId, pool.GetCurrentTick(), DefaultLowerTick, DefaultLiquidityAmt, false)
_, err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, poolId, pool.GetCurrentTick(), DefaultLowerTick, DefaultLiquidityAmt, false)
s.Require().NoError(err)

err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, poolId, pool.GetCurrentTick(), DefaultUpperTick, DefaultLiquidityAmt, true)
_, err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, poolId, pool.GetCurrentTick(), DefaultUpperTick, DefaultLiquidityAmt, true)
s.Require().NoError(err)

// InitOrUpdateSpreadRewardAccumulatorPosition #1 lower tick to upper tick
Expand Down
22 changes: 17 additions & 5 deletions x/concentrated-liquidity/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ import (
// initOrUpdateTick retrieves the tickInfo from the specified tickIndex and updates both the liquidityNet and LiquidityGross.
// The given currentTick value is used to determine the strategy for updating the spread factor accumulator.
// We update the tick's spread reward growth opposite direction of last traversal accumulator to the spread reward growth global when tick index is <= current tick.
// Otherwise, it is set to zero.
// Otherwise, it is set to zero. If the liquidityDelta causes the tick to be empty, a boolean flags that the tick is empty for the withdrawPosition method to handle later (removes the tick from state).
// Note that liquidityDelta can be either positive or negative depending on whether we are adding or removing liquidity.
// if we are initializing or updating an upper tick, we subtract the liquidityIn from the LiquidityNet
// if we are initializing or updating a lower tick, we add the liquidityIn from the LiquidityNet
// WARNING: this method may mutate the pool, make sure to refetch the pool after calling this method.
func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int64, tickIndex int64, liquidityDelta sdk.Dec, upper bool) (err error) {
func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int64, tickIndex int64, liquidityDelta sdk.Dec, upper bool) (tickIsEmpty bool, err error) {
tickInfo, err := k.GetTickInfo(ctx, poolId, tickIndex)
if err != nil {
return err
return false, err
}

// If both liquidity fields are zero, we consume the base gas spread factor for initializing a tick.
Expand All @@ -48,7 +48,7 @@ func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int
if tickIndex <= currentTick {
accum, err := k.GetSpreadRewardAccumulator(ctx, poolId)
if err != nil {
return err
return false, err
}

tickInfo.SpreadRewardGrowthOppositeDirectionOfLastTraversal = accum.GetValue()
Expand All @@ -68,8 +68,13 @@ func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int
tickInfo.LiquidityNet.AddMut(liquidityDelta)
}

// If liquidity is now zero, this tick is flagged to be un-initialized at the end of the withdrawPosition method.
if tickInfo.LiquidityGross.IsZero() && tickInfo.LiquidityNet.IsZero() {
tickIsEmpty = true
}

k.SetTickInfo(ctx, poolId, tickIndex, &tickInfo)
return nil
return tickIsEmpty, nil
}

// crossTick crosses the given tick. The tick is specified by its index and tick info.
Expand Down Expand Up @@ -160,6 +165,13 @@ func (k Keeper) SetTickInfo(ctx sdk.Context, poolId uint64, tickIndex int64, tic
osmoutils.MustSet(store, key, tickInfo)
}

// RemoveTickInfo removes the tickInfo from state.
func (k Keeper) RemoveTickInfo(ctx sdk.Context, poolId uint64, tickIndex int64) {
store := ctx.KVStore(k.storeKey)
key := types.KeyTick(poolId, tickIndex)
store.Delete(key)
}

func (k Keeper) GetAllInitializedTicksForPool(ctx sdk.Context, poolId uint64) ([]genesis.FullTick, error) {
return osmoutils.GatherValuesFromStorePrefixWithKeyParser(ctx.KVStore(k.storeKey), types.KeyTickPrefixByPoolId(poolId), ParseFullTickFromBytes)
}
Expand Down
15 changes: 11 additions & 4 deletions x/concentrated-liquidity/tick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ func (s *KeeperTestSuite) TestInitOrUpdateTick() {
if test.tickExists {
tickInfoBefore, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
s.Require().NoError(err)
err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, DefaultLiquidityAmt, test.param.upper)
tickIsEmpty, err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, DefaultLiquidityAmt, test.param.upper)
s.Require().False(tickIsEmpty)
s.Require().NoError(err)
if tickInfoBefore.LiquidityGross.IsZero() && test.param.tickIndex <= pool.GetCurrentTick() {
tickInfoAfter, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
Expand Down Expand Up @@ -328,7 +329,7 @@ func (s *KeeperTestSuite) TestInitOrUpdateTick() {

// System under test.
// Initialize or update the tick according to the test case
err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, test.param.liquidityIn, test.param.upper)
tickIsEmpty, err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, test.param.liquidityIn, test.param.upper)
if tickInfoAfter.LiquidityGross.IsZero() && test.param.tickIndex <= pool.GetCurrentTick() {
tickInfoAfter, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
s.Require().NoError(err)
Expand All @@ -340,6 +341,12 @@ func (s *KeeperTestSuite) TestInitOrUpdateTick() {
}
s.Require().NoError(err)

if test.expectedLiquidityGross.IsZero() && test.expectedLiquidityNet.IsZero() {
s.Require().True(tickIsEmpty)
} else {
s.Require().False(tickIsEmpty)
}

// Get the tick info for poolId 1 again
tickInfoAfter, err = s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
s.Require().NoError(err)
Expand Down Expand Up @@ -454,7 +461,7 @@ func (s *KeeperTestSuite) TestGetTickInfo() {
}

// Set up an initialized tick
err := clKeeper.InitOrUpdateTick(s.Ctx, validPoolId, DefaultCurrTick, preInitializedTickIndex, DefaultLiquidityAmt, true)
_, err := clKeeper.InitOrUpdateTick(s.Ctx, validPoolId, DefaultCurrTick, preInitializedTickIndex, DefaultLiquidityAmt, true)
s.Require().NoError(err)

// Charge spread factor to make sure that the global spread factor accumulator is always updated.
Expand Down Expand Up @@ -622,7 +629,7 @@ func (s *KeeperTestSuite) TestCrossTick() {
}

// Set up an initialized tick
err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, validPoolId, DefaultCurrTick, test.preInitializedTickIndex, DefaultLiquidityAmt, true)
_, err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, validPoolId, DefaultCurrTick, test.preInitializedTickIndex, DefaultLiquidityAmt, true)
s.Require().NoError(err)

// Update global uptime accums for edge case testing
Expand Down
Loading

0 comments on commit 325bf74

Please sign in to comment.