diff --git a/x/concentrated-liquidity/lp_test.go b/x/concentrated-liquidity/lp_test.go index a9a81a1e01a..74fdc8578a6 100644 --- a/x/concentrated-liquidity/lp_test.go +++ b/x/concentrated-liquidity/lp_test.go @@ -160,25 +160,6 @@ var ( amount0Expected: DefaultAmt0Expected.Add(roundingError), amount1Expected: DefaultAmt1Expected, }, - "use ticks that are not the canonical tick for a given price, expect them to be rounded to the proper tick": { - lowerTick: -161987600, - expectedLowerTick: -161000000, - upperTick: -160009800, - expectedUpperTick: -160000000, - currentTick: DefaultUpperTick, - - isNotFirstPositionWithSameAccount: true, - positionId: 2, - - liquidityAmount: sdk.MustNewDecFromStr("15731321859400083838.506717486806808937").MulInt64(2), - preSetChargeSpreadRewards: oneEth, - expectedSpreadRewardGrowthOutsideLower: oneEthCoins, - expectedSpreadRewardGrowthOutsideUpper: oneEthCoins, - - // Rounding up in favor of the pool. - amount0Expected: sdk.ZeroInt(), - amount1Expected: DefaultAmt1, - }, } ) @@ -447,8 +428,20 @@ func (s *KeeperTestSuite) TestWithdrawPosition() { setupConfig: baseCase, sutConfigOverwrite: &lpTest{ // Note: subtracting one due to truncations in favor of the pool when withdrawing. - amount0Expected: DefaultAmt0.Sub(sdk.OneInt()), - amount1Expected: DefaultAmt1.Sub(sdk.OneInt()), + // amount0Expected = (liquidity * (sqrtPriceB - sqrtPriceA)) / (sqrtPriceB * sqrtPriceA) + // Where: + // * liquidity = FullRangeLiquidityAmt + // * sqrtPriceB = MaxSqrtPrice + // * sqrtPriceA = DefaultCurrSqrtPrice + // Exact calculation: https://www.wolframalpha.com/input?i=70710678.118654752940000000+*+%2810000000000000000000.000000000000000000+-+70.710678118654752440%29+%2F+%2810000000000000000000.000000000000000000+*+70.710678118654752440%29 + amount0Expected: sdk.NewInt(999999), + // amount1Expected = liq * (sqrtPriceB - sqrtPriceA) + // Where: + // * liquidity = FullRangeLiquidityAmt + // * sqrtPriceB = DefaultCurrSqrtPrice + // * sqrtPriceA = MinSqrtPrice + // Exact calculation: https://www.wolframalpha.com/input?i=70710678.118654752940000000+*+%2870.710678118654752440+-+0.000001000000000000%29 + amount1Expected: sdk.NewInt(4999999929), liquidityAmount: FullRangeLiquidityAmt, underlyingLockId: 1, }, @@ -835,8 +828,11 @@ func (s *KeeperTestSuite) TestAddToPosition() { // 1998976eth (amount withdrawn with rounded down amounts) + 998977(token amount in) amount0Expected: sdk.NewInt(2997953), // tokens Provided for token1 is 9999999999 (amount withdrawn) + 5000000000 = 14999999999usdc. - // we calcualte calc amount1 by using: https://www.wolframalpha.com/input?i=70.728769315114743567+*+212041526.154556192320661969 - amount1Expected: sdk.NewInt(14997436189), + // We calculate calc amount1 by using the following equation: + // liq * (sqrtPriceB - sqrtPriceA), where liq is equal to the original joined liq + added liq, sqrtPriceB is current sqrt price, and sqrtPriceA is min sqrt price. + // Note that these numbers were calculated using `GetLiquidityFromAmounts` and `TickToSqrtPrice` and thus assume correctness of those functions. + // https://www.wolframalpha.com/input?i=212041526.154556192317664016+*+%2870.728769315114743566+-+0.000001000000000000%29 + amount1Expected: sdk.NewInt(14997435977), }, timeElapsed: defaultTimeElapsed, amount0ToAdd: amount0PerfectRatio, diff --git a/x/concentrated-liquidity/math/tick_test.go b/x/concentrated-liquidity/math/tick_test.go index 69f2a1c8e5a..21527660572 100644 --- a/x/concentrated-liquidity/math/tick_test.go +++ b/x/concentrated-liquidity/math/tick_test.go @@ -105,12 +105,12 @@ func (suite *ConcentratedMathTestSuite) TestTickToSqrtPrice() { expectedPrice: types.MaxSpotPrice, }, "Min tick and max k": { - tickIndex: -162000000, + tickIndex: types.MinTick, expectedPrice: types.MinSpotPrice, }, "error: tickIndex less than minimum": { - tickIndex: -162000000 - 1, - expectedError: types.TickIndexMinimumError{MinTick: -162000000}, + tickIndex: types.MinTick - 1, + expectedError: types.TickIndexMinimumError{MinTick: types.MinTick}, }, "error: tickIndex greater than maximum": { tickIndex: 342000000 + 1, @@ -234,25 +234,25 @@ func (suite *ConcentratedMathTestSuite) TestTicksToSqrtPrice() { expectedUpperPrice: sdk.MustNewDecFromStr("10733"), }, "Max tick and min k": { - lowerTickIndex: sdk.NewInt(-162000000), - upperTickIndex: sdk.NewInt(342000000), + lowerTickIndex: sdk.NewInt(types.MinTick), + upperTickIndex: sdk.NewInt(types.MaxTick), expectedUpperPrice: types.MaxSpotPrice, expectedLowerPrice: types.MinSpotPrice, }, "error: lowerTickIndex less than minimum": { - lowerTickIndex: sdk.NewInt(-162000000 - 1), + lowerTickIndex: sdk.NewInt(types.MinTick - 1), upperTickIndex: sdk.NewInt(36073300), - expectedError: types.TickIndexMinimumError{MinTick: -162000000}, + expectedError: types.TickIndexMinimumError{MinTick: types.MinTick}, }, "error: upperTickIndex greater than maximum": { - lowerTickIndex: sdk.NewInt(-162000000), - upperTickIndex: sdk.NewInt(342000000 + 1), - expectedError: types.TickIndexMaximumError{MaxTick: 342000000}, + lowerTickIndex: sdk.NewInt(types.MinTick), + upperTickIndex: sdk.NewInt(types.MaxTick + 1), + expectedError: types.TickIndexMaximumError{MaxTick: types.MaxTick}, }, "error: provided lower tick and upper tick are same": { - lowerTickIndex: sdk.NewInt(-162000000), - upperTickIndex: sdk.NewInt(-162000000), - expectedError: types.InvalidLowerUpperTickError{LowerTick: sdk.NewInt(-162000000).Int64(), UpperTick: sdk.NewInt(-162000000).Int64()}, + lowerTickIndex: sdk.NewInt(types.MinTick), + upperTickIndex: sdk.NewInt(types.MinTick), + expectedError: types.InvalidLowerUpperTickError{LowerTick: sdk.NewInt(types.MinTick).Int64(), UpperTick: sdk.NewInt(types.MinTick).Int64()}, }, } @@ -425,9 +425,11 @@ func (suite *ConcentratedMathTestSuite) TestPriceToTickRoundDown() { tickExpected: types.MinTick, }, "tick spacing 100, Spot price one tick above min, one tick above min -> MinTick": { - price: types.MinSpotPrice.Add(sdk.SmallestDec()), - tickSpacing: defaultTickSpacing, - tickExpected: closestTickAboveMinPriceDefaultTickSpacing.Int64(), + price: types.MinSpotPrice.Add(sdk.SmallestDec()), + tickSpacing: defaultTickSpacing, + // Since the tick should always be the closest tick below (and `smallestDec` isn't sufficient + // to push us into the next tick), we expect MinTick to be returned here. + tickExpected: types.MinTick, }, "tick spacing 100, Spot price one tick below max, one tick below max -> MaxTick - 1": { price: closestPriceBelowMaxPriceDefaultTickSpacing, @@ -505,49 +507,41 @@ func (suite *ConcentratedMathTestSuite) TestTickToSqrtPricePriceToTick_InverseRe }, "min spot price": { price: types.MinSpotPrice, - tickExpected: -162000000, + tickExpected: types.MinTick, }, - "smallest + min price increment": { - price: sdk.MustNewDecFromStr("0.000000000000000002"), - tickExpected: -161000000, + "smallest + min price + tick": { + price: sdk.MustNewDecFromStr("0.000000000001000001"), + tickExpected: types.MinTick + 1, }, "min price increment 10^1": { - price: sdk.MustNewDecFromStr("0.000000000000000009"), - tickExpected: -154000000, - }, - "smallest + min price increment 10^1": { - price: sdk.MustNewDecFromStr("0.000000000000000010"), - tickExpected: -153000000, + price: sdk.MustNewDecFromStr("0.000000000010000000"), + tickExpected: types.MinTick + (9 * 1e6), }, - "smallest + min price increment * 10^2": { - price: sdk.MustNewDecFromStr("0.000000000000000100"), - tickExpected: -144000000, + "min price increment 10^2": { + price: sdk.MustNewDecFromStr("0.000000000100000000"), + tickExpected: types.MinTick + (2 * 9 * 1e6), }, - "smallest + min price increment * 10^3": { - price: sdk.MustNewDecFromStr("0.000000000000001000"), - tickExpected: -135000000, + "min price increment 10^3": { + price: sdk.MustNewDecFromStr("0.000000001000000000"), + tickExpected: types.MinTick + (3 * 9 * 1e6), }, - "smallest + min price increment * 10^4": { - price: sdk.MustNewDecFromStr("0.000000000000010000"), - tickExpected: -126000000, + "min price increment 10^4": { + price: sdk.MustNewDecFromStr("0.000000010000000000"), + tickExpected: types.MinTick + (4 * 9 * 1e6), }, - "smallest + min price * increment 10^5": { - price: sdk.MustNewDecFromStr("0.000000000000100000"), - tickExpected: -117000000, + "min price increment 10^5": { + price: sdk.MustNewDecFromStr("0.000000100000000000"), + tickExpected: types.MinTick + (5 * 9 * 1e6), }, - "smallest + min price * increment 10^6": { - price: sdk.MustNewDecFromStr("0.000000000001000000"), - tickExpected: -108000000, + "min price increment 10^6": { + price: sdk.MustNewDecFromStr("0.000001000000000000"), + tickExpected: types.MinTick + (6 * 9 * 1e6), }, - "smallest + min price * increment 10^6 + tick": { - price: sdk.MustNewDecFromStr("0.000000000001000001"), - tickExpected: -107999999, - }, - "smallest + min price * increment 10^17": { + "min price * increment 10^11": { price: sdk.MustNewDecFromStr("0.100000000000000000"), tickExpected: -9000000, }, - "smallest + min price * increment 10^18": { + "min price * increment 10^12": { price: sdk.MustNewDecFromStr("1.000000000000000000"), tickExpected: 0, }, @@ -755,6 +749,10 @@ func (s *ConcentratedMathTestSuite) TestSqrtPriceToTickRoundDownSpacing() { s.Require().NoError(err) _, sqpMaxTickSubOne, err := math.TickToSqrtPrice(types.MaxTick - 1) s.Require().NoError(err) + _, sqpMinTickPlusOne, err := math.TickToSqrtPrice(types.MinTick + 1) + s.Require().NoError(err) + _, sqpMinTickPlusTwo, err := math.TickToSqrtPrice(types.MinTick + 2) + s.Require().NoError(err) testCases := map[string]struct { sqrtPrice sdk.Dec @@ -836,6 +834,40 @@ func (s *ConcentratedMathTestSuite) TestSqrtPriceToTickRoundDownSpacing() { tickSpacing: defaultTickSpacing, tickExpected: types.MaxTick - defaultTickSpacing, }, + "sqrt price corresponds exactly to min tick + 1 (tick spacing 1)": { + sqrtPrice: sqpMinTickPlusOne, + tickSpacing: 1, + tickExpected: types.MinTick + 1, + }, + "sqrt price corresponds exactly to min tick + 1 minus 1 ULP (tick spacing 1)": { + // Calculated using TickToSqrtPrice(types.MinTick + 1) - 1 ULP + sqrtPrice: sqpMinTickPlusOne.Sub(sdk.SmallestDec()), + tickSpacing: 1, + tickExpected: types.MinTick, + }, + "sqrt price corresponds exactly to min tick + 1 plus 1 ULP (tick spacing 1)": { + // Calculated using TickToSqrtPrice(types.MinTick + 1) + 1 ULP + sqrtPrice: sqpMinTickPlusOne.Add(sdk.SmallestDec()), + tickSpacing: 1, + tickExpected: types.MinTick + 1, + }, + "sqrt price corresponds exactly to min tick + 2 (tick spacing 1)": { + sqrtPrice: sqpMinTickPlusTwo, + tickSpacing: 1, + tickExpected: types.MinTick + 2, + }, + "sqrt price corresponds exactly to min tick + 2 plus 1 ULP (tick spacing 1)": { + // Calculated using TickToSqrtPrice(types.MinTick + 2) + 1 ULP + sqrtPrice: sqpMinTickPlusTwo.Add(sdk.SmallestDec()), + tickSpacing: 1, + tickExpected: types.MinTick + 2, + }, + "sqrt price corresponds exactly to min tick + 2 minus 1 ULP (tick spacing 1)": { + // Calculated using TickToSqrtPrice(types.MinTick + 2) - 1 ULP + sqrtPrice: sqpMinTickPlusTwo.Sub(sdk.SmallestDec()), + tickSpacing: 1, + tickExpected: types.MinTick + 1, + }, } for name, tc := range testCases { s.Run(name, func() { diff --git a/x/concentrated-liquidity/python/swap_test.py b/x/concentrated-liquidity/python/swap_test.py index ccbbadafaa4..8a2b81aa1d2 100644 --- a/x/concentrated-liquidity/python/swap_test.py +++ b/x/concentrated-liquidity/python/swap_test.py @@ -3,7 +3,7 @@ from math import * # Precomputed sqrt values using osmomath.MonotonicSqrt -minSqrtPrice = Decimal("0.000000001000000000") +minSqrtPrice = Decimal("0.000001000000000000") sqrt4000 = Decimal("63.245553203367586640") sqrt4545 = Decimal("67.416615162732695594") sqrt4994 = Decimal("70.668238976219012614") diff --git a/x/concentrated-liquidity/tick_test.go b/x/concentrated-liquidity/tick_test.go index 18f3e326cbf..319162c7323 100644 --- a/x/concentrated-liquidity/tick_test.go +++ b/x/concentrated-liquidity/tick_test.go @@ -1405,89 +1405,3 @@ func (s *KeeperTestSuite) TestValidateTickRangeIsValid() { }) } } - -func (s *KeeperTestSuite) TestRoundTickToCanonicalPriceTick() { - tests := []struct { - name string - lowerTick int64 - upperTick int64 - expectedNewLowerTick int64 - expectedNewUpperTick int64 - expectedError error - }{ - { - name: "exact upper tick for 0.000000000000000003 to exact lower tick for 0.000000000000000002", - lowerTick: -161000000, - expectedNewLowerTick: -161000000, - upperTick: -160000000, - expectedNewUpperTick: -160000000, - }, - { - name: "exact upper tick for 0.000000000000000003 to inexact lower tick for 0.000000000000000002", - lowerTick: -161001234, - expectedNewLowerTick: -161000000, - upperTick: -160000000, - expectedNewUpperTick: -160000000, - }, - { - name: "inexact upper tick for 0.000000000000000003 to exact lower tick for 0.000000000000000002", - lowerTick: -161000000, - expectedNewLowerTick: -161000000, - upperTick: -160001234, - expectedNewUpperTick: -160000000, - }, - { - name: "inexact upper tick for 0.000000000000000003 to inexact lower tick for 0.000000000000000002", - lowerTick: -161001234, - expectedNewLowerTick: -161000000, - upperTick: -160001234, - expectedNewUpperTick: -160000000, - }, - { - name: "upper tick one tick away from lower tick", - lowerTick: -161001234, - expectedNewLowerTick: -161000000, - upperTick: -160999999, - expectedNewUpperTick: -160000000, - }, - { - name: "error: new upper tick is lower than new lower tick", - lowerTick: -160001234, - expectedNewLowerTick: -160000000, - upperTick: -161001234, - expectedNewUpperTick: -161000000, - expectedError: types.InvalidLowerUpperTickError{LowerTick: -160000000, UpperTick: -161000000}, - }, - { - name: "error: new upper tick is the same as new lower tick", - lowerTick: -160001234, - expectedNewLowerTick: -160000000, - upperTick: -160000000, - expectedNewUpperTick: -160000000, - expectedError: types.InvalidLowerUpperTickError{LowerTick: -160000000, UpperTick: -160000000}, - }, - } - - for _, test := range tests { - s.Run(test.name, func() { - s.SetupTest() - - _, sqrtPriceTickLower, err := math.TickToSqrtPrice(test.lowerTick) - s.Require().NoError(err) - _, sqrtPriceTickUpper, err := math.TickToSqrtPrice(test.upperTick) - s.Require().NoError(err) - - // System Under Test - newLowerTick, newUpperTick, err := cl.RoundTickToCanonicalPriceTick(test.lowerTick, test.upperTick, sqrtPriceTickLower, sqrtPriceTickUpper, DefaultTickSpacing) - - if test.expectedError != nil { - s.Require().Error(err) - s.Require().ErrorContains(err, test.expectedError.Error()) - } else { - s.Require().NoError(err) - s.Require().Equal(test.expectedNewLowerTick, newLowerTick) - s.Require().Equal(test.expectedNewUpperTick, newUpperTick) - } - }) - } -} diff --git a/x/concentrated-liquidity/types/constants.go b/x/concentrated-liquidity/types/constants.go index 84e4ec06a88..bbdb40afa16 100644 --- a/x/concentrated-liquidity/types/constants.go +++ b/x/concentrated-liquidity/types/constants.go @@ -10,7 +10,7 @@ import ( const ( // Precomputed values for min and max tick - MinTick, MaxTick int64 = -162000000, 342000000 + MinTick, MaxTick int64 = -108000000, 342000000 ExponentAtPriceOne int64 = -6 ConcentratedGasFeeForSwap = 10_000 BaseGasFeeForNewIncentive = 10_000 @@ -19,7 +19,7 @@ const ( var ( MaxSpotPrice = sdk.MustNewDecFromStr("100000000000000000000000000000000000000") - MinSpotPrice = sdk.MustNewDecFromStr("0.000000000000000001") // 10^-18 + MinSpotPrice = sdk.MustNewDecFromStr("0.000000000001") // 10^-12 MaxSqrtPrice = osmomath.MustMonotonicSqrt(MaxSpotPrice) MinSqrtPrice = osmomath.MustMonotonicSqrt(MinSpotPrice) // Supported uptimes preset to 1 ns, 1 min, 1 hr, 1D, 1W, 2W diff --git a/x/gamm/keeper/migrate_test.go b/x/gamm/keeper/migrate_test.go index 34db1d3c7d7..9b11d491dbb 100644 --- a/x/gamm/keeper/migrate_test.go +++ b/x/gamm/keeper/migrate_test.go @@ -17,8 +17,27 @@ func (s *KeeperTestSuite) TestMigrate() { defaultGammShares := sdk.NewCoin("gamm/pool/1", sdk.MustNewDecFromStr("100000000000000000000").RoundInt()) invalidGammShares := sdk.NewCoin("gamm/pool/1", sdk.MustNewDecFromStr("190000000000000000001").RoundInt()) defaultAccountFunds := sdk.NewCoins(sdk.NewCoin("eth", sdk.NewInt(200000000000)), sdk.NewCoin("usdc", sdk.NewInt(200000000000))) + + // Explanation of additive tolerance of 100000: + // + // The balance in the CL pool should be equal to the portion of the user's previous GAMM balances that could be + // joined into a full range CL position. These are not exactly equivalent because GAMM pools covers prices (0, inf) + // while CL pools cover prices (minSpotPrice, maxSpotPrice), where minSpotPrice and maxSpotPrice are close to the GAMM + // boundaries but not exactly on them. + // + // # Base equations for full range asset amounts: + // Expected amount of asset 0: (liquidity * (maxSqrtPrice - curSqrtPrice)) / (maxSqrtPrice * curSqrtPrice) + // Expected amount of asset 1: liquidity * (curSqrtPrice - minSqrtPrice) + // + // # Using scripts in x/concentrated-liquidity/python/swap_test.py, we compute the following: + // expectedAsset0 = floor((liquidity * (maxSqrtPrice - curSqrtPrice)) / (maxSqrtPrice * curSqrtPrice)) = 99999999999.000000000000000000 + // expectedAsset1 = floor(liquidity * (curSqrtPrice - minSqrtPrice)) = 99999900000.000000000000000000 + // + // We add 1 to account for ExitPool rounding exit amount up. This is not an issue since the balance is deducted from the user regardless. + // These leaves us with full transfer of asset 0 and a (correct) transfer of asset 1 amounting to full GAMM balance minus 100000. + // We expect this tolerance to be sufficient as long as our test cases are on the same order of magnitude. defaultErrorTolerance := osmomath.ErrTolerance{ - AdditiveTolerance: sdk.NewDec(100), + AdditiveTolerance: sdk.NewDec(100000), RoundingDir: osmomath.RoundDown, } defaultJoinTime := s.Ctx.BlockTime() diff --git a/x/protorev/keeper/epoch_hook_test.go b/x/protorev/keeper/epoch_hook_test.go index ee3d85691de..60e3d3a834f 100644 --- a/x/protorev/keeper/epoch_hook_test.go +++ b/x/protorev/keeper/epoch_hook_test.go @@ -162,7 +162,7 @@ func (s *KeeperTestSuite) TestUpdateHighestLiquidityPools() { }, expectedBaseDenomPools: map[string]map[string]keeper.LiquidityPoolStruct{ "epochTwo": { - "uosmo": {Liquidity: sdk.Int(sdk.NewUintFromString("999999999000000001000000000000000000")), PoolId: 49}, + "uosmo": {Liquidity: sdk.Int(sdk.NewUintFromString("999999000000000001000000000000000000")), PoolId: 49}, }, }, }, diff --git a/x/superfluid/keeper/concentrated_liquidity_test.go b/x/superfluid/keeper/concentrated_liquidity_test.go index 7351de18bde..de420c7e344 100644 --- a/x/superfluid/keeper/concentrated_liquidity_test.go +++ b/x/superfluid/keeper/concentrated_liquidity_test.go @@ -155,9 +155,16 @@ func (s *KeeperTestSuite) TestAddToConcentratedLiquiditySuperfluidPosition() { } s.Require().NoError(err) - // Define error tolerance + // We allow for an downward additive tolerance of 101 to accommodate our single happy path case while efficiently checking exact balance diffs. + // + // Using our full range asset amount equations, we get the following: + // + // expectedAsset0 = floor((liquidityDelta * (maxSqrtPrice - curSqrtPrice)) / (maxSqrtPrice * curSqrtPrice)) = 99999998.000000000000000000 + // expectedAsset1 = floor(liquidityDelta * (curSqrtPrice - minSqrtPrice)) = 99999899.000000000000000000 + // + // Note that the expected difference valid additive difference of 101 on asset 1. var errTolerance osmomath.ErrTolerance - errTolerance.AdditiveTolerance = sdk.NewDec(1) + errTolerance.AdditiveTolerance = sdk.NewDec(101) errTolerance.RoundingDir = osmomath.RoundDown postAddToPositionStakeSupply := bankKeeper.GetSupply(ctx, bondDenom)