diff --git a/CHANGELOG.md b/CHANGELOG.md index 539562bd726..4ea2c07fa00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#5532](https://github.com/osmosis-labs/osmosis/pull/5532) fix: Fix x/tokenfactory genesis import denoms reset x/bank existing denom metadata * [#5869](https://github.com/osmosis-labs/osmosis/pull/5869) fix negative interval accumulation with spread rewards +* [#5872](https://github.com/osmosis-labs/osmosis/pull/5872) fix negative interval accumulation with incentive rewards * [#5883](https://github.com/osmosis-labs/osmosis/pull/5883) feat: Uninitialize empty ticks * [#5874](https://github.com/osmosis-labs/osmosis/pull/5874) Remove Partial Migration from superfluid migration to CL * [#5901](https://github.com/osmosis-labs/osmosis/pull/5901) Adding support for CW pools in ProtoRev diff --git a/go.mod b/go.mod index 9a593c13e35..047631e713a 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/ory/dockertest/v3 v3.10.0 github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3 github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230629191111-f375469de8b6 - github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230728163612-426afac90c44 + github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230801224523-e85e9a9cf445 github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304 github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230602130523-f9a94d8bbd10 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 62267978e7d..95bf93a83f5 100644 --- a/go.sum +++ b/go.sum @@ -952,6 +952,8 @@ github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230629191111-f375469de8b github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230629191111-f375469de8b6/go.mod h1:JTym95/bqrSnG5MPcXr1YDhv43JdCeo3p+iDbazoX68= github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230728163612-426afac90c44 h1:UOaBVxEMMv2FS1znU7kHBdtSeZQIjnmXL4r9r19XyBo= github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230728163612-426afac90c44/go.mod h1:Pl8Nzx6O6ow/+aqfMoMSz4hX+zz6RrnDYsooptECGxM= +github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230801224523-e85e9a9cf445 h1:V942btb00oVXHox7hEN8FrPfEaMTiVuInM7Tr00eOMU= +github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230801224523-e85e9a9cf445/go.mod h1:Pl8Nzx6O6ow/+aqfMoMSz4hX+zz6RrnDYsooptECGxM= github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304 h1:RIrWLzIiZN5Xd2JOfSOtGZaf6V3qEQYg6EaDTAkMnCo= github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304/go.mod h1:yPWoJTj5RKrXKUChAicp+G/4Ni/uVEpp27mi/FF/L9c= github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230602130523-f9a94d8bbd10 h1:XrES5AHZMZ/Y78boW35PTignkhN9h8VvJ1sP8EJDIu8= diff --git a/osmoutils/coin_helper.go b/osmoutils/coin_helper.go index 4b4ae78dc53..333c4623f6e 100644 --- a/osmoutils/coin_helper.go +++ b/osmoutils/coin_helper.go @@ -45,6 +45,23 @@ func SubDecCoinArrays(decCoinsArrayA []sdk.DecCoins, decCoinsArrayB []sdk.DecCoi return finalDecCoinArray, nil } +// SafeSubDecCoinArrays subtracts the contents of the second param from the first (decCoinsArrayA - decCoinsArrayB) +// Note that this takes in two _arrays_ of DecCoins, meaning that each term itself is of type DecCoins (i.e. an array of DecCoin). +// Contrary to SubDecCoinArrays, this subtractions allows for negative result values. +func SafeSubDecCoinArrays(decCoinsArrayA []sdk.DecCoins, decCoinsArrayB []sdk.DecCoins) ([]sdk.DecCoins, error) { + if len(decCoinsArrayA) != len(decCoinsArrayB) { + return []sdk.DecCoins{}, fmt.Errorf("DecCoin arrays must be of equal length to be subtracted") + } + + finalDecCoinArray := []sdk.DecCoins{} + for i := range decCoinsArrayA { + subResult, _ := decCoinsArrayA[i].SafeSub(decCoinsArrayB[i]) + finalDecCoinArray = append(finalDecCoinArray, subResult) + } + + return finalDecCoinArray, nil +} + // AddDecCoinArrays adds the contents of the second param from the first (decCoinsArrayA + decCoinsArrayB) // Note that this takes in two _arrays_ of DecCoins, meaning that each term itself is of type DecCoins (i.e. an array of DecCoin). func AddDecCoinArrays(decCoinsArrayA []sdk.DecCoins, decCoinsArrayB []sdk.DecCoins) ([]sdk.DecCoins, error) { diff --git a/osmoutils/coin_helper_test.go b/osmoutils/coin_helper_test.go index 1ea887671ca..ddcc8cc4b6a 100644 --- a/osmoutils/coin_helper_test.go +++ b/osmoutils/coin_helper_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/osmosis-labs/osmosis/osmoutils" + "github.com/osmosis-labs/osmosis/osmoutils/osmoassert" ) var ( @@ -32,6 +33,8 @@ func TestSubDecCoins(t *testing.T) { expectedOutput []sdk.DecCoins expectError bool + // whether unsafe subtraction should panic + expectPanicUnsafe bool }{ "[[100foo, 100bar], [100foo, 100bar]] - [[50foo, 50bar], [50foo, 100bar]]": { firstInput: []sdk.DecCoins{hundredEach, hundredEach}, @@ -69,19 +72,42 @@ func TestSubDecCoins(t *testing.T) { expectedOutput: []sdk.DecCoins{}, expectError: true, }, + + "negative result": { + firstInput: []sdk.DecCoins{fiftyEach}, + secondInput: []sdk.DecCoins{hundredEach}, + + expectedOutput: []sdk.DecCoins{{sdk.DecCoin{Denom: "bar", Amount: sdk.NewDec(-50)}, sdk.DecCoin{Denom: "foo", Amount: sdk.NewDec(-50)}}}, + expectPanicUnsafe: true, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - actualOutput, err := osmoutils.SubDecCoinArrays(tc.firstInput, tc.secondInput) + + var ( + actualOutput []sdk.DecCoins + err1 error + ) + osmoassert.ConditionalPanic(t, tc.expectPanicUnsafe, func() { + actualOutput, err1 = osmoutils.SubDecCoinArrays(tc.firstInput, tc.secondInput) + }) + + actualOutputSafe, err2 := osmoutils.SafeSubDecCoinArrays(tc.firstInput, tc.secondInput) if tc.expectError { - require.Error(t, err) + require.Error(t, err1) + require.Error(t, err2) require.Equal(t, tc.expectedOutput, actualOutput) + require.Equal(t, tc.expectedOutput, actualOutputSafe) return } - require.NoError(t, err) - require.Equal(t, tc.expectedOutput, actualOutput) + require.NoError(t, err1) + require.NoError(t, err2) + if !tc.expectPanicUnsafe { + require.Equal(t, tc.expectedOutput, actualOutput) + } + require.Equal(t, tc.expectedOutput, actualOutputSafe) }) } } diff --git a/x/concentrated-liquidity/README.md b/x/concentrated-liquidity/README.md index 7a3ce55bdd2..f039be91a4b 100644 --- a/x/concentrated-liquidity/README.md +++ b/x/concentrated-liquidity/README.md @@ -1355,6 +1355,9 @@ This returns the amount of spread rewards collected by the user. ## Interval Accumulation +Section pre-face: interval accumulation for incentives functions +similarly to the spread rewards. However, we focus on spread rewards only for brevity. + As mentioned in the previous sections, to collect spread rewards, we utilize a rewards accumulator abstraction with an interval accumulation extension. @@ -1405,6 +1408,8 @@ For this reason, there are 3 ways to compute the rewards accrued inside the posi ### Negative Interval Accumulation Edge Case Behavior +Case 1: Initialize lower tick snapshot to be greater than upper tick snapshot when current tick > upper tick + Note, that if we initialize the lower tick after the upper tick is already initialized, for example, by another position, this might lead to negative accumulation inside the interval. This is only possible if the current tick is greater than the lower tick @@ -1425,6 +1430,15 @@ The reason is that if the current tick is less than the tick we initialize, the As a result, the subtraction from the global accumulator for computing interval accumulation never leads to a negative value. +Case 2: Initialize lower tick snapshot to be zero while upper tick snapshot to be non-zero when current tick < lower tick + +Assume that initially current tick > upper tick and the upper tick gets initialized by some position. +Then, its accumulator snapshot is set to the global accumulator. Now, assume that the current tick +moves under the future position's lower tick. Then, the position gets initialized. + +As a result, the lower tick is set to 0, and interval accumulation is +`lower tick snapshot - upper tick snapshot = 0 - positive value = negative value` + ## Swaps Swapping within a single tick works as the regular `xy = k` curve. For swaps diff --git a/x/concentrated-liquidity/incentives.go b/x/concentrated-liquidity/incentives.go index 395cc57557a..4fb66652250 100644 --- a/x/concentrated-liquidity/incentives.go +++ b/x/concentrated-liquidity/incentives.go @@ -671,18 +671,28 @@ func (k Keeper) GetUptimeGrowthInsideRange(ctx sdk.Context, poolId uint64, lower upperTickUptimeValues := getUptimeTrackerValues(upperTickInfo.UptimeTrackers.List) // If current tick is below range, we subtract uptime growth of upper tick from that of lower tick if currentTick < lowerTick { - return osmoutils.SubDecCoinArrays(lowerTickUptimeValues, upperTickUptimeValues) + // Note: SafeSub with negative accumulation is possible if upper tick is initialized first + // while current tick > upper tick. Then, the current tick under the lower tick. The lower + // tick then gets initialized to zero. + // Therefore, we allow for negative result. + return osmoutils.SafeSubDecCoinArrays(lowerTickUptimeValues, upperTickUptimeValues) } else if currentTick < upperTick { // If current tick is within range, we subtract uptime growth of lower and upper tick from global growth + // Note: each individual tick snapshot never be greater than the global uptime accumulator. + // Therefore, we do not allow for negative result. globalMinusUpper, err := osmoutils.SubDecCoinArrays(globalUptimeValues, upperTickUptimeValues) if err != nil { return []sdk.DecCoins{}, err } - return osmoutils.SubDecCoinArrays(globalMinusUpper, lowerTickUptimeValues) + // Note: SafeSub with negative accumulation is possible if lower tick is initialized after upper tick + // and the current tick is between the two. + return osmoutils.SafeSubDecCoinArrays(globalMinusUpper, lowerTickUptimeValues) } else { // If current tick is above range, we subtract uptime growth of lower tick from that of upper tick - return osmoutils.SubDecCoinArrays(upperTickUptimeValues, lowerTickUptimeValues) + // Note: SafeSub with negative accumulation is possible if lower tick is initialized after upper tick + // and the current tick is above the two. + return osmoutils.SafeSubDecCoinArrays(upperTickUptimeValues, lowerTickUptimeValues) } } diff --git a/x/concentrated-liquidity/invariant_test.go b/x/concentrated-liquidity/invariant_test.go index 5e0ce34e4a3..f317fea021f 100644 --- a/x/concentrated-liquidity/invariant_test.go +++ b/x/concentrated-liquidity/invariant_test.go @@ -9,13 +9,10 @@ import ( ) type ExpectedGlobalRewardValues struct { - // By default, the global reward checks just ensure that rounding is done - // in the pools favor. - // The tolerance here ensures that it rounded in the pools favor by at - // _at most_ ExpectedAdditiveTolerance units. - ExpectedAdditiveTolerance sdk.Dec - TotalSpreadRewards sdk.Coins - TotalIncentives sdk.Coins + ExpectedAdditiveSpreadRewardTolerance sdk.Dec + ExpectedAdditiveIncentivesTolerance sdk.Dec + TotalSpreadRewards sdk.Coins + TotalIncentives sdk.Coins } // assertGlobalInvariants asserts all available global invariants (i.e. invariants that should hold on all valid states). @@ -107,23 +104,33 @@ func (s *KeeperTestSuite) assertTotalRewardsInvariant(expectedGlobalRewardValues totalCollectedIncentives = totalCollectedIncentives.Add(collectedIncentives...) } - additiveTolerance := sdk.Dec{} - if !expectedGlobalRewardValues.ExpectedAdditiveTolerance.IsNil() { - additiveTolerance = expectedGlobalRewardValues.ExpectedAdditiveTolerance + spreadRewardAdditiveTolerance := sdk.Dec{} + if !expectedGlobalRewardValues.ExpectedAdditiveSpreadRewardTolerance.IsNil() { + spreadRewardAdditiveTolerance = expectedGlobalRewardValues.ExpectedAdditiveSpreadRewardTolerance } - // For global invariant checks, we simply ensure that any rounding error was in the pool's favor. + incentivesAdditiveTolerance := sdk.Dec{} + if !expectedGlobalRewardValues.ExpectedAdditiveIncentivesTolerance.IsNil() { + incentivesAdditiveTolerance = expectedGlobalRewardValues.ExpectedAdditiveSpreadRewardTolerance + } + + // We ensure that any rounding error was in the pool's favor by rounding down. // This is to allow for cases where we slightly overround, which would otherwise fail here. - // TODO: create ErrTolerance type that allows for additive OR multiplicative tolerance to allow for + // TODO: multiplicative tolerance to allow for // tightening this check further. - errTolerance := osmomath.ErrTolerance{ - AdditiveTolerance: additiveTolerance, + spreadRewardErrTolerance := osmomath.ErrTolerance{ + AdditiveTolerance: spreadRewardAdditiveTolerance, + RoundingDir: osmomath.RoundDown, + } + + incentivesErrTolerance := osmomath.ErrTolerance{ + AdditiveTolerance: incentivesAdditiveTolerance, RoundingDir: osmomath.RoundDown, } // Assert total collected spread rewards and incentives equal to expected - s.Require().True(errTolerance.EqualCoins(expectedTotalSpreadRewards, totalCollectedSpread), "expected spread rewards vs. collected: %s vs. %s", expectedTotalSpreadRewards, totalCollectedSpread) - s.Require().True(errTolerance.EqualCoins(expectedTotalIncentives, totalCollectedIncentives), "expected incentives vs. collected: %s vs. %s", expectedTotalIncentives, totalCollectedIncentives) + s.Require().True(spreadRewardErrTolerance.EqualCoins(expectedTotalSpreadRewards, totalCollectedSpread), "expected spread rewards vs. collected: %s vs. %s", expectedTotalSpreadRewards, totalCollectedSpread) + s.Require().True(incentivesErrTolerance.EqualCoins(expectedTotalIncentives, totalCollectedIncentives), "expected incentives vs. collected: %s vs. %s", expectedTotalIncentives, totalCollectedIncentives) // Refetch total pool balances across all pools remainingPositions, finalTotalPoolLiquidity, remainingTotalSpreadRewards, remainingTotalIncentives := s.getAllPositionsAndPoolBalances(cachedCtx) diff --git a/x/concentrated-liquidity/position_test.go b/x/concentrated-liquidity/position_test.go index 70a86035f53..81df02c09d2 100644 --- a/x/concentrated-liquidity/position_test.go +++ b/x/concentrated-liquidity/position_test.go @@ -2607,7 +2607,8 @@ func (s *KeeperTestSuite) TestMultipleRanges() { } } -// This test validates the edge case where the range accumulator becomes negative. +// This test validates the edge case where the range accumulators become negative. +// It validates both spread and incentive rewards. // It happens if we initialize a lower tick after the upper AND the current tick is above the position's range. // To replicate, we create 3 positions full range, A and B. // Full range position is created to inject some base liquidity. @@ -2618,6 +2619,12 @@ func (s *KeeperTestSuite) TestMultipleRanges() { // Note, that we ensure that the current tick is above the position's range so that when we compute // the in-range accumulator, it becomes negative (computed as upper tick acc - lower tick acc when current tick > upper tick of a position). // +// Note that there is another edge case possible where we initialize an upper tick when current tick > upper tick to a positive value. +// Then, the current tick moves under the lower tick of a future position. As a result, when the position gets initialized, +// the lower tick gets the accumulator value of zero if it is new, resulting in interval accumulation of: +// lower tick accumulator snapshot - upper tick accumulator snapshot = 0 - positive value = negative value. +// This case is covered here implicitly. +// // Finally, there are 4 sub-tests run to ensure that the total rewards are collected correctly: // - Current tick is not moved. // - Current tick is moved under position B's range @@ -2628,13 +2635,17 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { var ( // Initialize pool with non-zero spread factor. - spreadFactor = sdk.NewDecWithPrec(3, 3) - pool = s.PrepareCustomConcentratedPool(s.TestAccs[0], DefaultCoin0.Denom, DefaultCoin1.Denom, 1, spreadFactor) - poolId = pool.GetId() - denom0 = pool.GetToken0() - denom1 = pool.GetToken1() + spreadFactor = sdk.NewDecWithPrec(3, 3) + pool = s.PrepareCustomConcentratedPool(s.TestAccs[0], DefaultCoin0.Denom, DefaultCoin1.Denom, 1, spreadFactor) + poolId = pool.GetId() + denom0 = pool.GetToken0() + denom1 = pool.GetToken1() + rewardsPerSecond = sdk.NewDec(1000) ) + _, err := s.clk.CreateIncentive(s.Ctx, poolId, s.TestAccs[0], sdk.NewCoin("uosmo", sdk.NewInt(1_000_000)), rewardsPerSecond, s.Ctx.BlockTime(), time.Nanosecond) + s.Require().NoError(err) + // Estimates how much to swap in to approximately reach the given tick // in the zero for one direction (left). Assumes current sqrt price // from the refeteched pool as well as its liquidity. Assumes that @@ -2671,18 +2682,24 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { s.FundAcc(s.TestAccs[0], DefaultCoins) s.CreateFullRangePosition(pool, DefaultCoins) - expectedTotalRewards := sdk.NewCoins() + expectedTotalSpreadRewards := sdk.NewCoins() + expectedTotalIncentiveRewards := sdk.ZeroDec() // Initialize position at a higher range s.FundAcc(s.TestAccs[0], DefaultCoins) - _, _, _, _, _, _, err := s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, poolId, s.TestAccs[0], DefaultCoins, sdk.ZeroInt(), sdk.ZeroInt(), DefaultCurrTick+50, DefaultCurrTick+100) + _, _, _, _, _, _, err = s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, poolId, s.TestAccs[0], DefaultCoins, sdk.ZeroInt(), sdk.ZeroInt(), DefaultCurrTick+50, DefaultCurrTick+100) s.Require().NoError(err) // Estimate how much to swap in to approximately DefaultCurrTick - 50 coinZeroIn := estimateCoinZeroIn(DefaultCurrTick - 50) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) s.swapZeroForOneLeftWithSpread(poolId, coinZeroIn, spreadFactor) @@ -2694,41 +2711,59 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { coinOneIn := estimateCoinOneIn(DefaultCurrTick + 150) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom1, coinOneIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom1, coinOneIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) s.swapOneForZeroRightWithSpread(poolId, coinOneIn, spreadFactor) + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) + // This previously paniced due to the lack of support for negative range accumulators. // See issue: https://github.com/osmosis-labs/osmosis/issues/5854 // We initialized the lower tick's accumulator (DefaultCurrTick - 25) to be greater than the upper tick's accumulator (DefaultCurrTick + 50) // Whenever the current tick is above the position's range, we compute in range accumulator as upper tick accumulator - lower tick accumulator // In this case, it ends up being negative, which is now supported. - _, _, _, _, _, _, err = s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, poolId, s.TestAccs[0], DefaultCoins, sdk.ZeroInt(), sdk.ZeroInt(), DefaultCurrTick-25, DefaultCurrTick+50) + negativeIntervalAccumPositionId, _, _, _, _, _, err := s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, poolId, s.TestAccs[0], DefaultCoins, sdk.ZeroInt(), sdk.ZeroInt(), DefaultCurrTick-25, DefaultCurrTick+50) s.Require().NoError(err) - // Refetch pool - pool, err = s.clk.GetPoolById(s.Ctx, poolId) - s.Require().NoError(err) + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) s.T().Run("assert rewards when current tick is not moved (stays above position with negative in-range accumulator)", func(t *testing.T) { // Assert global invariants s.assertGlobalInvariants(ExpectedGlobalRewardValues{ // Additive tolerance of 1 for each position. - ExpectedAdditiveTolerance: sdk.OneDec().MulInt64(3), - TotalSpreadRewards: expectedTotalRewards, + ExpectedAdditiveSpreadRewardTolerance: sdk.OneDec().MulInt64(3), + TotalSpreadRewards: expectedTotalSpreadRewards, + TotalIncentives: sdk.NewCoins(sdk.NewCoin("uosmo", expectedTotalIncentiveRewards.Ceil().TruncateInt())), }) }) s.RunTestCaseWithoutStateUpdates("assert rewards when current tick is below the position with negative accumulator", func(t *testing.T) { - // Make closure-local copy of expectedTotalRewards - expectedTotalRewards := expectedTotalRewards + // Make closure-local copy of expectedTotalSpreadRewards + expectedTotalSpreadRewards := expectedTotalSpreadRewards + expectedTotalIncentiveRewards := expectedTotalIncentiveRewards // Swap third time to cover the newly created position with negative range accumulator // Swap to approximately DefaultCurrTick - 50 coinZeroIn = estimateCoinZeroIn(DefaultCurrTick - 50) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) // Move current tick to be below the expected position s.swapZeroForOneLeftWithSpread(poolId, coinZeroIn, spreadFactor) @@ -2736,21 +2771,28 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { // Assert global invariants s.assertGlobalInvariants(ExpectedGlobalRewardValues{ // Additive tolerance of 1 for each position. - ExpectedAdditiveTolerance: sdk.OneDec().MulInt64(3), - TotalSpreadRewards: expectedTotalRewards, + ExpectedAdditiveSpreadRewardTolerance: sdk.OneDec().MulInt64(3), + TotalSpreadRewards: expectedTotalSpreadRewards, + TotalIncentives: sdk.NewCoins(sdk.NewCoin("uosmo", expectedTotalIncentiveRewards.Ceil().TruncateInt())), }) }) s.RunTestCaseWithoutStateUpdates("assert rewards when current tick is inside the position with negative accumulator", func(t *testing.T) { - // Make closure-local copy of expectedTotalRewards - expectedTotalRewards := expectedTotalRewards + // Make closure-local copy of expectedTotalSpreadRewards + expectedTotalSpreadRewards := expectedTotalSpreadRewards + expectedTotalIncentiveRewards := expectedTotalIncentiveRewards // Swap third time to cover the newly created position with negative range accumulator // Swap to approximately DefaultCurrTick - 10 coinZeroIn = estimateCoinZeroIn(DefaultCurrTick - 10) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) // Move current tick to be inside of the new position s.swapZeroForOneLeftWithSpread(poolId, coinZeroIn, spreadFactor) @@ -2758,21 +2800,28 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { // Assert global invariants s.assertGlobalInvariants(ExpectedGlobalRewardValues{ // Additive tolerance of 1 for each position. - ExpectedAdditiveTolerance: sdk.OneDec().MulInt64(3), - TotalSpreadRewards: expectedTotalRewards, + ExpectedAdditiveSpreadRewardTolerance: sdk.OneDec().MulInt64(3), + TotalSpreadRewards: expectedTotalSpreadRewards, + TotalIncentives: sdk.NewCoins(sdk.NewCoin("uosmo", expectedTotalIncentiveRewards.Ceil().TruncateInt())), }) }) s.RunTestCaseWithoutStateUpdates("assert rewards when current tick is above the position with negative accumulator", func(t *testing.T) { - // Make closure-local copy of expectedTotalRewards - expectedTotalRewards := expectedTotalRewards + // Make closure-local copy of expectedTotalSpreadRewards + expectedTotalSpreadRewards := expectedTotalSpreadRewards + expectedTotalIncentiveRewards := expectedTotalIncentiveRewards // Swap third time to cover the newly created position with negative range accumulator // Swap to approximately DefaultCurrTick - 50 coinZeroIn = estimateCoinZeroIn(DefaultCurrTick - 50) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom0, coinZeroIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) // Swap inside the new position so that it accumulates rewards s.swapZeroForOneLeftWithSpread(poolId, coinZeroIn, spreadFactor) @@ -2781,7 +2830,12 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { coinOneIn := estimateCoinOneIn(DefaultCurrTick + 150) // Update expected spread rewards - expectedTotalRewards = expectedTotalRewards.Add(sdk.NewCoin(denom1, coinOneIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + expectedTotalSpreadRewards = expectedTotalSpreadRewards.Add(sdk.NewCoin(denom1, coinOneIn.Amount.ToDec().Mul(spreadFactor).Ceil().TruncateInt())) + + // Increase block time + s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Second)) + // Update expected incentive rewards + expectedTotalIncentiveRewards = expectedTotalIncentiveRewards.Add(rewardsPerSecond) // Swap back to take current tick be above the new position s.swapOneForZeroRightWithSpread(poolId, coinOneIn, spreadFactor) @@ -2789,8 +2843,26 @@ func (s *KeeperTestSuite) TestNegativeTickRange_SpreadFactor() { // Assert global invariants s.assertGlobalInvariants(ExpectedGlobalRewardValues{ // Additive tolerance of 1 for each position. - ExpectedAdditiveTolerance: sdk.OneDec().MulInt64(3), - TotalSpreadRewards: expectedTotalRewards, + ExpectedAdditiveSpreadRewardTolerance: sdk.OneDec().MulInt64(3), + TotalSpreadRewards: expectedTotalSpreadRewards, + TotalIncentives: sdk.NewCoins(sdk.NewCoin("uosmo", expectedTotalIncentiveRewards.Ceil().TruncateInt())), }) }) + + // Export and import genesis to make sure that negative accumulation does not lead to unexpected + // panics in serialization and deserialization. + spreadRewardAccumulator, err := s.clk.GetSpreadRewardAccumulator(s.Ctx, poolId) + s.Require().NoError(err) + + accum, err := spreadRewardAccumulator.GetPosition(types.KeySpreadRewardPositionAccumulator(negativeIntervalAccumPositionId)) + s.Require().NoError(err) + + // Validate that at least one accumulator is negative for the test to be valid. + s.Require().True(accum.AccumValuePerShare.IsAnyNegative()) + + export := s.clk.ExportGenesis(s.Ctx) + + s.SetupTest() + + s.clk.InitGenesis(s.Ctx, *export) }