diff --git a/x/incentive/abci.go b/x/incentive/abci.go index 1cfc0480..ec6cf40f 100644 --- a/x/incentive/abci.go +++ b/x/incentive/abci.go @@ -39,7 +39,9 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { // New generic RewardPeriods for _, mrp := range params.RewardPeriods { for _, rp := range mrp.RewardPeriods { - k.AccumulateRewards(ctx, mrp.ClaimType, rp) + if err := k.AccumulateRewards(ctx, mrp.ClaimType, rp); err != nil { + panic(fmt.Errorf("failed to accumulate rewards for claim type %s: %w", mrp.ClaimType, err)) + } } } } diff --git a/x/incentive/genesis.go b/x/incentive/genesis.go index 848e1687..b5be797f 100644 --- a/x/incentive/genesis.go +++ b/x/incentive/genesis.go @@ -46,12 +46,12 @@ func InitGenesis( // Set Claims of all types for _, claim := range gs.Claims { - k.SetClaim(ctx, claim) + k.Store.SetClaim(ctx, claim) } // Set AccrualTimes of all types for _, accrualTime := range gs.AccrualTimes { - k.SetRewardAccrualTime( + k.Store.SetRewardAccrualTime( ctx, accrualTime.ClaimType, accrualTime.CollateralType, @@ -61,7 +61,7 @@ func InitGenesis( // Set RewardIndexes of all types for _, rewardIndex := range gs.RewardIndexes { - k.SetRewardIndexes(ctx, rewardIndex.ClaimType, rewardIndex.CollateralType, rewardIndex.RewardIndexes) + k.Store.SetRewardIndexes(ctx, rewardIndex.ClaimType, rewardIndex.CollateralType, rewardIndex.RewardIndexes) } // Legacy claims and indexes below @@ -168,9 +168,9 @@ func InitGenesis( func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState { params := k.GetParams(ctx) - claims := k.GetAllClaims(ctx) - accrualTimes := k.GetAllRewardAccrualTimes(ctx) - rewardIndexes := k.GetRewardIndexes(ctx) + claims := k.Store.GetAllClaims(ctx) + accrualTimes := k.Store.GetAllRewardAccrualTimes(ctx) + rewardIndexes := k.Store.GetRewardIndexes(ctx) usdxClaims := k.GetAllUSDXMintingClaims(ctx) usdxRewardState := getUSDXMintingGenesisRewardState(ctx, k) diff --git a/x/incentive/keeper/accumulators/accumulators_test.go b/x/incentive/keeper/accumulators/accumulators_test.go new file mode 100644 index 00000000..e0b88ac7 --- /dev/null +++ b/x/incentive/keeper/accumulators/accumulators_test.go @@ -0,0 +1,19 @@ +package accumulators_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var distantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func dc(denom string, amount string) sdk.DecCoin { + return sdk.NewDecCoinFromDec(denom, sdk.MustNewDecFromStr(amount)) +} +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } +func toDcs(coins ...sdk.Coin) sdk.DecCoins { return sdk.NewDecCoinsFromCoins(coins...) } +func dcs(coins ...sdk.DecCoin) sdk.DecCoins { return sdk.NewDecCoins(coins...) } diff --git a/x/incentive/keeper/accumulators/basic.go b/x/incentive/keeper/accumulators/basic.go new file mode 100644 index 00000000..65176b60 --- /dev/null +++ b/x/incentive/keeper/accumulators/basic.go @@ -0,0 +1,63 @@ +package accumulators + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/incentive/keeper/adapters" + "github.com/kava-labs/kava/x/incentive/keeper/store" + "github.com/kava-labs/kava/x/incentive/types" +) + +// BasicAccumulator is a default implementation of the RewardAccumulator +// interface. This applies to all claim types except for those with custom +// accumulator logic e.g. Earn. +type BasicAccumulator struct { + store store.IncentiveStore + adapters adapters.SourceAdapters +} + +var _ types.RewardAccumulator = BasicAccumulator{} + +// NewBasicAccumulator returns a new BasicAccumulator. +func NewBasicAccumulator( + store store.IncentiveStore, + adapters adapters.SourceAdapters, +) BasicAccumulator { + return BasicAccumulator{ + store: store, + adapters: adapters, + } +} + +// AccumulateRewards calculates new rewards to distribute this block and updates +// the global indexes to reflect this. The provided rewardPeriod must be valid +// to avoid panics in calculating time durations. +func (k BasicAccumulator) AccumulateRewards( + ctx sdk.Context, + claimType types.ClaimType, + rewardPeriod types.MultiRewardPeriod, +) error { + previousAccrualTime, found := k.store.GetRewardAccrualTime(ctx, claimType, rewardPeriod.CollateralType) + if !found { + previousAccrualTime = ctx.BlockTime() + } + + indexes, found := k.store.GetRewardIndexesOfClaimType(ctx, claimType, rewardPeriod.CollateralType) + if !found { + indexes = types.RewardIndexes{} + } + + acc := types.NewAccumulator(previousAccrualTime, indexes) + + totalSource := k.adapters.TotalSharesBySource(ctx, claimType, rewardPeriod.CollateralType) + + acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime()) + + k.store.SetRewardAccrualTime(ctx, claimType, rewardPeriod.CollateralType, acc.PreviousAccumulationTime) + if len(acc.Indexes) > 0 { + // the store panics when setting empty or nil indexes + k.store.SetRewardIndexes(ctx, claimType, rewardPeriod.CollateralType, acc.Indexes) + } + + return nil +} diff --git a/x/incentive/keeper/accumulators/basic_accum_test.go b/x/incentive/keeper/accumulators/basic_accum_test.go new file mode 100644 index 00000000..77809ad6 --- /dev/null +++ b/x/incentive/keeper/accumulators/basic_accum_test.go @@ -0,0 +1,404 @@ +package accumulators_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/app" + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" + swaptypes "github.com/kava-labs/kava/x/swap/types" +) + +type BasicAccumulatorTestSuite struct { + testutil.IntegrationTester + + keeper testutil.TestKeeper + userAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress + + pool string +} + +func TestBasicAccumulatorTestSuite(t *testing.T) { + suite.Run(t, new(BasicAccumulatorTestSuite)) +} + +func (suite *BasicAccumulatorTestSuite) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = testutil.TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + suite.userAddrs = addrs[0:2] + suite.valAddrs = []sdk.ValAddress{ + sdk.ValAddress(addrs[2]), + sdk.ValAddress(addrs[3]), + } + + poolDenomA := "btc" + poolDenomB := "usdx" + + // Setup app with test state + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(addrs[0], cs( + c("ukava", 1e12), + c(poolDenomA, 1e12), + c(poolDenomB, 1e12), + )). + WithSimpleAccount(addrs[1], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[2], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[3], cs(c("ukava", 1e12))) + + incentiveBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.GenesisTime). + WithSimpleRewardPeriod(types.CLAIM_TYPE_EARN, "bkava", cs()) + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + stakingBuilder := testutil.NewStakingGenesisBuilder() + + mintBuilder := testutil.NewMintGenesisBuilder(). + WithInflationMax(sdk.OneDec()). + WithInflationMin(sdk.OneDec()). + WithMinter(sdk.OneDec(), sdk.ZeroDec()). + WithMintDenom("ukava") + + suite.StartChainWithBuilders( + authBuilder, + incentiveBuilder, + savingsBuilder, + earnBuilder, + stakingBuilder, + mintBuilder, + ) + + suite.pool = swaptypes.PoolID(poolDenomA, poolDenomB) + + swapKeeper := suite.App.GetSwapKeeper() + swapKeeper.SetParams(suite.Ctx, swaptypes.NewParams( + swaptypes.NewAllowedPools( + swaptypes.NewAllowedPool(poolDenomA, poolDenomB), + ), + sdk.ZeroDec(), + )) + +} + +func TestAccumulateSwapRewards(t *testing.T) { + suite.Run(t, new(BasicAccumulatorTestSuite)) +} + +func (suite *BasicAccumulatorTestSuite) TestStateUpdatedWhenBlockTimeHasIncreased() { + pool := "btc:usdx" + + err := suite.DeliverSwapMsgDeposit(suite.userAddrs[0], c("btc", 1e6), c("usdx", 1e6), d("1.0")) + suite.Require().NoError(err) + + suite.keeper.StoreGlobalIndexes( + suite.Ctx, + types.CLAIM_TYPE_SWAP, + types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + }, + ) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + + newAccrualTime := previousAccrualTime.Add(1 * time.Hour) + suite.Ctx = suite.Ctx.WithBlockTime(newAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + err = suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, newAccrualTime) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64"), + }, + }) +} + +func (suite *BasicAccumulatorTestSuite) TestStateUnchangedWhenBlockTimeHasNotIncreased() { + pool := "btc:usdx" + + err := suite.DeliverSwapMsgDeposit(suite.userAddrs[0], c("btc", 1e6), c("usdx", 1e6), d("1.0")) + suite.Require().NoError(err) + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.StoreGlobalIndexes( + suite.Ctx, + types.CLAIM_TYPE_SWAP, + previousIndexes, + ) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + + suite.Ctx = suite.Ctx.WithBlockTime(previousAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + err = suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + expected, f := previousIndexes.Get(pool) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, expected) +} + +func (suite *BasicAccumulatorTestSuite) TestNoAccumulationWhenSourceSharesAreZero() { + pool := "btc:usdx" + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.StoreGlobalIndexes( + suite.Ctx, + types.CLAIM_TYPE_SWAP, previousIndexes) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + + firstAccrualTime := previousAccrualTime.Add(7 * time.Second) + suite.Ctx = suite.Ctx.WithBlockTime(firstAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + err := suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, firstAccrualTime) + expected, f := previousIndexes.Get(pool) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, expected) +} + +func (suite *BasicAccumulatorTestSuite) TestStateAddedWhenStateDoesNotExist() { + pool := "btc:usdx" + + err := suite.DeliverSwapMsgDeposit(suite.userAddrs[0], c("btc", 1e6), c("usdx", 1e6), d("1.0")) + suite.Require().NoError(err) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), + ) + + firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.Ctx = suite.Ctx.WithBlockTime(firstAccrualTime) + + err = suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // After the first accumulation only the current block time should be stored. + // The indexes will be empty as no time has passed since the previous block because it didn't exist. + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, firstAccrualTime) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, nil) + + secondAccrualTime := firstAccrualTime.Add(10 * time.Second) + suite.Ctx = suite.Ctx.WithBlockTime(secondAccrualTime) + + err = suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // After the second accumulation both current block time and indexes should be stored. + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, secondAccrualTime) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.01"), + }, + }) +} + +func (suite *BasicAccumulatorTestSuite) TestNoPanicWhenStateDoesNotExist() { + pool := "btc:usdx" + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(), + ) + + accrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.Ctx = suite.Ctx.WithBlockTime(accrualTime) + + // Accumulate with no swap shares and no rewards per second will result in no increment to the indexes. + // No increment and no previous indexes stored, results in an updated of nil. Setting this in the state panics. + // Check there is no panic. + suite.NotPanics(func() { + err := suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + }) + + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, accrualTime) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, nil) +} + +func (suite *BasicAccumulatorTestSuite) TestNoAccumulationWhenBeforeStartTime() { + pool := "btc:usdx" + + err := suite.DeliverSwapMsgDeposit(suite.userAddrs[0], c("btc", 1e6), c("usdx", 1e6), d("1.0")) + suite.Require().NoError(err) + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.StoreGlobalIndexes( + suite.Ctx, + types.CLAIM_TYPE_SWAP, previousIndexes) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + + firstAccrualTime := previousAccrualTime.Add(10 * time.Second) + + period := types.NewMultiRewardPeriod( + true, + pool, + firstAccrualTime.Add(time.Nanosecond), // start time after accrual time + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), + ) + + suite.Ctx = suite.Ctx.WithBlockTime(firstAccrualTime) + + err = suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + suite.Require().NoError(err) + + // The accrual time should be updated, but the indexes unchanged + suite.StoredTimeEquals(types.CLAIM_TYPE_SWAP, pool, firstAccrualTime) + expectedIndexes, f := previousIndexes.Get(pool) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_SWAP, pool, expectedIndexes) +} + +func (suite *BasicAccumulatorTestSuite) TestPanicWhenCurrentTimeLessThanPrevious() { + pool := "btc:usdx" + + err := suite.DeliverSwapMsgDeposit(suite.userAddrs[0], c("btc", 1e6), c("usdx", 1e6), d("1.0")) + suite.Require().NoError(err) + + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_SWAP, pool, previousAccrualTime) + + firstAccrualTime := time.Time{} + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Time{}, // start time after accrual time + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), + ) + + suite.Ctx = suite.Ctx.WithBlockTime(firstAccrualTime) + + suite.Panics(func() { + suite.keeper.AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_SWAP, period) + }) +} diff --git a/x/incentive/keeper/accumulators/earn.go b/x/incentive/keeper/accumulators/earn.go new file mode 100644 index 00000000..e6e5600d --- /dev/null +++ b/x/incentive/keeper/accumulators/earn.go @@ -0,0 +1,245 @@ +package accumulators + +import ( + "errors" + "fmt" + "sort" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/keeper/adapters" + "github.com/kava-labs/kava/x/incentive/keeper/store" + "github.com/kava-labs/kava/x/incentive/types" +) + +// EarnAccumulator is an accumulator for Earn claim types. This includes +// claiming staking rewards and reward distribution for liquid kava. +type EarnAccumulator struct { + store store.IncentiveStore + liquidKeeper types.LiquidKeeper + earnKeeper types.EarnKeeper + adapters adapters.SourceAdapters +} + +var _ types.RewardAccumulator = EarnAccumulator{} + +// NewEarnAccumulator returns a new EarnAccumulator. +func NewEarnAccumulator( + store store.IncentiveStore, + liquidKeeper types.LiquidKeeper, + earnKeeper types.EarnKeeper, + adapters adapters.SourceAdapters, +) EarnAccumulator { + return EarnAccumulator{ + store: store, + liquidKeeper: liquidKeeper, + earnKeeper: earnKeeper, + adapters: adapters, + } +} + +// AccumulateRewards calculates new rewards to distribute this block and updates +// the global indexes to reflect this. The provided rewardPeriod must be valid +// to avoid panics in calculating time durations. +func (a EarnAccumulator) AccumulateRewards( + ctx sdk.Context, + claimType types.ClaimType, + rewardPeriod types.MultiRewardPeriod, +) error { + if claimType != types.CLAIM_TYPE_EARN { + panic(fmt.Sprintf( + "invalid claim type for earn accumulator, expected %s but got %s", + types.CLAIM_TYPE_EARN, + claimType, + )) + } + + if rewardPeriod.CollateralType == "bkava" { + return a.accumulateEarnBkavaRewards(ctx, rewardPeriod) + } + + // Non bkava vaults use the basic accumulator. + return NewBasicAccumulator(a.store, a.adapters).AccumulateRewards(ctx, claimType, rewardPeriod) +} + +// accumulateEarnBkavaRewards does the same as AccumulateEarnRewards but for +// *all* bkava vaults. +func (k EarnAccumulator) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { + // All bkava vault denoms + bkavaVaultsDenoms := make(map[string]bool) + + // bkava vault denoms from earn records (non-empty vaults) + k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) { + if k.liquidKeeper.IsDerivativeDenom(ctx, record.TotalShares.Denom) { + bkavaVaultsDenoms[record.TotalShares.Denom] = true + } + + return false + }) + + // bkava vault denoms from past incentive indexes, may include vaults + // that were fully withdrawn. + k.store.IterateRewardIndexesByClaimType( + ctx, + types.CLAIM_TYPE_EARN, + func(reward types.TypedRewardIndexes) (stop bool) { + if k.liquidKeeper.IsDerivativeDenom(ctx, reward.CollateralType) { + bkavaVaultsDenoms[reward.CollateralType] = true + } + + return false + }) + + totalBkavaValue, err := k.liquidKeeper.GetTotalDerivativeValue(ctx) + if err != nil { + return err + } + + i := 0 + sortedBkavaVaultsDenoms := make([]string, len(bkavaVaultsDenoms)) + for vaultDenom := range bkavaVaultsDenoms { + sortedBkavaVaultsDenoms[i] = vaultDenom + i++ + } + + // Sort the vault denoms to ensure deterministic iteration order. + sort.Strings(sortedBkavaVaultsDenoms) + + // Accumulate rewards for each bkava vault. + for _, bkavaDenom := range sortedBkavaVaultsDenoms { + derivativeValue, err := k.liquidKeeper.GetDerivativeValue(ctx, bkavaDenom) + if err != nil { + return err + } + + k.accumulateBkavaEarnRewards( + ctx, + bkavaDenom, + rewardPeriod.Start, + rewardPeriod.End, + GetProportionalRewardsPerSecond( + rewardPeriod, + totalBkavaValue.Amount, + derivativeValue.Amount, + ), + ) + } + + return nil +} + +func GetProportionalRewardsPerSecond( + rewardPeriod types.MultiRewardPeriod, + totalBkavaSupply sdk.Int, + singleBkavaSupply sdk.Int, +) sdk.DecCoins { + // Rate per bkava-xxx = rewardsPerSecond * % of bkava-xxx + // = rewardsPerSecond * (bkava-xxx / total bkava) + // = (rewardsPerSecond * bkava-xxx) / total bkava + + newRate := sdk.NewDecCoins() + + // Prevent division by zero, if there are no total shares then there are no + // rewards. + if totalBkavaSupply.IsZero() { + return newRate + } + + for _, rewardCoin := range rewardPeriod.RewardsPerSecond { + scaledAmount := rewardCoin.Amount.ToDec(). + Mul(singleBkavaSupply.ToDec()). + Quo(totalBkavaSupply.ToDec()) + + newRate = newRate.Add(sdk.NewDecCoinFromDec(rewardCoin.Denom, scaledAmount)) + } + + return newRate +} + +func (k EarnAccumulator) accumulateBkavaEarnRewards( + ctx sdk.Context, + collateralType string, + periodStart time.Time, + periodEnd time.Time, + periodRewardsPerSecond sdk.DecCoins, +) { + // Collect staking rewards for this validator, does not have any start/end + // period time restrictions. + stakingRewards := k.collectDerivativeStakingRewards(ctx, collateralType) + + // Collect incentive rewards + // **Total rewards** for vault per second, NOT per share + perSecondRewards := k.collectPerSecondRewards( + ctx, + collateralType, + periodStart, + periodEnd, + periodRewardsPerSecond, + ) + + // **Total rewards** for vault per second, NOT per share + rewards := stakingRewards.Add(perSecondRewards...) + + // Distribute rewards by incrementing indexes + indexes, found := k.store.GetRewardIndexesOfClaimType(ctx, types.CLAIM_TYPE_EARN, collateralType) + if !found { + indexes = types.RewardIndexes{} + } + + totalSourceShares := k.adapters.TotalSharesBySource(ctx, types.CLAIM_TYPE_EARN, collateralType) + var increment types.RewardIndexes + if totalSourceShares.GT(sdk.ZeroDec()) { + // Divide total rewards by total shares to get the reward **per share** + // Leave as nil if no source shares + increment = types.NewRewardIndexesFromCoins(rewards).Quo(totalSourceShares) + } + updatedIndexes := indexes.Add(increment) + + if len(updatedIndexes) > 0 { + // the store panics when setting empty or nil indexes + k.store.SetRewardIndexes(ctx, types.CLAIM_TYPE_EARN, collateralType, updatedIndexes) + } +} + +func (k EarnAccumulator) collectDerivativeStakingRewards(ctx sdk.Context, collateralType string) sdk.DecCoins { + rewards, err := k.liquidKeeper.CollectStakingRewardsByDenom(ctx, collateralType, types.IncentiveMacc) + if err != nil { + if !errors.Is(err, distrtypes.ErrNoValidatorDistInfo) && + !errors.Is(err, distrtypes.ErrEmptyDelegationDistInfo) { + panic(fmt.Sprintf("failed to collect staking rewards for %s: %s", collateralType, err)) + } + + // otherwise there's no validator or delegation yet + rewards = nil + } + return sdk.NewDecCoinsFromCoins(rewards...) +} + +func (k EarnAccumulator) collectPerSecondRewards( + ctx sdk.Context, + collateralType string, + periodStart time.Time, + periodEnd time.Time, + periodRewardsPerSecond sdk.DecCoins, +) sdk.DecCoins { + previousAccrualTime, found := k.store.GetRewardAccrualTime(ctx, types.CLAIM_TYPE_EARN, collateralType) + if !found { + previousAccrualTime = ctx.BlockTime() + } + + rewards, accumulatedTo := types.CalculatePerSecondRewards( + periodStart, + periodEnd, + periodRewardsPerSecond, + previousAccrualTime, + ctx.BlockTime(), + ) + + k.store.SetRewardAccrualTime(ctx, types.CLAIM_TYPE_EARN, collateralType, accumulatedTo) + + // Don't need to move funds as they're assumed to be in the IncentiveMacc module account already. + return rewards +} diff --git a/x/incentive/keeper/accumulators/earn_accum_test.go b/x/incentive/keeper/accumulators/earn_accum_test.go new file mode 100644 index 00000000..dbdcc7f6 --- /dev/null +++ b/x/incentive/keeper/accumulators/earn_accum_test.go @@ -0,0 +1,667 @@ +package accumulators_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/keeper/accumulators" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" +) + +type AccumulateEarnRewardsIntegrationTests struct { + testutil.IntegrationTester + + keeper testutil.TestKeeper + userAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestAccumulateEarnRewardsIntegrationTests(t *testing.T) { + suite.Run(t, new(AccumulateEarnRewardsIntegrationTests)) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = testutil.TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + suite.userAddrs = addrs[0:2] + suite.valAddrs = []sdk.ValAddress{ + sdk.ValAddress(addrs[2]), + sdk.ValAddress(addrs[3]), + } + + // Setup app with test state + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(addrs[0], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[1], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[2], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[3], cs(c("ukava", 1e12))) + + incentiveBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.GenesisTime). + WithSimpleRewardPeriod(types.CLAIM_TYPE_EARN, "bkava", cs()) + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + stakingBuilder := testutil.NewStakingGenesisBuilder() + + mintBuilder := testutil.NewMintGenesisBuilder(). + WithInflationMax(sdk.OneDec()). + WithInflationMin(sdk.OneDec()). + WithMinter(sdk.OneDec(), sdk.ZeroDec()). + WithMintDenom("ukava") + + suite.StartChainWithBuilders( + authBuilder, + incentiveBuilder, + savingsBuilder, + earnBuilder, + stakingBuilder, + mintBuilder, + ) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUpdatedWhenBlockTimeHasIncreased() { + suite.AddIncentiveMultiRewardPeriod( + types.CLAIM_TYPE_EARN, + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 800000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 200000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + + suite.keeper.StoreGlobalIndexes(suite.Ctx, types.CLAIM_TYPE_EARN, globalIndexes) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 0") + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + stakingRewardIndexes0 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + stakingRewardIndexes1 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64").Add(stakingRewardIndexes0), + }, + }) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64").Add(stakingRewardIndexes1), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUpdatedWhenBlockTimeHasIncreased_partialDeposit() { + suite.AddIncentiveMultiRewardPeriod( + types.CLAIM_TYPE_EARN, + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + // 800000bkava0 minted, 700000 deposited + // 200000bkava1 minted, 100000 deposited + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 800000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 200000)) + suite.NoError(err) + + depositAmount0 := c(derivative0.Denom, 700000) + depositAmount1 := c(derivative1.Denom, 100000) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], depositAmount0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], depositAmount1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + + suite.keeper.StoreGlobalIndexes(suite.Ctx, types.CLAIM_TYPE_EARN, globalIndexes) + + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 0") + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + // Divided by deposit amounts, not bank supply amounts + stakingRewardIndexes0 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(depositAmount0.Amount.ToDec()) + + stakingRewardIndexes1 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(depositAmount1.Amount.ToDec()) + + // Slightly increased rewards due to less bkava deposited + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("8.248571428571428571"), + }, + { + CollateralType: "ukava", + RewardFactor: d("4.154285714285714285").Add(stakingRewardIndexes0), + }, + }) + + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("14.42"), + }, + { + CollateralType: "ukava", + RewardFactor: d("7.24").Add(stakingRewardIndexes1), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() { + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.StoreGlobalIndexes(suite.Ctx, types.CLAIM_TYPE_EARN, previousIndexes) + + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + period := types.NewMultiRewardPeriod( + true, + "bkava", + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + // Must manually accumulate rewards as BeginBlockers only run when the block time increases + // This does not run any x/mint or x/distribution BeginBlockers + earnKeeper := suite.App.GetEarnKeeper() + err = accumulators. + NewEarnAccumulator(suite.keeper.Store, suite.App.GetLiquidKeeper(), &earnKeeper, suite.keeper.Adapters). + AccumulateRewards(suite.Ctx, types.CLAIM_TYPE_EARN, period) + suite.NoError(err) + + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + expected, f := previousIndexes.Get(derivative0.Denom) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, expected) + + expected, f = previousIndexes.Get(derivative1.Denom) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, expected) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestNoAccumulationWhenSourceSharesAreZero() { + suite.AddIncentiveMultiRewardPeriod( + types.CLAIM_TYPE_EARN, + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + // No earn deposits + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.StoreGlobalIndexes(suite.Ctx, types.CLAIM_TYPE_EARN, previousIndexes) + + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, _ = suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + // check time and factors + + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + expected, f := previousIndexes.Get(derivative0.Denom) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, expected) + + expected, f = previousIndexes.Get(derivative1.Denom) + suite.True(f) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, expected) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateAddedWhenStateDoesNotExist() { + suite.AddIncentiveMultiRewardPeriod( + types.CLAIM_TYPE_EARN, + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + // After the second accumulation both current block time and indexes should be stored. + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + validatorRewards0, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + firstStakingRewardIndexes0 := validatorRewards0[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + firstStakingRewardIndexes1 := validatorRewards0[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + // After the first accumulation only the current block time should be stored. + // The indexes will be empty as no time has passed since the previous block because it didn't exist. + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + // First accumulation can have staking rewards, but no other rewards + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: firstStakingRewardIndexes0, + }, + }) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: firstStakingRewardIndexes1, + }, + }) + + _, resBeginBlock = suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + // After the second accumulation both current block time and indexes should be stored. + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, suite.Ctx.BlockTime()) + + validatorRewards1, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + secondStakingRewardIndexes0 := validatorRewards1[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + secondStakingRewardIndexes1 := validatorRewards1[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + // Second accumulation has both staking rewards and incentive rewards + // ukava incentive rewards: 3600 * 1000 / (2 * 1000000) == 1.8 + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + // Incentive rewards + both staking rewards + RewardFactor: d("1.8").Add(firstStakingRewardIndexes0).Add(secondStakingRewardIndexes0), + }, + { + CollateralType: "earn", + RewardFactor: d("3.6"), + }, + }) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + // Incentive rewards + both staking rewards + RewardFactor: d("1.8").Add(firstStakingRewardIndexes1).Add(secondStakingRewardIndexes1), + }, + { + CollateralType: "earn", + RewardFactor: d("3.6"), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestNoPanicWhenStateDoesNotExist() { + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + period := types.NewMultiRewardPeriod( + true, + "bkava", + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(), + ) + + // Accumulate with no earn shares and no rewards per second will result in no increment to the indexes. + // No increment and no previous indexes stored, results in an updated of nil. Setting this in the state panics. + // Check there is no panic. + suite.NotPanics(func() { + // This does not update any state, as there are no bkava vaults + // to iterate over, denoms are unknown + err := suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + suite.NoError(err) + }) + + // Times are not stored for vaults with no state + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative0.Denom, time.Time{}) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, derivative1.Denom, time.Time{}) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative0.Denom, nil) + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, derivative1.Denom, nil) +} diff --git a/x/incentive/keeper/rewards_earn_proportional_test.go b/x/incentive/keeper/accumulators/earn_proportional_test.go similarity index 91% rename from x/incentive/keeper/rewards_earn_proportional_test.go rename to x/incentive/keeper/accumulators/earn_proportional_test.go index bdae0469..7561d848 100644 --- a/x/incentive/keeper/rewards_earn_proportional_test.go +++ b/x/incentive/keeper/accumulators/earn_proportional_test.go @@ -1,11 +1,11 @@ -package keeper_test +package accumulators_test import ( "testing" "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/keeper/accumulators" "github.com/kava-labs/kava/x/incentive/types" "github.com/stretchr/testify/require" ) @@ -74,7 +74,7 @@ func TestGetProportionalRewardPeriod(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rewardsPerSecond := keeper.GetProportionalRewardsPerSecond( + rewardsPerSecond := accumulators.GetProportionalRewardsPerSecond( tt.giveRewardPeriod, tt.giveTotalBkavaSupply, tt.giveSingleBkavaSupply, diff --git a/x/incentive/keeper/accumulators/earn_staking_test.go b/x/incentive/keeper/accumulators/earn_staking_test.go new file mode 100644 index 00000000..93ab1c07 --- /dev/null +++ b/x/incentive/keeper/accumulators/earn_staking_test.go @@ -0,0 +1,193 @@ +package accumulators_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" +) + +type EarnAccumulatorStakingRewardsTestSuite struct { + testutil.IntegrationTester + + keeper testutil.TestKeeper + userAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestEarnStakingRewardsIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(EarnAccumulatorStakingRewardsTestSuite)) +} + +func (suite *EarnAccumulatorStakingRewardsTestSuite) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = testutil.TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + suite.userAddrs = addrs[0:2] + suite.valAddrs = []sdk.ValAddress{ + sdk.ValAddress(addrs[2]), + sdk.ValAddress(addrs[3]), + } + + // Setup app with test state + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(addrs[0], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[1], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[2], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[3], cs(c("ukava", 1e12))) + + incentiveBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.GenesisTime). + WithSimpleRewardPeriod(types.CLAIM_TYPE_EARN, "bkava", cs()) + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + stakingBuilder := testutil.NewStakingGenesisBuilder() + + mintBuilder := testutil.NewMintGenesisBuilder(). + WithInflationMax(sdk.OneDec()). + WithInflationMin(sdk.OneDec()). + WithMinter(sdk.OneDec(), sdk.ZeroDec()). + WithMintDenom("ukava") + + suite.StartChainWithBuilders( + authBuilder, + incentiveBuilder, + savingsBuilder, + earnBuilder, + stakingBuilder, + mintBuilder, + ) +} + +func (suite *EarnAccumulatorStakingRewardsTestSuite) TestStakingRewardsDistributed() { + // derivative 1: 8 total staked, 7 to earn, 1 not in earn + // derivative 2: 2 total staked, 1 to earn, 1 not in earn + userMintAmount0 := c("ukava", 8e9) + userMintAmount1 := c("ukava", 2e9) + + userDepositAmount0 := i(7e9) + userDepositAmount1 := i(1e9) + + // Create two validators + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], userMintAmount0) + suite.Require().NoError(err) + + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[1], userMintAmount1) + suite.Require().NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], sdk.NewCoin(derivative0.Denom, userDepositAmount0), earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], sdk.NewCoin(derivative1.Denom, userDepositAmount1), earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + // Get derivative denoms + lq := suite.App.GetLiquidKeeper() + vaultDenom1 := lq.GetLiquidStakingTokenDenom(suite.valAddrs[0]) + vaultDenom2 := lq.GetLiquidStakingTokenDenom(suite.valAddrs[1]) + + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.Ctx = suite.Ctx.WithBlockTime(previousAccrualTime) + + initialVault1RewardFactor := d("0.04") + initialVault2RewardFactor := d("0.04") + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: vaultDenom1, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault1RewardFactor, + }, + }, + }, + { + CollateralType: vaultDenom2, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault2RewardFactor, + }, + }, + }, + } + + suite.keeper.StoreGlobalIndexes(suite.Ctx, types.CLAIM_TYPE_EARN, globalIndexes) + + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom1, suite.Ctx.BlockTime()) + suite.keeper.Store.SetRewardAccrualTime(suite.Ctx, types.CLAIM_TYPE_EARN, vaultDenom2, suite.Ctx.BlockTime()) + + val := suite.GetAbciValidator(suite.valAddrs[0]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: true, + }}, + }, + }, + ) + + // check time and factors + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, vaultDenom1, suite.Ctx.BlockTime()) + suite.StoredTimeEquals(types.CLAIM_TYPE_EARN, vaultDenom2, suite.Ctx.BlockTime()) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 2") + + // Total staking rewards / total source shares (**deposited in earn** not total minted) + // types.RewardIndexes.Quo() uses Dec.Quo() which uses bankers rounding. + // So we need to use Dec.Quo() to also round vs Dec.QuoInt() which truncates + expectedIndexes1 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(userDepositAmount0.ToDec()) + + expectedIndexes2 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(userDepositAmount1.ToDec()) + + // Only contains staking rewards + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, vaultDenom1, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault1RewardFactor.Add(expectedIndexes1), + }, + }) + + suite.StoredIndexesEqual(types.CLAIM_TYPE_EARN, vaultDenom2, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault2RewardFactor.Add(expectedIndexes2), + }, + }) +} diff --git a/x/incentive/keeper/accumulators/earn_test.go b/x/incentive/keeper/accumulators/earn_test.go new file mode 100644 index 00000000..5bc5c33f --- /dev/null +++ b/x/incentive/keeper/accumulators/earn_test.go @@ -0,0 +1,50 @@ +package accumulators_test + +import ( + "fmt" + "time" + + "github.com/kava-labs/kava/x/incentive/keeper/accumulators" + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *AccumulateEarnRewardsIntegrationTests) TestEarnAccumulator_OnlyEarnClaimType() { + period := types.NewMultiRewardPeriod( + true, + "bkava", + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + earnKeeper := suite.App.GetEarnKeeper() + + for _, claimTypeValue := range types.ClaimType_value { + claimType := types.ClaimType(claimTypeValue) + + if claimType == types.CLAIM_TYPE_EARN { + suite.NotPanics(func() { + err := accumulators. + NewEarnAccumulator(suite.keeper.Store, suite.App.GetLiquidKeeper(), &earnKeeper, suite.keeper.Adapters). + AccumulateRewards(suite.Ctx, claimType, period) + suite.NoError(err) + }) + + continue + } + + suite.PanicsWithValue( + fmt.Sprintf( + "invalid claim type for earn accumulator, expected %s but got %s", + types.CLAIM_TYPE_EARN, + claimType, + ), + func() { + err := accumulators. + NewEarnAccumulator(suite.keeper.Store, suite.App.GetLiquidKeeper(), &earnKeeper, suite.keeper.Adapters). + AccumulateRewards(suite.Ctx, claimType, period) + suite.NoError(err) + }, + ) + } +} diff --git a/x/incentive/keeper/adapters/earn/adapter_test.go b/x/incentive/keeper/adapters/earn/adapter_test.go index 414f4100..bff948da 100644 --- a/x/incentive/keeper/adapters/earn/adapter_test.go +++ b/x/incentive/keeper/adapters/earn/adapter_test.go @@ -101,22 +101,22 @@ func (suite *EarnAdapterTestSuite) TestEarnAdapter_OwnerSharesBySource() { }, )) - suite.app.FundAccount( + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[0], sdk.NewCoins( sdk.NewCoin(vaultDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(vaultDenomB, sdk.NewInt(1000000000000)), ), - ) - suite.app.FundAccount( + )) + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[1], sdk.NewCoins( sdk.NewCoin(vaultDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(vaultDenomB, sdk.NewInt(1000000000000)), ), - ) + )) err := earnKeeper.Deposit( suite.ctx, @@ -235,22 +235,22 @@ func (suite *EarnAdapterTestSuite) TestEarnAdapter_TotalSharesBySource() { }, )) - suite.app.FundAccount( + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[0], sdk.NewCoins( sdk.NewCoin(vaultDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(vaultDenomB, sdk.NewInt(1000000000000)), ), - ) - suite.app.FundAccount( + )) + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[1], sdk.NewCoins( sdk.NewCoin(vaultDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(vaultDenomB, sdk.NewInt(1000000000000)), ), - ) + )) err := earnKeeper.Deposit( suite.ctx, diff --git a/x/incentive/keeper/adapters/swap/adapter_test.go b/x/incentive/keeper/adapters/swap/adapter_test.go index 1905069b..89da8626 100644 --- a/x/incentive/keeper/adapters/swap/adapter_test.go +++ b/x/incentive/keeper/adapters/swap/adapter_test.go @@ -92,7 +92,7 @@ func (suite *SwapAdapterTestSuite) TestSwapAdapter_OwnerSharesBySource() { sdk.ZeroDec(), )) - suite.app.FundAccount( + err := suite.app.FundAccount( suite.ctx, suite.addrs[0], sdk.NewCoins( @@ -100,7 +100,9 @@ func (suite *SwapAdapterTestSuite) TestSwapAdapter_OwnerSharesBySource() { sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)), ), ) - suite.app.FundAccount( + suite.NoError(err) + + err = suite.app.FundAccount( suite.ctx, suite.addrs[1], sdk.NewCoins( @@ -108,8 +110,9 @@ func (suite *SwapAdapterTestSuite) TestSwapAdapter_OwnerSharesBySource() { sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)), ), ) + suite.NoError(err) - err := swapKeeper.Deposit( + err = swapKeeper.Deposit( suite.ctx, suite.addrs[0], sdk.NewCoin(poolDenomA, sdk.NewInt(100)), @@ -219,22 +222,22 @@ func (suite *SwapAdapterTestSuite) TestSwapAdapter_TotalSharesBySource() { sdk.ZeroDec(), )) - suite.app.FundAccount( + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[0], sdk.NewCoins( sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)), ), - ) - suite.app.FundAccount( + )) + suite.NoError(suite.app.FundAccount( suite.ctx, suite.addrs[1], sdk.NewCoins( sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)), sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)), ), - ) + )) err := swapKeeper.Deposit( suite.ctx, diff --git a/x/incentive/keeper/keeper.go b/x/incentive/keeper/keeper.go index fa88917b..c75cca8f 100644 --- a/x/incentive/keeper/keeper.go +++ b/x/incentive/keeper/keeper.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/incentive/keeper/adapters" + "github.com/kava-labs/kava/x/incentive/keeper/store" "github.com/kava-labs/kava/x/incentive/types" ) @@ -26,7 +27,8 @@ type Keeper struct { liquidKeeper types.LiquidKeeper earnKeeper types.EarnKeeper - adapters adapters.SourceAdapters + Adapters adapters.SourceAdapters + Store store.IncentiveStore // Keepers used for APY queries mintKeeper types.MintKeeper @@ -59,10 +61,11 @@ func NewKeeper( liquidKeeper: lqk, earnKeeper: ek, - adapters: adapters.NewSourceAdapters( + Adapters: adapters.NewSourceAdapters( swpk, ek, ), + Store: store.NewIncentiveStore(cdc, key), mintKeeper: mk, distrKeeper: dk, @@ -890,277 +893,3 @@ func (k Keeper) IterateEarnRewardAccrualTimes(ctx sdk.Context, cb func(string, t } } } - -// ----------------------------------------------------------------------------- -// New deduplicated methods - -// GetClaim returns the claim in the store corresponding the the owner and -// claimType, and a boolean for if the claim was found -func (k Keeper) GetClaim( - ctx sdk.Context, - claimType types.ClaimType, - addr sdk.AccAddress, -) (types.Claim, bool) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) - bz := store.Get(addr) - if bz == nil { - return types.Claim{}, false - } - var c types.Claim - k.cdc.MustUnmarshal(bz, &c) - return c, true -} - -// SetClaim sets the claim in the store corresponding to the owner and claimType -func (k Keeper) SetClaim( - ctx sdk.Context, - c types.Claim, -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(c.Type)) - bz := k.cdc.MustMarshal(&c) - store.Set(c.Owner, bz) -} - -// DeleteClaim deletes the claim in the store corresponding to the owner and claimType -func (k Keeper) DeleteClaim( - ctx sdk.Context, - claimType types.ClaimType, - owner sdk.AccAddress, -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) - store.Delete(owner) -} - -// IterateClaimsByClaimType iterates over all claim objects in the store of a given -// claimType and preforms a callback function -func (k Keeper) IterateClaimsByClaimType( - ctx sdk.Context, - claimType types.ClaimType, - cb func(c types.Claim) (stop bool), -) { - iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var c types.Claim - k.cdc.MustUnmarshal(iterator.Value(), &c) - if cb(c) { - break - } - } -} - -// GetClaims returns all Claim objects in the store of a given claimType -func (k Keeper) GetClaims( - ctx sdk.Context, - claimType types.ClaimType, -) types.Claims { - var cs types.Claims - k.IterateClaimsByClaimType(ctx, claimType, func(c types.Claim) (stop bool) { - cs = append(cs, c) - return false - }) - - return cs -} - -// IterateClaims iterates over all claim objects of any claimType in the -// store and preforms a callback function -func (k Keeper) IterateClaims( - ctx sdk.Context, - cb func(c types.Claim) (stop bool), -) { - iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.ClaimKeyPrefix) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var c types.Claim - k.cdc.MustUnmarshal(iterator.Value(), &c) - if cb(c) { - break - } - } -} - -// GetAllClaims returns all Claim objects in the store of any claimType -func (k Keeper) GetAllClaims(ctx sdk.Context) types.Claims { - var cs types.Claims - k.IterateClaims(ctx, func(c types.Claim) (stop bool) { - cs = append(cs, c) - return false - }) - - return cs -} - -// GetRewardAccrualTime fetches the last time rewards were accrued for the -// specified ClaimType and sourceID. -func (k Keeper) GetRewardAccrualTime( - ctx sdk.Context, - claimType types.ClaimType, - sourceID string, -) (time.Time, bool) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) - b := store.Get(types.GetKeyFromSourceID(sourceID)) - if b == nil { - return time.Time{}, false - } - var accrualTime types.AccrualTime - k.cdc.MustUnmarshal(b, &accrualTime) - - return accrualTime.PreviousAccumulationTime, true -} - -// SetRewardAccrualTime stores the last time rewards were accrued for the -// specified ClaimType and sourceID. -func (k Keeper) SetRewardAccrualTime( - ctx sdk.Context, - claimType types.ClaimType, - sourceID string, - blockTime time.Time, -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) - - at := types.NewAccrualTime(claimType, sourceID, blockTime) - bz := k.cdc.MustMarshal(&at) - store.Set(types.GetKeyFromSourceID(sourceID), bz) -} - -// IterateRewardAccrualTimesByClaimType iterates over all reward accrual times of a given -// claimType and performs a callback function. -func (k Keeper) IterateRewardAccrualTimesByClaimType( - ctx sdk.Context, - claimType types.ClaimType, - cb func(string, time.Time) (stop bool), -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var accrualTime types.AccrualTime - k.cdc.MustUnmarshal(iterator.Value(), &accrualTime) - - if cb(accrualTime.CollateralType, accrualTime.PreviousAccumulationTime) { - break - } - } -} - -// IterateRewardAccrualTimes iterates over all reward accrual times of any -// claimType and performs a callback function. -func (k Keeper) IterateRewardAccrualTimes( - ctx sdk.Context, - cb func(types.AccrualTime) (stop bool), -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousRewardAccrualTimeKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var accrualTime types.AccrualTime - k.cdc.MustUnmarshal(iterator.Value(), &accrualTime) - - if cb(accrualTime) { - break - } - } -} - -// GetAllRewardAccrualTimes returns all reward accrual times of any claimType. -func (k Keeper) GetAllRewardAccrualTimes(ctx sdk.Context) types.AccrualTimes { - var ats types.AccrualTimes - k.IterateRewardAccrualTimes( - ctx, - func(accrualTime types.AccrualTime) bool { - ats = append(ats, accrualTime) - return false - }, - ) - - return ats -} - -// SetRewardIndexes stores the global reward indexes that track total rewards of -// a given claim type and collateralType. -func (k Keeper) SetRewardIndexes( - ctx sdk.Context, - claimType types.ClaimType, - collateralType string, - indexes types.RewardIndexes, -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) - bz := k.cdc.MustMarshal(&types.TypedRewardIndexes{ - ClaimType: claimType, - CollateralType: collateralType, - RewardIndexes: indexes, - }) - store.Set(types.GetKeyFromSourceID(collateralType), bz) -} - -// GetRewardIndexesOfClaimType fetches the global reward indexes that track total rewards -// of a given claimType and collateralType. -func (k Keeper) GetRewardIndexesOfClaimType( - ctx sdk.Context, - claimType types.ClaimType, - collateralType string, -) (types.RewardIndexes, bool) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) - bz := store.Get(types.GetKeyFromSourceID(collateralType)) - if bz == nil { - return types.RewardIndexes{}, false - } - - var proto types.TypedRewardIndexes - k.cdc.MustUnmarshal(bz, &proto) - return proto.RewardIndexes, true -} - -// IterateRewardIndexesByClaimType iterates over all reward index objects in the store of a -// given ClaimType and performs a callback function. -func (k Keeper) IterateRewardIndexesByClaimType( - ctx sdk.Context, - claimType types.ClaimType, - cb func(types.TypedRewardIndexes) (stop bool), -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var typedRewardIndexes types.TypedRewardIndexes - k.cdc.MustUnmarshal(iterator.Value(), &typedRewardIndexes) - - if cb(typedRewardIndexes) { - break - } - } -} - -// IterateRewardIndexes iterates over all reward index objects in the store -// of all ClaimTypes and performs a callback function. -func (k Keeper) IterateRewardIndexes( - ctx sdk.Context, - cb func(types.TypedRewardIndexes) (stop bool), -) { - store := prefix.NewStore(ctx.KVStore(k.key), types.RewardIndexesKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var typedRewardIndexes types.TypedRewardIndexes - k.cdc.MustUnmarshal(iterator.Value(), &typedRewardIndexes) - - if cb(typedRewardIndexes) { - break - } - } -} - -// GetRewardIndexes returns all reward indexes of any claimType. -func (k Keeper) GetRewardIndexes(ctx sdk.Context) types.TypedRewardIndexesList { - var tril types.TypedRewardIndexesList - k.IterateRewardIndexes( - ctx, - func(typedRewardIndexes types.TypedRewardIndexes) bool { - tril = append(tril, typedRewardIndexes) - return false - }, - ) - - return tril -} diff --git a/x/incentive/keeper/keeper_state_test.go b/x/incentive/keeper/keeper_state_test.go index f702b132..a8581778 100644 --- a/x/incentive/keeper/keeper_state_test.go +++ b/x/incentive/keeper/keeper_state_test.go @@ -20,13 +20,13 @@ func (suite *KeeperTestSuite) TestGetSetDeleteClaims() { nonEmptyMultiRewardIndexes, ) - _, found := suite.keeper.GetClaim(suite.ctx, claimType, suite.addrs[0]) + _, found := suite.keeper.Store.GetClaim(suite.ctx, claimType, suite.addrs[0]) suite.Require().False(found) suite.Require().NotPanics(func() { - suite.keeper.SetClaim(suite.ctx, c) + suite.keeper.Store.SetClaim(suite.ctx, c) }) - testC, found := suite.keeper.GetClaim(suite.ctx, claimType, suite.addrs[0]) + testC, found := suite.keeper.Store.GetClaim(suite.ctx, claimType, suite.addrs[0]) suite.Require().True(found) suite.Require().Equal(c, testC) @@ -38,14 +38,14 @@ func (suite *KeeperTestSuite) TestGetSetDeleteClaims() { } otherClaimType := types.ClaimType(otherClaimTypeValue) - _, found := suite.keeper.GetClaim(suite.ctx, otherClaimType, suite.addrs[0]) + _, found := suite.keeper.Store.GetClaim(suite.ctx, otherClaimType, suite.addrs[0]) suite.Require().False(found, "claim type %s should not exist", otherClaimTypeName) } suite.Require().NotPanics(func() { - suite.keeper.DeleteClaim(suite.ctx, claimType, suite.addrs[0]) + suite.keeper.Store.DeleteClaim(suite.ctx, claimType, suite.addrs[0]) }) - _, found = suite.keeper.GetClaim(suite.ctx, claimType, suite.addrs[0]) + _, found = suite.keeper.Store.GetClaim(suite.ctx, claimType, suite.addrs[0]) suite.Require().False(found) }) } @@ -65,14 +65,14 @@ func (suite *KeeperTestSuite) TestIterateClaims() { } for _, claim := range claims { - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) } for _, claimTypeValue := range types.ClaimType_value { claimType := types.ClaimType(claimTypeValue) // Claims of specific claim type only should be returned - claims := suite.keeper.GetClaims(suite.ctx, claimType) + claims := suite.keeper.Store.GetClaims(suite.ctx, claimType) suite.Require().Len(claims, 2) suite.Require().Equalf( claims, types.Claims{ @@ -83,7 +83,7 @@ func (suite *KeeperTestSuite) TestIterateClaims() { ) } - allClaims := suite.keeper.GetAllClaims(suite.ctx) + allClaims := suite.keeper.Store.GetAllClaims(suite.ctx) suite.Require().Len(allClaims, len(claims)) suite.Require().ElementsMatch(allClaims, claims, "GetAllClaims() should return claims of all types") } @@ -111,11 +111,11 @@ func (suite *KeeperTestSuite) TestGetSetRewardAccrualTimes() { suite.Run(tc.name, func() { suite.SetupApp() - _, found := suite.keeper.GetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey) + _, found := suite.keeper.Store.GetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey) suite.False(found) setFunc := func() { - suite.keeper.SetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey, tc.accrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey, tc.accrualTime) } if tc.panics { suite.Panics(setFunc) @@ -131,11 +131,11 @@ func (suite *KeeperTestSuite) TestGetSetRewardAccrualTimes() { continue } - _, found := suite.keeper.GetRewardAccrualTime(suite.ctx, claimType, tc.subKey) + _, found := suite.keeper.Store.GetRewardAccrualTime(suite.ctx, claimType, tc.subKey) suite.False(found, "reward accrual time for claim type %s should not exist", claimType) } - storedTime, found := suite.keeper.GetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey) + storedTime, found := suite.keeper.Store.GetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, tc.subKey) suite.True(found) suite.Equal(tc.accrualTime, storedTime) }) @@ -213,11 +213,11 @@ func (suite *KeeperTestSuite) TestGetSetRewardIndexes() { suite.Run(tc.name, func() { suite.SetupApp() - _, found := suite.keeper.GetRewardIndexesOfClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType) + _, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType) suite.False(found) setFunc := func() { - suite.keeper.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType, tc.indexes) + suite.keeper.Store.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType, tc.indexes) } if tc.panics { suite.Panics(setFunc) @@ -226,7 +226,7 @@ func (suite *KeeperTestSuite) TestGetSetRewardIndexes() { suite.NotPanics(setFunc) } - storedIndexes, found := suite.keeper.GetRewardIndexesOfClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType) + storedIndexes, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, tc.collateralType) suite.True(found) suite.Equal(tc.wantIndex, storedIndexes) @@ -239,7 +239,7 @@ func (suite *KeeperTestSuite) TestGetSetRewardIndexes() { otherClaimType := types.ClaimType(otherClaimTypeValue) // Other claim types should not be affected - _, found := suite.keeper.GetRewardIndexesOfClaimType(suite.ctx, otherClaimType, tc.collateralType) + _, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.ctx, otherClaimType, tc.collateralType) suite.False(found) } }) @@ -252,11 +252,11 @@ func (suite *KeeperTestSuite) TestIterateRewardAccrualTimes() { expectedAccrualTimes := nonEmptyAccrualTimes for _, at := range expectedAccrualTimes { - suite.keeper.SetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, at.denom, at.time) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, at.denom, at.time) } var actualAccrualTimes []accrualtime - suite.keeper.IterateRewardAccrualTimesByClaimType(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, func(denom string, accrualTime time.Time) bool { + suite.keeper.Store.IterateRewardAccrualTimesByClaimType(suite.ctx, types.CLAIM_TYPE_USDX_MINTING, func(denom string, accrualTime time.Time) bool { actualAccrualTimes = append(actualAccrualTimes, accrualtime{denom: denom, time: accrualTime}) return false }) @@ -278,7 +278,7 @@ func (suite *KeeperTestSuite) TestIterateAllRewardAccrualTimes() { } for _, at := range nonEmptyAccrualTimes { - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, at.denom, at.time) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, at.denom, at.time) expectedAccrualTimes = append(expectedAccrualTimes, types.NewAccrualTime( claimType, @@ -290,7 +290,7 @@ func (suite *KeeperTestSuite) TestIterateAllRewardAccrualTimes() { } var actualAccrualTimes types.AccrualTimes - suite.keeper.IterateRewardAccrualTimes( + suite.keeper.Store.IterateRewardAccrualTimes( suite.ctx, func(accrualTime types.AccrualTime) bool { actualAccrualTimes = append(actualAccrualTimes, accrualTime) @@ -354,16 +354,16 @@ func (suite *KeeperTestSuite) TestIterateRewardIndexes() { } for _, mi := range swapMultiIndexes { - suite.keeper.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_SWAP, mi.CollateralType, mi.RewardIndexes) + suite.keeper.Store.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_SWAP, mi.CollateralType, mi.RewardIndexes) } for _, mi := range earnMultiIndexes { // These should be excluded when iterating over swap indexes - suite.keeper.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_EARN, mi.CollateralType, mi.RewardIndexes) + suite.keeper.Store.SetRewardIndexes(suite.ctx, types.CLAIM_TYPE_EARN, mi.CollateralType, mi.RewardIndexes) } actualMultiIndexesMap := make(map[types.ClaimType]types.MultiRewardIndexes) - suite.keeper.IterateRewardIndexesByClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, func(rewardIndex types.TypedRewardIndexes) bool { + suite.keeper.Store.IterateRewardIndexesByClaimType(suite.ctx, types.CLAIM_TYPE_SWAP, func(rewardIndex types.TypedRewardIndexes) bool { actualMultiIndexesMap[rewardIndex.ClaimType] = actualMultiIndexesMap[rewardIndex.ClaimType].With(rewardIndex.CollateralType, rewardIndex.RewardIndexes) return false }) @@ -407,12 +407,12 @@ func (suite *KeeperTestSuite) TestIterateAllRewardIndexes() { claimType := types.ClaimType(claimTypeValue) for _, mi := range multiIndexes { - suite.keeper.SetRewardIndexes(suite.ctx, claimType, mi.CollateralType, mi.RewardIndexes) + suite.keeper.Store.SetRewardIndexes(suite.ctx, claimType, mi.CollateralType, mi.RewardIndexes) } } actualMultiIndexesMap := make(map[types.ClaimType]types.MultiRewardIndexes) - suite.keeper.IterateRewardIndexes(suite.ctx, func(rewardIndex types.TypedRewardIndexes) bool { + suite.keeper.Store.IterateRewardIndexes(suite.ctx, func(rewardIndex types.TypedRewardIndexes) bool { actualMultiIndexesMap[rewardIndex.ClaimType] = actualMultiIndexesMap[rewardIndex.ClaimType].With(rewardIndex.CollateralType, rewardIndex.RewardIndexes) return false }) diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index 9f80f892..c079a846 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/keeper/accumulators" "github.com/kava-labs/kava/x/incentive/types" ) @@ -13,28 +14,17 @@ func (k Keeper) AccumulateRewards( ctx sdk.Context, claimType types.ClaimType, rewardPeriod types.MultiRewardPeriod, -) { - previousAccrualTime, found := k.GetRewardAccrualTime(ctx, claimType, rewardPeriod.CollateralType) - if !found { - previousAccrualTime = ctx.BlockTime() +) error { + var accumulator types.RewardAccumulator + + switch claimType { + case types.CLAIM_TYPE_EARN: + accumulator = accumulators.NewEarnAccumulator(k.Store, k.liquidKeeper, k.earnKeeper, k.Adapters) + default: + accumulator = accumulators.NewBasicAccumulator(k.Store, k.Adapters) } - indexes, found := k.GetRewardIndexesOfClaimType(ctx, claimType, rewardPeriod.CollateralType) - if !found { - indexes = types.RewardIndexes{} - } - - acc := types.NewAccumulator(previousAccrualTime, indexes) - - totalSource := k.adapters.TotalSharesBySource(ctx, claimType, rewardPeriod.CollateralType) - - acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime()) - - k.SetRewardAccrualTime(ctx, claimType, rewardPeriod.CollateralType, acc.PreviousAccumulationTime) - if len(acc.Indexes) > 0 { - // the store panics when setting empty or nil indexes - k.SetRewardIndexes(ctx, claimType, rewardPeriod.CollateralType, acc.Indexes) - } + return accumulator.AccumulateRewards(ctx, claimType, rewardPeriod) } // InitializeClaim creates a new claim with zero rewards and indexes matching @@ -45,18 +35,18 @@ func (k Keeper) InitializeClaim( sourceID string, owner sdk.AccAddress, ) { - claim, found := k.GetClaim(ctx, claimType, owner) + claim, found := k.Store.GetClaim(ctx, claimType, owner) if !found { claim = types.NewClaim(claimType, owner, sdk.Coins{}, nil) } - globalRewardIndexes, found := k.GetRewardIndexesOfClaimType(ctx, claimType, sourceID) + globalRewardIndexes, found := k.Store.GetRewardIndexesOfClaimType(ctx, claimType, sourceID) if !found { globalRewardIndexes = types.RewardIndexes{} } claim.RewardIndexes = claim.RewardIndexes.With(sourceID, globalRewardIndexes) - k.SetClaim(ctx, claim) + k.Store.SetClaim(ctx, claim) } // SynchronizeClaim updates the claim object by adding any accumulated rewards @@ -68,13 +58,13 @@ func (k Keeper) SynchronizeClaim( owner sdk.AccAddress, shares sdk.Dec, ) { - claim, found := k.GetClaim(ctx, claimType, owner) + claim, found := k.Store.GetClaim(ctx, claimType, owner) if !found { return } claim = k.synchronizeClaim(ctx, claim, sourceID, owner, shares) - k.SetClaim(ctx, claim) + k.Store.SetClaim(ctx, claim) } // synchronizeClaim updates the reward and indexes in a claim for one sourceID. @@ -85,7 +75,7 @@ func (k *Keeper) synchronizeClaim( owner sdk.AccAddress, shares sdk.Dec, ) types.Claim { - globalRewardIndexes, found := k.GetRewardIndexesOfClaimType(ctx, claim.Type, sourceID) + globalRewardIndexes, found := k.Store.GetRewardIndexesOfClaimType(ctx, claim.Type, sourceID) if !found { // The global factor is only not found if // - the pool has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit) @@ -124,19 +114,19 @@ func (k Keeper) GetSynchronizedClaim( claimType types.ClaimType, owner sdk.AccAddress, ) (types.Claim, bool) { - claim, found := k.GetClaim(ctx, claimType, owner) + claim, found := k.Store.GetClaim(ctx, claimType, owner) if !found { return types.Claim{}, false } // Fetch all source IDs from indexes var sourceIDs []string - k.IterateRewardIndexesByClaimType(ctx, claimType, func(rewardIndexes types.TypedRewardIndexes) bool { + k.Store.IterateRewardIndexesByClaimType(ctx, claimType, func(rewardIndexes types.TypedRewardIndexes) bool { sourceIDs = append(sourceIDs, rewardIndexes.CollateralType) return false }) - accShares := k.adapters.OwnerSharesBySource(ctx, claimType, owner, sourceIDs) + accShares := k.Adapters.OwnerSharesBySource(ctx, claimType, owner, sourceIDs) // Synchronize claim for each source ID for _, share := range accShares { diff --git a/x/incentive/keeper/rewards_accumulate_test.go b/x/incentive/keeper/rewards_accumulate_test.go index ae3f1ca8..fa517c06 100644 --- a/x/incentive/keeper/rewards_accumulate_test.go +++ b/x/incentive/keeper/rewards_accumulate_test.go @@ -21,7 +21,7 @@ func (suite *AccumulateTestSuite) storedTimeEquals( poolID string, expected time.Time, ) { - storedTime, found := suite.keeper.GetRewardAccrualTime(suite.ctx, claimType, poolID) + storedTime, found := suite.keeper.Store.GetRewardAccrualTime(suite.ctx, claimType, poolID) suite.True(found) suite.Equal(expected, storedTime) } @@ -31,7 +31,7 @@ func (suite *AccumulateTestSuite) storedIndexesEquals( poolID string, expected types.RewardIndexes, ) { - storedIndexes, found := suite.keeper.GetRewardIndexesOfClaimType(suite.ctx, claimType, poolID) + storedIndexes, found := suite.keeper.Store.GetRewardIndexesOfClaimType(suite.ctx, claimType, poolID) suite.Equal(found, expected != nil) if found { suite.Equal(expected, storedIndexes) @@ -63,7 +63,7 @@ func (suite *AccumulateTestSuite) TestStateUpdatedWhenBlockTimeHasIncreased() { }, }) previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) newAccrualTime := previousAccrualTime.Add(1 * time.Hour) suite.ctx = suite.ctx.WithBlockTime(newAccrualTime) @@ -117,7 +117,7 @@ func (suite *AccumulateTestSuite) TestStateUnchangedWhenBlockTimeHasNotIncreased } suite.storeGlobalIndexes(claimType, previousIndexes) previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime) @@ -163,7 +163,7 @@ func (suite *AccumulateTestSuite) TestNoAccumulationWhenSourceSharesAreZero() { } suite.storeGlobalIndexes(claimType, previousIndexes) previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) firstAccrualTime := previousAccrualTime.Add(7 * time.Second) suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime) @@ -283,7 +283,7 @@ func (suite *AccumulateTestSuite) TestNoAccumulationWhenBeforeStartTime() { } suite.storeGlobalIndexes(claimType, previousIndexes) previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) firstAccrualTime := previousAccrualTime.Add(10 * time.Second) @@ -314,7 +314,7 @@ func (suite *AccumulateTestSuite) TestPanicWhenCurrentTimeLessThanPrevious() { suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper, nil, nil, nil) previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) - suite.keeper.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) + suite.keeper.Store.SetRewardAccrualTime(suite.ctx, claimType, pool, previousAccrualTime) firstAccrualTime := time.Time{} diff --git a/x/incentive/keeper/rewards_earn.go b/x/incentive/keeper/rewards_earn.go index 34dd8c46..c73d14f8 100644 --- a/x/incentive/keeper/rewards_earn.go +++ b/x/incentive/keeper/rewards_earn.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/keeper/accumulators" "github.com/kava-labs/kava/x/incentive/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" @@ -32,34 +33,6 @@ func (k Keeper) AccumulateEarnRewards(ctx sdk.Context, rewardPeriod types.MultiR return nil } -func GetProportionalRewardsPerSecond( - rewardPeriod types.MultiRewardPeriod, - totalBkavaSupply sdk.Int, - singleBkavaSupply sdk.Int, -) sdk.DecCoins { - // Rate per bkava-xxx = rewardsPerSecond * % of bkava-xxx - // = rewardsPerSecond * (bkava-xxx / total bkava) - // = (rewardsPerSecond * bkava-xxx) / total bkava - - newRate := sdk.NewDecCoins() - - // Prevent division by zero, if there are no total shares then there are no - // rewards. - if totalBkavaSupply.IsZero() { - return newRate - } - - for _, rewardCoin := range rewardPeriod.RewardsPerSecond { - scaledAmount := rewardCoin.Amount.ToDec(). - Mul(singleBkavaSupply.ToDec()). - Quo(totalBkavaSupply.ToDec()) - - newRate = newRate.Add(sdk.NewDecCoinFromDec(rewardCoin.Denom, scaledAmount)) - } - - return newRate -} - // accumulateEarnBkavaRewards does the same as AccumulateEarnRewards but for // *all* bkava vaults. func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error { @@ -112,7 +85,7 @@ func (k Keeper) accumulateEarnBkavaRewards(ctx sdk.Context, rewardPeriod types.M bkavaDenom, rewardPeriod.Start, rewardPeriod.End, - GetProportionalRewardsPerSecond( + accumulators.GetProportionalRewardsPerSecond( rewardPeriod, totalBkavaValue.Amount, derivativeValue.Amount, diff --git a/x/incentive/keeper/rewards_earn_accum_integration_test.go b/x/incentive/keeper/rewards_earn_accum_integration_test.go index 51e3dfa0..e215ab25 100644 --- a/x/incentive/keeper/rewards_earn_accum_integration_test.go +++ b/x/incentive/keeper/rewards_earn_accum_integration_test.go @@ -383,7 +383,8 @@ func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUnchangedWhenBlockT // Must manually accumulate rewards as BeginBlockers only run when the block time increases // This does not run any x/mint or x/distribution BeginBlockers - suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + err = suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + suite.NoError(err) // check time and factors @@ -646,7 +647,8 @@ func (suite *AccumulateEarnRewardsIntegrationTests) TestNoPanicWhenStateDoesNotE suite.NotPanics(func() { // This does not update any state, as there are no bkava vaults // to iterate over, denoms are unknown - suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + err := suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + suite.NoError(err) }) // Times are not stored for vaults with no state diff --git a/x/incentive/keeper/rewards_init_test.go b/x/incentive/keeper/rewards_init_test.go index 0123968a..1870a478 100644 --- a/x/incentive/keeper/rewards_init_test.go +++ b/x/incentive/keeper/rewards_init_test.go @@ -37,7 +37,7 @@ func (suite *InitializeClaimTests) TestClaimAddedWhenClaimDoesNotExistAndNoRewar suite.keeper.InitializeClaim(suite.ctx, claimType, collateralType, owner) - syncedClaim, found := suite.keeper.GetClaim(suite.ctx, claimType, owner) + syncedClaim, found := suite.keeper.Store.GetClaim(suite.ctx, claimType, owner) suite.True(found) // A new claim should have empty indexes. It doesn't strictly need the collateralType either. expectedIndexes := types.MultiRewardIndexes{{ @@ -72,7 +72,7 @@ func (suite *InitializeClaimTests) TestClaimAddedWhenClaimDoesNotExistAndRewards suite.keeper.InitializeClaim(suite.ctx, claimType, collateralType, owner) - syncedClaim, found := suite.keeper.GetClaim(suite.ctx, claimType, owner) + syncedClaim, found := suite.keeper.Store.GetClaim(suite.ctx, claimType, owner) suite.True(found) // a new claim should start with the current global indexes suite.Equal(globalIndexes, syncedClaim.RewardIndexes) @@ -107,13 +107,13 @@ func (suite *InitializeClaimTests) TestClaimUpdatedWhenClaimExistsAndNoRewards() }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) // no global indexes stored as the new pool is not rewarded suite.keeper.InitializeClaim(suite.ctx, claimType, newCollateralType, claim.Owner) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // The preexisting indexes shouldn't be changed. It doesn't strictly need the new collateralType either. expectedIndexes := types.MultiRewardIndexes{ { @@ -163,7 +163,7 @@ func (suite *InitializeClaimTests) TestClaimUpdatedWhenClaimExistsAndRewardsExis }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -179,7 +179,7 @@ func (suite *InitializeClaimTests) TestClaimUpdatedWhenClaimExistsAndRewardsExis suite.keeper.InitializeClaim(suite.ctx, claimType, newCollateralType, claim.Owner) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // only the indexes for the new pool should be updated expectedIndexes := types.MultiRewardIndexes{ { diff --git a/x/incentive/keeper/rewards_sync_test.go b/x/incentive/keeper/rewards_sync_test.go index 07ba1165..5e10ebcf 100644 --- a/x/incentive/keeper/rewards_sync_test.go +++ b/x/incentive/keeper/rewards_sync_test.go @@ -51,7 +51,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesHaveIncreas }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -70,7 +70,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesHaveIncreas suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // indexes updated from global suite.Equal(globalIndexes, syncedClaim.RewardIndexes) // new reward is (new index - old index) * user shares @@ -104,7 +104,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenGlobalIndexesUnchanged Reward: arbitraryCoins(), RewardIndexes: unchangingIndexes, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) suite.storeGlobalIndexes(claimType, unchangingIndexes) @@ -112,7 +112,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenGlobalIndexesUnchanged suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // claim should have the same rewards and indexes as before suite.Equal(claim, syncedClaim) } @@ -141,7 +141,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardAdded() { }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -171,7 +171,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardAdded() { suite.keeper.SynchronizeClaim(suite.ctx, claimType, newlyRewardcollateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // the new indexes should be added to the claim, but the old ones should be unchanged newlyRewrdedIndexes, _ := globalIndexes.Get(newlyRewardcollateralType) expectedIndexes := claim.RewardIndexes.With(newlyRewardcollateralType, newlyRewrdedIndexes) @@ -197,7 +197,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenNoReward() { Reward: arbitraryCoins(), RewardIndexes: nonEmptyMultiRewardIndexes, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) // No global indexes stored as this pool is not rewarded @@ -205,7 +205,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenNoReward() { suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) suite.Equal(claim, syncedClaim) } @@ -233,7 +233,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardDenomAdded() { }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -258,7 +258,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardDenomAdded() { suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // indexes should have the new reward denom added suite.Equal(globalIndexes, syncedClaim.RewardIndexes) // new reward is (new index - old index) * shares @@ -293,7 +293,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesIncreasedAn }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -312,7 +312,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesIncreasedAn suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec()) - syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) + syncedClaim, _ := suite.keeper.Store.GetClaim(suite.ctx, claimType, claim.Owner) // indexes updated from global suite.Equal(globalIndexes, syncedClaim.RewardIndexes) // reward is unchanged @@ -339,7 +339,7 @@ func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUnchangedWhenNoGloba }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) // no global indexes for any pool @@ -377,7 +377,7 @@ func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUpdatedWhenMissingIn }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { @@ -437,7 +437,7 @@ func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUpdatedWhenMissingIn }, }, } - suite.keeper.SetClaim(suite.ctx, claim) + suite.keeper.Store.SetClaim(suite.ctx, claim) globalIndexes := types.MultiRewardIndexes{ { diff --git a/x/incentive/keeper/store/accrual_time.go b/x/incentive/keeper/store/accrual_time.go new file mode 100644 index 00000000..1b987d92 --- /dev/null +++ b/x/incentive/keeper/store/accrual_time.go @@ -0,0 +1,95 @@ +package store + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// GetRewardAccrualTime fetches the last time rewards were accrued for the +// specified ClaimType and sourceID. +func (k IncentiveStore) GetRewardAccrualTime( + ctx sdk.Context, + claimType types.ClaimType, + sourceID string, +) (time.Time, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) + b := store.Get(types.GetKeyFromSourceID(sourceID)) + if b == nil { + return time.Time{}, false + } + var accrualTime types.AccrualTime + k.cdc.MustUnmarshal(b, &accrualTime) + + return accrualTime.PreviousAccumulationTime, true +} + +// SetRewardAccrualTime stores the last time rewards were accrued for the +// specified ClaimType and sourceID. +func (k IncentiveStore) SetRewardAccrualTime( + ctx sdk.Context, + claimType types.ClaimType, + sourceID string, + blockTime time.Time, +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) + + at := types.NewAccrualTime(claimType, sourceID, blockTime) + bz := k.cdc.MustMarshal(&at) + store.Set(types.GetKeyFromSourceID(sourceID), bz) +} + +// IterateRewardAccrualTimesByClaimType iterates over all reward accrual times of a given +// claimType and performs a callback function. +func (k IncentiveStore) IterateRewardAccrualTimesByClaimType( + ctx sdk.Context, + claimType types.ClaimType, + cb func(string, time.Time) (stop bool), +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetPreviousRewardAccrualTimeKeyPrefix(claimType)) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var accrualTime types.AccrualTime + k.cdc.MustUnmarshal(iterator.Value(), &accrualTime) + + if cb(accrualTime.CollateralType, accrualTime.PreviousAccumulationTime) { + break + } + } +} + +// IterateRewardAccrualTimes iterates over all reward accrual times of any +// claimType and performs a callback function. +func (k IncentiveStore) IterateRewardAccrualTimes( + ctx sdk.Context, + cb func(types.AccrualTime) (stop bool), +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousRewardAccrualTimeKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var accrualTime types.AccrualTime + k.cdc.MustUnmarshal(iterator.Value(), &accrualTime) + + if cb(accrualTime) { + break + } + } +} + +// GetAllRewardAccrualTimes returns all reward accrual times of any claimType. +func (k IncentiveStore) GetAllRewardAccrualTimes(ctx sdk.Context) types.AccrualTimes { + var ats types.AccrualTimes + k.IterateRewardAccrualTimes( + ctx, + func(accrualTime types.AccrualTime) bool { + ats = append(ats, accrualTime) + return false + }, + ) + + return ats +} diff --git a/x/incentive/keeper/store/claim.go b/x/incentive/keeper/store/claim.go new file mode 100644 index 00000000..0da96f63 --- /dev/null +++ b/x/incentive/keeper/store/claim.go @@ -0,0 +1,105 @@ +package store + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/cosmos-sdk/store/prefix" + "github.com/kava-labs/kava/x/incentive/types" +) + +// GetClaim returns the claim in the store corresponding the the owner and +// claimType, and a boolean for if the claim was found +func (k IncentiveStore) GetClaim( + ctx sdk.Context, + claimType types.ClaimType, + addr sdk.AccAddress, +) (types.Claim, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) + bz := store.Get(addr) + if bz == nil { + return types.Claim{}, false + } + var c types.Claim + k.cdc.MustUnmarshal(bz, &c) + return c, true +} + +// SetClaim sets the claim in the store corresponding to the owner and claimType +func (k IncentiveStore) SetClaim( + ctx sdk.Context, + c types.Claim, +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(c.Type)) + bz := k.cdc.MustMarshal(&c) + store.Set(c.Owner, bz) +} + +// DeleteClaim deletes the claim in the store corresponding to the owner and claimType +func (k IncentiveStore) DeleteClaim( + ctx sdk.Context, + claimType types.ClaimType, + owner sdk.AccAddress, +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) + store.Delete(owner) +} + +// IterateClaimsByClaimType iterates over all claim objects in the store of a given +// claimType and preforms a callback function +func (k IncentiveStore) IterateClaimsByClaimType( + ctx sdk.Context, + claimType types.ClaimType, + cb func(c types.Claim) (stop bool), +) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.GetClaimKeyPrefix(claimType)) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var c types.Claim + k.cdc.MustUnmarshal(iterator.Value(), &c) + if cb(c) { + break + } + } +} + +// GetClaims returns all Claim objects in the store of a given claimType +func (k IncentiveStore) GetClaims( + ctx sdk.Context, + claimType types.ClaimType, +) types.Claims { + var cs types.Claims + k.IterateClaimsByClaimType(ctx, claimType, func(c types.Claim) (stop bool) { + cs = append(cs, c) + return false + }) + + return cs +} + +// IterateClaims iterates over all claim objects of any claimType in the +// store and preforms a callback function +func (k IncentiveStore) IterateClaims( + ctx sdk.Context, + cb func(c types.Claim) (stop bool), +) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.ClaimKeyPrefix) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var c types.Claim + k.cdc.MustUnmarshal(iterator.Value(), &c) + if cb(c) { + break + } + } +} + +// GetAllClaims returns all Claim objects in the store of any claimType +func (k IncentiveStore) GetAllClaims(ctx sdk.Context) types.Claims { + var cs types.Claims + k.IterateClaims(ctx, func(c types.Claim) (stop bool) { + cs = append(cs, c) + return false + }) + + return cs +} diff --git a/x/incentive/keeper/store/reward_index.go b/x/incentive/keeper/store/reward_index.go new file mode 100644 index 00000000..09180f5f --- /dev/null +++ b/x/incentive/keeper/store/reward_index.go @@ -0,0 +1,95 @@ +package store + +import ( + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +// SetRewardIndexes stores the global reward indexes that track total rewards of +// a given claim type and collateralType. +func (k IncentiveStore) SetRewardIndexes( + ctx sdk.Context, + claimType types.ClaimType, + collateralType string, + indexes types.RewardIndexes, +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) + bz := k.cdc.MustMarshal(&types.TypedRewardIndexes{ + ClaimType: claimType, + CollateralType: collateralType, + RewardIndexes: indexes, + }) + store.Set(types.GetKeyFromSourceID(collateralType), bz) +} + +// GetRewardIndexesOfClaimType fetches the global reward indexes that track total rewards +// of a given claimType and collateralType. +func (k IncentiveStore) GetRewardIndexesOfClaimType( + ctx sdk.Context, + claimType types.ClaimType, + collateralType string, +) (types.RewardIndexes, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) + bz := store.Get(types.GetKeyFromSourceID(collateralType)) + if bz == nil { + return types.RewardIndexes{}, false + } + + var proto types.TypedRewardIndexes + k.cdc.MustUnmarshal(bz, &proto) + return proto.RewardIndexes, true +} + +// IterateRewardIndexesByClaimType iterates over all reward index objects in the store of a +// given ClaimType and performs a callback function. +func (k IncentiveStore) IterateRewardIndexesByClaimType( + ctx sdk.Context, + claimType types.ClaimType, + cb func(types.TypedRewardIndexes) (stop bool), +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.GetRewardIndexesKeyPrefix(claimType)) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var typedRewardIndexes types.TypedRewardIndexes + k.cdc.MustUnmarshal(iterator.Value(), &typedRewardIndexes) + + if cb(typedRewardIndexes) { + break + } + } +} + +// IterateRewardIndexes iterates over all reward index objects in the store +// of all ClaimTypes and performs a callback function. +func (k IncentiveStore) IterateRewardIndexes( + ctx sdk.Context, + cb func(types.TypedRewardIndexes) (stop bool), +) { + store := prefix.NewStore(ctx.KVStore(k.key), types.RewardIndexesKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var typedRewardIndexes types.TypedRewardIndexes + k.cdc.MustUnmarshal(iterator.Value(), &typedRewardIndexes) + + if cb(typedRewardIndexes) { + break + } + } +} + +// GetRewardIndexes returns all reward indexes of any claimType. +func (k IncentiveStore) GetRewardIndexes(ctx sdk.Context) types.TypedRewardIndexesList { + var tril types.TypedRewardIndexesList + k.IterateRewardIndexes( + ctx, + func(typedRewardIndexes types.TypedRewardIndexes) bool { + tril = append(tril, typedRewardIndexes) + return false + }, + ) + + return tril +} diff --git a/x/incentive/keeper/store/store.go b/x/incentive/keeper/store/store.go new file mode 100644 index 00000000..5b6aaa4b --- /dev/null +++ b/x/incentive/keeper/store/store.go @@ -0,0 +1,20 @@ +package store + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// IncentiveStore provides methods for interacting with the incentive store. +type IncentiveStore struct { + cdc codec.Codec + key sdk.StoreKey +} + +// NewIncentiveStore creates a new IncentiveStore +func NewIncentiveStore(cdc codec.Codec, key sdk.StoreKey) IncentiveStore { + return IncentiveStore{ + cdc: cdc, + key: key, + } +} diff --git a/x/incentive/keeper/unit_test.go b/x/incentive/keeper/unit_test.go index dee48e27..b632aaa7 100644 --- a/x/incentive/keeper/unit_test.go +++ b/x/incentive/keeper/unit_test.go @@ -121,7 +121,7 @@ func (suite *unitTester) storeGlobalEarnIndexes(indexes types.MultiRewardIndexes func (suite *unitTester) storeGlobalIndexes(claimType types.ClaimType, indexes types.MultiRewardIndexes) { for _, i := range indexes { - suite.keeper.SetRewardIndexes(suite.ctx, claimType, i.CollateralType, i.RewardIndexes) + suite.keeper.Store.SetRewardIndexes(suite.ctx, claimType, i.CollateralType, i.RewardIndexes) } } diff --git a/x/incentive/testutil/builder.go b/x/incentive/testutil/builder.go index 3190bedd..0874d3bb 100644 --- a/x/incentive/testutil/builder.go +++ b/x/incentive/testutil/builder.go @@ -1,6 +1,7 @@ package testutil import ( + "fmt" "time" "github.com/cosmos/cosmos-sdk/codec" @@ -191,6 +192,48 @@ func (builder IncentiveGenesisBuilder) WithSimpleEarnRewardPeriod(ctype string, return builder.WithInitializedEarnRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond)) } +// WithInitializedRewardPeriod adds an initialized typed reward period. +func (builder IncentiveGenesisBuilder) WithInitializedRewardPeriod( + claimType types.ClaimType, + periods types.MultiRewardPeriods, +) IncentiveGenesisBuilder { + // Append to claim type if it exists -- claim types must be unique in RewardPeriods field + for _, rewardPeriod := range builder.Params.RewardPeriods { + if rewardPeriod.ClaimType == claimType { + rewardPeriod.RewardPeriods = append(rewardPeriod.RewardPeriods, periods...) + return builder + } + } + + // Add new reward period for claim type + builder.Params.RewardPeriods = append(builder.Params.RewardPeriods, types.NewTypedMultiRewardPeriod(claimType, periods)) + + for _, period := range periods { + accumulationTimeForPeriod := types.NewAccrualTime(claimType, period.CollateralType, builder.genesisTime) + builder.AccrualTimes = append( + builder.AccrualTimes, + accumulationTimeForPeriod, + ) + + if err := builder.AccrualTimes.Validate(); err != nil { + panic(fmt.Errorf("invalid accrual times: %w", err)) + } + } + + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleRewardPeriod( + claimType types.ClaimType, + collateralType string, + rewardsPerSecond sdk.Coins, +) IncentiveGenesisBuilder { + return builder.WithInitializedRewardPeriod( + claimType, + types.MultiRewardPeriods{builder.simpleRewardPeriod(collateralType, rewardsPerSecond)}, + ) +} + func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.MultipliersPerDenoms) IncentiveGenesisBuilder { builder.Params.ClaimMultipliers = multipliers diff --git a/x/incentive/testutil/integration.go b/x/incentive/testutil/integration.go index 0bf1054d..291f6e24 100644 --- a/x/incentive/testutil/integration.go +++ b/x/incentive/testutil/integration.go @@ -442,7 +442,8 @@ func (suite *IntegrationTester) AddTestAddrsFromPubKeys(ctx sdk.Context, pubKeys initCoins := sdk.NewCoins(sdk.NewCoin(suite.App.GetStakingKeeper().BondDenom(ctx), accAmt)) for _, pk := range pubKeys { - suite.App.FundAccount(ctx, sdk.AccAddress(pk.Address()), initCoins) + err := suite.App.FundAccount(ctx, sdk.AccAddress(pk.Address()), initCoins) + suite.Require().NoError(err) } } @@ -468,6 +469,28 @@ func (suite *IntegrationTester) StoredEarnIndexesEqual(denom string, expected ty } } +func (suite *IntegrationTester) StoredTimeEquals(claimType types.ClaimType, denom string, expected time.Time) { + storedTime, found := suite.App.GetIncentiveKeeper().Store.GetRewardAccrualTime(suite.Ctx, claimType, denom) + suite.Equal(found, expected != time.Time{}, "expected time is %v but time found = %v", expected, found) + if found { + suite.Equal(expected, storedTime) + } else { + suite.Empty(storedTime) + } +} + +func (suite *IntegrationTester) StoredIndexesEqual(claimType types.ClaimType, denom string, expected types.RewardIndexes) { + storedIndexes, found := suite.App.GetIncentiveKeeper().Store.GetRewardIndexesOfClaimType(suite.Ctx, claimType, denom) + suite.Equal(found, expected != nil) + + if found { + suite.Equal(expected, storedIndexes) + } else { + // Can't compare Equal for types.RewardIndexes(nil) vs types.RewardIndexes{} + suite.Empty(storedIndexes) + } +} + func (suite *IntegrationTester) AddIncentiveEarnMultiRewardPeriod(period types.MultiRewardPeriod) { ik := suite.App.GetIncentiveKeeper() params := ik.GetParams(suite.Ctx) @@ -489,6 +512,44 @@ func (suite *IntegrationTester) AddIncentiveEarnMultiRewardPeriod(period types.M ik.SetParams(suite.Ctx, params) } +func (suite *IntegrationTester) AddIncentiveMultiRewardPeriod( + claimType types.ClaimType, + period types.MultiRewardPeriod, +) { + ik := suite.App.GetIncentiveKeeper() + params := ik.GetParams(suite.Ctx) + + for i, rewardPeriod := range params.RewardPeriods { + if claimType == rewardPeriod.ClaimType { + for j, reward := range rewardPeriod.RewardPeriods { + if reward.CollateralType == period.CollateralType { + // Replace existing reward period if the collateralType exists. + // Params are invalid if there are multiple reward periods for the + // same collateral type. + params.RewardPeriods[i].RewardPeriods[j] = period + ik.SetParams(suite.Ctx, params) + return + } + } + + // Claim type period exists but not the specific collateral type + params.RewardPeriods[i].RewardPeriods = append(params.RewardPeriods[i].RewardPeriods, period) + return + } + } + + // Claim type does not exist in params + params.RewardPeriods = append(params.RewardPeriods, types.NewTypedMultiRewardPeriod( + claimType, + types.MultiRewardPeriods{ + period, + }, + )) + + suite.NoError(params.Validate()) + ik.SetParams(suite.Ctx, params) +} + // ----------------------------------------------------------------------------- // x/router diff --git a/x/incentive/testutil/keeper.go b/x/incentive/testutil/keeper.go new file mode 100644 index 00000000..3482225b --- /dev/null +++ b/x/incentive/testutil/keeper.go @@ -0,0 +1,54 @@ +package testutil + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/types" +) + +// TestKeeper is a test wrapper for the keeper which contains useful methods for testing +type TestKeeper struct { + keeper.Keeper +} + +func (keeper TestKeeper) StoreGlobalBorrowIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetHardBorrowRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) StoreGlobalSupplyIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetHardSupplyRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) StoreGlobalDelegatorIndexes(ctx sdk.Context, multiRewardIndexes types.MultiRewardIndexes) { + // Hardcoded to use bond denom + multiRewardIndex, _ := multiRewardIndexes.GetRewardIndex(types.BondDenom) + keeper.SetDelegatorRewardIndexes(ctx, types.BondDenom, multiRewardIndex.RewardIndexes) +} + +func (keeper TestKeeper) StoreGlobalSwapIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetSwapRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) StoreGlobalSavingsIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetSavingsRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) StoreGlobalEarnIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetEarnRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) StoreGlobalIndexes(ctx sdk.Context, claimType types.ClaimType, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.Store.SetRewardIndexes(ctx, claimType, i.CollateralType, i.RewardIndexes) + } +} diff --git a/x/incentive/types/genesis.go b/x/incentive/types/genesis.go index ef53a97f..05a7afc9 100644 --- a/x/incentive/types/genesis.go +++ b/x/incentive/types/genesis.go @@ -216,10 +216,18 @@ type AccrualTimes []AccrualTime // Validate performs validation of AccrualTimes func (gats AccrualTimes) Validate() error { + seenAccrualTimes := make(map[string]bool) + for _, gat := range gats { if err := gat.Validate(); err != nil { return err } + + key := fmt.Sprintf("%s-%s", gat.ClaimType, gat.CollateralType) + if seenAccrualTimes[key] { + return fmt.Errorf("duplicate accrual time found for %s", key) + } + seenAccrualTimes[key] = true } return nil } diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index a0fc0e81..ac241efc 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -133,6 +133,10 @@ func (p Params) Validate() error { return err } + if err := validatedRewardPeriodsParam(p.RewardPeriods); err != nil { + return err + } + return nil } diff --git a/x/incentive/types/reward_accumulator.go b/x/incentive/types/reward_accumulator.go new file mode 100644 index 00000000..6bc498eb --- /dev/null +++ b/x/incentive/types/reward_accumulator.go @@ -0,0 +1,11 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type RewardAccumulator interface { + AccumulateRewards( + ctx sdk.Context, + claimType ClaimType, + rewardPeriod MultiRewardPeriod, + ) error +}