Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: prototype reducing iterator overhead in swaps #5177

Merged
merged 16 commits into from
May 17, 2023
6 changes: 5 additions & 1 deletion x/concentrated-liquidity/math/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func Liquidity0(amount sdk.Int, sqrtPriceA, sqrtPriceB sdk.Dec) sdk.Dec {

product := sqrtPriceABigDec.Mul(sqrtPriceBBigDec)
diff := sqrtPriceBBigDec.Sub(sqrtPriceABigDec)
if diff.Equal(osmomath.ZeroDec()) {
panic(fmt.Sprintf("liquidity0 diff is zero: sqrtPriceA %s sqrtPriceB %s", sqrtPriceA, sqrtPriceB))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a more descriptive error for whenever this happens

}

return amountBigDec.Mul(product).Quo(diff).SDKDec()
}

Expand All @@ -47,7 +51,7 @@ func Liquidity1(amount sdk.Int, sqrtPriceA, sqrtPriceB sdk.Dec) sdk.Dec {

diff := sqrtPriceBBigDec.Sub(sqrtPriceABigDec)
if diff.Equal(osmomath.ZeroDec()) {
panic(fmt.Sprintf("diff is zero: sqrtPriceA %s sqrtPriceB %s", sqrtPriceA, sqrtPriceB))
panic(fmt.Sprintf("liquidity1 diff is zero: sqrtPriceA %s sqrtPriceB %s", sqrtPriceA, sqrtPriceB))
}

return amountBigDec.Quo(diff).SDKDec()
Expand Down
29 changes: 23 additions & 6 deletions x/concentrated-liquidity/swaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ func (k Keeper) computeOutAmtGivenIn(
feeGrowthGlobal: sdk.ZeroDec(),
}

iter := swapStrategy.InitializeTickIterator(ctx, poolId, swapState.tick.Int64())
defer iter.Close()
if !iter.Valid() {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("no ticks found for pool %d", poolId)
}

// Iterate and update swapState until we swap all tokenIn or we reach the specific sqrtPriceLimit
// TODO: for now, we check if amountSpecifiedRemaining is GT 0.0000001. This is because there are times when the remaining
// amount may be extremely small, and that small amount cannot generate and amountIn/amountOut and we are therefore left
Expand All @@ -322,17 +328,21 @@ func (k Keeper) computeOutAmtGivenIn(
// Log the sqrtPrice we start the iteration with
sqrtPriceStart := swapState.sqrtPrice

if !iter.Valid() {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("no more ticks found for pool %d", poolId)
}

// We first check to see what the position of the nearest initialized tick is
// if zeroForOneStrategy, we look to the left of the tick the current sqrt price is at
// if oneForZeroStrategy, we look to the right of the tick the current sqrt price is at
// if no ticks are initialized (no users have created liquidity positions) then we return an error
nextTick, ok := swapStrategy.NextInitializedTick(ctx, poolId, swapState.tick.Int64())
if !ok {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("there are no more ticks initialized to fill the swap")
nextTick, err := types.TickIndexFromBytes(iter.Key())
if err != nil {
panic(fmt.Errorf("invalid tick index (%s): %v", string(iter.Key()), err))
}

// Utilizing the next initialized tick, we find the corresponding nextPrice (the target price).
_, nextTickSqrtPrice, err := math.TickToSqrtPrice(nextTick)
_, nextTickSqrtPrice, err := math.TickToSqrtPrice(sdk.NewInt(nextTick))
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("could not convert next tick (%v) to nextSqrtPrice", nextTick)
}
Expand Down Expand Up @@ -371,17 +381,24 @@ func (k Keeper) computeOutAmtGivenIn(
// tick has been consumed and we must move on to the next tick to complete the swap
if nextTickSqrtPrice.Equal(sqrtPrice) {
// Retrieve the liquidity held in the next closest initialized tick
liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick.Int64(), sdk.NewDecCoinFromDec(tokenInMin.Denom, swapState.feeGrowthGlobal))
liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick, sdk.NewDecCoinFromDec(tokenInMin.Denom, swapState.feeGrowthGlobal))
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
}

if !iter.Valid() {
return sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("no more ticks found for pool %d", poolId)
}

iter.Next()

liquidityNet = swapStrategy.SetLiquidityDeltaSign(liquidityNet)
// Update the swapState's liquidity with the new tick's liquidity
newLiquidity := math.AddLiquidity(swapState.liquidity, liquidityNet)
swapState.liquidity = newLiquidity

// Update the swapState's tick with the tick we retrieved liquidity from
swapState.tick = nextTick
swapState.tick = sdk.NewInt(nextTick)
} else if !sqrtPriceStart.Equal(sqrtPrice) {
// Otherwise if the sqrtPrice calculated from computeSwapStep does not equal the sqrtPrice we started with at the
// beginning of this iteration, we set the swapState tick to the corresponding tick of the sqrtPrice calculated from computeSwapStep
Expand Down
25 changes: 25 additions & 0 deletions x/concentrated-liquidity/swapstrategy/one_for_zero.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
dbm "github.com/tendermint/tm-db"

"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/math"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
Expand Down Expand Up @@ -147,6 +148,30 @@ func (s oneForZeroStrategy) ComputeSwapStepInGivenOut(sqrtPriceCurrent, sqrtPric
return sqrtPriceNext, amountZeroOut, amountOneIn, feeChargeTotal
}

// TODO: spec
func (s oneForZeroStrategy) InitializeTickIterator(ctx sdk.Context, poolId uint64, tickIndex int64) dbm.Iterator {
store := ctx.KVStore(s.storeKey)
prefixBz := types.KeyTickPrefixByPoolId(poolId)
prefixStore := prefix.NewStore(store, prefixBz)
startKey := types.TickIndexToBytes(tickIndex)
iter := prefixStore.Iterator(startKey, nil)

for ; iter.Valid(); iter.Next() {
// Since, we constructed our prefix store with <TickPrefix | poolID>, the
// key is the encoding of a tick index.
tick, err := types.TickIndexFromBytes(iter.Key())
if err != nil {
iter.Close()
panic(fmt.Errorf("invalid tick index (%s): %v", string(iter.Key()), err))
}

if tick > tickIndex {
break
}
}
return iter
}

// InitializeTickValue returns the initial tick value for computing swaps based
// on the actual current tick.
//
Expand Down
110 changes: 110 additions & 0 deletions x/concentrated-liquidity/swapstrategy/one_for_zero_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package swapstrategy_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"

cl "github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/math"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/swapstrategy"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
Expand Down Expand Up @@ -268,3 +269,112 @@ func (suite *StrategyTestSuite) TestComputeSwapStepInGivenOut_OneForZero() {
})
}
}

func (suite *StrategyTestSuite) TestInitializeTickIterator_OneForZero() {
tests := map[string]struct {
currentTick int64
preSetPositions []position

expectIsValid bool
expectNextTick int64
expectError error
}{
"1 position, one for zero": {
preSetPositions: []position{
{
lowerTick: -100,
upperTick: 100,
},
},
expectIsValid: true,
expectNextTick: 100,
},
"2 positions, one for zero": {
preSetPositions: []position{
{
lowerTick: -400,
upperTick: 300,
},
{
lowerTick: -200,
upperTick: 200,
},
},
expectIsValid: true,
expectNextTick: 200,
},
"lower tick lands on current tick, one for zero": {
preSetPositions: []position{
{
lowerTick: 0,
upperTick: 100,
},
},
expectIsValid: true,
expectNextTick: 100,
},
"upper tick lands on current tick, one for zero": {
preSetPositions: []position{
{
lowerTick: -100,
upperTick: 0,
},
{
lowerTick: 100,
upperTick: 200,
},
},
expectIsValid: true,
expectNextTick: 100,
},
"no ticks, one for zero": {
preSetPositions: []position{},
expectIsValid: false,
},
}

for name, tc := range tests {
tc := tc
suite.Run(name, func() {
suite.SetupTest()
strategy := swapstrategy.New(false, types.MaxSqrtPrice, suite.App.GetKey(types.ModuleName), sdk.ZeroDec(), defaultTickSpacing)

pool := suite.PrepareConcentratedPool()

clMsgServer := cl.NewMsgServerImpl(suite.App.ConcentratedLiquidityKeeper)

for _, pos := range tc.preSetPositions {
suite.FundAcc(suite.TestAccs[0], DefaultCoins.Add(DefaultCoins...))
_, err := clMsgServer.CreatePosition(sdk.WrapSDKContext(suite.Ctx), &types.MsgCreatePosition{
PoolId: pool.GetId(),
Sender: suite.TestAccs[0].String(),
LowerTick: pos.lowerTick,
UpperTick: pos.upperTick,
TokensProvided: DefaultCoins.Add(sdk.NewCoin(USDC, sdk.OneInt())),
TokenMinAmount0: sdk.ZeroInt(),
TokenMinAmount1: sdk.ZeroInt(),
})
suite.Require().NoError(err)
}

// refetch pool
pool, err := suite.App.ConcentratedLiquidityKeeper.GetConcentratedPoolById(suite.Ctx, pool.GetId())
suite.Require().NoError(err)

currentTick := pool.GetCurrentTick()
suite.Require().Equal(int64(0), currentTick.Int64())

tickIndex := strategy.InitializeTickValue(currentTick)

iter := strategy.InitializeTickIterator(suite.Ctx, defaultPoolId, tickIndex.Int64())
defer iter.Close()

suite.Require().Equal(tc.expectIsValid, iter.Valid())
if tc.expectIsValid {
actualNextTick, err := types.TickIndexFromBytes(iter.Key())
suite.Require().NoError(err)
suite.Require().Equal(tc.expectNextTick, actualNextTick)
}
})
}
}
5 changes: 5 additions & 0 deletions x/concentrated-liquidity/swapstrategy/swap_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package swapstrategy

import (
sdk "github.com/cosmos/cosmos-sdk/types"
dbm "github.com/tendermint/tm-db"

"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
)
Expand Down Expand Up @@ -49,6 +50,10 @@ type swapStrategy interface {
// * feeChargeTotal is the total fee charge. The fee is charged on the amount of token in.
// See oneForZeroStrategy or zeroForOneStrategy for implementation details.
ComputeSwapStepInGivenOut(sqrtPriceCurrent, sqrtPriceTarget, liquidity, amountRemainingOut sdk.Dec) (sqrtPriceNext, amountOutConsumed, amountInComputed, feeChargeTotal sdk.Dec)
// InitializeTickIterator returns iterator that seeks to the given tickIndex.
// If tick index does not exist in the store, it will return an invalid iterator.
// See oneForZeroStrategy or zeroForOneStrategy for implementation details.
InitializeTickIterator(ctx sdk.Context, poolId uint64, tickIndex int64) dbm.Iterator
// InitializeTickValue returns the initial tick value for computing swaps based
// on the actual current tick.
// See oneForZeroStrategy or zeroForOneStrategy for implementation details.
Expand Down
14 changes: 14 additions & 0 deletions x/concentrated-liquidity/swapstrategy/swap_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ type StrategyTestSuite struct {
apptesting.KeeperTestHelper
}

type position struct {
lowerTick int64
upperTick int64
}

const (
defaultPoolId = uint64(1)
initialCurrentTick = int64(0)
ETH = "eth"
USDC = "usdc"
)

var (
two = sdk.NewDec(2)
three = sdk.NewDec(2)
Expand All @@ -30,6 +42,8 @@ var (
defaultLiquidity = sdk.MustNewDecFromStr("3035764687.503020836176699298")
defaultFee = sdk.MustNewDecFromStr("0.03")
defaultTickSpacing = uint64(100)
defaultAmountReserves = sdk.NewInt(1_000_000_000)
DefaultCoins = sdk.NewCoins(sdk.NewCoin(ETH, defaultAmountReserves), sdk.NewCoin(USDC, defaultAmountReserves))
)

func TestStrategyTestSuite(t *testing.T) {
Expand Down
25 changes: 25 additions & 0 deletions x/concentrated-liquidity/swapstrategy/zero_for_one.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
dbm "github.com/tendermint/tm-db"

"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/math"
"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
Expand Down Expand Up @@ -144,6 +145,30 @@ func (s zeroForOneStrategy) ComputeSwapStepInGivenOut(sqrtPriceCurrent, sqrtPric
return sqrtPriceNext, amountOneOut, amountZeroIn, feeChargeTotal
}

// TODO: spec
func (s zeroForOneStrategy) InitializeTickIterator(ctx sdk.Context, poolId uint64, tickIndex int64) dbm.Iterator {
store := ctx.KVStore(s.storeKey)
prefixBz := types.KeyTickPrefixByPoolId(poolId)
prefixStore := prefix.NewStore(store, prefixBz)
startKey := types.TickIndexToBytes(tickIndex)

iter := prefixStore.ReverseIterator(nil, startKey)

for ; iter.Valid(); iter.Next() {
// Since, we constructed our prefix store with <TickPrefix | poolID>, the
// key is the encoding of a tick index.
tick, err := types.TickIndexFromBytes(iter.Key())
if err != nil {
iter.Close()
panic(fmt.Errorf("invalid tick index (%s): %v", string(iter.Key()), err))
}
if tick <= tickIndex {
break
}
}
return iter
}

// InitializeTickValue returns the initial tick value for computing swaps based
// on the actual current tick.
//
Expand Down
Loading