diff --git a/app/test_common.go b/app/test_common.go index 9715d624..4e511384 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -56,13 +56,16 @@ var ( // TestApp is a simple wrapper around an App. It exposes internal keepers for use in integration tests. // This file also contains test helpers. Ideally they would be in separate package. // Basic Usage: -// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution. +// +// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution. +// // Advanced Usage: -// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)). -// However this leads to a lot of duplicated logic similar to InitGenesis methods. -// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. +// +// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)). +// However this leads to a lot of duplicated logic similar to InitGenesis methods. +// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. // and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state -// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. +// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. type TestApp struct { App } @@ -115,6 +118,10 @@ func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.li func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper } func (tApp TestApp) GetRouterKeeper() routerkeeper.Keeper { return tApp.routerKeeper } +func (tApp TestApp) GetKeys() map[string]*sdk.KVStoreKey { + return tApp.keys +} + // LegacyAmino returns the app's amino codec. func (app *App) LegacyAmino() *codec.LegacyAmino { return app.legacyAmino diff --git a/x/incentive/migrations/v3/keys.go b/x/incentive/migrations/v3/keys.go new file mode 100644 index 00000000..065b389a --- /dev/null +++ b/x/incentive/migrations/v3/keys.go @@ -0,0 +1,52 @@ +package v3 + +import ( + "fmt" + + "github.com/kava-labs/kava/x/incentive/types" +) + +// Legacy store key prefixes +var ( + EarnClaimKeyPrefix = []byte{0x18} // prefix for keys that store earn claims + EarnRewardIndexesKeyPrefix = []byte{0x19} // prefix for key that stores earn reward indexes + PreviousEarnRewardAccrualTimeKeyPrefix = []byte{0x20} // prefix for key that stores the previous time earn rewards accrued +) + +func LegacyAccrualTimeKeyFromClaimType(claimType types.ClaimType) []byte { + switch claimType { + case types.CLAIM_TYPE_HARD_BORROW: + panic("todo") + case types.CLAIM_TYPE_HARD_SUPPLY: + panic("todo") + case types.CLAIM_TYPE_EARN: + return PreviousEarnRewardAccrualTimeKeyPrefix + case types.CLAIM_TYPE_SAVINGS: + panic("todo") + case types.CLAIM_TYPE_SWAP: + panic("todo") + case types.CLAIM_TYPE_USDX_MINTING: + panic("todo") + default: + panic(fmt.Sprintf("unrecognized claim type: %s", claimType)) + } +} + +func LegacyRewardIndexesKeyFromClaimType(claimType types.ClaimType) []byte { + switch claimType { + case types.CLAIM_TYPE_HARD_BORROW: + panic("todo") + case types.CLAIM_TYPE_HARD_SUPPLY: + panic("todo") + case types.CLAIM_TYPE_EARN: + return EarnRewardIndexesKeyPrefix + case types.CLAIM_TYPE_SAVINGS: + panic("todo") + case types.CLAIM_TYPE_SWAP: + panic("todo") + case types.CLAIM_TYPE_USDX_MINTING: + panic("todo") + default: + panic(fmt.Sprintf("unrecognized claim type: %s", claimType)) + } +} diff --git a/x/incentive/migrations/v3/store.go b/x/incentive/migrations/v3/store.go new file mode 100644 index 00000000..5a09e68a --- /dev/null +++ b/x/incentive/migrations/v3/store.go @@ -0,0 +1,142 @@ +package v3 + +import ( + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// MigrateStore performs in-place migrations from incentive ConsensusVersion 2 to 3. +func MigrateStore(ctx sdk.Context, storeKey storetypes.StoreKey, cdc codec.BinaryCodec) error { + store := ctx.KVStore(storeKey) + + if err := MigrateEarnClaims(store, cdc); err != nil { + return err + } + + if err := MigrateAccrualTimes(store, cdc, types.CLAIM_TYPE_EARN); err != nil { + return err + } + + if err := MigrateRewardIndexes(store, cdc, types.CLAIM_TYPE_EARN); err != nil { + return err + } + + return nil +} + +// MigrateEarnClaims migrates earn claims from v2 to v3 +func MigrateEarnClaims(store sdk.KVStore, cdc codec.BinaryCodec) error { + newStore := prefix.NewStore(store, types.GetClaimKeyPrefix(types.CLAIM_TYPE_EARN)) + + oldStore := prefix.NewStore(store, EarnClaimKeyPrefix) + iterator := sdk.KVStorePrefixIterator(oldStore, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var c types.EarnClaim + cdc.MustUnmarshal(iterator.Value(), &c) + + if err := c.Validate(); err != nil { + return fmt.Errorf("invalid v2 EarnClaim: %w", err) + } + + // Convert to the new Claim type + newClaim := types.NewClaim( + types.CLAIM_TYPE_EARN, + c.Owner, + c.Reward, + c.RewardIndexes, + ) + + if err := newClaim.Validate(); err != nil { + return fmt.Errorf("invalid v3 EarnClaim: %w", err) + } + + // Set in the **newStore** for the new store prefix + newStore.Set(c.Owner, cdc.MustMarshal(&newClaim)) + + // Remove the old claim in the old store + oldStore.Delete(iterator.Key()) + } + + return nil +} + +// MigrateAccrualTimes migrates accrual times from v2 to v3 +func MigrateAccrualTimes( + store sdk.KVStore, + cdc codec.BinaryCodec, + claimType types.ClaimType, +) error { + newStore := prefix.NewStore(store, types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) + + // Need prefix.NewStore instead of using it directly in the iterator, as + // there would be an extra space in the key + legacyPrefix := LegacyAccrualTimeKeyFromClaimType(claimType) + oldStore := prefix.NewStore(store, legacyPrefix) + iterator := sdk.KVStorePrefixIterator(oldStore, []byte{}) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var blockTime time.Time + if err := blockTime.UnmarshalBinary(iterator.Value()); err != nil { + panic(err) + } + + sourceID := string(iterator.Key()) + + at := types.NewAccrualTime(claimType, sourceID, blockTime) + if err := at.Validate(); err != nil { + return fmt.Errorf("invalid v3 accrual time for claim type %s: %w", claimType, err) + } + + // Set in the **newStore** for the new store prefix + bz := cdc.MustMarshal(&at) + newStore.Set(types.GetKeyFromSourceID(sourceID), bz) + + // Remove the old accrual time in the old store + oldStore.Delete(iterator.Key()) + } + + return nil +} + +// MigrateRewardIndexes migrates reward indexes from v2 to v3 +func MigrateRewardIndexes( + store sdk.KVStore, + cdc codec.BinaryCodec, + claimType types.ClaimType, +) error { + newStore := prefix.NewStore(store, types.GetRewardIndexesKeyPrefix(claimType)) + + legacyPrefix := LegacyRewardIndexesKeyFromClaimType(claimType) + oldStore := prefix.NewStore(store, legacyPrefix) + iterator := sdk.KVStorePrefixIterator(oldStore, []byte{}) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var proto types.RewardIndexesProto + cdc.MustUnmarshal(iterator.Value(), &proto) + + sourceID := string(iterator.Key()) + + rewardIndex := types.NewTypedRewardIndexes( + claimType, + sourceID, + proto.RewardIndexes, + ) + + bz := cdc.MustMarshal(&rewardIndex) + newStore.Set(types.GetKeyFromSourceID(sourceID), bz) + + // Remove the old reward indexes in the old store + oldStore.Delete(iterator.Key()) + } + + return nil +} diff --git a/x/incentive/migrations/v3/store_test.go b/x/incentive/migrations/v3/store_test.go new file mode 100644 index 00000000..33f984f9 --- /dev/null +++ b/x/incentive/migrations/v3/store_test.go @@ -0,0 +1,159 @@ +package v3_test + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" + "github.com/stretchr/testify/suite" + + v3 "github.com/kava-labs/kava/x/incentive/migrations/v3" +) + +type StoreMigrateTestSuite struct { + testutil.IntegrationTester + + Addrs []sdk.AccAddress + + keeper testutil.TestKeeper + storeKey sdk.StoreKey + cdc codec.Codec +} + +func TestStoreMigrateTestSuite(t *testing.T) { + suite.Run(t, new(StoreMigrateTestSuite)) +} + +func (suite *StoreMigrateTestSuite) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = testutil.TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, suite.Addrs = app.GeneratePrivKeyAddressPairs(5) + suite.cdc = suite.App.AppCodec() + suite.storeKey = suite.App.GetKeys()[types.StoreKey] + + suite.StartChain() +} + +func (suite *StoreMigrateTestSuite) TestMigrateEarnClaims() { + store := suite.Ctx.KVStore(suite.storeKey) + + // Create v2 earn claims + claim1 := types.NewEarnClaim( + suite.Addrs[0], + sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), + types.MultiRewardIndexes{ + types.NewMultiRewardIndex("bnb-a", types.RewardIndexes{ + types.NewRewardIndex("bnb", sdk.NewDec(1)), + }), + }, + ) + + claim2 := types.NewEarnClaim( + suite.Addrs[1], + sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100))), + types.MultiRewardIndexes{ + types.NewMultiRewardIndex("ukava", types.RewardIndexes{ + types.NewRewardIndex("ukava", sdk.NewDec(1)), + }), + }, + ) + + suite.keeper.SetEarnClaim(suite.Ctx, claim1) + suite.keeper.SetEarnClaim(suite.Ctx, claim2) + + // Run earn claim migrations + err := v3.MigrateEarnClaims(store, suite.cdc) + suite.Require().NoError(err) + + // Check that the claim was migrated correctly + newClaim1, found := suite.keeper.Store.GetClaim(suite.Ctx, types.CLAIM_TYPE_EARN, claim1.Owner) + suite.Require().True(found) + suite.Require().Equal(claim1.Owner, newClaim1.Owner) + + newClaim2, found := suite.keeper.Store.GetClaim(suite.Ctx, types.CLAIM_TYPE_EARN, claim2.Owner) + suite.Require().True(found) + suite.Require().Equal(claim2.Owner, newClaim2.Owner) + + // Ensure removed from old store + _, found = suite.keeper.GetEarnClaim(suite.Ctx, claim1.Owner) + suite.Require().False(found) + + _, found = suite.keeper.GetEarnClaim(suite.Ctx, claim2.Owner) + suite.Require().False(found) +} + +func (suite *StoreMigrateTestSuite) TestMigrateAccrualTimes() { + store := suite.Ctx.KVStore(suite.storeKey) + vaultDenom1 := "ukava" + vaultDenom2 := "usdc" + + // Create v2 accrual times + accrualTime1 := time.Now() + accrualTime2 := time.Now().Add(time.Hour * 24) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, vaultDenom1, accrualTime1) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, vaultDenom2, accrualTime2) + + // Run accrual time migrations + err := v3.MigrateAccrualTimes(store, suite.cdc, types.CLAIM_TYPE_EARN) + suite.Require().NoError(err) + + // Check that the accrual time was migrated correctly + newAccrualTime1, found := suite.keeper.Store.GetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom1) + suite.Require().True(found) + suite.Require().Equal(accrualTime1.Unix(), newAccrualTime1.Unix()) + + newAccrualTime2, found := suite.keeper.Store.GetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom2) + suite.Require().True(found) + suite.Require().Equal(accrualTime2.Unix(), newAccrualTime2.Unix()) + + // Ensure removed from old store + _, found = suite.keeper.GetEarnRewardAccrualTime(suite.Ctx, vaultDenom1) + suite.Require().False(found) + _, found = suite.keeper.GetEarnRewardAccrualTime(suite.Ctx, vaultDenom2) + suite.Require().False(found) +} + +func (suite *StoreMigrateTestSuite) TestMigrateRewardIndexes() { + store := suite.Ctx.KVStore(suite.storeKey) + vaultDenom1 := "ukava" + vaultDenom2 := "usdc" + + rewardIndexes1 := types.RewardIndexes{ + types.NewRewardIndex("ukava", sdk.NewDec(1)), + types.NewRewardIndex("hard", sdk.NewDec(2)), + } + rewardIndexes2 := types.RewardIndexes{ + types.NewRewardIndex("ukava", sdk.NewDec(4)), + types.NewRewardIndex("swp", sdk.NewDec(10)), + } + + suite.keeper.SetEarnRewardIndexes(suite.Ctx, vaultDenom1, rewardIndexes1) + suite.keeper.SetEarnRewardIndexes(suite.Ctx, vaultDenom2, rewardIndexes2) + + err := v3.MigrateRewardIndexes(store, suite.cdc, types.CLAIM_TYPE_EARN) + suite.Require().NoError(err) + + newRewardIndexes1, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom1) + suite.Require().True(found) + suite.Require().Equal(rewardIndexes1, newRewardIndexes1) + + newRewardIndexes2, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom2) + suite.Require().True(found) + suite.Require().Equal(rewardIndexes2, newRewardIndexes2) + + // Ensure removed from old store + _, found = suite.keeper.GetEarnRewardIndexes(suite.Ctx, vaultDenom1) + suite.Require().False(found) + + _, found = suite.keeper.GetEarnRewardIndexes(suite.Ctx, vaultDenom2) + suite.Require().False(found) +} diff --git a/x/incentive/module.go b/x/incentive/module.go index 16f89f35..eae93d0d 100644 --- a/x/incentive/module.go +++ b/x/incentive/module.go @@ -21,6 +21,9 @@ import ( "github.com/kava-labs/kava/x/incentive/types" ) +// ConsensusVersion defines the current module consensus version. +const ConsensusVersion = 3 + var ( _ module.AppModule = AppModule{} _ module.AppModuleBasic = AppModuleBasic{} @@ -81,7 +84,7 @@ func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sd // ConsensusVersion implements AppModule/ConsensusVersion. func (AppModule) ConsensusVersion() uint64 { - return 1 + return ConsensusVersion } // GetTxCmd returns the root tx command for the incentive module. diff --git a/x/incentive/types/genesis.go b/x/incentive/types/genesis.go index 05a7afc9..d0127316 100644 --- a/x/incentive/types/genesis.go +++ b/x/incentive/types/genesis.go @@ -200,6 +200,10 @@ func NewAccrualTime(claimType ClaimType, collateralType string, prevTime time.Ti // Validate performs validation of AccrualTime func (at AccrualTime) Validate() error { + if at.PreviousAccumulationTime.IsZero() { + return fmt.Errorf("previous accumulation time cannot be zero") + } + if err := at.ClaimType.Validate(); err != nil { return err }