Incentive: savings module reward accumulation logic (#1207)

* implement savings reward accumulator logic

* update builder/params with util methods

* accumulation test cases
This commit is contained in:
Denali Marsh 2022-04-12 16:14:14 +02:00 committed by GitHub
parent 72e8f2f40f
commit db5e839079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 236 additions and 0 deletions

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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))
}