Skip to content

Commit

Permalink
refactor: prototype reducing iterator overhead in swaps (#5177)
Browse files Browse the repository at this point in the history
* refactor: prototype reducing iterator overhead in swaps

* updates

* updates

* clean up

* godoc updates

* updates

* updates

* updates

* clean up

* updates
  • Loading branch information
p0mvn authored May 17, 2023
1 parent a61f5ec commit 9e1ca7b
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 6 deletions.
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))
}

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

0 comments on commit 9e1ca7b

Please sign in to comment.