diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6347a58aa..7bce979b7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/cl-go-client/go.mod b/tests/cl-go-client/go.mod index 3a056c79dc5..9d0244ce56b 100644 --- a/tests/cl-go-client/go.mod +++ b/tests/cl-go-client/go.mod @@ -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 @@ -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 diff --git a/x/concentrated-liquidity/client/query_proto_wrap.go b/x/concentrated-liquidity/client/query_proto_wrap.go index 82b4dea7243..c06beac5117 100644 --- a/x/concentrated-liquidity/client/query_proto_wrap.go +++ b/x/concentrated-liquidity/client/query_proto_wrap.go @@ -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 diff --git a/x/concentrated-liquidity/swaps.go b/x/concentrated-liquidity/swaps.go index 6ab291669b6..6f9a1a07c59 100644 --- a/x/concentrated-liquidity/swaps.go +++ b/x/concentrated-liquidity/swaps.go @@ -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 +} diff --git a/x/concentrated-liquidity/swaps_test.go b/x/concentrated-liquidity/swaps_test.go index 1f3bb0efa0a..ac2bf255352 100644 --- a/x/concentrated-liquidity/swaps_test.go +++ b/x/concentrated-liquidity/swaps_test.go @@ -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 +}