Skip to content

Commit

Permalink
feat: query max amountIn based on max ticks willing to traverse (#5889)
Browse files Browse the repository at this point in the history
* max amountIn based on max ticks willing to trav

* update changelog

* clean up

* use cache context in the query

* change amount in from max spot price to pool bal

* change to tokenoutdenom
  • Loading branch information
czarcas7ic committed Jul 28, 2023
1 parent 6ba7191 commit 7523559
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [#5534](https://github.com/osmosis-labs/osmosis/pull/5534) fix: fix the account number of x/tokenfactory module account
* [#5750](https://github.com/osmosis-labs/osmosis/pull/5750) feat: add cli commmand for converting proto structs to proto marshalled bytes
* [#5889](https://github.com/osmosis-labs/osmosis/pull/5889) provides an API for protorev to determine max amountIn that can be swapped based on max ticks willing to be traversed
* [#5849] (https://github.com/osmosis-labs/osmosis/pull/5849) CL: Lower gas for leaving a position and withdrawing rewards
* [#5855](https://github.com/osmosis-labs/osmosis/pull/5855) feat(x/cosmwasmpool): Sending token_in_max_amount to the contract before running contract msg
* [#5855](https://github.com/osmosis-labs/osmosis/pull/5855) feat(x/cosmwasmpool): Sending token_in_max_amount to the contract before running contract msg
* [#5893] (https://github.com/osmosis-labs/osmosis/pull/5893) Export createPosition method in CL so other modules can use it in testing
* [#5870] (https://github.com/osmosis-labs/osmosis/pull/5870) Remove v14/ separator in protorev rest endpoints

Expand Down
2 changes: 1 addition & 1 deletion tests/cl-go-client/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/cosmos/cosmos-sdk v0.47.3
github.com/ignite/cli v0.23.0
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230623115558-38aaab07d343
github.com/osmosis-labs/osmosis/v16 v16.0.0-20230630175215-d5fcd089a71c
github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304

Expand Down Expand Up @@ -90,7 +91,6 @@ require (
github.com/mtibben/percent v0.2.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230621002052-afb82fbaa312 // indirect
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230623115558-38aaab07d343 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions x/concentrated-liquidity/client/query_proto_wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ func (q Querier) UserUnbondingPositions(ctx sdk.Context, req clquery.UserUnbondi
}

cfmmPoolId, err := q.Keeper.GetUserUnbondingPositions(ctx, sdkAddr)
if err != nil {
return nil, err
}

return &clquery.UserUnbondingPositionsResponse{
PositionsWithPeriodLock: cfmmPoolId,
}, nil
Expand Down
122 changes: 122 additions & 0 deletions x/concentrated-liquidity/swaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,3 +808,125 @@ func edgeCaseInequalityBasedOnSwapStrategy(isZeroForOne bool, nextInitializedTic
}
return nextInitializedTickSqrtPrice.LT(computedSqrtPrice)
}

// ComputeMaxInAmtGivenMaxTicksCrossed calculates the maximum amount of the tokenInDenom that can be swapped
// into the pool to swap through all the liquidity from the current tick through the maxTicksCrossed tick,
// but not exceed it.
func (k Keeper) ComputeMaxInAmtGivenMaxTicksCrossed(
ctx sdk.Context,
poolId uint64,
tokenInDenom string,
maxTicksCrossed uint64,
) (maxTokenIn, resultingTokenOut sdk.Coin, err error) {
cacheCtx, _ := ctx.CacheContext()

p, err := k.getPoolForSwap(cacheCtx, poolId)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}

// Validate tokenInDenom exists in the pool
if tokenInDenom != p.GetToken0() && tokenInDenom != p.GetToken1() {
return sdk.Coin{}, sdk.Coin{}, types.TokenInDenomNotInPoolError{TokenInDenom: tokenInDenom}
}

// Determine the tokenOutDenom based on the tokenInDenom
var tokenOutDenom string
if tokenInDenom == p.GetToken0() {
tokenOutDenom = p.GetToken1()
} else {
tokenOutDenom = p.GetToken0()
}

// Setup the swap strategy
swapStrategy, _, err := k.setupSwapStrategy(p, p.GetSpreadFactor(cacheCtx), tokenInDenom, sdk.ZeroDec())
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}

// Initialize swap state
// Utilize the total amount of tokenOutDenom in the pool as the specified amountOut, since we want
// the limitation to be the tick crossing, not the amountOut.
balances := k.bankKeeper.GetAllBalances(ctx, p.GetAddress())
swapState := newSwapState(balances.AmountOf(tokenOutDenom), p, swapStrategy)

nextInitTickIter := swapStrategy.InitializeNextTickIterator(cacheCtx, poolId, swapState.tick)
defer nextInitTickIter.Close()

totalTokenOut := sdk.ZeroDec()

for i := uint64(0); i < maxTicksCrossed; i++ {
// Check if the iterator is valid
if !nextInitTickIter.Valid() {
break
}

nextInitializedTick, err := types.TickIndexFromBytes(nextInitTickIter.Key())
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}

_, nextInitializedTickSqrtPrice, err := math.TickToSqrtPrice(nextInitializedTick)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, types.TickToSqrtPriceConversionError{NextTick: nextInitializedTick}
}

sqrtPriceTarget := swapStrategy.GetSqrtTargetPrice(nextInitializedTickSqrtPrice)

// Compute the swap
computedSqrtPrice, amountOut, amountIn, spreadRewardChargeTotal := swapStrategy.ComputeSwapWithinBucketInGivenOut(
swapState.sqrtPrice,
sqrtPriceTarget,
swapState.liquidity,
swapState.amountSpecifiedRemaining,
)

swapState.sqrtPrice = computedSqrtPrice
swapState.amountSpecifiedRemaining.SubMut(amountOut)
swapState.amountCalculated.AddMut(amountIn.Add(spreadRewardChargeTotal))

totalTokenOut = totalTokenOut.Add(amountOut)

// Check if the tick needs to be updated
nextInitializedTickSqrtPriceBigDec := osmomath.BigDecFromSDKDec(nextInitializedTickSqrtPrice)

// We do not need to track spread rewards or uptime accums here since we are not actually swapping.
if nextInitializedTickSqrtPriceBigDec.Equal(computedSqrtPrice) {
nextInitializedTickInfo, err := ParseTickFromBz(nextInitTickIter.Value())
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
liquidityNet := nextInitializedTickInfo.LiquidityNet

nextInitTickIter.Next()

liquidityNet = swapState.swapStrategy.SetLiquidityDeltaSign(liquidityNet)
swapState.liquidity.AddMut(liquidityNet)

swapState.tick = swapStrategy.UpdateTickAfterCrossing(nextInitializedTick)
} else if edgeCaseInequalityBasedOnSwapStrategy(swapStrategy.ZeroForOne(), nextInitializedTickSqrtPriceBigDec, computedSqrtPrice) {
return sdk.Coin{}, sdk.Coin{}, types.ComputedSqrtPriceInequalityError{
IsZeroForOne: swapStrategy.ZeroForOne(),
ComputedSqrtPrice: computedSqrtPrice,
NextInitializedTickSqrtPrice: nextInitializedTickSqrtPriceBigDec,
}
} else if !swapState.sqrtPrice.Equal(computedSqrtPrice) {
newTick, err := math.CalculateSqrtPriceToTick(computedSqrtPrice)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
swapState.tick = newTick
}

// Break the loop early if nothing was consumed from swapState.amountSpecifiedRemaining
if amountOut.IsZero() {
break
}
}

maxAmt := swapState.amountCalculated.Ceil().TruncateInt()
maxTokenIn = sdk.NewCoin(tokenInDenom, maxAmt)
resultingTokenOut = sdk.NewCoin(tokenOutDenom, totalTokenOut.TruncateInt())

return maxTokenIn, resultingTokenOut, nil
}
108 changes: 108 additions & 0 deletions x/concentrated-liquidity/swaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3411,3 +3411,111 @@ func (s *KeeperTestSuite) TestInfiniteSwapLoop_OutGivenIn() {
_, _, _, err = s.clk.SwapOutAmtGivenIn(s.Ctx, swapAddress, pool, tokenOut, ETH, pool.GetSpreadFactor(s.Ctx), sdk.ZeroDec())
s.Require().NoError(err)
}

func (s *KeeperTestSuite) TestComputeMaxInAmtGivenMaxTicksCrossed() {
tests := []struct {
name string
tokenInDenom string
tokenOutDenom string
maxTicksCrossed uint64
expectedError error
}{
{
name: "happy path, ETH in, max ticks equal to number of initialized ticks in swap direction",
tokenInDenom: ETH,
tokenOutDenom: USDC,
maxTicksCrossed: 3,
},
{
name: "happy path, USDC in, max ticks equal to number of initialized ticks in swap direction",
tokenInDenom: USDC,
tokenOutDenom: ETH,
maxTicksCrossed: 3,
},
{
name: "ETH in, max ticks less than number of initialized ticks in swap direction",
tokenInDenom: ETH,
tokenOutDenom: USDC,
maxTicksCrossed: 2,
},
{
name: "USDC in, max ticks less than number of initialized ticks in swap direction",
tokenInDenom: USDC,
tokenOutDenom: ETH,
maxTicksCrossed: 2,
},
{
name: "ETH in, max ticks greater than number of initialized ticks in swap direction",
tokenInDenom: ETH,
tokenOutDenom: USDC,
maxTicksCrossed: 4,
},
{
name: "USDC in, max ticks greater than number of initialized ticks in swap direction",
tokenInDenom: USDC,
tokenOutDenom: ETH,
maxTicksCrossed: 4,
},
{
name: "error: tokenInDenom not in pool",
tokenInDenom: "BTC",
tokenOutDenom: ETH,
maxTicksCrossed: 4,
expectedError: types.TokenInDenomNotInPoolError{TokenInDenom: "BTC"},
},
}

for _, test := range tests {
s.Run(test.name, func() {
s.SetupTest()
clPool := s.PrepareConcentratedPool()
expectedResultingTokenOutAmount := sdk.ZeroInt()

// Create positions and calculate expected resulting tokens
positions := []struct {
lowerTick, upperTick int64
maxTicks uint64
}{
{DefaultLowerTick, DefaultUpperTick, 0}, // Surrounding the current price
{DefaultLowerTick - 10000, DefaultLowerTick, 1}, // Below the position surrounding the current price
{DefaultLowerTick - 20000, DefaultLowerTick - 10000, 2}, // Below the position below the position surrounding the current price
{DefaultUpperTick, DefaultUpperTick + 10000, 1}, // Above the position surrounding the current price
{DefaultUpperTick + 10000, DefaultUpperTick + 20000, 2}, // Above the position above the position surrounding the current price
}

// Create positions and determine how much token out we should expect given the maxTicksCrossed provided.
for _, pos := range positions {
amt0, amt1 := s.createPositionAndFundAcc(clPool, pos.lowerTick, pos.upperTick)
expectedResultingTokenOutAmount = s.calculateExpectedTokens(test.tokenInDenom, test.maxTicksCrossed, pos.maxTicks, amt0, amt1, expectedResultingTokenOutAmount)
}

// System Under Test
_, resultingTokenOut, err := s.App.ConcentratedLiquidityKeeper.ComputeMaxInAmtGivenMaxTicksCrossed(s.Ctx, clPool.GetId(), test.tokenInDenom, test.maxTicksCrossed)

if test.expectedError != nil {
s.Require().Error(err)
s.Require().ErrorContains(err, test.expectedError.Error())
} else {
s.Require().NoError(err)

errTolerance := osmomath.ErrTolerance{AdditiveTolerance: sdk.NewDec(int64(test.maxTicksCrossed))}
s.Require().Equal(0, errTolerance.Compare(expectedResultingTokenOutAmount, resultingTokenOut.Amount), "expected: %s, got: %s", expectedResultingTokenOutAmount, resultingTokenOut.Amount)
}
})
}
}

func (s *KeeperTestSuite) createPositionAndFundAcc(clPool types.ConcentratedPoolExtension, lowerTick, upperTick int64) (amt0, amt1 sdk.Int) {
s.FundAcc(s.TestAccs[0], DefaultCoins)
_, amt0, amt1, _, _, _, _ = s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, clPool.GetId(), s.TestAccs[0], DefaultCoins, sdk.ZeroInt(), sdk.ZeroInt(), lowerTick, upperTick)
return
}

func (s *KeeperTestSuite) calculateExpectedTokens(tokenInDenom string, testMaxTicks, positionMaxTicks uint64, amt0, amt1, currentTotal sdk.Int) sdk.Int {
if tokenInDenom == ETH && testMaxTicks > positionMaxTicks {
return currentTotal.Add(amt1)
} else if tokenInDenom == USDC && testMaxTicks > positionMaxTicks {
return currentTotal.Add(amt0)
}
return currentTotal
}

0 comments on commit 7523559

Please sign in to comment.