Skip to content

Commit

Permalink
[VS Incentives] refactor/test: CreateGroup takes pool IDs; new tests (#…
Browse files Browse the repository at this point in the history
…6525)

* [Volume Splitting] feat: implement initGaugeInfo helper

* comments

* refactor/test: CreateGroup takes pool IDs; new tests

* lint

* Update x/incentives/keeper/gauge_test.go

Co-authored-by: Adam Tucker <adam@osmosis.team>

* Update x/incentives/keeper/gauge_test.go

Co-authored-by: Adam Tucker <adam@osmosis.team>

---------

Co-authored-by: Adam Tucker <adam@osmosis.team>
  • Loading branch information
2 people authored and pysel committed Sep 27, 2023
1 parent 1d39895 commit fc6c55a
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 316 deletions.
181 changes: 18 additions & 163 deletions x/incentives/keeper/distribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ var (
FilledEpochs: 1,
DistributedCoins: defaultCoins,
}

defaultZeroWeightGaugeRecord = types.InternalGaugeRecord{GaugeId: 1, CurrentWeight: osmomath.ZeroInt(), CumulativeWeight: osmomath.ZeroInt()}
)

type GroupCreationFields struct {
Expand Down Expand Up @@ -1198,168 +1200,6 @@ func (s *KeeperTestSuite) WithBaseCaseDifferentInternalGauges(baseCase GroupCrea
return baseCase
}

func (s *KeeperTestSuite) TestCreateGroupAndDistribute() {
// We skip these test until group initialization refactor is complete
s.T().Skip()

hundredKUosmo := sdk.NewCoin("uosmo", osmomath.NewInt(100_000_000))
hundredKUatom := sdk.NewCoin("uatom", osmomath.NewInt(100_000_000))
fifetyKUosmo := sdk.NewCoin("uosmo", osmomath.NewInt(50_000_000))
fifetyKUatom := sdk.NewCoin("uatom", osmomath.NewInt(50_000_000))
twentyfiveKUosmo := sdk.NewCoin("uosmo", osmomath.NewInt(25_000_000))
twentyfiveKUatom := sdk.NewCoin("uatom", osmomath.NewInt(25_000_000))

baseCase := &GroupCreationFields{
coins: sdk.NewCoins(hundredKUosmo),
numEpochPaidOver: 1,
owner: s.TestAccs[1],
internalGaugeIds: []uint64{2, 3, 4, 5},
}

tests := []struct {
name string
createGauge GroupCreationFields
expectedCoinsPerInternalGauge sdk.Coins
expectedCoinsDistributedPerEpoch sdk.Coins
expectCreateGroupError bool
expectDistributeToInternalGaugeError bool
}{
{
name: "Valid case: Valid perp-Group Creation and Distribution",
createGauge: *baseCase,
expectedCoinsPerInternalGauge: sdk.NewCoins(twentyfiveKUosmo), // 100osmo / 4 = 25osmo
expectedCoinsDistributedPerEpoch: sdk.NewCoins(hundredKUosmo),
},
{
name: "Valid case: Valid perp-Group Creation with only CL internal gauges and Distribution",
createGauge: s.WithBaseCaseDifferentInternalGauges(*baseCase, []uint64{2, 3, 4}),
expectedCoinsPerInternalGauge: sdk.NewCoins(sdk.NewCoin("uosmo", osmomath.NewInt(33_333_333))),
expectedCoinsDistributedPerEpoch: sdk.NewCoins(hundredKUosmo),
},
{
name: "Valid case: Valid perp-Group Creation with only GAMM internal gauge and Distribution",
createGauge: s.WithBaseCaseDifferentInternalGauges(*baseCase, []uint64{5}),
expectedCoinsPerInternalGauge: sdk.NewCoins(hundredKUosmo),
expectedCoinsDistributedPerEpoch: sdk.NewCoins(hundredKUosmo),
},
{
name: "Valid case: Valid non-perpGroup Creation with and Distribution",
createGauge: s.WithBaseCaseDifferentEpochPaidOver(*baseCase, uint64(4)),
expectedCoinsPerInternalGauge: sdk.NewCoins(sdk.NewCoin("uosmo", osmomath.NewInt(6_250_000))),
expectedCoinsDistributedPerEpoch: sdk.NewCoins(twentyfiveKUosmo),
},
{
name: "Valid case: Valid perp-Group Creation with 2 coins and Distribution",
createGauge: s.WithBaseCaseDifferentCoins(*baseCase, sdk.NewCoins(hundredKUosmo, hundredKUatom)),
expectedCoinsPerInternalGauge: sdk.NewCoins(twentyfiveKUosmo, twentyfiveKUatom),
expectedCoinsDistributedPerEpoch: sdk.NewCoins(hundredKUosmo, hundredKUatom),
},
{
name: "Valid case: Valid non-perp Group Creation with 2 coins and Distribution",
createGauge: s.WithBaseCaseDifferentEpochPaidOver(s.WithBaseCaseDifferentCoins(*baseCase, sdk.NewCoins(hundredKUosmo, hundredKUatom)), uint64(2)),
expectedCoinsPerInternalGauge: sdk.NewCoins(sdk.NewCoin("uosmo", osmomath.NewInt(12_500_000)), sdk.NewCoin("uatom", osmomath.NewInt(12_500_000))),
expectedCoinsDistributedPerEpoch: sdk.NewCoins(fifetyKUosmo, fifetyKUatom),
},
{
name: "InValid case: Creating a Group with invalid internalIds",
createGauge: s.WithBaseCaseDifferentInternalGauges(*baseCase, []uint64{100, 101}),
expectCreateGroupError: true,
},
{
name: "InValid case: Creating a Group with non-perpetual internalId",
createGauge: s.WithBaseCaseDifferentInternalGauges(*baseCase, []uint64{2, 3, 4, 6}),
expectCreateGroupError: true,
},
}

for _, tc := range tests {
s.Run(tc.name, func() {
s.SetupTest()
s.FundAcc(s.TestAccs[1], sdk.NewCoins(hundredKUosmo, hundredKUatom)) // 100osmo, 100atom

// Setup
clPool := s.PrepareConcentratedPool()
lockOwner := sdk.AccAddress([]byte("addr1---------------"))
epochInfo := s.App.IncentivesKeeper.GetEpochInfo(s.Ctx)
s.SetupGroupGauge(clPool.GetId(), lockOwner, uint64(3), uint64(1))

//create 1 non-perp internal Gauge
s.CreateNoLockExternalGauges(clPool.GetId(), sdk.NewCoins(), s.TestAccs[1], uint64(2)) // gauge id = 6

groupGaugeId, err := s.App.IncentivesKeeper.CreateGroup(s.Ctx, tc.createGauge.coins, tc.createGauge.numEpochPaidOver, tc.createGauge.owner, tc.createGauge.internalGaugeIds, lockuptypes.ByGroup) // gauge id = 6
if tc.expectCreateGroupError {
s.Require().Error(err)
return
}

s.Require().NoError(err)

// check internalGauges matches what we expect
// TODO: assert initialization logic correctness once it is implemented
// Tracked in issue https://github.com/osmosis-labs/osmosis/issues/6404

for epoch := uint64(1); epoch <= tc.createGauge.numEpochPaidOver; epoch++ {
// ******************** EPOCH PASSED ********************* //
s.Ctx = s.Ctx.WithBlockTime(s.Ctx.BlockTime().Add(epochInfo.Duration))
s.App.EpochsKeeper.AfterEpochEnd(s.Ctx, epochInfo.GetIdentifier(), int64(epoch))

// Validate GroupGauge
groupGauge, err := s.App.IncentivesKeeper.GetGaugeByID(s.Ctx, groupGaugeId)
s.Require().NoError(err)

var expectedDistributedCoins []sdk.Coin
for _, coin := range tc.expectedCoinsDistributedPerEpoch {
expectedDistributedCoins = append(expectedDistributedCoins, sdk.NewCoin(coin.Denom, coin.Amount.Mul(osmomath.NewIntFromUint64(epoch))))
}

s.ValidateDistributedGauge(groupGauge.Id, epoch, expectedDistributedCoins)

// Validate Internal Gauges
internalGauges, err := s.App.IncentivesKeeper.GetGaugeFromIDs(s.Ctx, tc.createGauge.internalGaugeIds)
s.Require().NoError(err)

for _, internalGauge := range internalGauges {
var expectedDistributedCoinsPerInternalGauge []sdk.Coin
for _, coin := range tc.expectedCoinsPerInternalGauge {
expectedDistributedCoinsPerInternalGauge = append(expectedDistributedCoinsPerInternalGauge, (sdk.NewCoin(coin.Denom, coin.Amount.Mul(osmomath.NewIntFromUint64(epoch)))))
}
s.ValidateDistributedGauge(internalGauge.Id, epoch, expectedDistributedCoinsPerInternalGauge)
}

// Validate CL Incentive distribution
poolIncentives, err := s.App.ConcentratedLiquidityKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPool.GetId())
s.Require().NoError(err)

for i := 0; i < len(poolIncentives); i++ {
idx := 0
// the logic below is for indexing incentiveRecord, flips idx from 0,1,0,1 or 1,0,1,0 etc.
if len(tc.expectedCoinsPerInternalGauge) > 1 {
if epoch == 2 {
idx = 1 - (i % 2)
} else {
idx = i % 2
}
}
s.ValidateIncentiveRecord(clPool.GetId(), tc.expectedCoinsPerInternalGauge[idx], poolIncentives[i])
}

// Validate GAMM incentive distribution
balances := s.App.BankKeeper.GetAllBalances(s.Ctx, lockOwner)
if len(balances) != 0 {
var coins sdk.Coins
for _, bal := range tc.expectedCoinsPerInternalGauge {
coin := sdk.NewCoin(bal.Denom, bal.Amount.Mul(osmomath.NewIntFromUint64(epoch)))
coins = append(coins, coin)
}

s.Require().Equal(balances, coins)
}
}
})
}

}

// deepCopyGroup creates a deep copy of the passed in group.
func deepCopyGroup(src types.Group) types.Group {
gaugeRecords := make([]types.InternalGaugeRecord, len(src.InternalGaugeInfo.GaugeRecords))
Expand Down Expand Up @@ -1411,12 +1251,27 @@ func addGaugeRecords(gaugeInfo types.InternalGaugeInfo, gaugeRecords []types.Int
copy := deepCopyGaugeInfo(gaugeInfo)

for _, gaugeRecord := range gaugeRecords {
copy.GaugeRecords = append(copy.GaugeRecords, gaugeRecord)
copy.GaugeRecords = append(copy.GaugeRecords, deepCopyGaugeRecords(gaugeRecord))
copy.TotalWeight = copy.TotalWeight.Add(gaugeRecord.CurrentWeight)
}
return copy
}

// deepCopyGaugeRecords returns a deep copy of the passed in gauge record.
func deepCopyGaugeRecords(gaugeRecord types.InternalGaugeRecord) types.InternalGaugeRecord {
copy := gaugeRecord
copy.CurrentWeight = osmomath.NewIntFromBigInt(gaugeRecord.CurrentWeight.BigInt())
copy.CumulativeWeight = osmomath.NewIntFromBigInt(gaugeRecord.CumulativeWeight.BigInt())
return copy
}

// withRecordGaugeId returns a deep copy of the passed in gauge record with the gauge id set to the passed in value.
func withRecordGaugeId(gaugeRecord types.InternalGaugeRecord, GaugeID uint64) types.InternalGaugeRecord {
copy := deepCopyGaugeRecords(gaugeRecord)
copy.GaugeId = GaugeID
return copy
}

// withUpdatedVolumes takes in a group and a list of updated cumulative volumes (ordered) and updates the contents of the gauge to
// reflect these new volumes.
// It is only intended to be used to set expected values for test cases.
Expand Down
2 changes: 2 additions & 0 deletions x/incentives/keeper/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/osmosis-labs/osmosis/v19/x/incentives/types"
)

const PerpetualNumEpochsPaidOver = perpetualNumEpochsPaidOver

// AddGaugeRefByKey appends the provided gauge ID into an array associated with the provided key.
func (k Keeper) AddGaugeRefByKey(ctx sdk.Context, key []byte, gaugeID uint64) error {
return k.addGaugeRefByKey(ctx, key, gaugeID)
Expand Down
67 changes: 44 additions & 23 deletions x/incentives/keeper/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import (
epochtypes "github.com/osmosis-labs/osmosis/x/epochs/types"
)

// numEpochPaidOver is the number of epochs that must be given
// for a gauge to be perpetual. For any other number of epochs
// other than zero, the gauge is non-perpetual. Zero is invalid.
const perpetualNumEpochsPaidOver = 1

// getGaugesFromIterator iterates over everything in a gauge's iterator, until it reaches the end. Return all gauges iterated over.
func (k Keeper) getGaugesFromIterator(ctx sdk.Context, iterator db.Iterator) []types.Gauge {
gauges := []types.Gauge{}
Expand Down Expand Up @@ -51,8 +56,6 @@ func (k Keeper) setGauge(ctx sdk.Context, gauge *types.Gauge) error {
return err
}

gauge.IsLastNonPerpetualDistribution()

store.Set(gaugeStoreKey(gauge.Id), bz)
return nil
}
Expand Down Expand Up @@ -212,31 +215,57 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr
return gauge.Id, nil
}

// CreateGroup creates a new group. The group is 1:1 mapped to a group gauage that allocates rewards dynamically across its internal pool gauges based on the given splitting policy.
// Note: we should expect that the internal gauges consist of the gauges that are automatically created for each pool upon pool creation, as even non-perpetual
// external incentives would likely want to route through these.
// TODO: change this to take in list of pool IDs instead and fetch the gauge IDs under the hood.
// Tracked in issue https://github.com/osmosis-labs/osmosis/issues/6404
func (k Keeper) CreateGroup(ctx sdk.Context, coins sdk.Coins, numEpochPaidOver uint64, owner sdk.AccAddress, internalGaugeIds []uint64, gaugetype lockuptypes.LockQueryType) (uint64, error) {
if len(internalGaugeIds) == 0 {
return 0, fmt.Errorf("No internalGauge provided.")
// CreateGroup creates a new group. The group is 1:1 mapped to a group gauage that allocates rewards dynamically across its internal pool gauges based on
// the volume splitting policy.
// For each pool ID in the given slice, its main internal gauge is used to create gauge records to be associated with the Group.
// Note, that implies that only perpetual pool gauges can be associated with the Group.
// For Group's own distribution policy, a 1:1 group Gauge is created. This is the Gauge that receives incentives at the end of an epoch
// in the pool incentives as defined by the DistrRecord. The Group's Gauge can either be perpetual or non-perpetual.
// If numEpochPaidOver is 1, then the Group's Gauge is perpetual. Otherwise, it is non-perpetual.
// Returns nil on success.
// Returns error if:
// - given pool IDs slice is empty or has 1 pool only
// - numEpochPaidOver is 0
// - fails to initialize gauge information for every pool ID
// - fails to send coins from owner to the incentives module for the Group's Gauge
// - fails to set the Group's Gauge to state
func (k Keeper) CreateGroup(ctx sdk.Context, coins sdk.Coins, numEpochPaidOver uint64, owner sdk.AccAddress, poolIDs []uint64) (uint64, error) {
if len(poolIDs) == 0 {
return 0, types.ErrNoPoolIDsGiven
}
if len(poolIDs) == 1 {
return 0, types.OnePoolIDGroupError{PoolID: poolIDs[0]}
}
if numEpochPaidOver == 0 {
return 0, types.ErrZeroNumEpochsPaidOver
}

if gaugetype != lockuptypes.ByGroup {
return 0, fmt.Errorf("Invalid gauge type needs to be ByGroup, got %s.", gaugetype)
// Initialize gauge information for every pool ID.
initialInternalGaugeInfo, err := k.initGaugeInfo(ctx, poolIDs)
if err != nil {
return 0, err
}

// TODO: charge fixed fee. Make sure to update method spec and tests.
// https://github.com/osmosis-labs/osmosis/issues/6506

// TODO: subdao to bypass
// TODO: governance to bypass
// Make sure to update method spec and tests.
// https://github.com/osmosis-labs/osmosis/issues/6507

// TODO: remove gauge creation logic from here.
// Instead, call `CreateGauge` directly
// Update `CreateGauge` to be able to handle the group type.
// Make sure to update method spec and tests.
// https://github.com/osmosis-labs/osmosis/issues/6513
nextGaugeId := k.GetLastGaugeID(ctx) + 1

gauge := types.Gauge{
Id: nextGaugeId,
IsPerpetual: numEpochPaidOver == 1,
IsPerpetual: numEpochPaidOver == perpetualNumEpochsPaidOver,
DistributeTo: lockuptypes.QueryCondition{
LockQueryType: gaugetype,
LockQueryType: lockuptypes.ByGroup,
},
Coins: coins,
StartTime: ctx.BlockTime(),
Expand All @@ -251,14 +280,6 @@ func (k Keeper) CreateGroup(ctx sdk.Context, coins sdk.Coins, numEpochPaidOver u
return 0, err
}

// TODO: change CreateGroup method to take in pool IDs
// Tracked in issue https://github.com/osmosis-labs/osmosis/issues/6404
poolIDs := []uint64{}
initialInternalGaugeInfo, err := k.initGaugeInfo(ctx, poolIDs)
if err != nil {
return 0, err
}

newGroup := types.Group{
GroupGaugeId: nextGaugeId,
InternalGaugeInfo: initialInternalGaugeInfo,
Expand Down Expand Up @@ -462,7 +483,7 @@ func (k Keeper) chargeFeeIfSufficientFeeDenomBalance(ctx sdk.Context, address sd
return nil
}

// initGaugeInfo takes in a list of pool IDs and a splitting policy and returns a InternalGaugeInfo struct with weights initialized to zero.
// initGaugeInfo takes in a list of pool IDs and returns a InternalGaugeInfo struct with weights initialized to zero.
// Returns error if fails to retrieve gauge ID for a pool.
func (k Keeper) initGaugeInfo(ctx sdk.Context, poolIds []uint64) (types.InternalGaugeInfo, error) {
gaugeRecords := make([]types.InternalGaugeRecord, 0, len(poolIds))
Expand Down
Loading

0 comments on commit fc6c55a

Please sign in to comment.