From 7d853612402c7bd5c1a97414aa42948647f356c7 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Fri, 20 Aug 2021 03:12:34 +0100 Subject: [PATCH] Fix hard incentive state in migrations (#1001) * extract hard claim migration function * tidy up claim delegator index migration * fix invalid hard claims * fix comment missed in merge conflict reolution * fix tests --- migrate/v0_15/incentive.go | 253 ++++++++++++++++++++--- migrate/v0_15/incentive_test.go | 343 ++++++++++++++++++++++++++++++++ migrate/v0_15/migrate.go | 6 +- migrate/v0_15/migrate_test.go | 126 +++++++++++- 4 files changed, 688 insertions(+), 40 deletions(-) diff --git a/migrate/v0_15/incentive.go b/migrate/v0_15/incentive.go index bf5cda43..9dd8ccb7 100644 --- a/migrate/v0_15/incentive.go +++ b/migrate/v0_15/incentive.go @@ -2,18 +2,21 @@ package v0_15 import ( "fmt" + "sort" "time" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + v0_15hard "github.com/kava-labs/kava/x/hard/types" + v0_15cdp "github.com/kava-labs/kava/x/cdp/types" v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_15incentive "github.com/kava-labs/kava/x/incentive/types" ) // Incentive migrates from a v0.14 incentive genesis state to a v0.15 incentive genesis state -func Incentive(cdc *codec.Codec, incentiveGS v0_14incentive.GenesisState, cdps v0_15cdp.CDPs) v0_15incentive.GenesisState { +func Incentive(cdc *codec.Codec, incentiveGS v0_14incentive.GenesisState, cdps v0_15cdp.CDPs, hardGS v0_15hard.GenesisState) v0_15incentive.GenesisState { // Migrate params claimMultipliers := v0_15incentive.Multipliers{} for _, m := range incentiveGS.Params.ClaimMultipliers { @@ -99,37 +102,20 @@ func Incentive(cdc *codec.Codec, incentiveGS v0_14incentive.GenesisState, cdps v usdxMintingClaims = addRewards(usdxMintingClaims, missedRewards) // Migrate Hard protocol claims (includes creating new Delegator claims) - hardClaims := v0_15incentive.HardLiquidityProviderClaims{} - delegatorClaims := v0_15incentive.DelegatorClaims{} - for _, claim := range incentiveGS.HardLiquidityProviderClaims { - // Migrate supply multi reward indexes - supplyMultiRewardIndexes := migrateMultiRewardIndexes(claim.SupplyRewardIndexes) - - // Migrate borrow multi reward indexes - borrowMultiRewardIndexes := migrateMultiRewardIndexes(claim.BorrowRewardIndexes) - - // Migrate delegator reward indexes to multi reward indexes inside DelegatorClaims - delegatorMultiRewardIndexes := v0_15incentive.MultiRewardIndexes{} - delegatorRewardIndexes := v0_15incentive.RewardIndexes{} - for _, ri := range claim.DelegatorRewardIndexes { - // TODO add checks to ensure old reward indexes are as expected - delegatorRewardIndex := v0_15incentive.NewRewardIndex(v0_14incentive.HardLiquidityRewardDenom, ri.RewardFactor) - delegatorRewardIndexes = append(delegatorRewardIndexes, delegatorRewardIndex) - } - // TODO should this include indexes if none exist on the old claim? - delegatorMultiRewardIndex := v0_15incentive.NewMultiRewardIndex(v0_14incentive.BondDenom, delegatorRewardIndexes) - delegatorMultiRewardIndexes = append(delegatorMultiRewardIndexes, delegatorMultiRewardIndex) - - // TODO: It's impossible to distinguish between rewards from delegation vs. liquidity providing - // as they're all combined inside claim.Reward, so I'm just putting them all inside - // the hard claim to avoid duplicating rewards. - delegatorClaim := v0_15incentive.NewDelegatorClaim(claim.Owner, sdk.NewCoins(), delegatorMultiRewardIndexes) - delegatorClaims = append(delegatorClaims, delegatorClaim) - - hardClaim := v0_15incentive.NewHardLiquidityProviderClaim(claim.Owner, claim.Reward, - supplyMultiRewardIndexes, borrowMultiRewardIndexes) - hardClaims = append(hardClaims, hardClaim) + hardClaims, delegatorClaims, err := migrateHardLiquidityProviderClaims(incentiveGS.HardLiquidityProviderClaims) + if err != nil { + panic(fmt.Sprintf("could not migrate hard claims: %v", err.Error())) } + hardClaims = addMissingHardClaims( + hardClaims, + hardGS.Deposits, hardGS.Borrows, + hardSupplyGenesisRewardState.MultiRewardIndexes, hardBorrowGenesisRewardState.MultiRewardIndexes, + ) + hardClaims = alignClaimIndexes( + hardClaims, + hardGS.Deposits, hardGS.Borrows, + hardSupplyGenesisRewardState.MultiRewardIndexes, hardBorrowGenesisRewardState.MultiRewardIndexes, + ) // Add Swap Claims swapClaims := v0_15incentive.DefaultSwapClaims @@ -148,6 +134,61 @@ func Incentive(cdc *codec.Codec, incentiveGS v0_14incentive.GenesisState, cdps v ) } +func migrateHardLiquidityProviderClaims(oldClaims v0_14incentive.HardLiquidityProviderClaims) (v0_15incentive.HardLiquidityProviderClaims, v0_15incentive.DelegatorClaims, error) { + + hardClaims := v0_15incentive.HardLiquidityProviderClaims{} + delegatorClaims := v0_15incentive.DelegatorClaims{} + + for _, claim := range oldClaims { + // Migrate supply multi reward indexes + supplyMultiRewardIndexes := migrateMultiRewardIndexes(claim.SupplyRewardIndexes) + + // Migrate borrow multi reward indexes + borrowMultiRewardIndexes := migrateMultiRewardIndexes(claim.BorrowRewardIndexes) + + // Migrate delegator reward indexes to multi reward indexes inside DelegatorClaims + delegatorMultiRewardIndexes, err := migrateDelegatorRewardIndexes(claim.DelegatorRewardIndexes) + if err != nil { + return nil, nil, fmt.Errorf("invalid claim found '%s': %w", claim.Owner, err) + } + + // It's impossible to distinguish between rewards from delegation vs. liquidity provisioning + // as they're all combined inside claim.Reward, so put them all inside the hard claim to + // avoid duplicating rewards. + delegatorClaim := v0_15incentive.NewDelegatorClaim(claim.Owner, sdk.NewCoins(), delegatorMultiRewardIndexes) + delegatorClaims = append(delegatorClaims, delegatorClaim) + + hardClaim := v0_15incentive.NewHardLiquidityProviderClaim(claim.Owner, claim.Reward, + supplyMultiRewardIndexes, borrowMultiRewardIndexes) + hardClaims = append(hardClaims, hardClaim) + } + + return hardClaims, delegatorClaims, nil +} + +func migrateDelegatorRewardIndexes(oldIndexes v0_14incentive.RewardIndexes) (v0_15incentive.MultiRewardIndexes, error) { + newRIs := v0_15incentive.RewardIndexes{} + + if len(oldIndexes) > 1 { + return nil, fmt.Errorf("delegator claims should not have more than one rewarded denom") + } + + if len(oldIndexes) > 0 { + oldRI := oldIndexes[0] + + if oldRI.CollateralType != v0_15incentive.BondDenom { + return nil, fmt.Errorf("delegator claims should only reward staked '%s', not '%s'", v0_15incentive.BondDenom, oldRI.CollateralType) + } + newRIs = newRIs.With(v0_14incentive.HardLiquidityRewardDenom, oldRI.RewardFactor) + } + + newIndexes := v0_15incentive.MultiRewardIndexes{ + v0_15incentive.NewMultiRewardIndex(v0_15incentive.BondDenom, newRIs), + } + + return newIndexes, nil +} + // migrateUSDXMintingClaims converts the a slice of v0.14 USDX minting claims into v0.15 USDX minting claims. // As both types are the same underneath, this just converts types and does no other modification. func migrateUSDXMintingClaims(oldClaims v0_14incentive.USDXMintingClaims) v0_15incentive.USDXMintingClaims { @@ -277,3 +318,153 @@ func migrateRewardIndexes(oldIndexes v0_14incentive.RewardIndexes) v0_15incentiv } return newIndexes } + +// addMissingHardClaims checks for hard deposits and borrows without claims. If found it creates new +// claims with indexes set to the global indexes so rewards start accumulating from launch. +func addMissingHardClaims(hardClaims v0_15incentive.HardLiquidityProviderClaims, deposits v0_15hard.Deposits, borrows v0_15hard.Borrows, depositGlobalIndexes, borrowGlobalIndexes v0_15incentive.MultiRewardIndexes) v0_15incentive.HardLiquidityProviderClaims { + + missingClaims := map[string]v0_15incentive.HardLiquidityProviderClaim{} + + for _, deposit := range deposits { + _, found := getHardClaimByOwner(hardClaims, deposit.Depositor) + + if !found { + missingClaims[deposit.Depositor.String()] = v0_15incentive.NewHardLiquidityProviderClaim( + deposit.Depositor, + sdk.NewCoins(), // do not calculate missing rewards + getIndexesForCoins(deposit.Amount, depositGlobalIndexes), + v0_15incentive.MultiRewardIndexes{}, // the depositor may also have a borrow, this will be added below + ) + } + } + + for _, borrow := range borrows { + _, found := getHardClaimByOwner(hardClaims, borrow.Borrower) + + if !found { + c, ok := missingClaims[borrow.Borrower.String()] + borrowIndexes := getIndexesForCoins(borrow.Amount, borrowGlobalIndexes) + if ok { + c.BorrowRewardIndexes = borrowIndexes + missingClaims[borrow.Borrower.String()] = c + } else { + missingClaims[borrow.Borrower.String()] = v0_15incentive.NewHardLiquidityProviderClaim( + borrow.Borrower, + sdk.NewCoins(), // do not calculate missing rewards + v0_15incentive.MultiRewardIndexes{}, // this borrow does not have any deposits as it would have been found above + borrowIndexes, + ) + } + } + } + + // New claims need to be sorted to ensure the output genesis state is the same for everyone. + // Sorting by address should be give deterministic order as the addresses are unique. + sortedNewClaims := make(v0_15incentive.HardLiquidityProviderClaims, 0, len(missingClaims)) + for _, claim := range missingClaims { + sortedNewClaims = append(sortedNewClaims, claim) + } + sort.Slice(sortedNewClaims, func(i, j int) bool { + return sortedNewClaims[i].Owner.String() < sortedNewClaims[j].Owner.String() + }) + + return append(hardClaims, sortedNewClaims...) +} + +// getIndexesForCoins returns indexes with collateral types matching the coins denoms. RewardIndexes values are taken +// from the provided indexes, or left empty if not found. +func getIndexesForCoins(coins sdk.Coins, indexes v0_15incentive.MultiRewardIndexes) v0_15incentive.MultiRewardIndexes { + newIndexes := v0_15incentive.MultiRewardIndexes{} + for _, c := range coins { + ri, found := indexes.Get(c.Denom) + if !found { + ri = v0_15incentive.RewardIndexes{} + } + newIndexes = newIndexes.With(c.Denom, ri) + } + return newIndexes +} + +// getHardClaimByOwner picks out the first claim matching an address. +func getHardClaimByOwner(claims v0_15incentive.HardLiquidityProviderClaims, owner sdk.AccAddress) (v0_15incentive.HardLiquidityProviderClaim, bool) { + for _, claim := range claims { + if claim.Owner.Equals(owner) { + return claim, true + } + } + return v0_15incentive.HardLiquidityProviderClaim{}, false +} + +// getHardDepositByOwner picks out the first deposit matching an address. +func getHardDepositByOwner(deposits v0_15hard.Deposits, owner sdk.AccAddress) (v0_15hard.Deposit, bool) { + for _, deposit := range deposits { + if deposit.Depositor.Equals(owner) { + return deposit, true + } + } + return v0_15hard.Deposit{}, false +} + +// getHardBorrowByOwner picks out the first borrow matching an address. +func getHardBorrowByOwner(borrows v0_15hard.Borrows, owner sdk.AccAddress) (v0_15hard.Borrow, bool) { + for _, borrow := range borrows { + if borrow.Borrower.Equals(owner) { + return borrow, true + } + } + return v0_15hard.Borrow{}, false +} + +// alignClaimIndexes fixes the supply and borrow indexes on the hard claims to ensure they match the deposits and borrows. +func alignClaimIndexes(claims v0_15incentive.HardLiquidityProviderClaims, deposits v0_15hard.Deposits, borrows v0_15hard.Borrows, depositGlobalIndexes, borrowGlobalIndexes v0_15incentive.MultiRewardIndexes) v0_15incentive.HardLiquidityProviderClaims { + newClaims := make(v0_15incentive.HardLiquidityProviderClaims, 0, len(claims)) + + for _, claim := range claims { + + deposit, found := getHardDepositByOwner(deposits, claim.Owner) + amt := deposit.Amount + if !found { + amt = sdk.NewCoins() + } + claim.SupplyRewardIndexes = alignIndexes(claim.SupplyRewardIndexes, amt, depositGlobalIndexes) + + borrow, found := getHardBorrowByOwner(borrows, claim.Owner) + amt = borrow.Amount + if !found { + amt = sdk.NewCoins() + } + claim.BorrowRewardIndexes = alignIndexes(claim.BorrowRewardIndexes, amt, depositGlobalIndexes) + + newClaims = append(newClaims, claim) + } + + return newClaims +} + +// alignIndexes adds or remove items from indexes so all the collateral types match the coins denoms. +// Missing index values are filled from globalIndexes if found. Otherwise they're set to empty. +// It preserves the order of the original index to avoid unnecessary churn in the migrated claims. +func alignIndexes(indexes v0_15incentive.MultiRewardIndexes, coins sdk.Coins, globalIndexes v0_15incentive.MultiRewardIndexes) v0_15incentive.MultiRewardIndexes { + newIndexes := indexes + + // add missing indexes + for _, coin := range coins { + if _, found := indexes.Get(coin.Denom); !found { + ri, f := globalIndexes.Get(coin.Denom) + if !f { + ri = v0_15incentive.RewardIndexes{} + } + newIndexes = newIndexes.With(coin.Denom, ri) + } + } + + // remove extra indexes + for _, index := range indexes { + if coins.AmountOf(index.CollateralType).Equal(sdk.ZeroInt()) { + // RemoveRewardIndex returns a copy of the underlying array so the loop is not distupted + newIndexes = newIndexes.RemoveRewardIndex(index.CollateralType) + } + } + + return newIndexes +} diff --git a/migrate/v0_15/incentive_test.go b/migrate/v0_15/incentive_test.go index 383e5b0d..dcf88b4e 100644 --- a/migrate/v0_15/incentive_test.go +++ b/migrate/v0_15/incentive_test.go @@ -7,7 +7,9 @@ import ( "github.com/stretchr/testify/require" v0_15cdp "github.com/kava-labs/kava/x/cdp/types" + v0_15hard "github.com/kava-labs/kava/x/hard/types" "github.com/kava-labs/kava/x/incentive" + v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_15incentive "github.com/kava-labs/kava/x/incentive/types" ) @@ -15,6 +17,347 @@ import ( // It is an alias for sdk.MustNewDecFromStr. var d = sdk.MustNewDecFromStr +func TestMigrateDelegatorRewardIndexes(t *testing.T) { + type expect struct { + err bool + indexes v0_15incentive.MultiRewardIndexes + } + testCases := []struct { + name string + oldRIs v0_14incentive.RewardIndexes + expected expect + }{ + { + name: "single index is migrated correctly", + oldRIs: v0_14incentive.RewardIndexes{{ + CollateralType: "ukava", + RewardFactor: sdk.MustNewDecFromStr("0.1"), + }}, + expected: expect{ + indexes: v0_15incentive.MultiRewardIndexes{{ + CollateralType: "ukava", + RewardIndexes: v0_15incentive.RewardIndexes{{ + CollateralType: "hard", + RewardFactor: sdk.MustNewDecFromStr("0.1"), + }}, + }}, + }, + }, + { + name: "empty index is migrated correctly", + oldRIs: v0_14incentive.RewardIndexes{}, + expected: expect{ + indexes: v0_15incentive.MultiRewardIndexes{{ + CollateralType: "ukava", + RewardIndexes: v0_15incentive.RewardIndexes{}, + }}, + }, + }, + { + name: "too many indexes errors", + oldRIs: v0_14incentive.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: sdk.MustNewDecFromStr("0.1"), + }, + { + CollateralType: "btcb", + RewardFactor: sdk.MustNewDecFromStr("0.2"), + }, + }, + expected: expect{ + err: true, + }, + }, + { + name: "incorrect rewarded denom errors", + oldRIs: v0_14incentive.RewardIndexes{{ + CollateralType: "btcb", + RewardFactor: sdk.MustNewDecFromStr("0.1"), + }}, + expected: expect{ + err: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualIndexes, err := migrateDelegatorRewardIndexes(tc.oldRIs) + if tc.expected.err { + require.Error(t, err) + } else { + require.Equal(t, tc.expected.indexes, actualIndexes) + } + }) + } +} + +func TestAddMissingHardClaims_Basic(t *testing.T) { + claims := v0_15incentive.HardLiquidityProviderClaims{ + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("hard", 1e9)), + v0_15incentive.MultiRewardIndexes{}, + v0_15incentive.MultiRewardIndexes{}, + ), + } + + deposits := v0_15hard.Deposits{ + v0_15hard.NewDeposit( + sdk.AccAddress("address1"), + nil, + nil, + ), + v0_15hard.NewDeposit( + sdk.AccAddress("address2"), + nil, + nil, + ), + } + borrows := v0_15hard.Borrows{ + v0_15hard.NewBorrow( + sdk.AccAddress("address1"), + nil, // only need the owner address for this test + nil, + ), + v0_15hard.NewBorrow( + sdk.AccAddress("address3"), + nil, // only need the owner address for this test + nil, + ), + } + + actualClaims := addMissingHardClaims(claims, deposits, borrows, nil, nil) + + expectedClaims := v0_15incentive.HardLiquidityProviderClaims{ + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("hard", 1e9)), + v0_15incentive.MultiRewardIndexes{}, + v0_15incentive.MultiRewardIndexes{}, + ), + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address3"), + sdk.NewCoins(), + v0_15incentive.MultiRewardIndexes{}, + v0_15incentive.MultiRewardIndexes{}, + ), + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address2"), + sdk.NewCoins(), + v0_15incentive.MultiRewardIndexes{}, + v0_15incentive.MultiRewardIndexes{}, + ), + } + + require.Equal(t, expectedClaims, actualClaims) +} + +func TestAlignClaimIndexes_Basic(t *testing.T) { + claims := v0_15incentive.HardLiquidityProviderClaims{ + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("hard", 1e9)), + v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", + RewardIndexes: nil, + }, + }, + v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "busd", + RewardIndexes: nil, + }, + { + CollateralType: "xrpb", + RewardIndexes: nil, + }, + }, + ), + } + + deposits := v0_15hard.Deposits{ + v0_15hard.NewDeposit( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("ukava", 1)), + nil, + ), + v0_15hard.NewDeposit( + sdk.AccAddress("address2"), + nil, + nil, + ), + } + borrows := v0_15hard.Borrows{ + v0_15hard.NewBorrow( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("xrpb", 1)), + nil, + ), + v0_15hard.NewBorrow( + sdk.AccAddress("address3"), + nil, + nil, + ), + } + + actualClaims := alignClaimIndexes(claims, deposits, borrows, nil, nil) + + expectedClaims := v0_15incentive.HardLiquidityProviderClaims{ + v0_15incentive.NewHardLiquidityProviderClaim( + sdk.AccAddress("address1"), + sdk.NewCoins(sdk.NewInt64Coin("hard", 1e9)), + v0_15incentive.MultiRewardIndexes{{ + CollateralType: "ukava", + RewardIndexes: nil, + }}, + v0_15incentive.MultiRewardIndexes{{ + CollateralType: "xrpb", + RewardIndexes: nil, + }}, + ), + } + + require.Equal(t, expectedClaims, actualClaims) +} + +func TestAlignIndexes(t *testing.T) { + globalRI := v0_15incentive.RewardIndexes{{ + CollateralType: "hard", + RewardFactor: sdk.OneDec(), + }} + globalIndexes := v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: globalRI, + }, + { + CollateralType: "hard", + RewardIndexes: globalRI, + }, + } + + testCases := []struct { + name string + indexes v0_15incentive.MultiRewardIndexes + coins sdk.Coins + expected v0_15incentive.MultiRewardIndexes + }{ + { + name: "indexes matching coins are unchanged", + indexes: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", // not in alphabetic order + RewardIndexes: nil, + }, + }, + coins: sdk.NewCoins(sdk.NewInt64Coin("hard", 1), sdk.NewInt64Coin("ukava", 1)), + expected: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", // order is preserved + RewardIndexes: nil, + }, + }, + }, + { + name: "missing indexes are added from global values", + indexes: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + }, + coins: sdk.NewCoins(sdk.NewInt64Coin("hard", 1), sdk.NewInt64Coin("ukava", 1)), + expected: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", + RewardIndexes: globalRI, + }, + }, + }, + { + name: "extra indexes are removed", + indexes: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", + RewardIndexes: nil, + }, + }, + coins: sdk.NewCoins(sdk.NewInt64Coin("hard", 1)), + expected: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "hard", + RewardIndexes: nil, + }, + }, + }, + { + name: "missing indexes are added even when not in global values", + indexes: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "hard", + RewardIndexes: nil, + }, + }, + coins: sdk.NewCoins(sdk.NewInt64Coin("hard", 1), sdk.NewInt64Coin("btcb", 1)), + expected: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "hard", + RewardIndexes: nil, + }, + { + CollateralType: "btcb", + RewardIndexes: v0_15incentive.RewardIndexes{}, + }, + }, + }, + { + name: "empty coins results in empty indexes", + indexes: v0_15incentive.MultiRewardIndexes{ + { + CollateralType: "ukava", + RewardIndexes: nil, + }, + { + CollateralType: "hard", + RewardIndexes: nil, + }, + }, + coins: sdk.NewCoins(), + expected: v0_15incentive.MultiRewardIndexes{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, alignIndexes(tc.indexes, tc.coins, globalIndexes)) + }) + } + +} + func TestReplaceUSDXClaimIndexes(t *testing.T) { claims := incentive.USDXMintingClaims{ incentive.NewUSDXMintingClaim( diff --git a/migrate/v0_15/migrate.go b/migrate/v0_15/migrate.go index 321e114b..4cff1a83 100644 --- a/migrate/v0_15/migrate.go +++ b/migrate/v0_15/migrate.go @@ -17,6 +17,7 @@ import ( v0_15cdp "github.com/kava-labs/kava/x/cdp/types" v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14" v0_15committee "github.com/kava-labs/kava/x/committee/types" + v0_15hard "github.com/kava-labs/kava/x/hard/types" v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_15incentive "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/kavadist" @@ -78,11 +79,14 @@ func MigrateAppState(v0_14AppState genutil.AppMap) { v0_14Codec.MustUnmarshalJSON(v0_14AppState[v0_14incentive.ModuleName], &incentiveGenState) delete(v0_14AppState, v0_14incentive.ModuleName) + var hardGenState v0_15hard.GenesisState // v0_14 hard genesis state is the same as v0_15 + v0_15Codec.MustUnmarshalJSON(v0_14AppState[v0_15hard.ModuleName], &hardGenState) + // unmarshal all cdps using v0_15 types as there has been no changes since v0_14 var cdpGenState v0_15cdp.GenesisState v0_15Codec.MustUnmarshalJSON(v0_14AppState[v0_15cdp.ModuleName], &cdpGenState) - v0_14AppState[v0_15incentive.ModuleName] = v0_15Codec.MustMarshalJSON(Incentive(v0_15Codec, incentiveGenState, cdpGenState.CDPs)) + v0_14AppState[v0_15incentive.ModuleName] = v0_15Codec.MustMarshalJSON(Incentive(v0_15Codec, incentiveGenState, cdpGenState.CDPs, hardGenState)) } // Migrate commmittee app state diff --git a/migrate/v0_15/migrate_test.go b/migrate/v0_15/migrate_test.go index a743da4d..82a27dd8 100644 --- a/migrate/v0_15/migrate_test.go +++ b/migrate/v0_15/migrate_test.go @@ -21,7 +21,7 @@ import ( v0_15cdp "github.com/kava-labs/kava/x/cdp/types" v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14" v0_15committee "github.com/kava-labs/kava/x/committee/types" - "github.com/kava-labs/kava/x/hard" + v0_15hard "github.com/kava-labs/kava/x/hard/types" v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_15incentive "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/swap" @@ -80,14 +80,86 @@ func TestIncentive_Full(t *testing.T) { var oldIncentiveGenState v0_14incentive.GenesisState cdc.MustUnmarshalJSON(oldState[v0_14incentive.ModuleName], &oldIncentiveGenState) + var oldHardGenState v0_15hard.GenesisState + cdc.MustUnmarshalJSON(oldState[v0_15hard.ModuleName], &oldHardGenState) + var oldCDPGenState v0_15cdp.GenesisState cdc.MustUnmarshalJSON(oldState[v0_15cdp.ModuleName], &oldCDPGenState) - newGenState := Incentive(app.MakeCodec(), oldIncentiveGenState, oldCDPGenState.CDPs) + newGenState := Incentive(app.MakeCodec(), oldIncentiveGenState, oldCDPGenState.CDPs, oldHardGenState) require.NoError(t, newGenState.Validate()) // TODO check params, indexes, and accumulation times + // ensure every hard deposit has a claim with correct indexes + for _, deposit := range oldHardGenState.Deposits { + foundClaim := false + for _, claim := range newGenState.HardLiquidityProviderClaims { + if claim.Owner.Equals(deposit.Depositor) { + foundClaim = true + // check indexes are valid + err := collateralTypesMatch(deposit.Amount, claim.SupplyRewardIndexes) + require.NoErrorf(t, err, "invalid claim %s, %s", claim, deposit) + break + } + } + require.True(t, foundClaim, "missing claim for hard deposit") + + // also ensure hard deposit indexes are valid + for _, i := range deposit.Index { + require.Truef(t, i.Value.GTE(sdk.OneDec()), "found invalid hard deposit index %s", deposit) + } + } + + // ensure every hard borrow has a claim with correct indexes + for _, borrow := range oldHardGenState.Borrows { + foundClaim := false + for _, claim := range newGenState.HardLiquidityProviderClaims { + if claim.Owner.Equals(borrow.Borrower) { + foundClaim = true + // check indexes are valid + err := collateralTypesMatch(borrow.Amount, claim.BorrowRewardIndexes) + require.NoErrorf(t, err, "invalid claim %s, %s", claim, borrow) + break + } + } + require.True(t, foundClaim, "missing claim for hard borrow") + + // also ensure hard borrow indexes are valid + for _, i := range borrow.Index { + require.Truef(t, i.Value.GTE(sdk.OneDec()), "found invalid hard borrow index %s", borrow) + } + } + + // ensure all claim indexes are ≤ global values + for _, claim := range newGenState.HardLiquidityProviderClaims { + + for _, ri := range claim.BorrowRewardIndexes { + global, found := newGenState.HardBorrowRewardState.MultiRewardIndexes.Get(ri.CollateralType) + if !found { + global = v0_15incentive.RewardIndexes{} + } + require.Truef(t, indexesAllLessThanOrEqual(ri.RewardIndexes, global), "invalid claim supply indexes %s %s", ri.RewardIndexes, global) + } + + for _, ri := range claim.SupplyRewardIndexes { + global, found := newGenState.HardSupplyRewardState.MultiRewardIndexes.Get(ri.CollateralType) + if !found { + global = v0_15incentive.RewardIndexes{} + } + require.Truef(t, indexesAllLessThanOrEqual(ri.RewardIndexes, global), "invalid claim borrow indexes %s %s", ri.RewardIndexes, global) + } + } + + // ensure (synced) reward amounts are unchanged + for _, claim := range newGenState.HardLiquidityProviderClaims { + for _, oldClaim := range oldIncentiveGenState.HardLiquidityProviderClaims { + if oldClaim.Owner.Equals(claim.Owner) { + require.Equal(t, claim.Reward, oldClaim.Reward) + } + } + } + // Ensure the usdx claim indexes match global globalIndexes := newGenState.USDXRewardState.MultiRewardIndexes for _, claim := range newGenState.USDXMintingClaims { @@ -152,11 +224,41 @@ func TestIncentive_Full(t *testing.T) { } } - require.Equal(t, len(oldIncentiveGenState.HardLiquidityProviderClaims), len(newGenState.HardLiquidityProviderClaims)) // 1 new DelegatorClaim should have been created for each existing HardLiquidityProviderClaim require.Equal(t, len(oldIncentiveGenState.HardLiquidityProviderClaims), len(newGenState.DelegatorClaims)) } +// collateralTypesMatch checks if the set of coin denoms is equal to the set of CollateralTypes in the indexes. +func collateralTypesMatch(coins sdk.Coins, indexes v0_15incentive.MultiRewardIndexes) error { + for _, index := range indexes { + if coins.AmountOf(index.CollateralType).Equal(sdk.ZeroInt()) { + return fmt.Errorf("index contains denom not found in coins") + } + } + for _, coin := range coins { + _, found := indexes.Get(coin.Denom) + if !found { + return fmt.Errorf("coins contain denom not found in indexes") + } + } + return nil +} + +// indexesAllLessThanOrEqual computes if all factors in A are ≤ factors in B. +// Missing indexes are taken to be zero. +func indexesAllLessThanOrEqual(indexesA, indexesB v0_15incentive.RewardIndexes) bool { + allLT := true + for _, ri := range indexesA { + factor, found := indexesB.Get(ri.CollateralType) + if !found { + // value not found is same as it being zero + factor = sdk.ZeroDec() + } + allLT = allLT && ri.RewardFactor.LTE(factor) + } + return allLT +} + func TestIncentive_Full_TotalRewards(t *testing.T) { t.Skip() // skip to avoid having to commit a large genesis file to the repo @@ -171,10 +273,13 @@ func TestIncentive_Full_TotalRewards(t *testing.T) { var oldIncentiveGenState v0_14incentive.GenesisState cdc.MustUnmarshalJSON(oldState[v0_14incentive.ModuleName], &oldIncentiveGenState) + var oldHardGenState v0_15hard.GenesisState + cdc.MustUnmarshalJSON(oldState[v0_15hard.ModuleName], &oldHardGenState) + var oldCDPGenState v0_15cdp.GenesisState cdc.MustUnmarshalJSON(oldState[v0_15cdp.ModuleName], &oldCDPGenState) - newGenState := Incentive(app.MakeCodec(), oldIncentiveGenState, oldCDPGenState.CDPs) + newGenState := Incentive(app.MakeCodec(), oldIncentiveGenState, oldCDPGenState.CDPs, oldHardGenState) // total previous rewards oldTotalRewards := sdk.NewCoins() // total synced unclaimed rewards @@ -228,7 +333,7 @@ func TestIncentive_SwpLPRewards(t *testing.T) { newGenState := v0_15incentive.GenesisState{} require.NotPanics(t, func() { - newGenState = Incentive(app.MakeCodec(), oldIncentiveGenState, v0_15cdp.CDPs{}) + newGenState = Incentive(app.MakeCodec(), oldIncentiveGenState, v0_15cdp.CDPs{}, v0_15hard.DefaultGenesisState()) }) err = newGenState.Validate() require.NoError(t, err) @@ -264,7 +369,7 @@ func TestIncentive_SwpDelegatorRewards(t *testing.T) { newGenState := v0_15incentive.GenesisState{} require.NotPanics(t, func() { - newGenState = Incentive(app.MakeCodec(), oldIncentiveGenState, v0_15cdp.CDPs{}) + newGenState = Incentive(app.MakeCodec(), oldIncentiveGenState, v0_15cdp.CDPs{}, v0_15hard.DefaultGenesisState()) }) for _, rp := range newGenState.Params.DelegatorRewardPeriods { @@ -279,7 +384,12 @@ func TestIncentive_SwpDelegatorRewards(t *testing.T) { func TestIncentive_SwpPoolsValid(t *testing.T) { bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-7-test-incentive-state.json")) require.NoError(t, err) - appState := genutil.AppMap{v0_14incentive.ModuleName: bz, v0_15cdp.ModuleName: app.MakeCodec().MustMarshalJSON(v0_15cdp.DefaultGenesisState())} + + appState := genutil.AppMap{ + v0_14incentive.ModuleName: bz, + v0_15cdp.ModuleName: app.MakeCodec().MustMarshalJSON(v0_15cdp.DefaultGenesisState()), + v0_15hard.ModuleName: app.MakeCodec().MustMarshalJSON(v0_15hard.DefaultGenesisState()), + } MigrateAppState(appState) cdc := app.MakeCodec() @@ -439,7 +549,7 @@ func TestAuth_MakeAirdropMap(t *testing.T) { } func TestAuth_TestAllDepositorsIncluded(t *testing.T) { - var deposits hard.Deposits + var deposits v0_15hard.Deposits cdc := app.MakeCodec() bz, err := ioutil.ReadFile("./data/hard-deposits-block-1543671.json") if err != nil {