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

[Uptime Incentives]: Add authorized uptime validation for new NoLock gauges #7376

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

* [#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

### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions x/incentives/keeper/distribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,7 @@ func (s *KeeperTestSuite) CreateNoLockExternalGauges(clPoolId uint64, externalGa
clPoolExternalGaugeId, err := s.App.IncentivesKeeper.CreateGauge(s.Ctx, numEpochsPaidOver == 1, gaugeCreator, externalGaugeCoins,
lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to reviewer: CL gauges with unauthorized uptimes can no longer be created due to the new validation logic, so existing tests are updated to use 1ns which is authorized by default. See #7375 for discussion on backwards compatibility with gauges currently live on mainnet with unauthorized uptimes.

},
s.Ctx.BlockTime(),
numEpochsPaidOver,
Expand Down
39 changes: 34 additions & 5 deletions x/incentives/keeper/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,38 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr
return 0, types.ErrZeroNumEpochsPaidOver
}

// If the gauge has no lock, then we assume it is a concentrated pool and ensure
// the gauge "lock" duration is an authorized uptime.
isConcentratedPoolGauge := distrTo.LockQueryType == lockuptypes.NoLock
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved

// If the gauge is being created by the pool incentives module, it is for an internal
// gauge and should not be blocked on creation here.
//
// Two important reminders:
// 1. `NoLock` gauges are required to have empty denoms in `ValidateBasic`, so this
// check cannot be controlled by user input
// 2. The safety of this leans on the special-casing of internal gauge logic during
// distributions, which should be using the internal incentive duration gov param instead of the duration value.
isInternalConcentratedPoolGauge := distrTo.Denom == types.NoLockInternalGaugeDenom(poolId)
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
isExternalConcentratedPoolGauge := isConcentratedPoolGauge && !isInternalConcentratedPoolGauge

// Ensure that this gauge's duration is one of the allowed durations on chain
durations := k.GetLockableDurations(ctx)
if distrTo.LockQueryType == lockuptypes.ByDuration {
// Concentrated pool gauges (excluding internal) check against authorized uptimes,
// while all other gauges check against the default set of lockable durations.
var durations []time.Duration
if isExternalConcentratedPoolGauge {
durations = k.clk.GetParams(ctx).AuthorizedUptimes
} else if isInternalConcentratedPoolGauge {
// Internal CL gauges use epoch time as their duration. This is a legacy
// property that does not affect the uptime on created records, which is
// determined by the gov param for internal incentive uptimes.
durations = []time.Duration{k.GetEpochInfo(ctx).Duration}
} else {
durations = k.GetLockableDurations(ctx)
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
}

// We check durations if the gauge is a regular duration based gauge, or if it is an external CL gauge.
if distrTo.LockQueryType == lockuptypes.ByDuration || isConcentratedPoolGauge {
durationOk := false
for _, duration := range durations {
if duration == distrTo.Duration {
Expand All @@ -146,7 +175,7 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr

// For no lock gauges, a pool id must be set.
// A pool with such id must exist and be a concentrated pool.
if distrTo.LockQueryType == lockuptypes.NoLock {
if isConcentratedPoolGauge {
if poolId == 0 {
return 0, fmt.Errorf("'no lock' type gauges must have a pool id")
}
Expand All @@ -155,7 +184,7 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr
// and get overwritten with the external prefix + pool id
// for internal query purposes.
distrToDenom := distrTo.Denom
if distrToDenom != types.NoLockInternalGaugeDenom(poolId) {
if !isInternalConcentratedPoolGauge {
// If denom is set, then fails.
if distrToDenom != "" {
return 0, fmt.Errorf("'no lock' type external gauges must have an empty denom set, was %s", distrToDenom)
Expand All @@ -177,7 +206,7 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr
// That being said, internal gauges have an additional linking
// by duration where duration is the incentives epoch duration.
// The internal incentive linking is set in x/pool-incentives CreateConcentratedLiquidityPoolGauge.
k.pik.SetPoolGaugeIdNoLock(ctx, poolId, nextGaugeId)
k.pik.SetPoolGaugeIdNoLock(ctx, poolId, nextGaugeId, distrTo.Duration)
} else {
// For all other gauges, pool id must be 0.
if poolId != 0 {
Expand Down
45 changes: 37 additions & 8 deletions x/incentives/keeper/gauge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,8 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
// Note: this assumes the gauge is external
Denom: "",
Denom: "",
Copy link
Member

Choose a reason for hiding this comment

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

Can we also add non-failing cases for the three branches that has been added to nolock createGauge?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The non-failing case should be covered by existing tests, but will re review to make sure I'm not missing anything

Copy link
Member

Choose a reason for hiding this comment

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

Sg! On first site of existing tests, test cases did not seem obvious in which case touches which branch. Please feel free to merge once you have re-checked!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right – since 1 nanosecond is an authorized uptime by default, the test cases with that duration should be covering the non-failing branches

Duration: time.Nanosecond,
},
poolId: concentratedPoolId,

Expand All @@ -520,7 +521,10 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
// Note: this assumes the gauge is internal
Denom: types.NoLockInternalGaugeDenom(concentratedPoolId),
// We intentionally do not set a gauge duration as it should make no
// difference for internal gauges.
Denom: types.NoLockInternalGaugeDenom(concentratedPoolId),
Duration: s.App.IncentivesKeeper.GetEpochInfo(s.Ctx).Duration,
},
poolId: concentratedPoolId,

Expand All @@ -533,7 +537,8 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
// Note: this is invalid for NoLock gauges
Denom: "uosmo",
Denom: "uosmo",
Duration: time.Nanosecond,
},
poolId: concentratedPoolId,

Expand All @@ -543,6 +548,7 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
name: "fail to create no lock gauge with balancer pool",
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
},
poolId: balancerPoolId,

Expand All @@ -552,6 +558,7 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
name: "fail to create no lock gauge with non-existent pool",
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
},
poolId: invalidPool,

Expand All @@ -561,9 +568,35 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {
name: "fail to create no lock gauge with zero pool id",
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
},
poolId: zeroPoolId,

expectErr: true,
},
{
name: "fail to create external no lock gauge due to unauthorized uptime",
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
// Note: this assumes the gauge is external
Denom: "",
Duration: 2 * time.Nanosecond,
},
poolId: concentratedPoolId,
expectErr: true,
},
{
name: "fail to create an internal gauge with an unexpected duration",
distrTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
// Note: this assumes the gauge is internal
// We intentionally do not set a gauge duration as it should make no
// difference for internal gauges.
Denom: types.NoLockInternalGaugeDenom(concentratedPoolId),
Duration: time.Nanosecond,
},
poolId: concentratedPoolId,

expectErr: true,
},
}
Expand All @@ -589,10 +622,6 @@ func (s *KeeperTestSuite) TestCreateGauge_NoLockGauges() {

s.Require().Equal(tc.expectedGaugeId, gaugeId)

// Assert that pool id and gauge id link meant for internally incentivized gauges is unset.
_, err := s.App.PoolIncentivesKeeper.GetPoolGaugeId(s.Ctx, tc.poolId, tc.distrTo.Duration)
s.Require().Error(err)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Highlighting this for review @p0mvn. The key-related change in this PR means that the "general linking" between gauges and pools is now based on duration, which means that this check is no longer necessary.

It also technically means that we can skip the "internal gauge link" component of the internal gauge setup, but that logic is not well abstracted so it would require meaningful additional work to save on the duplicate write that would be happening here.

// Confirm that the general pool id to gauge id link is set.
gaugeIds, err := s.App.PoolIncentivesKeeper.GetNoLockGaugeIdsFromPool(s.Ctx, tc.poolId)
s.Require().NoError(err)
Expand Down Expand Up @@ -774,7 +803,7 @@ func (s *KeeperTestSuite) createGaugeNoRestrictions(isPerpetual bool, coins sdk.
}

if poolID != 0 {
s.App.PoolIncentivesKeeper.SetPoolGaugeIdNoLock(s.Ctx, poolID, nextGaugeID)
s.App.PoolIncentivesKeeper.SetPoolGaugeIdNoLock(s.Ctx, poolID, nextGaugeID, distrTo.Duration)
}

err := s.App.IncentivesKeeper.SetGauge(s.Ctx, &gauge)
Expand Down
1 change: 1 addition & 0 deletions x/incentives/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (

distrToNoLock = lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
}

distrToNoLockPool1 = lockuptypes.QueryCondition{
Expand Down
3 changes: 2 additions & 1 deletion x/incentives/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type TxFeesKeeper interface {
type ConcentratedLiquidityKeeper interface {
CreateIncentive(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, incentiveCoin sdk.Coin, emissionRate osmomath.Dec, startTime time.Time, minUptime time.Duration) (cltypes.IncentiveRecord, error)
GetConcentratedPoolById(ctx sdk.Context, poolId uint64) (cltypes.ConcentratedPoolExtension, error)
GetParams(ctx sdk.Context) (params cltypes.Params)
}

type AccountKeeper interface {
Expand All @@ -59,7 +60,7 @@ type AccountKeeper interface {
type PoolIncentiveKeeper interface {
GetPoolIdFromGaugeId(ctx sdk.Context, gaugeId uint64, lockableDuration time.Duration) (uint64, error)
GetInternalGaugeIDForPool(ctx sdk.Context, poolID uint64) (uint64, error)
SetPoolGaugeIdNoLock(ctx sdk.Context, poolId uint64, gaugeId uint64)
SetPoolGaugeIdNoLock(ctx sdk.Context, poolId uint64, gaugeId uint64, uptime time.Duration)
GetLongestLockableDuration(ctx sdk.Context) (time.Duration, error)
}

Expand Down
2 changes: 1 addition & 1 deletion x/pool-incentives/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) {
}
if genState.ConcentratedPoolToNoLockGauges != nil {
for _, record := range genState.ConcentratedPoolToNoLockGauges.PoolToGauge {
k.SetPoolGaugeIdNoLock(ctx, record.PoolId, record.GaugeId)
k.SetPoolGaugeIdNoLock(ctx, record.PoolId, record.GaugeId, record.Duration)
Copy link
Member

Choose a reason for hiding this comment

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

Are we planning for any migration PR after this? 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We shouldn't need to - please see #7375 (if you see any problems with the reasoning in that issue please flag them!)

}
}
}
Expand Down
1 change: 1 addition & 0 deletions x/pool-incentives/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ func (s *KeeperTestSuite) TestExternalIncentiveGauges_NoLock() {
defaultNoLockGaugeConfig = gaugeConfig{
distributeTo: lockuptypes.QueryCondition{
LockQueryType: lockuptypes.NoLock,
Duration: time.Nanosecond,
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
},
poolId: concentratedPoolId,
}
Expand Down
7 changes: 2 additions & 5 deletions x/pool-incentives/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (k Keeper) SetPoolGaugeIdInternalIncentive(ctx sdk.Context, poolId uint64,

// SetPoolGaugeIdNoLock sets the link between pool id and gauge id for "NoLock" gauges.
// CONTRACT: the gauge of the given id must be "NoLock" gauge.
func (k Keeper) SetPoolGaugeIdNoLock(ctx sdk.Context, poolId uint64, gaugeId uint64) {
func (k Keeper) SetPoolGaugeIdNoLock(ctx sdk.Context, poolId uint64, gaugeId uint64, incentivizedUptime time.Duration) {
store := ctx.KVStore(k.storeKey)
// maps pool id and gauge id to gauge id.
// Note: this could be pool id and gauge id to empty byte array,
Expand All @@ -170,10 +170,7 @@ func (k Keeper) SetPoolGaugeIdNoLock(ctx sdk.Context, poolId uint64, gaugeId uin
store.Set(key, sdk.Uint64ToBigEndian(gaugeId))

// Note: this index is used for general linking.
// We supply zero for incentivized duration as "NoLock" gauges are not
// associated with any lockable duration. Instead, they incentivize
// pools directly.
key = types.GetPoolIdFromGaugeIdStoreKey(gaugeId, 0)
key = types.GetPoolIdFromGaugeIdStoreKey(gaugeId, incentivizedUptime)
store.Set(key, sdk.Uint64ToBigEndian(poolId))
}

Expand Down