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
24 changes: 21 additions & 3 deletions x/concentrated-liquidity/swaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ func (k Keeper) computeOutAmtGivenIn(
feeGrowthGlobal: sdk.ZeroDec(),
}

nextTickIter := swapStrategy.InitializeNextTickIterator(ctx, poolId, swapState.tick)
defer nextTickIter.Close()
if !nextTickIter.Valid() {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, types.RanOutOfTicksForPoolError{PoolId: 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 @@ -326,13 +332,18 @@ func (k Keeper) computeOutAmtGivenIn(
// Log the sqrtPrice we start the iteration with
sqrtPriceStart := swapState.sqrtPrice

// Iterator must be valid to be able to retrieve the next tick from it below.
if !nextTickIter.Valid() {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, types.RanOutOfTicksForPoolError{PoolId: 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)
if !ok {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("there are no more ticks initialized to fill the swap")
nextTick, err := types.TickIndexFromBytes(nextTickIter.Key())
if err != nil {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, err
}

// Utilizing the next initialized tick, we find the corresponding nextPrice (the target price).
Expand Down Expand Up @@ -379,6 +390,13 @@ func (k Keeper) computeOutAmtGivenIn(
if err != nil {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, err
}

// Move next tick iterator to the next tick as the tick is crossed.
if !nextTickIter.Valid() {
return sdk.Coin{}, sdk.Coin{}, 0, sdk.Dec{}, sdk.Dec{}, types.RanOutOfTicksForPoolError{PoolId: poolId}
}
nextTickIter.Next()

liquidityNet = swapStrategy.SetLiquidityDeltaSign(liquidityNet)
// Update the swapState's liquidity with the new tick's liquidity
newLiquidity := math.AddLiquidity(swapState.liquidity, liquidityNet)
Expand Down
36 changes: 35 additions & 1 deletion 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,11 +148,44 @@ func (s oneForZeroStrategy) ComputeSwapStepInGivenOut(sqrtPriceCurrent, sqrtPric
return sqrtPriceNext, amountZeroOut, amountOneIn, feeChargeTotal
}

// InitializeNextTickIterator returns iterator that seeks to the next tick from the given tickIndex.
// If nex tick relative to tickINdex does not exist in the store, it will return an invalid iterator.
//
// oneForZeroStrategy assumes moving to the right of the current square root price.
// As a result, we use forward iterator to seek to the next tick index relative to the currentTickIndex.
// Since start key of the forward iterator is inclusive, we search directly from the tickIndex
// forwards in increasing lexicographic order until a tick greater than currentTickIndex is found.
// Returns an invalid iterator if tickIndex is not in the store.
// Panics if fails to parse tick index from bytes.
// The caller is responsible for closing the iterator on success.
func (s oneForZeroStrategy) InitializeNextTickIterator(ctx sdk.Context, poolId uint64, currentTickIndex int64) dbm.Iterator {
store := ctx.KVStore(s.storeKey)
prefixBz := types.KeyTickPrefixByPoolId(poolId)
prefixStore := prefix.NewStore(store, prefixBz)
startKey := types.TickIndexToBytes(currentTickIndex)
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 > currentTickIndex {
break
}
}
return iter
}

// InitializeTickValue returns the initial tick value for computing swaps based
// on the actual current tick.
//
// oneForZeroStrategy assumes moving to the right of the current square root price.
// As a result, we use forward iterator in NextInitializedTick to find the next
// As a result, we use forward iterator in InitializeNextTickIterator to find the next
// tick to the left of current. The end cursor for forward iteration is inclusive.
// Therefore, this method is, essentially a no-op. The logic is reversed for
// zeroForOneStrategy where we use reverse iterator and have to add one to
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) TestInitializeNextTickIterator_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)

tickIndex := strategy.InitializeTickValue(currentTick)

iter := strategy.InitializeNextTickIterator(suite.Ctx, defaultPoolId, tickIndex)
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)
// InitializeNextTickIterator returns iterator that seeks to the next tick from the given tickIndex.
// If nex tick relative to tickINdex does not exist in the store, it will return an invalid iterator.
// See oneForZeroStrategy or zeroForOneStrategy for implementation details.
InitializeNextTickIterator(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
36 changes: 35 additions & 1 deletion 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,11 +145,44 @@ func (s zeroForOneStrategy) ComputeSwapStepInGivenOut(sqrtPriceCurrent, sqrtPric
return sqrtPriceNext, amountOneOut, amountZeroIn, feeChargeTotal
}

// InitializeNextTickIterator returns iterator that seeks to the next tick from the given tickIndex.
// If nex tick relative to tickINdex does not exist in the store, it will return an invalid iterator.
//
// oneForZeroStrategy assumes moving to the left of the current square root price.
// As a result, we use a reverse iterator to seek to the next tick index relative to the currentTickIndexPlusOne.
// Since end key of the reverse iterator is exclusive, we search from current + 1 tick index.
// in decrasing lexicographic order until a tick one smaller than current is found.
// Returns an invalid iterator if currentTickIndexPlusOne is not in the store.
// Panics if fails to parse tick index from bytes.
// The caller is responsible for closing the iterator on success.
func (s zeroForOneStrategy) InitializeNextTickIterator(ctx sdk.Context, poolId uint64, currentTickIndexPlusOne int64) dbm.Iterator {
store := ctx.KVStore(s.storeKey)
prefixBz := types.KeyTickPrefixByPoolId(poolId)
prefixStore := prefix.NewStore(store, prefixBz)
startKey := types.TickIndexToBytes(currentTickIndexPlusOne)

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 < currentTickIndexPlusOne {
break
}
}
return iter
}

// InitializeTickValue returns the initial tick value for computing swaps based
// on the actual current tick.
//
// zeroForOneStrategy assumes moving to the left of the current square root price.
// As a result, we use reverse iterator in NextInitializedTick to find the next
// As a result, we use reverse iterator in InitializeNextTickIterator to find the next
// tick to the left of current. The end cursor for reverse iteration is non-inclusive
// so must add one here to make sure that the current tick is included in the search.
func (s zeroForOneStrategy) InitializeTickValue(currentTick int64) int64 {
Expand Down
Loading