diff --git a/x/incentive/keeper/integration_test.go b/x/incentive/keeper/integration_test.go index a1931aad..1381a7e7 100644 --- a/x/incentive/keeper/integration_test.go +++ b/x/incentive/keeper/integration_test.go @@ -4,9 +4,14 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" + committeetypes "github.com/kava-labs/kava/x/committee/types" "github.com/kava-labs/kava/x/hard" "github.com/kava-labs/kava/x/pricefeed" ) @@ -109,6 +114,7 @@ func NewPricefeedGenStateMulti() app.GenesisState { {MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "busd:usd", BaseAsset: "busd", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "zzz:usd", BaseAsset: "zzz", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ @@ -142,6 +148,12 @@ func NewPricefeedGenStateMulti() app.GenesisState { Price: sdk.OneDec(), Expiry: time.Now().Add(1 * time.Hour), }, + { + MarketID: "zzz:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, }, } return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} @@ -158,6 +170,7 @@ func NewHardGenStateMulti() app.GenesisState { hard.NewMoneyMarket("bnb", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "bnb:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), hard.NewMoneyMarket("btcb", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "btc:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), hard.NewMoneyMarket("xrp", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "xrp:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), + hard.NewMoneyMarket("zzz", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "zzz:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, sdk.NewDec(10), ), hard.DefaultAccumulationTimes, hard.DefaultDeposits, hard.DefaultBorrows, @@ -166,3 +179,72 @@ func NewHardGenStateMulti() app.GenesisState { return app.GenesisState{hard.ModuleName: hard.ModuleCdc.MustMarshalJSON(hardGS)} } + +func NewAuthGenState(addresses []sdk.AccAddress, coins sdk.Coins) app.GenesisState { + coinsList := []sdk.Coins{} + for range addresses { + coinsList = append(coinsList, coins) + } + + // Load up our primary user address + if len(addresses) >= 4 { + coinsList[3] = sdk.NewCoins( + sdk.NewCoin("bnb", sdk.NewInt(1000000000000000)), + sdk.NewCoin("ukava", sdk.NewInt(1000000000000000)), + sdk.NewCoin("btcb", sdk.NewInt(1000000000000000)), + sdk.NewCoin("xrp", sdk.NewInt(1000000000000000)), + sdk.NewCoin("zzz", sdk.NewInt(1000000000000000)), + ) + } + + return app.NewAuthGenState(addresses, coinsList) +} + +func NewStakingGenesisState() app.GenesisState { + genState := staking.DefaultGenesisState() + genState.Params.BondDenom = "ukava" + return app.GenesisState{ + staking.ModuleName: staking.ModuleCdc.MustMarshalJSON(genState), + } +} + +func (suite *KeeperTestSuite) SetupWithGenState() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + _, allAddrs := app.GeneratePrivKeyAddressPairs(10) + suite.addrs = allAddrs[:5] + for _, a := range allAddrs[5:] { + suite.validatorAddrs = append(suite.validatorAddrs, sdk.ValAddress(a)) + } + + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + tApp.InitializeFromGenesisStates( + NewAuthGenState(allAddrs, cs(c("ukava", 5_000_000))), + NewStakingGenesisState(), + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + NewHardGenStateMulti(), + ) + + // Set up a god committee + committeeModKeeper := tApp.GetCommitteeKeeper() + godCommittee := committeetypes.Committee{ + ID: 1, + Description: "This committee is for testing.", + Members: suite.addrs[:2], + Permissions: []committeetypes.Permission{committeetypes.GodPermission{}}, + VoteThreshold: d("0.667"), + ProposalDuration: time.Hour * 24 * 7, + } + committeeModKeeper.SetCommittee(ctx, godCommittee) + + suite.app = tApp + suite.ctx = ctx + suite.keeper = tApp.GetIncentiveKeeper() + suite.hardKeeper = tApp.GetHardKeeper() + suite.stakingKeeper = tApp.GetStakingKeeper() + suite.committeeKeeper = committeeModKeeper +} diff --git a/x/incentive/keeper/keeper_test.go b/x/incentive/keeper/keeper_test.go index 89303f4c..e58d59dc 100644 --- a/x/incentive/keeper/keeper_test.go +++ b/x/incentive/keeper/keeper_test.go @@ -16,6 +16,7 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + committeekeeper "github.com/kava-labs/kava/x/committee/keeper" hardkeeper "github.com/kava-labs/kava/x/hard/keeper" "github.com/kava-labs/kava/x/incentive/keeper" "github.com/kava-labs/kava/x/incentive/types" @@ -25,13 +26,14 @@ import ( type KeeperTestSuite struct { suite.Suite - keeper keeper.Keeper - hardKeeper hardkeeper.Keeper - stakingKeeper stakingkeeper.Keeper - app app.TestApp - ctx sdk.Context - addrs []sdk.AccAddress - validatorAddrs []sdk.ValAddress + keeper keeper.Keeper + hardKeeper hardkeeper.Keeper + stakingKeeper stakingkeeper.Keeper + committeeKeeper committeekeeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress + validatorAddrs []sdk.ValAddress } // The default state used by each test diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index dd0baac0..9df65385 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -262,10 +262,12 @@ func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.De var supplyRewardIndexes types.MultiRewardIndexes for _, coin := range deposit.Amount { globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue + var multiRewardIndex types.MultiRewardIndex + if foundGlobalRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) } - multiRewardIndex := types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) } @@ -296,8 +298,8 @@ func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.D continue } - userRewardIndexes, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom) - if !foundUserRewardIndexes { + userMultiRewardIndex, foundUserMultiRewardIndex := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom) + if !foundUserMultiRewardIndex { continue } @@ -308,9 +310,14 @@ func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.D } for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) if !foundUserRewardIndex { - continue + // User deposited this coin type before it had rewards. When new rewards are added, legacy depositors + // should immediately begin earning rewards. Enable users to do so by updating their claim with the global + // reward index denom and start their reward factor at 0.0 + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) + claim.SupplyRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex } globalRewardFactor := globalRewardIndex.RewardFactor @@ -324,7 +331,7 @@ func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.D continue } - factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) if !foundFactorIndex { fmt.Printf("[LOG]: factor index for %s should always be found", coin.Denom) // TODO: remove before production continue @@ -348,10 +355,12 @@ func (k Keeper) InitializeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bor var borrowRewardIndexes types.MultiRewardIndexes for _, coin := range borrow.Amount { globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue + var multiRewardIndex types.MultiRewardIndex + if foundGlobalRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) } - multiRewardIndex := types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) } @@ -373,8 +382,8 @@ func (k Keeper) SynchronizeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bo continue } - userRewardIndexes, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom) - if !foundUserRewardIndexes { + userMultiRewardIndex, foundUserMultiRewardIndex := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom) + if !foundUserMultiRewardIndex { continue } @@ -385,9 +394,14 @@ func (k Keeper) SynchronizeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bo } for _, globalRewardIndex := range globalRewardIndexes { - userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) + userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) if !foundUserRewardIndex { - continue + // User borrowed this coin type before it had rewards. When new rewards are added, legacy borrowers + // should immediately begin earning rewards. Enable users to do so by updating their claim with the global + // reward index denom and start their reward factor at 0.0 + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex) + claim.BorrowRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex } globalRewardFactor := globalRewardIndex.RewardFactor @@ -401,7 +415,7 @@ func (k Keeper) SynchronizeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bo continue } - factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) + factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) if !foundFactorIndex { fmt.Printf("\n[LOG]: factor index for %s should always be found", coin.Denom) // TODO: remove before production continue @@ -425,11 +439,13 @@ func (k Keeper) UpdateHardSupplyIndexDenoms(ctx sdk.Context, deposit hardtypes.D for _, coin := range deposit.Amount { _, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom) if !foundUserRewardIndexes { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue // No rewards for this coin type + globalSupplyRewardIndexes, foundGlobalSupplyRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalSupplyRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalSupplyRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) } - multiRewardIndex := types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex) } } @@ -451,11 +467,13 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo for _, coin := range borrow.Amount { _, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom) if !foundUserRewardIndexes { - globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) - if !foundGlobalRewardIndexes { - continue // No rewards for this coin type + globalBorrowRewardIndexes, foundGlobalBorrowRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom) + var multiRewardIndex types.MultiRewardIndex + if foundGlobalBorrowRewardIndexes { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalBorrowRewardIndexes) + } else { + multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{}) } - multiRewardIndex := types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes) borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex) } } @@ -678,14 +696,16 @@ func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiq userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(ri.CollateralType) if !foundUserRewardIndexIndex { - fmt.Printf("\n[LOG]: factor index for %s should always be found", ri.CollateralType) // TODO: remove before production + fmt.Printf("\n[LOG]: claim.SupplyRewardIndexes.GetRewardIndexIndex for %s should always be found", ri.CollateralType) // TODO: remove before production continue } for _, globalRewardIndex := range globalRewardIndexes { userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) if !foundUserRewardIndex { - continue + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) + claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) } globalRewardFactor := globalRewardIndex.RewardFactor @@ -705,7 +725,7 @@ func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiq factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) if !foundFactorIndex { - fmt.Printf("[LOG]: factor index for %s should always be found", ri.CollateralType) // TODO: remove before production + fmt.Printf("[LOG]: userRewardIndexes.RewardIndexes.GetFactorIndex for %s should always be found", globalRewardIndex.CollateralType) // TODO: remove before production continue } claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor @@ -728,14 +748,16 @@ func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiq userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(ri.CollateralType) if !foundUserRewardIndexIndex { - fmt.Printf("\n[LOG]: factor index for %s should always be found", ri.CollateralType) // TODO: remove before production + fmt.Printf("\n[LOG]: claim.BorrowRewardIndexes.GetRewardIndexIndex for %s should always be found", ri.CollateralType) // TODO: remove before production continue } for _, globalRewardIndex := range globalRewardIndexes { userRewardIndex, foundUserRewardIndex := userRewardIndexes.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType) if !foundUserRewardIndex { - continue + userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec()) + userRewardIndexes.RewardIndexes = append(userRewardIndexes.RewardIndexes, userRewardIndex) + claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes = append(claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes, userRewardIndex) } globalRewardFactor := globalRewardIndex.RewardFactor @@ -755,7 +777,7 @@ func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiq factorIndex, foundFactorIndex := userRewardIndexes.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType) if !foundFactorIndex { - fmt.Printf("[LOG]: factor index for %s should always be found", ri.CollateralType) // TODO: remove before production + fmt.Printf("[LOG]: userRewardIndexes.RewardIndexes.GetFactorIndex for %s should always be found", globalRewardIndex.CollateralType) // TODO: remove before production continue } claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index 71166a30..508a0479 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -4,13 +4,13 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/staking" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" - tmtime "github.com/tendermint/tendermint/types/time" - "github.com/kava-labs/kava/app" cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/hard" hardtypes "github.com/kava-labs/kava/x/hard/types" "github.com/kava-labs/kava/x/incentive/types" @@ -370,12 +370,19 @@ func (suite *KeeperTestSuite) TestAccumulateHardBorrowRewards() { func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { type args struct { - borrow sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins + incentiveBorrowRewardDenom string + borrow sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + updateRewardsViaCommmittee bool + updatedBaseDenom string + updatedRewardsPerSecond sdk.Coins + updatedExpectedRewardIndexes types.RewardIndexes + updatedExpectedRewards sdk.Coins + updatedTimeDuration int } type test struct { name string @@ -386,32 +393,36 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { { "10 blocks", args{ - borrow: c("bnb", 10000000000), // TODO: 2 decimal diff from TestAccumulateHardBorrowRewards's borrow - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, - expectedRewards: cs(c("hard", 12235400)), + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000173228"))}, + expectedRewards: cs(c("hard", 12235400)), + updateRewardsViaCommmittee: false, }, }, { "10 blocks - long block time", args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, - expectedRewards: cs(c("hard", 105713856031)), + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385603126235340"))}, + expectedRewards: cs(c("hard", 105713856031)), }, }, { "multiple reward denoms: 10 blocks", args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("0.001223540000173228")), types.NewRewardIndex("ukava", d("0.001223540000173228")), @@ -422,10 +433,11 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { { "multiple reward denoms: 10 blocks - long block time", args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("10.571385603126235340")), types.NewRewardIndex("ukava", d("10.571385603126235340")), @@ -436,10 +448,11 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { { "multiple reward denoms with different rewards per second: 10 blocks", args{ - borrow: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("0.001223540000173228")), types.NewRewardIndex("ukava", d("0.005555550000786558")), @@ -447,6 +460,113 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), }, }, + { + "denom is in incentive's hard borrow reward params but it has no rewards; add reward", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in incentive's hard borrow reward params and has rewards; add new reward type", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("1.057138560060101160")), + }, + expectedRewards: cs(c("hard", 10571385601)), + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), + updatedExpectedRewards: cs(c("hard", 21142771202), c("ukava", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("2.114277120120202320")), + types.NewRewardIndex("ukava", d("0.864000000049120715")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom incentive's hard borrow reward params but it has no rewards; add multiple reward types", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + types.NewRewardIndex("ukava", d("0.868320000050052081")), + types.NewRewardIndex("swap", d("0.004320000000249015")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", + args{ + incentiveBorrowRewardDenom: "bnb", + borrow: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200001), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864000000049803065")), + types.NewRewardIndex("ukava", d("0.868320000050052081")), + types.NewRewardIndex("swap", d("0.004320000000249015")), + }, + updatedTimeDuration: 86400, + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -458,23 +578,25 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + // Set up incentive state + incentiveParams := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), sdk.Coins{})}, // Don't set any supply rewards for easier accounting + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, tc.args.initialTime.Add(time.Hour*24*365*5), ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.borrow.Denom, tc.args.initialTime) + suite.keeper.SetParams(suite.ctx, incentiveParams) + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, tc.args.incentiveBorrowRewardDenom, tc.args.initialTime) var rewardIndexes types.RewardIndexes for _, rewardCoin := range tc.args.rewardsPerSecond { rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) rewardIndexes = append(rewardIndexes, rewardIndex) } - suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom, rewardIndexes) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, tc.args.incentiveBorrowRewardDenom, rewardIndexes) + } // Set up hard state (interest factor for the relevant denom) suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.borrow.Denom, sdk.MustNewDecFromStr("1.0")) @@ -490,7 +612,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { suite.Require().NoError(err) // Check that Hard hooks initialized a HardLiquidityProviderClaim - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) suite.Require().True(found) multiRewardIndex, _ := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { @@ -513,31 +635,142 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { // Accumulate hard borrow-side rewards multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom) - suite.Require().True(found) - err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) + if found { + err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } } updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) // After we've accumulated, run synchronize - borrow, found := hardKeeper.GetBorrow(suite.ctx, suite.addrs[3]) + borrow, found := hardKeeper.GetBorrow(suite.ctx, userAddr) suite.Require().True(found) suite.Require().NotPanics(func() { suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) }) // Check that the global reward index's reward factor and user's claim have been updated as expected - globalRewardIndexes, found := suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) suite.Require().True(found) - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) + if len(tc.args.rewardsPerSecond) > 0 { + suite.Require().True(foundGlobalRewardIndexes) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + } + + // Only test cases with reward param updates continue past this point + if !tc.args.updateRewardsViaCommmittee { + return + } + + // If are no initial rewards per second, add new rewards through a committee param change + // 1. Construct incentive's new HardBorrowRewardPeriods param + currIncentiveHardBorrowRewardPeriods := suite.keeper.GetParams(suite.ctx).HardBorrowRewardPeriods + multiRewardPeriod, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriod(tc.args.borrow.Denom) + if found { + // Borrow denom's reward period exists, but it doesn't have any rewards per second + index, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) + suite.Require().True(found) + multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond + currIncentiveHardBorrowRewardPeriods[index] = multiRewardPeriod + } else { + // Borrow denom's reward period does not exist + _, found := currIncentiveHardBorrowRewardPeriods.GetMultiRewardPeriodIndex(tc.args.borrow.Denom) + suite.Require().False(found) + newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.borrow.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) + currIncentiveHardBorrowRewardPeriods = append(currIncentiveHardBorrowRewardPeriods, newMultiRewardPeriod) + } + + // 2. Construct the parameter change proposal to update HardBorrowRewardPeriods param + pubProposal := params.NewParameterChangeProposal( + "Update hard borrow rewards", "Adds a new reward coin to the incentive module's hard borrow rewards.", + []params.ParamChange{ + { + Subspace: types.ModuleName, // target incentive module + Key: string(types.KeyHardBorrowRewardPeriods), // target hard borrow rewards key + Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardBorrowRewardPeriods)), + }, + }, + ) + + // 3. Ensure proposal is properly formed + err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) + suite.Require().NoError(err) + + // 4. Committee creates proposal + committeeMemberOne := suite.addrs[0] + committeeMemberTwo := suite.addrs[1] + proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) + suite.Require().NoError(err) + + // 5. Committee votes and passes proposal + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) + + // 6. Check proposal passed + proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) + suite.Require().NoError(err) + suite.Require().True(proposalPasses) + + // 7. Run committee module's begin blocker to enact proposal + suite.NotPanics(func() { + committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) + }) + + // We need to accumulate hard supply-side rewards again + multiRewardPeriod, found = suite.keeper.GetHardBorrowRewardPeriods(suite.ctx, tc.args.borrow.Denom) suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + + // But new borrow denoms don't have their PreviousHardBorrowRewardAccrualTime set yet, + // so we need to call the accumulation method once to set the initial reward accrual time + if tc.args.borrow.Denom != tc.args.incentiveBorrowRewardDenom { + err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + } + + // Now we can jump forward in time and accumulate rewards + updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + + // After we've accumulated, run synchronize + borrow, found = hardKeeper.GetBorrow(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + globalRewardIndexes, found = suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, tc.args.borrow.Denom) + suite.Require().True(found) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + + for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { // Check that global reward index has been updated as expected globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) suite.Require().True(found) suite.Require().Equal(expectedRewardIndex, globalRewardIndex) - // Check that the user's claim's reward index matches the corresponding global reward index multiRewardIndex, found := claim.BorrowRewardIndexes.GetRewardIndex(tc.args.borrow.Denom) suite.Require().True(found) @@ -547,7 +780,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() { // Check that the user's claim holds the expected amount of reward coins suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), claim.Reward.AmountOf(expectedRewardIndex.CollateralType), ) } @@ -650,6 +883,183 @@ func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() { } } +func (suite *KeeperTestSuite) TestInitializeHardSupplyRewards() { + + type args struct { + moneyMarketRewardDenoms map[string][]string + deposit sdk.Coins + initialTime time.Time + expectedClaimSupplyRewardIndexes types.MultiRewardIndexes + } + type test struct { + name string + args args + } + + standardMoneyMarketRewardDenoms := map[string][]string{ + "bnb": {"hard"}, + "btcb": {"hard", "ukava"}, + "xrp": {}, + } + + testCases := []test{ + { + "single deposit denom, single reward denom", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, multiple reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("btcb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, no reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("xrp", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + { + "multiple deposit denoms, multiple overlapping reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "multiple deposit denoms, correct discrete reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimSupplyRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + userAddr := suite.addrs[3] + + // Prepare money market + reward params + i := 0 + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms + var rewardsPerSecond sdk.Coins + for _, rewardDenom := range rewardDenoms { + rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + + // Set up generic reward periods for usdx minting/delegator indexes + if i == 0 && len(rewardDenoms) > 0 { + rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + i++ + } + } + + // Initialize and set incentive params + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each money market's previous accrual time and supply reward indexes + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + var rewardIndexes types.RewardIndexes + for _, rewardDenom := range rewardDenoms { + rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) + } + } + + // User deposits + hardKeeper := suite.app.GetHardKeeper() + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + + claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(foundClaim) + suite.Require().Equal(tc.args.expectedClaimSupplyRewardIndexes, claim.SupplyRewardIndexes) + }) + } +} + func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { type args struct { deposit sdk.Coin @@ -745,6 +1155,16 @@ func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { }, }, }, + { + "single reward denom, no rewards", + args{ + deposit: c("bnb", 1000000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{}, + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -758,10 +1178,10 @@ func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { // Set up incentive state params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, tc.args.initialTime.Add(time.Hour*24*365*5), ) @@ -772,7 +1192,9 @@ func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) rewardIndexes = append(rewardIndexes, rewardIndex) } - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) + } // Set up hard state (interest factor for the relevant denom) suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) @@ -797,26 +1219,38 @@ func (suite *KeeperTestSuite) TestAccumulateHardSupplyRewards() { err = suite.keeper.AccumulateHardSupplyRewards(runCtx, multiRewardPeriod) suite.Require().NoError(err) - // Check that each expected reward index matches the current stored reward index for theh denom + // Check that each expected reward index matches the current stored reward index for the denom globalRewardIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(runCtx, tc.args.deposit.Denom) - suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { - globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + if len(tc.args.rewardsPerSecond) > 0 { suite.Require().True(found) - suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + } + } else { + suite.Require().False(found) } + }) } } func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { type args struct { - deposit sdk.Coin - rewardsPerSecond sdk.Coins - initialTime time.Time - blockTimes []int - expectedRewardIndexes types.RewardIndexes - expectedRewards sdk.Coins + incentiveSupplyRewardDenom string + deposit sdk.Coin + rewardsPerSecond sdk.Coins + initialTime time.Time + blockTimes []int + expectedRewardIndexes types.RewardIndexes + expectedRewards sdk.Coins + updateRewardsViaCommmittee bool + updatedBaseDenom string + updatedRewardsPerSecond sdk.Coins + updatedExpectedRewardIndexes types.RewardIndexes + updatedExpectedRewards sdk.Coins + updatedTimeDuration int } type test struct { name string @@ -827,65 +1261,182 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { { "single reward denom: 10 blocks", args{ - deposit: c("bnb", 10000000000), // TODO: 2 decimal diff - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, - expectedRewards: cs(c("hard", 12235400)), + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("0.001223540000000000"))}, + expectedRewards: cs(c("hard", 12235400)), + updateRewardsViaCommmittee: false, }, }, { "single reward denom: 10 blocks - long block time", args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, - expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, - expectedRewards: cs(c("hard", 105713856000)), + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + expectedRewardIndexes: types.RewardIndexes{types.NewRewardIndex("hard", d("10.571385600000000000"))}, + expectedRewards: cs(c("hard", 105713856000)), + updateRewardsViaCommmittee: false, }, }, { "multiple reward denoms: 10 blocks", args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("0.001223540000000000")), types.NewRewardIndex("ukava", d("0.001223540000000000")), }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), + expectedRewards: cs(c("hard", 12235400), c("ukava", 12235400)), + updateRewardsViaCommmittee: false, }, }, { "multiple reward denoms: 10 blocks - long block time", args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("10.571385600000000000")), types.NewRewardIndex("ukava", d("10.571385600000000000")), }, - expectedRewards: cs(c("hard", 105713856000), c("ukava", 105713856000)), + expectedRewards: cs(c("hard", 105713856000), c("ukava", 105713856000)), + updateRewardsViaCommmittee: false, }, }, { "multiple reward denoms with different rewards per second: 10 blocks", args{ - deposit: c("bnb", 10000000000), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), - initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), - blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 555555)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardIndexes: types.RewardIndexes{ types.NewRewardIndex("hard", d("0.001223540000000000")), types.NewRewardIndex("ukava", d("0.005555550000000000")), }, - expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), + expectedRewards: cs(c("hard", 12235400), c("ukava", 55555500)), + updateRewardsViaCommmittee: false, + }, + }, + { + "denom is in incentive's hard supply reward params but it has no rewards; add reward", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in incentive's hard supply reward params and has rewards; add new reward type", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{86400}, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("1.057138560000000000")), + }, + expectedRewards: cs(c("hard", 10571385600)), + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 122354), c("ukava", 100000)), + updatedExpectedRewards: cs(c("hard", 21142771200), c("ukava", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("2.114277120000000000")), + types.NewRewardIndex("ukava", d("0.864000000000000000")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add reward", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000)), + updatedExpectedRewards: cs(c("hard", 8640000000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom incentive's hard supply reward params but it has no rewards; add multiple reward types", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("bnb", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "bnb", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + types.NewRewardIndex("ukava", d("0.86832")), + types.NewRewardIndex("swap", d("0.00432")), + }, + updatedTimeDuration: 86400, + }, + }, + { + "denom is in hard's money market params but not in incentive's hard supply reward params; add multiple reward types", + args{ + incentiveSupplyRewardDenom: "bnb", + deposit: c("zzz", 10000000000), + rewardsPerSecond: sdk.Coins{}, + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + blockTimes: []int{100}, + expectedRewardIndexes: types.RewardIndexes{}, + expectedRewards: sdk.Coins{}, + updateRewardsViaCommmittee: true, + updatedBaseDenom: "zzz", + updatedRewardsPerSecond: cs(c("hard", 100000), c("ukava", 100500), c("swap", 500)), + updatedExpectedRewards: cs(c("hard", 8640000000), c("ukava", 8683200000), c("swap", 43200000)), + updatedExpectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.864")), + types.NewRewardIndex("ukava", d("0.86832")), + types.NewRewardIndex("swap", d("0.00432")), + }, + updatedTimeDuration: 86400, }, }, } @@ -899,28 +1450,30 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) - // setup incentive state - params := types.NewParams( - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, - types.RewardPeriods{types.NewRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0])}, + // Set up incentive state + incentiveParams := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), c("hard", 1))}, types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, tc.args.initialTime.Add(time.Hour*24*365*5), ) - suite.keeper.SetParams(suite.ctx, params) - suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + suite.keeper.SetParams(suite.ctx, incentiveParams) + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) var rewardIndexes types.RewardIndexes for _, rewardCoin := range tc.args.rewardsPerSecond { rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) rewardIndexes = append(rewardIndexes, rewardIndex) } - suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom, rewardIndexes) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, tc.args.incentiveSupplyRewardDenom, rewardIndexes) + } // Set up hard state (interest factor for the relevant denom) - suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.deposit.Denom, sdk.MustNewDecFromStr("1.0")) - suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.deposit.Denom, tc.args.initialTime) + suite.hardKeeper.SetSupplyInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetBorrowInterestFactor(suite.ctx, tc.args.incentiveSupplyRewardDenom, sdk.MustNewDecFromStr("1.0")) + suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.incentiveSupplyRewardDenom, tc.args.initialTime) // User deposits and borrows to increase total borrowed amount hardKeeper := suite.app.GetHardKeeper() @@ -929,7 +1482,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { suite.Require().NoError(err) // Check that Hard hooks initialized a HardLiquidityProviderClaim with 0 reward indexes - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) suite.Require().True(found) multiRewardIndex, _ := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { @@ -952,26 +1505,137 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { // Accumulate hard supply-side rewards multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom) - suite.Require().True(found) - err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) - suite.Require().NoError(err) + if found { + err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod) + suite.Require().NoError(err) + } } updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) // After we've accumulated, run synchronize - deposit, found := hardKeeper.GetDeposit(suite.ctx, suite.addrs[3]) + deposit, found := hardKeeper.GetDeposit(suite.ctx, userAddr) suite.Require().True(found) suite.Require().NotPanics(func() { suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) }) // Check that the global reward index's reward factor and user's claim have been updated as expected - globalRewardIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) suite.Require().True(found) - claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + globalRewardIndexes, foundGlobalRewardIndexes := suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) + if len(tc.args.rewardsPerSecond) > 0 { + suite.Require().True(foundGlobalRewardIndexes) + for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + // Check that global reward index has been updated as expected + globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, globalRewardIndex) + + // Check that the user's claim's reward index matches the corresponding global reward index + multiRewardIndex, found := claim.SupplyRewardIndexes.GetRewardIndex(tc.args.deposit.Denom) + suite.Require().True(found) + rewardIndex, found := multiRewardIndex.RewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) + suite.Require().True(found) + suite.Require().Equal(expectedRewardIndex, rewardIndex) + + // Check that the user's claim holds the expected amount of reward coins + suite.Require().Equal( + tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + claim.Reward.AmountOf(expectedRewardIndex.CollateralType), + ) + } + } + + // Only test cases with reward param updates continue past this point + if !tc.args.updateRewardsViaCommmittee { + return + } + + // If are no initial rewards per second, add new rewards through a committee param change + // 1. Construct incentive's new HardSupplyRewardPeriods param + currIncentiveHardSupplyRewardPeriods := suite.keeper.GetParams(suite.ctx).HardSupplyRewardPeriods + multiRewardPeriod, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriod(tc.args.deposit.Denom) + if found { + // Deposit denom's reward period exists, but it doesn't have any rewards per second + index, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) + suite.Require().True(found) + multiRewardPeriod.RewardsPerSecond = tc.args.updatedRewardsPerSecond + currIncentiveHardSupplyRewardPeriods[index] = multiRewardPeriod + } else { + // Deposit denom's reward period does not exist + _, found := currIncentiveHardSupplyRewardPeriods.GetMultiRewardPeriodIndex(tc.args.deposit.Denom) + suite.Require().False(found) + newMultiRewardPeriod := types.NewMultiRewardPeriod(true, tc.args.deposit.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.updatedRewardsPerSecond) + currIncentiveHardSupplyRewardPeriods = append(currIncentiveHardSupplyRewardPeriods, newMultiRewardPeriod) + } + + // 2. Construct the parameter change proposal to update HardSupplyRewardPeriods param + pubProposal := params.NewParameterChangeProposal( + "Update hard supply rewards", "Adds a new reward coin to the incentive module's hard supply rewards.", + []params.ParamChange{ + { + Subspace: types.ModuleName, // target incentive module + Key: string(types.KeyHardSupplyRewardPeriods), // target hard supply rewards key + Value: string(suite.app.Codec().MustMarshalJSON(currIncentiveHardSupplyRewardPeriods)), + }, + }, + ) + + // 3. Ensure proposal is properly formed + err = suite.committeeKeeper.ValidatePubProposal(suite.ctx, pubProposal) + suite.Require().NoError(err) + + // 4. Committee creates proposal + committeeMemberOne := suite.addrs[0] + committeeMemberTwo := suite.addrs[1] + proposalID, err := suite.committeeKeeper.SubmitProposal(suite.ctx, committeeMemberOne, 1, pubProposal) + suite.Require().NoError(err) + + // 5. Committee votes and passes proposal + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) + err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) + + // 6. Check proposal passed + proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) + suite.Require().NoError(err) + suite.Require().True(proposalPasses) + + // 7. Run committee module's begin blocker to enact proposal + suite.NotPanics(func() { + committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.committeeKeeper) + }) + + // We need to accumulate hard supply-side rewards again + multiRewardPeriod, found = suite.keeper.GetHardSupplyRewardPeriods(suite.ctx, tc.args.deposit.Denom) suite.Require().True(found) - for _, expectedRewardIndex := range tc.args.expectedRewardIndexes { + + // But new deposit denoms don't have their PreviousHardSupplyRewardAccrualTime set yet, + // so we need to call the accumulation method once to set the initial reward accrual time + if tc.args.deposit.Denom != tc.args.incentiveSupplyRewardDenom { + err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + } + + // Now we can jump forward in time and accumulate rewards + updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod) + suite.Require().NoError(err) + + // After we've accumulated, run synchronize + deposit, found = hardKeeper.GetDeposit(suite.ctx, userAddr) + suite.Require().True(found) + suite.Require().NotPanics(func() { + suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) + }) + + // Check that the global reward index's reward factor and user's claim have been updated as expected + globalRewardIndexes, found = suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, tc.args.deposit.Denom) + suite.Require().True(found) + claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(found) + for _, expectedRewardIndex := range tc.args.updatedExpectedRewardIndexes { // Check that global reward index has been updated as expected globalRewardIndex, found := globalRewardIndexes.GetRewardIndex(expectedRewardIndex.CollateralType) suite.Require().True(found) @@ -986,7 +1650,7 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() { // Check that the user's claim holds the expected amount of reward coins suite.Require().Equal( - tc.args.expectedRewards.AmountOf(expectedRewardIndex.CollateralType), + tc.args.updatedExpectedRewards.AmountOf(expectedRewardIndex.CollateralType), claim.Reward.AmountOf(expectedRewardIndex.CollateralType), ) } @@ -1142,6 +1806,192 @@ func (suite *KeeperTestSuite) TestUpdateHardSupplyIndexDenoms() { } } +func (suite *KeeperTestSuite) TestInitializeHardBorrowRewards() { + + type args struct { + moneyMarketRewardDenoms map[string][]string + deposit sdk.Coins + borrow sdk.Coins + initialTime time.Time + expectedClaimBorrowRewardIndexes types.MultiRewardIndexes + } + type test struct { + name string + args args + } + + standardMoneyMarketRewardDenoms := map[string][]string{ + "bnb": {"hard"}, + "btcb": {"hard", "ukava"}, + "xrp": {}, + } + + testCases := []test{ + { + "single deposit denom, single reward denom", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000)), + borrow: cs(c("bnb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, multiple reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("btcb", 1000000000000)), + borrow: cs(c("btcb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "single deposit denom, no reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("xrp", 1000000000000)), + borrow: cs(c("xrp", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + { + "multiple deposit denoms, multiple overlapping reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("btcb", 1000000000000)), + borrow: cs(c("bnb", 100000000000), c("btcb", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "btcb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + types.NewRewardIndex("ukava", d("0.0")), + }, + ), + }, + }, + }, + { + "multiple deposit denoms, correct discrete reward denoms", + args{ + moneyMarketRewardDenoms: standardMoneyMarketRewardDenoms, + deposit: cs(c("bnb", 1000000000000), c("xrp", 1000000000000)), + borrow: cs(c("bnb", 100000000000), c("xrp", 100000000000)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + expectedClaimBorrowRewardIndexes: types.MultiRewardIndexes{ + types.NewMultiRewardIndex( + "bnb", + types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + ), + types.NewMultiRewardIndex( + "xrp", + nil, + ), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // Mint coins to hard module account + supplyKeeper := suite.app.GetSupplyKeeper() + hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000))) + supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins) + + userAddr := suite.addrs[3] + + // Prepare money market + reward params + i := 0 + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + // Set up multi reward periods for supply/borrow indexes with dynamic money market denoms/reward denoms + var rewardsPerSecond sdk.Coins + for _, rewardDenom := range rewardDenoms { + rewardsPerSecond = append(rewardsPerSecond, sdk.NewCoin(rewardDenom, sdk.OneInt())) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + + // Set up generic reward periods for usdx minting/delegator indexes + if i == 0 && len(rewardDenoms) > 0 { + rewardPeriod := types.NewRewardPeriod(true, moneyMarketDenom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), rewardsPerSecond[i]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + i++ + } + } + + // Initialize and set incentive params + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each money market's previous accrual time and supply reward indexes + for moneyMarketDenom, rewardDenoms := range tc.args.moneyMarketRewardDenoms { + var rewardIndexes types.RewardIndexes + for _, rewardDenom := range rewardDenoms { + rewardIndex := types.NewRewardIndex(rewardDenom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, moneyMarketDenom, tc.args.initialTime) + if len(rewardIndexes) > 0 { + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, moneyMarketDenom, rewardIndexes) + } + } + + hardKeeper := suite.app.GetHardKeeper() + // User deposits + err := hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + // User borrows + err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) + suite.Require().NoError(err) + + claim, foundClaim := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) + suite.Require().True(foundClaim) + suite.Require().Equal(tc.args.expectedClaimBorrowRewardIndexes, claim.BorrowRewardIndexes) + }) + } +} + func (suite *KeeperTestSuite) TestUpdateHardBorrowIndexDenoms() { type args struct { initialDeposit sdk.Coins @@ -1919,61 +2769,6 @@ func (suite *KeeperTestSuite) TestSimulateUSDXMintingRewardSynchronization() { } } -func (suite *KeeperTestSuite) SetupWithGenState() { - config := sdk.GetConfig() - app.SetBech32AddressPrefixes(config) - - _, allAddrs := app.GeneratePrivKeyAddressPairs(10) - suite.addrs = allAddrs[:5] - for _, a := range allAddrs[5:] { - suite.validatorAddrs = append(suite.validatorAddrs, sdk.ValAddress(a)) - } - - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) - - tApp.InitializeFromGenesisStates( - coinsAuthGenState(allAddrs, cs(c("ukava", 5_000_000))), - stakingGenesisState(), - NewPricefeedGenStateMulti(), - NewCDPGenStateMulti(), - NewHardGenStateMulti(), - ) - - suite.app = tApp - suite.ctx = ctx - suite.keeper = tApp.GetIncentiveKeeper() - suite.hardKeeper = tApp.GetHardKeeper() - suite.stakingKeeper = tApp.GetStakingKeeper() -} - -func coinsAuthGenState(addresses []sdk.AccAddress, coins sdk.Coins) app.GenesisState { - coinsList := []sdk.Coins{} - for range addresses { - coinsList = append(coinsList, coins) - } - - // Load up our primary user address - if len(addresses) >= 4 { - coinsList[3] = sdk.NewCoins( - sdk.NewCoin("bnb", sdk.NewInt(1000000000000000)), - sdk.NewCoin("ukava", sdk.NewInt(1000000000000000)), - sdk.NewCoin("btcb", sdk.NewInt(1000000000000000)), - sdk.NewCoin("xrp", sdk.NewInt(1000000000000000)), - ) - } - - return app.NewAuthGenState(addresses, coinsList) -} - -func stakingGenesisState() app.GenesisState { - genState := staking.DefaultGenesisState() - genState.Params.BondDenom = "ukava" - return app.GenesisState{ - staking.ModuleName: staking.ModuleCdc.MustMarshalJSON(genState), - } -} - func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error { msg := staking.NewMsgCreateValidator( address, diff --git a/x/incentive/types/claims.go b/x/incentive/types/claims.go index 06358f00..0239fa44 100644 --- a/x/incentive/types/claims.go +++ b/x/incentive/types/claims.go @@ -264,9 +264,101 @@ func (cs HardLiquidityProviderClaims) Validate() error { return nil } -// -------------- Subcomponents of Custom Claim Types -------------- +// ---------------------- Reward periods are used by the params ---------------------- -// TODO: refactor RewardPeriod name from 'collateralType' to 'denom' +// MultiRewardPeriod supports multiple reward types +type MultiRewardPeriod struct { + Active bool `json:"active" yaml:"active"` + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + Start time.Time `json:"start" yaml:"start"` + End time.Time `json:"end" yaml:"end"` + RewardsPerSecond sdk.Coins `json:"rewards_per_second" yaml:"rewards_per_second"` // per second reward payouts +} + +// String implements fmt.Stringer +func (mrp MultiRewardPeriod) String() string { + return fmt.Sprintf(`Reward Period: + Collateral Type: %s, + Start: %s, + End: %s, + Rewards Per Second: %s, + Active %t, + `, mrp.CollateralType, mrp.Start, mrp.End, mrp.RewardsPerSecond, mrp.Active) +} + +// NewMultiRewardPeriod returns a new MultiRewardPeriod +func NewMultiRewardPeriod(active bool, collateralType string, start time.Time, end time.Time, reward sdk.Coins) MultiRewardPeriod { + return MultiRewardPeriod{ + Active: active, + CollateralType: collateralType, + Start: start, + End: end, + RewardsPerSecond: reward, + } +} + +// Validate performs a basic check of a MultiRewardPeriod. +func (mrp MultiRewardPeriod) Validate() error { + if mrp.Start.IsZero() { + return errors.New("reward period start time cannot be 0") + } + if mrp.End.IsZero() { + return errors.New("reward period end time cannot be 0") + } + if mrp.Start.After(mrp.End) { + return fmt.Errorf("end period time %s cannot be before start time %s", mrp.End, mrp.Start) + } + if !mrp.RewardsPerSecond.IsValid() { + return fmt.Errorf("invalid reward amount: %s", mrp.RewardsPerSecond) + } + if strings.TrimSpace(mrp.CollateralType) == "" { + return fmt.Errorf("reward period collateral type cannot be blank: %s", mrp) + } + return nil +} + +// MultiRewardPeriods array of MultiRewardPeriod +type MultiRewardPeriods []MultiRewardPeriod + +// GetMultiRewardPeriod fetches a MultiRewardPeriod from an array of MultiRewardPeriods by its denom +func (mrps MultiRewardPeriods) GetMultiRewardPeriod(denom string) (MultiRewardPeriod, bool) { + for _, rp := range mrps { + if rp.CollateralType == denom { + return rp, true + } + } + return MultiRewardPeriod{}, false +} + +// GetMultiRewardPeriodIndex returns the index of a MultiRewardPeriod inside array MultiRewardPeriods +func (mrps MultiRewardPeriods) GetMultiRewardPeriodIndex(denom string) (int, bool) { + for i, rp := range mrps { + if rp.CollateralType == denom { + return i, true + } + } + return -1, false +} + +// Validate checks if all the RewardPeriods are valid and there are no duplicated +// entries. +func (mrps MultiRewardPeriods) Validate() error { + seenPeriods := make(map[string]bool) + for _, rp := range mrps { + if seenPeriods[rp.CollateralType] { + return fmt.Errorf("duplicated reward period with collateral type %s", rp.CollateralType) + } + + if err := rp.Validate(); err != nil { + return err + } + seenPeriods[rp.CollateralType] = true + } + + return nil +} + +// ---------------------- Reward indexes are used internally in the store ---------------------- // RewardIndex stores reward accumulation information type RewardIndex struct { @@ -330,80 +422,6 @@ func (ris RewardIndexes) Validate() error { return nil } -// ------------------------------------------------------------------------- - -// MultiRewardPeriod supports multiple reward types -type MultiRewardPeriod struct { - Active bool `json:"active" yaml:"active"` - CollateralType string `json:"collateral_type" yaml:"collateral_type"` - Start time.Time `json:"start" yaml:"start"` - End time.Time `json:"end" yaml:"end"` - RewardsPerSecond sdk.Coins `json:"rewards_per_second" yaml:"rewards_per_second"` // per second reward payouts -} - -// String implements fmt.Stringer -func (mrp MultiRewardPeriod) String() string { - return fmt.Sprintf(`Reward Period: - Collateral Type: %s, - Start: %s, - End: %s, - Rewards Per Second: %s, - Active %t, - `, mrp.CollateralType, mrp.Start, mrp.End, mrp.RewardsPerSecond, mrp.Active) -} - -// NewMultiRewardPeriod returns a new MultiRewardPeriod -func NewMultiRewardPeriod(active bool, collateralType string, start time.Time, end time.Time, reward sdk.Coins) MultiRewardPeriod { - return MultiRewardPeriod{ - Active: active, - CollateralType: collateralType, - Start: start, - End: end, - RewardsPerSecond: reward, - } -} - -// Validate performs a basic check of a MultiRewardPeriod. -func (mrp MultiRewardPeriod) Validate() error { - if mrp.Start.IsZero() { - return errors.New("reward period start time cannot be 0") - } - if mrp.End.IsZero() { - return errors.New("reward period end time cannot be 0") - } - if mrp.Start.After(mrp.End) { - return fmt.Errorf("end period time %s cannot be before start time %s", mrp.End, mrp.Start) - } - if !mrp.RewardsPerSecond.IsValid() { - return fmt.Errorf("invalid reward amount: %s", mrp.RewardsPerSecond) - } - if strings.TrimSpace(mrp.CollateralType) == "" { - return fmt.Errorf("reward period collateral type cannot be blank: %s", mrp) - } - return nil -} - -// MultiRewardPeriods array of MultiRewardPeriod -type MultiRewardPeriods []MultiRewardPeriod - -// Validate checks if all the RewardPeriods are valid and there are no duplicated -// entries. -func (mrps MultiRewardPeriods) Validate() error { - seenPeriods := make(map[string]bool) - for _, rp := range mrps { - if seenPeriods[rp.CollateralType] { - return fmt.Errorf("duplicated reward period with collateral type %s", rp.CollateralType) - } - - if err := rp.Validate(); err != nil { - return err - } - seenPeriods[rp.CollateralType] = true - } - - return nil -} - // MultiRewardIndex stores reward accumulation information on multiple reward types type MultiRewardIndex struct { CollateralType string `json:"collateral_type" yaml:"collateral_type"`