diff --git a/x/incentive/abci.go b/x/incentive/abci.go index ecc63ca8..92306e6b 100644 --- a/x/incentive/abci.go +++ b/x/incentive/abci.go @@ -25,4 +25,7 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { for _, rp := range params.SwapRewardPeriods { k.AccumulateSwapRewards(ctx, rp) } + for _, rp := range params.SavingsRewardPeriods { + k.AccumulateSavingsRewards(ctx, rp) + } } diff --git a/x/incentive/keeper/params.go b/x/incentive/keeper/params.go index e32b6b0c..c98e4bd9 100644 --- a/x/incentive/keeper/params.go +++ b/x/incentive/keeper/params.go @@ -64,6 +64,17 @@ func (k Keeper) GetDelegatorRewardPeriods(ctx sdk.Context, denom string) (types. return types.MultiRewardPeriod{}, false } +// GetSavingsRewardPeriods returns the reward period with the specified collateral type if it's found in the params +func (k Keeper) GetSavingsRewardPeriods(ctx sdk.Context, denom string) (types.MultiRewardPeriod, bool) { + params := k.GetParams(ctx) + for _, rp := range params.SavingsRewardPeriods { + if rp.CollateralType == denom { + return rp, true + } + } + return types.MultiRewardPeriod{}, false +} + // GetMultiplierByDenom fetches a multiplier from the params matching the denom and name. func (k Keeper) GetMultiplierByDenom(ctx sdk.Context, denom string, name string) (types.Multiplier, bool) { params := k.GetParams(ctx) diff --git a/x/incentive/keeper/rewards_savings.go b/x/incentive/keeper/rewards_savings.go new file mode 100644 index 00000000..6cb40077 --- /dev/null +++ b/x/incentive/keeper/rewards_savings.go @@ -0,0 +1,37 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/incentive/types" + savingstypes "github.com/kava-labs/kava/x/savings/types" +) + +// AccumulateSavingsRewards calculates new rewards to distribute this block and updates the global indexes +func (k Keeper) AccumulateSavingsRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) { + + previousAccrualTime, found := k.GetSavingsRewardAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + previousAccrualTime = ctx.BlockTime() + } + + indexes, found := k.GetSavingsRewardIndexes(ctx, rewardPeriod.CollateralType) + if !found { + indexes = types.RewardIndexes{} + } + + acc := types.NewAccumulator(previousAccrualTime, indexes) + + savingsMacc := k.accountKeeper.GetModuleAccount(ctx, savingstypes.ModuleName) + maccCoins := k.bankKeeper.GetAllBalances(ctx, savingsMacc.GetAddress()) + denomBalance := maccCoins.AmountOf(rewardPeriod.CollateralType) + + acc.Accumulate(rewardPeriod, denomBalance.ToDec(), ctx.BlockTime()) + + k.SetSavingsRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime) + + if len(acc.Indexes) > 0 { + // the store panics when setting empty or nil indexes + k.SetSavingsRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes) + } +} diff --git a/x/incentive/keeper/rewards_savings_accum_test.go b/x/incentive/keeper/rewards_savings_accum_test.go new file mode 100644 index 00000000..cd03e4d7 --- /dev/null +++ b/x/incentive/keeper/rewards_savings_accum_test.go @@ -0,0 +1,162 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + savingskeeper "github.com/kava-labs/kava/x/savings/keeper" + savingstypes "github.com/kava-labs/kava/x/savings/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" +) + +// Test suite used for all keeper tests +type SavingsRewardsTestSuite struct { + suite.Suite + + keeper keeper.Keeper + savingsKeeper savingskeeper.Keeper + + app app.TestApp + ctx sdk.Context + + genesisTime time.Time + addrs []sdk.AccAddress +} + +// SetupTest is run automatically before each suite test +func (suite *SavingsRewardsTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + _, allAddrs := app.GeneratePrivKeyAddressPairs(10) + suite.addrs = allAddrs[:5] + suite.genesisTime = time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) +} + +func (suite *SavingsRewardsTestSuite) SetupApp() { + suite.app = app.NewTestApp() + + suite.keeper = suite.app.GetIncentiveKeeper() + suite.savingsKeeper = suite.app.GetSavingsKeeper() + + suite.ctx = suite.app.NewContext(true, tmproto.Header{Height: 1, Time: suite.genesisTime}) +} + +func (suite *SavingsRewardsTestSuite) SetupWithGenState(authBuilder *app.AuthBankGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder, + savingsGenesis savingstypes.GenesisState) { + suite.SetupApp() + + suite.app.InitializeFromGenesisStatesWithTime( + suite.genesisTime, + authBuilder.BuildMarshalled(suite.app.AppCodec()), + app.GenesisState{savingstypes.ModuleName: suite.app.AppCodec().MustMarshalJSON(&savingsGenesis)}, + incentBuilder.BuildMarshalled(suite.app.AppCodec()), + ) +} + +func (suite *SavingsRewardsTestSuite) TestAccumulateSavingsRewards() { + type args struct { + deposit sdk.Coin + rewardsPerSecond sdk.Coins + timeElapsed int + expectedRewardIndexes types.RewardIndexes + } + type test struct { + name string + args args + } + testCases := []test{ + { + "7 seconds", + args{ + deposit: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354)), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.856478000000000000")), + }, + }, + }, + { + "1 day", + args{ + deposit: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354)), + timeElapsed: 86400, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("10571.385600000000000000")), + }, + }, + }, + { + "0 seconds", + args{ + deposit: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354)), + timeElapsed: 0, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("hard", d("0.0")), + }, + }, + }, + { + "multiple reward coins", + args{ + deposit: c("ukava", 1_000_000), + rewardsPerSecond: cs(c("hard", 122354), c("bnb", 567889)), + timeElapsed: 7, + expectedRewardIndexes: types.RewardIndexes{ + types.NewRewardIndex("bnb", d("3.97522300000000000")), + types.NewRewardIndex("hard", d("0.856478000000000000")), + }, + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + params := savingstypes.NewParams( + []string{"ukava"}, + ) + deposits := savingstypes.Deposits{ + savingstypes.NewDeposit( + suite.addrs[0], + sdk.NewCoins(tc.args.deposit), + ), + } + savingsGenesis := savingstypes.NewGenesisState(params, deposits) + + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(suite.addrs[0], cs(c("ukava", 1e9))). + WithSimpleModuleAccount(savingstypes.ModuleName, sdk.NewCoins(tc.args.deposit)) + + incentBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.genesisTime). + WithSimpleSavingsRewardPeriod(tc.args.deposit.Denom, tc.args.rewardsPerSecond) + + suite.SetupWithGenState(authBuilder, incentBuilder, savingsGenesis) + + // Set up chain context at future time + runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + runCtx := suite.ctx.WithBlockTime(runAtTime) + + rewardPeriods, found := suite.keeper.GetSavingsRewardPeriods(runCtx, tc.args.deposit.Denom) + suite.Require().True(found) + suite.keeper.AccumulateSavingsRewards(runCtx, rewardPeriods) + + rewardIndexes, _ := suite.keeper.GetSavingsRewardIndexes(runCtx, tc.args.deposit.Denom) + suite.Require().Equal(tc.args.expectedRewardIndexes, rewardIndexes) + }) + } +} + +func TestSavingsRewardsTestSuite(t *testing.T) { + suite.Run(t, new(SavingsRewardsTestSuite)) +} diff --git a/x/incentive/testutil/builder.go b/x/incentive/testutil/builder.go index 2954ade7..201e6b2e 100644 --- a/x/incentive/testutil/builder.go +++ b/x/incentive/testutil/builder.go @@ -252,3 +252,26 @@ func NewStandardMoneyMarket(denom string) hardtypes.MoneyMarket { sdk.ZeroDec(), ) } + +// WithInitializedSavingsRewardPeriod sets the genesis time as the previous accumulation time for the specified period. +// This can be helpful in tests. With no prev time set, the first block accrues no rewards as it just sets the prev time to the current. +func (builder IncentiveGenesisBuilder) WithInitializedSavingsRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.SavingsRewardPeriods = append(builder.Params.SavingsRewardPeriods, period) + + accumulationTimeForPeriod := types.NewAccumulationTime(period.CollateralType, builder.genesisTime) + builder.SavingsRewardState.AccumulationTimes = append( + builder.SavingsRewardState.AccumulationTimes, + accumulationTimeForPeriod, + ) + + builder.SavingsRewardState.MultiRewardIndexes = builder.SavingsRewardState.MultiRewardIndexes.With( + period.CollateralType, + newZeroRewardIndexesFromCoins(period.RewardsPerSecond...), + ) + + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleSavingsRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedSavingsRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond)) +}