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

feat: scaling factor for pool uptime accumulator to avoid truncation #7409

Merged
merged 17 commits into from
Feb 7, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### State Breaking

* [#7409](https://github.com/osmosis-labs/osmosis/pull/7409) Scaling factor for pool uptime accumulator to avoid truncation
* [#7181](https://github.com/osmosis-labs/osmosis/pull/7181) Improve errors for out of gas
* [#7376](https://github.com/osmosis-labs/osmosis/pull/7376) Add uptime validation logic for `NoLock` (CL) gauges and switch CL gauge to pool ID links to be duration-based
* [#7357](https://github.com/osmosis-labs/osmosis/pull/7357) Fix: Ensure rate limits are not applied to packets that aren't ics20s
Expand Down
8 changes: 8 additions & 0 deletions app/apptesting/test_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,11 @@ func (s *KeeperTestHelper) SetupVolumeForPools(poolIDs []uint64, volumesForEachP
func (s *KeeperTestHelper) IncreaseVolumeForPools(poolIDs []uint64, volumesForEachPool []osmomath.Int) {
s.SetupVolumeForPools(poolIDs, volumesForEachPool, map[uint64]osmomath.Int{})
}

// CompareDecCoinsSlice compares two slices of DecCoins
func (s *KeeperTestHelper) CompareDecCoinsSlice(expected, actual []sdk.DecCoins) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: something along the lines of RequireDecCoinsSlice seems more accurate

Copy link
Member Author

Choose a reason for hiding this comment

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

done

s.Require().Equal(len(expected), len(actual))
for i := range actual {
s.Require().Equal(expected[i].String(), actual[i].String())
}
}
15 changes: 10 additions & 5 deletions x/concentrated-liquidity/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ const (
)

var (
EmptyCoins = emptyCoins
HundredFooCoins = sdk.NewDecCoin("foo", osmomath.NewInt(100))
HundredBarCoins = sdk.NewDecCoin("bar", osmomath.NewInt(100))
TwoHundredFooCoins = sdk.NewDecCoin("foo", osmomath.NewInt(200))
TwoHundredBarCoins = sdk.NewDecCoin("bar", osmomath.NewInt(200))
EmptyCoins = emptyCoins
HundredFooCoins = sdk.NewDecCoin("foo", osmomath.NewInt(100))
HundredBarCoins = sdk.NewDecCoin("bar", osmomath.NewInt(100))
TwoHundredFooCoins = sdk.NewDecCoin("foo", osmomath.NewInt(200))
TwoHundredBarCoins = sdk.NewDecCoin("bar", osmomath.NewInt(200))
PerUnitLiqScalingFactor = perUnitLiqScalingFactor
)

func (k Keeper) SetPool(ctx sdk.Context, pool types.ConcentratedPoolExtension) error {
Expand Down Expand Up @@ -344,3 +345,7 @@ func (k Keeper) CallPoolActionListener(ctx sdk.Context, msgBz []byte, poolId uin
func (k Keeper) GetPoolHookContract(ctx sdk.Context, poolId uint64, actionPrefix string) string {
return k.getPoolHookContract(ctx, poolId, actionPrefix)
}

func ScaleUpTotalEmittedAmount(totalEmittedAmount osmomath.Dec) (scaledTotalEmittedAmount osmomath.Dec, err error) {
return scaleUpTotalEmittedAmount(totalEmittedAmount)
}
67 changes: 63 additions & 4 deletions x/concentrated-liquidity/incentives.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import (
"github.com/osmosis-labs/osmosis/v22/x/concentrated-liquidity/types"
)

// We choose 10^27 to allow sufficient buffer before the accumulator starts getting truncated again.
// Internally, multiply the number of seconds passed since the last liquidity update by the emission rate per second
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
// Then, we scale that value by 10^27 to avoid truncation to zero when dividing by the liquidity in the accumulator.
// As a result, we do not go for a higher scaling factor to allow for enough room before hitting the maximum integer value of 2^256.
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
// In the intermediary multiplications.
//
// More analysis on the choice of scaling factor can be found here:
// https://hackmd.io/o3oqT8VhSPKAiqNl_mlxXQ
var perUnitLiqScalingFactor = osmomath.NewDec(1e15).MulMut(osmomath.NewDec(1e12))

// createUptimeAccumulators creates accumulator objects in store for each supported uptime for the given poolId.
// The accumulators are initialized with the default (zero) values.
func (k Keeper) createUptimeAccumulators(ctx sdk.Context, poolId uint64) error {
Expand Down Expand Up @@ -240,11 +250,21 @@ func calcAccruedIncentivesForAccum(ctx sdk.Context, accumUptime time.Duration, l
}

// Total amount emitted = time elapsed * emission
totalEmittedAmount := timeElapsed.Mul(incentiveRecordBody.EmissionRate)
totalEmittedAmount := timeElapsed.MulTruncate(incentiveRecordBody.EmissionRate)
Copy link
Member

@ValarDragon ValarDragon Feb 6, 2024

Choose a reason for hiding this comment

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

to future proof this a bit more, thoughts on adding comments in line here for the expected range of each term?

This is basically whats blocking approve for me rn (trying to rederive these)

Actual scale-up change LGTM

Copy link
Member Author

Choose a reason for hiding this comment

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

Attempted to address here:
9e10789

Let me know what you think


// We scale up the remaining rewards to avoid truncation to zero
// when dividing by the liquidity in the accumulator.
scaledTotalEmittedAmount, err := scaleUpTotalEmittedAmount(totalEmittedAmount)
if err != nil {
ctx.Logger().Info(types.IncentiveOverflowPlaceholderName, "pool_id", poolID, "incentive_id", incentiveRecord.IncentiveId, "time_elapsed", timeElapsed, "emission_rate", incentiveRecordBody.EmissionRate, "error", err.Error())
// Silently ignore the truncated incentive record to avoid halting the entire accumulator update
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
// Continue to the next incentive record.
continue
}

// Incentives to emit per unit of qualifying liquidity = total emitted / liquidityInAccum
// Note that we truncate to ensure we do not overdistribute incentives
incentivesPerLiquidity := totalEmittedAmount.QuoTruncate(liquidityInAccum)
incentivesPerLiquidity := scaledTotalEmittedAmount.QuoTruncate(liquidityInAccum)

// Emit telemetry for accumulator updates
emitAccumulatorUpdateTelemetry(ctx, types.IncentiveTruncationPlaceholderName, types.IncentiveEmissionPlaceholderName, incentivesPerLiquidity, totalEmittedAmount, poolID, liquidityInAccum)
Expand All @@ -266,7 +286,18 @@ func calcAccruedIncentivesForAccum(ctx sdk.Context, accumUptime time.Duration, l
} else {
// If there are not enough incentives remaining to be emitted, we emit the remaining rewards.
// When the returned records are set in state, all records with remaining rewards of zero will be cleared.
remainingIncentivesPerLiquidity := remainingRewards.QuoTruncate(liquidityInAccum)

// We scale up the remaining rewards to avoid truncation to zero
// when dividing by the liquidity in the accumulator.
remainingRewardsScaled := remainingRewards.MulTruncate(perUnitLiqScalingFactor)
remainingIncentivesPerLiquidity := remainingRewardsScaled.QuoTruncateMut(liquidityInAccum)

// Detect truncation and emit telemetry to alert us of the issue.
if remainingIncentivesPerLiquidity.IsZero() && !remainingRewardsScaled.IsZero() {
telemetry.IncrCounter(1, types.IncentiveTruncationPlaceholderName)
ctx.Logger().Error(types.IncentiveTruncationPlaceholderName, "pool_id", poolID, "total_liq", liquidityInAccum, "per_unit_liq", incentivesPerLiquidity, "total_amt", remainingRewards, "total_amt_scaled", remainingRewardsScaled)
}

emittedIncentivesPerLiquidity = sdk.NewDecCoinFromDec(incentiveRecordBody.RemainingCoin.Denom, remainingIncentivesPerLiquidity)

// Emit telemetry for accumulator updates
Expand All @@ -281,6 +312,23 @@ func calcAccruedIncentivesForAccum(ctx sdk.Context, accumUptime time.Duration, l
return incentivesToAddToCurAccum, copyPoolIncentiveRecords, nil
}

// scaleUpTotalEmittedAmount scales up the total emitted amount to avoid truncation to zero.
// Returns error if the total emitted amount is too high and causes overflow when applying scaling factor.
func scaleUpTotalEmittedAmount(totalEmittedAmount osmomath.Dec) (scaledTotalEmittedAmount osmomath.Dec, err error) {
defer func() {
r := recover()

if r != nil {
telemetry.IncrCounter(1, types.IncentiveOverflowPlaceholderName)
err = types.IncentiveOverflowError{
PanicMessage: fmt.Sprintf("%v", r),
}
}
}()

return totalEmittedAmount.MulTruncate(perUnitLiqScalingFactor), nil
}

// findUptimeIndex finds the uptime index for the passed in min uptime.
// Returns error if uptime index cannot be found.
func findUptimeIndex(uptime time.Duration) (int, error) {
Expand Down Expand Up @@ -705,11 +753,22 @@ func (k Keeper) prepareClaimAllIncentivesForPosition(ctx sdk.Context, positionId

// If the accumulator contains the position, claim the position's incentives.
if hasPosition {
collectedIncentivesForUptime, _, err := updateAccumAndClaimRewards(uptimeAccum, positionName, uptimeGrowthOutside[uptimeIndex])
collectedIncentivesForUptimeScaled, _, err := updateAccumAndClaimRewards(uptimeAccum, positionName, uptimeGrowthOutside[uptimeIndex])
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
}

// We scale the uptime per-unit of liquidity accumulator up to avoid truncation to zero.
// However, once we compute the total for the liquidity entitlement, we must scale it back down.
// We always truncate down in the pool's favor.
collectedIncentivesForUptime := sdk.NewCoins()
for _, incentiveCoin := range collectedIncentivesForUptimeScaled {
incentiveCoin.Amount = incentiveCoin.Amount.ToLegacyDec().QuoTruncateMut(perUnitLiqScalingFactor).TruncateInt()
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
if incentiveCoin.Amount.IsPositive() {
collectedIncentivesForUptime = append(collectedIncentivesForUptime, incentiveCoin)
}
Comment on lines +798 to +800
Copy link
Member

Choose a reason for hiding this comment

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

Is this to handle the case is which, even after scaling, we still truncate to zero?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. It doesn't make sense to collect a bunch of zero coins so decided to simply skip them

}

if positionAge < supportedUptimes[uptimeIndex] {
// 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.
Expand Down
Loading