Skip to content

Commit

Permalink
[Uptime Incentives]: Use gauge duration as uptime with appropriate va…
Browse files Browse the repository at this point in the history
…lidation and fallback (#7417)

* implement validation, fallback, and wiring for using gauge duration as uptime

* add changelog

* extract uptime fetch into helper
  • Loading branch information
AlpinYukseloglu authored Feb 7, 2024
1 parent 149f552 commit 29ed48f
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#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
* [#7417](https://github.com/osmosis-labs/osmosis/pull/7417) Update CL gauges to use gauge duration as uptime, falling back to default if unauthorized or invalid

### Bug Fixes

Expand Down
42 changes: 40 additions & 2 deletions x/incentives/keeper/distribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,39 @@ func (k Keeper) syncVolumeSplitGroup(ctx sdk.Context, group types.Group) error {
return nil
}

// getNoLockGaugeUptime retrieves the uptime corresponding to the passed in gauge.
// For external gauges, it returns the uptime specified in the gauge.
// For internal gauges, it returns the module param for internal gauge uptime.
//
// In either case, if the fetched uptime is invalid or unauthorized, it falls back to a default uptime.
func (k Keeper) getNoLockGaugeUptime(ctx sdk.Context, gauge types.Gauge) time.Duration {
// TODO: use module param as uptime for internal gauges once it is implemented.
// (Ref: https://github.com/osmosis-labs/osmosis/issues/7371)

// Validate that the gauge's corresponding uptime is authorized.
authorizedUptimes := k.clk.GetParams(ctx).AuthorizedUptimes
gaugeUptime := gauge.DistributeTo.Duration
isUptimeAuthorized := false
for _, authorizedUptime := range authorizedUptimes {
if gaugeUptime == authorizedUptime {
isUptimeAuthorized = true
}
}

// If the gauge's uptime is not authorized, we fall back to a default instead of erroring.
//
// This is for two reasons:
// 1. To allow uptimes to be unauthorized without entirely freezing existing gauges
// 2. To avoid having to do a state migration on existing gauges at time of adding
// this change, since prior to this, CL gauges were not required to associate with
// an uptime that was authorized.
if !isUptimeAuthorized {
gaugeUptime = types.DefaultConcentratedUptime
}

return gaugeUptime
}

// distributeInternal runs the distribution logic for a gauge, and adds the sends to
// the distrInfo struct. It also updates the gauge for the distribution.
// It handles any kind of gauges:
Expand Down Expand Up @@ -602,6 +635,10 @@ func (k Keeper) distributeInternal(
// Get distribution epoch duration. This is used to calculate the emission rate.
currentEpoch := k.GetEpochInfo(ctx)

// Get the uptime for the gauge. Note that if the gauge's uptime is not authorized,
// this falls back to a default value of 1ns.
gaugeUptime := k.getNoLockGaugeUptime(ctx, gauge)

// For every coin in the gauge, calculate the remaining reward per epoch
// and create a concentrated liquidity incentive record for it that
// is supposed to distribute over that epoch.
Expand All @@ -625,8 +662,9 @@ func (k Keeper) distributeInternal(
// Gauge start time should be checked whenever moving between active
// and inactive gauges. By the time we get here, the gauge should be active.
ctx.BlockTime(),
// Only default uptime is supported at launch.
types.DefaultConcentratedUptime,
// The uptime for each distribution is determined by the gauge's duration field.
// If it is unauthorized, we fall back to a default above.
gaugeUptime,
)

ctx.Logger().Info(fmt.Sprintf("distributeInternal CL for pool id %d finished", pool.GetId()))
Expand Down
89 changes: 85 additions & 4 deletions x/incentives/keeper/distribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
startTime time.Time
numEpochsPaidOver uint64
poolId uint64
authorizedUptime bool

// expected
expectErr bool
Expand All @@ -451,6 +452,7 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
startTime: oneHourAfterDefault,
numEpochsPaidOver: 1,
poolId: defaultCLPool,
authorizedUptime: false,
expectErr: false,

expectedDistributions: sdk.NewCoins(fiveKRewardCoins),
Expand Down Expand Up @@ -508,6 +510,18 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
return tc
}

withAuthorizedUptime := func(tc test, duration time.Duration) test {
tc.distrTo.Duration = duration
tc.authorizedUptime = true
return tc
}

withUnauthorizedUptime := func(tc test, duration time.Duration) test {
tc.distrTo.Duration = duration
tc.authorizedUptime = false
return tc
}

withError := func(tc test) test {
tc.expectErr = true
return tc
Expand All @@ -520,8 +534,24 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
"perpetual, 2 coins, paid over 1 epoch": withIsPerpetual(withGaugeCoins(defaultTest, defaultBothCoins), true),
"non-perpetual, 1 coin, paid over 2 epochs": withNumEpochs(defaultTest, 2),
"non-perpetual, 2 coins, paid over 3 epochs": withNumEpochs(withGaugeCoins(defaultTest, defaultBothCoins), 3),
"error: balancer pool id": withError(withPoolId(defaultTest, defaultBalancerPool)),
"error: inactive gauge": withError(withNumEpochs(defaultTest, 0)),

// We expect incentives with the set uptime to be created
"non-perpetual, 1 coin, paid over 1 epoch, authorized 1d uptime": withAuthorizedUptime(defaultTest, time.Hour*24),
"non-perpetual, 2 coins, paid over 3 epochs, authorized 7d uptime": withAuthorizedUptime(withNumEpochs(withGaugeCoins(defaultTest, defaultBothCoins), 3), time.Hour*24*7),
"perpetual, 2 coins, authorized 1h uptime": withAuthorizedUptime(withIsPerpetual(withGaugeCoins(defaultTest, defaultBothCoins), true), time.Hour),

// We expect incentives to fall back to default uptime of 1ns
"non-perpetual, 1 coin, paid over 1 epoch, unauthorized 1d uptime": withUnauthorizedUptime(defaultTest, time.Hour*24),
"non-perpetual, 2 coins, paid over 3 epochs, unauthorized 7d uptime": withUnauthorizedUptime(withNumEpochs(withGaugeCoins(defaultTest, defaultBothCoins), 3), time.Hour*24*7),
"perpetual, 2 coins, unauthorized 1h uptime": withUnauthorizedUptime(withIsPerpetual(withGaugeCoins(defaultTest, defaultBothCoins), true), time.Hour),

// 3h is not a valid uptime, so we expect this to fall back to 1ns
"non-perpetual, 1 coin, paid over 1 epoch, unauthorized and invalid uptime": withUnauthorizedUptime(defaultTest, time.Hour*3),
"non-perpetual, 2 coins, paid over 3 epochs, unauthorized and invalid uptime": withUnauthorizedUptime(withNumEpochs(withGaugeCoins(defaultTest, defaultBothCoins), 3), time.Hour*3),
"perpetual, 2 coins, unauthorized and invalid uptime": withUnauthorizedUptime(withIsPerpetual(withGaugeCoins(defaultTest, defaultBothCoins), true), time.Hour*3),

"error: balancer pool id": withError(withPoolId(defaultTest, defaultBalancerPool)),
"error: inactive gauge": withError(withNumEpochs(defaultTest, 0)),
}

for name, tc := range tests {
Expand All @@ -540,6 +570,13 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
// can function properly.
s.Ctx = s.Ctx.WithBlockTime(oneHourAfterDefault)

// If applicable, authorize gauge's uptime in CL module
if tc.authorizedUptime {
clParams := s.App.ConcentratedLiquidityKeeper.GetParams(s.Ctx)
clParams.AuthorizedUptimes = append(clParams.AuthorizedUptimes, tc.distrTo.Duration)
s.App.ConcentratedLiquidityKeeper.SetParams(s.Ctx, clParams)
}

// Create gauge and get it from state
externalGauge := s.createGaugeNoRestrictions(tc.isPerpertual, tc.gaugeCoins, tc.distrTo, tc.startTime, tc.numEpochsPaidOver, defaultCLPool)

Expand Down Expand Up @@ -569,9 +606,15 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
incentivesEpochDuration := s.App.IncentivesKeeper.GetEpochInfo(s.Ctx).Duration
incentivesEpochDurationSeconds := osmomath.NewDec(incentivesEpochDuration.Milliseconds()).QuoInt(osmomath.NewInt(1000))

// If uptime is not authorized, we expect to fall back to default
expectedUptime := types.DefaultConcentratedUptime
if tc.authorizedUptime {
expectedUptime = tc.distrTo.Duration
}

// Check that incentive records were created
for i, coin := range tc.expectedDistributions {
incentiveRecords, err := s.App.ConcentratedLiquidityKeeper.GetIncentiveRecord(s.Ctx, tc.poolId, time.Nanosecond, uint64(i+1))
incentiveRecords, err := s.App.ConcentratedLiquidityKeeper.GetIncentiveRecord(s.Ctx, tc.poolId, expectedUptime, uint64(i+1))
s.Require().NoError(err)

expectedEmissionRatePerEpoch := coin.Amount.ToLegacyDec().QuoTruncate(incentivesEpochDurationSeconds)
Expand All @@ -580,7 +623,7 @@ func (s *KeeperTestSuite) TestDistribute_ExternalIncentives_NoLock() {
s.Require().Equal(coin.Denom, incentiveRecords.IncentiveRecordBody.RemainingCoin.Denom)
s.Require().Equal(tc.expectedRemainingAmountIncentiveRecord[i], incentiveRecords.IncentiveRecordBody.RemainingCoin.Amount)
s.Require().Equal(expectedEmissionRatePerEpoch, incentiveRecords.IncentiveRecordBody.EmissionRate)
s.Require().Equal(time.Nanosecond, incentiveRecords.MinUptime)
s.Require().Equal(expectedUptime, incentiveRecords.MinUptime)
}

// Check that the gauge's distribution state was updated
Expand Down Expand Up @@ -2408,3 +2451,41 @@ func (s *KeeperTestSuite) TestHandleGroupPostDistribute() {
validateLastEpochNonPerpetualPruning(currentGauge.Id, currentGauge.DistributedCoins.Add(defaultCoins...), initialDistributionCoins, s.App.BankKeeper.GetAllBalances(s.Ctx, s.App.AccountKeeper.GetModuleAddress(types.ModuleName)))
})
}

func (s *KeeperTestSuite) TestGetNoLockGaugeUptime() {
tests := map[string]struct {
gauge types.Gauge
authorizedUptimes []time.Duration
expectedUptime time.Duration
}{
"external gauge with authorized uptime": {
gauge: types.Gauge{
DistributeTo: lockuptypes.QueryCondition{Duration: time.Hour},
},
authorizedUptimes: []time.Duration{types.DefaultConcentratedUptime, time.Hour},
expectedUptime: time.Hour,
},
"external gauge with unauthorized uptime": {
gauge: types.Gauge{
DistributeTo: lockuptypes.QueryCondition{Duration: time.Minute},
},
authorizedUptimes: []time.Duration{types.DefaultConcentratedUptime},
expectedUptime: types.DefaultConcentratedUptime,
},
}

for name, tc := range tests {
s.Run(name, func() {
// Setup CL params with authorized uptimes
clParams := s.App.ConcentratedLiquidityKeeper.GetParams(s.Ctx)
clParams.AuthorizedUptimes = tc.authorizedUptimes
s.App.ConcentratedLiquidityKeeper.SetParams(s.Ctx, clParams)

// System under test
actualUptime := s.App.IncentivesKeeper.GetNoLockGaugeUptime(s.Ctx, tc.gauge)

// Ensure correct uptime was returned
s.Require().Equal(tc.expectedUptime, actualUptime)
})
}
}
4 changes: 4 additions & 0 deletions x/incentives/keeper/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ func (k Keeper) CreateGroupInternal(ctx sdk.Context, coins sdk.Coins, numEpochPa
func (k Keeper) CalculateGroupWeights(ctx sdk.Context, group types.Group) (types.Group, error) {
return k.calculateGroupWeights(ctx, group)
}

func (k Keeper) GetNoLockGaugeUptime(ctx sdk.Context, gauge types.Gauge) time.Duration {
return k.getNoLockGaugeUptime(ctx, gauge)
}

0 comments on commit 29ed48f

Please sign in to comment.