Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Implement redepositing of forfeited incentives #7746

Merged
merged 20 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#7685](https://github.com/osmosis-labs/osmosis/pull/7685) Speedup CL actions by only marshalling for CL hooks if they will be used.
* [#7503](https://github.com/osmosis-labs/osmosis/pull/7503) Add IBC wasm light clients module
* [#7689](https://github.com/osmosis-labs/osmosis/pull/7689) Make CL price estimations not cause state writes (speed and gas improvements)
* [#7746](https://github.com/osmosis-labs/osmosis/pull/7746) Make forfeited incentives redeposit into the pool instead of sending to community pool

## v23.0.5 & v23.0.5-iavl-v1

Expand Down
8 changes: 6 additions & 2 deletions x/concentrated-liquidity/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func (k Keeper) GetAllIncentiveRecordsForUptime(ctx sdk.Context, poolId uint64,
return k.getAllIncentiveRecordsForUptime(ctx, poolId, minUptime)
}

func (k Keeper) CollectIncentives(ctx sdk.Context, owner sdk.AccAddress, positionId uint64) (sdk.Coins, sdk.Coins, error) {
func (k Keeper) CollectIncentives(ctx sdk.Context, owner sdk.AccAddress, positionId uint64) (sdk.Coins, sdk.Coins, map[int]sdk.Coins, error) {
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
return k.collectIncentives(ctx, owner, positionId)
}

Expand All @@ -270,7 +270,7 @@ func UpdateAccumAndClaimRewards(accum *accum.AccumulatorObject, positionKey stri
return updateAccumAndClaimRewards(accum, positionKey, growthOutside)
}

func (k Keeper) PrepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, error) {
func (k Keeper) PrepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, map[int]sdk.Coins, error) {
return k.prepareClaimAllIncentivesForPosition(ctx, positionId)
}

Expand Down Expand Up @@ -355,3 +355,7 @@ func ComputeTotalIncentivesToEmit(timeElapsedSeconds osmomath.Dec, emissionRate
func (k Keeper) GetIncentiveScalingFactorForPool(ctx sdk.Context, poolID uint64) (osmomath.Dec, error) {
return k.getIncentiveScalingFactorForPool(ctx, poolID)
}

func ScaleDownIncentiveAmount(incentiveAmount osmomath.Int, scalingFactor osmomath.Dec) (scaledTotalEmittedAmount osmomath.Int) {
return scaleDownIncentiveAmount(incentiveAmount, scalingFactor)
}
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
60 changes: 31 additions & 29 deletions x/concentrated-liquidity/incentives.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,36 +753,36 @@ func moveRewardsToNewPositionAndDeleteOldAcc(accum *accum.AccumulatorObject, old
// The parent function (collectIncentives) does the actual bank sends for both the collected and forfeited incentives.
//
// Returns error if the position/uptime accumulators don't exist, or if there is an issue that arises while claiming.
func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, error) {
func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, map[int]sdk.Coins, error) {
// Retrieve the position with the given ID.
position, err := k.GetPosition(ctx, positionId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

err = k.UpdatePoolUptimeAccumulatorsToNow(ctx, position.PoolId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// Compute the age of the position.
positionAge := ctx.BlockTime().Sub(position.JoinTime)

// Should never happen, defense in depth.
if positionAge < 0 {
return sdk.Coins{}, sdk.Coins{}, types.NegativeDurationError{Duration: positionAge}
return sdk.Coins{}, sdk.Coins{}, nil, types.NegativeDurationError{Duration: positionAge}
}

// Retrieve the uptime accumulators for the position's pool.
uptimeAccumulators, err := k.GetUptimeAccumulators(ctx, position.PoolId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// Compute uptime growth outside of the range between lower tick and upper tick
uptimeGrowthOutside, err := k.GetUptimeGrowthOutsideRange(ctx, position.PoolId, position.LowerTick, position.UpperTick)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// Create a variable to hold the name of the position.
Expand All @@ -796,10 +796,11 @@ func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId

incentiveScalingFactor, err := k.getIncentiveScalingFactorForPool(ctx, position.PoolId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// Loop through each uptime accumulator for the pool.
scaledForfeitedIncentivesByUptime := make(map[int]sdk.Coins)
for uptimeIndex, uptimeAccum := range uptimeAccumulators {
// Check if the accumulator contains the position.
// There should never be a case where you can have a position for 1 accumulator, and not the rest.
Expand All @@ -809,7 +810,7 @@ func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId
if hasPosition {
collectedIncentivesForUptimeScaled, _, err := updateAccumAndClaimRewards(uptimeAccum, positionName, uptimeGrowthOutside[uptimeIndex])
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// We scale the uptime per-unit of liquidity accumulator up to avoid truncation to zero.
Expand All @@ -824,6 +825,13 @@ func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId
}

if positionAge < supportedUptimes[uptimeIndex] {
// We track forfeited incentives by uptime accumulator to allow for efficient redepositing.
// To avoid descaling and rescaling, we keep the forfeited incentives in scaled form.
// This is slightly unwieldy as it means we return a map of scaled coins, but doing it this way
// allows us to efficiently handle all cases related to forfeited incentives without recomputing
// expensive operations.
scaledForfeitedIncentivesByUptime[uptimeIndex] = collectedIncentivesForUptimeScaled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, at first I was trying to understand why the Coins array but this makes sense!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah also we'd need to track forfeits separately regardless to know which incentives were forfeited from which accumulators, so something of this form would be necessary regardless


// If the age of the position is less than the current uptime we are iterating through, then the position's
// incentives are forfeited to the community pool. The parent function does the actual bank send.
forfeitedIncentivesForPosition = forfeitedIncentivesForPosition.Add(collectedIncentivesForUptime...)
Expand All @@ -835,13 +843,15 @@ func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId
}
}

return collectedIncentivesForPosition, forfeitedIncentivesForPosition, nil
return collectedIncentivesForPosition, forfeitedIncentivesForPosition, scaledForfeitedIncentivesByUptime, nil
}

func (k Keeper) GetClaimableIncentives(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, error) {
// Since this is a query, we don't want to modify the state and therefore use a cache context.
cacheCtx, _ := ctx.CacheContext()
return k.prepareClaimAllIncentivesForPosition(cacheCtx, positionId)
// We omit the by-uptime forfeited incentives map as it is not needed for this query.
collectedIncentives, forfeitedIncentives, _, err := k.prepareClaimAllIncentivesForPosition(cacheCtx, positionId)
return collectedIncentives, forfeitedIncentives, err
}

// collectIncentives collects incentives for all uptime accumulators for the specified position id.
Expand All @@ -850,49 +860,41 @@ func (k Keeper) GetClaimableIncentives(ctx sdk.Context, positionId uint64) (sdk.
// Returns error if:
// - position with the given id does not exist
// - other internal database or math errors.
func (k Keeper) collectIncentives(ctx sdk.Context, sender sdk.AccAddress, positionId uint64) (sdk.Coins, sdk.Coins, error) {
func (k Keeper) collectIncentives(ctx sdk.Context, sender sdk.AccAddress, positionId uint64) (sdk.Coins, sdk.Coins, map[int]sdk.Coins, error) {
// Retrieve the position with the given ID.
position, err := k.GetPosition(ctx, positionId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

if sender.String() != position.Address {
return sdk.Coins{}, sdk.Coins{}, types.NotPositionOwnerError{
return sdk.Coins{}, sdk.Coins{}, nil, types.NotPositionOwnerError{
PositionId: positionId,
Address: sender.String(),
}
}

// Claim all incentives for the position.
collectedIncentivesForPosition, forfeitedIncentivesForPosition, err := k.prepareClaimAllIncentivesForPosition(ctx, position.PositionId)
collectedIncentivesForPosition, totalForfeitedIncentivesForPosition, scaledAmountForfeitedByUptime, err := k.prepareClaimAllIncentivesForPosition(ctx, position.PositionId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// If no incentives were collected, return an empty coin set.
if collectedIncentivesForPosition.IsZero() && forfeitedIncentivesForPosition.IsZero() {
return collectedIncentivesForPosition, forfeitedIncentivesForPosition, nil
if collectedIncentivesForPosition.IsZero() && totalForfeitedIncentivesForPosition.IsZero() {
return collectedIncentivesForPosition, totalForfeitedIncentivesForPosition, scaledAmountForfeitedByUptime, nil
}

// Send the collected incentives to the position's owner.
pool, err := k.getPoolById(ctx, position.PoolId)
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}

// Send the collected incentives to the position's owner from the pool's address.
if !collectedIncentivesForPosition.IsZero() {
if err := k.bankKeeper.SendCoins(ctx, pool.GetIncentivesAddress(), sender, collectedIncentivesForPosition); err != nil {
return sdk.Coins{}, sdk.Coins{}, err
}
}

// Send the forfeited incentives to the community pool from the pool's address.
if !forfeitedIncentivesForPosition.IsZero() {
err = k.communityPoolKeeper.FundCommunityPool(ctx, forfeitedIncentivesForPosition, pool.GetIncentivesAddress())
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
return sdk.Coins{}, sdk.Coins{}, nil, err
}
}

Expand All @@ -904,11 +906,11 @@ func (k Keeper) collectIncentives(ctx sdk.Context, sender sdk.AccAddress, positi
sdk.NewAttribute(types.AttributeKeyPoolId, strconv.FormatUint(pool.GetId(), 10)),
sdk.NewAttribute(types.AttributeKeyPositionId, strconv.FormatUint(positionId, 10)),
sdk.NewAttribute(types.AttributeKeyTokensOut, collectedIncentivesForPosition.String()),
sdk.NewAttribute(types.AttributeKeyForfeitedTokens, forfeitedIncentivesForPosition.String()),
sdk.NewAttribute(types.AttributeKeyForfeitedTokens, totalForfeitedIncentivesForPosition.String()),
),
})

return collectedIncentivesForPosition, forfeitedIncentivesForPosition, nil
return collectedIncentivesForPosition, totalForfeitedIncentivesForPosition, scaledAmountForfeitedByUptime, nil
}

// createIncentive creates an incentive record in state for the given pool.
Expand Down
48 changes: 36 additions & 12 deletions x/concentrated-liquidity/incentives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2311,7 +2311,7 @@ func (s *KeeperTestSuite) TestQueryAndCollectIncentives() {
s.Require().Equal(tc.expectedIncentivesClaimed, incentivesClaimedQuery)
s.Require().Equal(tc.expectedForfeitedIncentives, incentivesForfeitedQuery)
}
actualIncentivesClaimed, actualIncetivesForfeited, err := clKeeper.CollectIncentives(s.Ctx, ownerWithValidPosition, DefaultPositionId)
actualIncentivesClaimed, actualIncetivesForfeited, _, err := clKeeper.CollectIncentives(s.Ctx, ownerWithValidPosition, DefaultPositionId)

// Assertions
s.Require().Equal(incentivesClaimedQuery, actualIncentivesClaimed)
Expand Down Expand Up @@ -2341,7 +2341,9 @@ func (s *KeeperTestSuite) TestQueryAndCollectIncentives() {
s.Require().Equal(tc.expectedForfeitedIncentives.String(), actualIncetivesForfeited.String())

// Ensure balances are updated by the correct amounts
s.Require().Equal(tc.expectedIncentivesClaimed.Add(tc.expectedForfeitedIncentives...).String(), (incentivesBalanceBeforeCollect.Sub(incentivesBalanceAfterCollect...)).String())
// Note that we expect the forfeited incentives to remain in the pool incentives balance since they are
// redeposited, so we only expect the diff in incentives balance to be the amount successfully claimed.
s.Require().Equal(tc.expectedIncentivesClaimed.String(), (incentivesBalanceBeforeCollect.Sub(incentivesBalanceAfterCollect...)).String())
s.Require().Equal(tc.expectedIncentivesClaimed.String(), (ownerBalancerAfterCollect.Sub(ownerBalancerBeforeCollect...)).String())
})
}
Expand Down Expand Up @@ -2775,6 +2777,26 @@ func (s *KeeperTestSuite) TestUpdateAccumAndClaimRewards() {
})
}

// checkForfeitedCoinsByUptime checks that the sum of forfeited coins by uptime matches the expected total forfeited coins.
// It adds up the Coins corresponding to each uptime in the map and asserts that the result is equal to the input totalForfeitedCoins.
func (s *KeeperTestSuite) checkForfeitedCoinsByUptime(totalForfeitedCoins sdk.Coins, scaledForfeitedCoinsByUptime map[int]sdk.Coins) {
forfeitedCoins := sdk.NewCoins()
// Iterate through the constant uptime indexes
for uptimeIndex := range types.SupportedUptimes {
// If there are forfeited coins for the current uptime, add them to the sum
if coins, ok := scaledForfeitedCoinsByUptime[uptimeIndex]; ok {
for _, coin := range coins {
// Scale down the actual forfeited coin amount
scaledDownAmount := cl.ScaleDownIncentiveAmount(coin.Amount, cl.PerUnitLiqScalingFactor)
forfeitedCoins = forfeitedCoins.Add(sdk.NewCoin(coin.Denom, scaledDownAmount))
}
}
}

// Use Equal for assertion to compare the totalForfeitedCoins with the scaled down summedCoins
s.Require().Equal(totalForfeitedCoins, forfeitedCoins, "Total forfeited coins do not match the sum of forfeited coins by uptime after scaling down")
}

// Note that the non-forfeit cases are thoroughly tested in `TestCollectIncentives`
func (s *KeeperTestSuite) TestQueryAndClaimAllIncentives() {
uptimeHelper := getExpectedUptimes()
Expand Down Expand Up @@ -2905,7 +2927,7 @@ func (s *KeeperTestSuite) TestQueryAndClaimAllIncentives() {
s.Require().Equal(initSenderBalances, newSenderBalances)
s.Require().Equal(initPoolBalances, newPoolBalances)

amountClaimed, amountForfeited, err := s.Clk.PrepareClaimAllIncentivesForPosition(s.Ctx, tc.positionIdClaim)
amountClaimed, totalAmountForfeited, scaledAmountForfeitedByUptime, err := s.Clk.PrepareClaimAllIncentivesForPosition(s.Ctx, tc.positionIdClaim)

// --- Assertions ---

Expand All @@ -2914,7 +2936,8 @@ func (s *KeeperTestSuite) TestQueryAndClaimAllIncentives() {
newPoolBalances = s.App.BankKeeper.GetAllBalances(s.Ctx, clPool.GetAddress())

s.Require().Equal(amountClaimedQuery, amountClaimed)
s.Require().Equal(amountForfeitedQuery, amountForfeited)
s.Require().Equal(amountForfeitedQuery, totalAmountForfeited)
s.checkForfeitedCoinsByUptime(totalAmountForfeited, scaledAmountForfeitedByUptime)

if tc.expectedError != nil {
s.Require().ErrorIs(err, tc.expectedError)
Expand All @@ -2939,7 +2962,7 @@ func (s *KeeperTestSuite) TestQueryAndClaimAllIncentives() {
expectedCoins = expectedCoins.Add(sdk.NormalizeCoins(growthInside)...)
}
s.Require().Equal(expectedCoins.String(), amountClaimed.String())
s.Require().Equal(sdk.Coins{}, amountForfeited)
s.Require().Equal(sdk.Coins{}, totalAmountForfeited)
}

// Ensure balances have not been mutated
Expand Down Expand Up @@ -3078,10 +3101,11 @@ func (s *KeeperTestSuite) TestPrepareClaimAllIncentivesForPosition() {
}

// System under test
collectedInc, forfeitedIncentives, err := s.Clk.PrepareClaimAllIncentivesForPosition(s.Ctx, positionOneData.ID)
collectedInc, totalForfeitedIncentives, scaledForfeitedIncentivesByUptime, err := s.Clk.PrepareClaimAllIncentivesForPosition(s.Ctx, positionOneData.ID)
s.Require().NoError(err)
s.Require().Equal(tc.expectedCoins.String(), collectedInc.String())
s.Require().Equal(expectedForfeitedIncentives.String(), forfeitedIncentives.String())
s.Require().Equal(expectedForfeitedIncentives.String(), totalForfeitedIncentives.String())
s.checkForfeitedCoinsByUptime(totalForfeitedIncentives, scaledForfeitedIncentivesByUptime)

// The difference accumulator value should have increased if we forfeited incentives by claiming.
uptimeAccumsDiffPostClaim := sdk.NewDecCoins()
Expand Down Expand Up @@ -3169,7 +3193,7 @@ func (s *KeeperTestSuite) TestFunctional_ClaimIncentives_LiquidityChange_Varying
s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(testFullChargeDuration))

// Claim incentives.
collected, _, err := s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionOneData.ID)
collected, _, _, err := s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionOneData.ID)
s.Require().NoError(err)
s.Require().Equal(expectedCoinsPerFullCharge.String(), collected.String())

Expand All @@ -3184,13 +3208,13 @@ func (s *KeeperTestSuite) TestFunctional_ClaimIncentives_LiquidityChange_Varying
s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(testFullChargeDuration))

// Claim for second position. Must only claim half of the original expected amount since now there are 2 positions.
collected, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionTwoData.ID)
collected, _, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionTwoData.ID)
s.Require().NoError(err)
s.Require().Equal(expectedHalfOfExpectedCoinsPerFullCharge.String(), collected.String())

// Claim for first position and observe that claims full expected charge for the period between 1st claim and 2nd position creation
// and half of the full charge amount since the 2nd position was created.
collected, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionOneData.ID)
collected, _, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, defaultAddress, positionOneData.ID)
s.Require().NoError(err)
// Note, adding one since both expected amounts already subtract one (-2 in total)
s.Require().Equal(expectedCoinsPerFullCharge.Add(expectedHalfOfExpectedCoinsPerFullCharge.Add(oneUUSDCCoin)...).String(), collected.String())
Expand Down Expand Up @@ -3667,13 +3691,13 @@ func (s *KeeperTestSuite) TestIncentiveTruncation() {

// The check below shows that the incentive is not claimed due to truncation
s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Minute * 50))
incentives, _, err := s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, s.TestAccs[0], positionData.ID)
incentives, _, _, err := s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, s.TestAccs[0], positionData.ID)
s.Require().NoError(err)
s.Require().True(incentives.IsZero())

// Incentives should now be claimed due to lack of truncation
s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(time.Hour * 6))
incentives, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, s.TestAccs[0], positionData.ID)
incentives, _, _, err = s.App.ConcentratedLiquidityKeeper.CollectIncentives(s.Ctx, s.TestAccs[0], positionData.ID)
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
s.Require().NoError(err)
s.Require().False(incentives.IsZero())
}
Expand Down
2 changes: 1 addition & 1 deletion x/concentrated-liquidity/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (s *KeeperTestSuite) assertTotalRewardsInvariant(expectedGlobalRewardValues
//
// Balancer full range incentives are also not factored in because they are claimed and sent to
// gauge immediately upon distribution.
collectedIncentives, _, err := s.Clk.CollectIncentives(cachedCtx, owner, position.PositionId)
collectedIncentives, _, _, err := s.Clk.CollectIncentives(cachedCtx, owner, position.PositionId)
s.Require().NoError(err)

// Ensure position owner's balance was updated correctly
Expand Down
Loading