Refactor incentive accumulators to be the same (#970)

* add test for validate multi reward periods

* tidy up: combine files

* don't accumulate global indexes containing zeros
Previously if the time since last block was 0,
indexes were added containing 0s.
Now leave them out. Missing is assumed to be 0.

* move state independent test to types folder

* clarify reward source concept to "source shares"
- rename variables and update doc comments
- extract method from swap accumulation

* tidy up and expand swap accumulation unit tests

* rename swap test file to match others

* update swap pool id format in tests

* refactor borrow accumulation, use new accumulator

* refactor supply accumulation, use new accumulator

* refactor delegator accumulation, use accumulator

* refactor usdx accumulation, use new accumulator

* fix types const

* remove unsed methods

* more usdx minting param validation.
Protect against the rewards per second denom changing.
It should always be "ukava".

* add safety check in InitGenesis
It prevents huge accumulations on the first block by limiting all
previous accumulation times to be within one year of genesis

* add todo for adding swp token distirbution info
This commit is contained in:
Ruaridh 2021-07-22 13:53:18 +01:00 committed by GitHub
parent cbb8b04292
commit 6f193c7f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2117 additions and 525 deletions

View File

@ -8,30 +8,20 @@ import (
// BeginBlocker runs at the start of every block
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
params := k.GetParams(ctx)
for _, rp := range params.USDXMintingRewardPeriods {
err := k.AccumulateUSDXMintingRewards(ctx, rp)
if err != nil {
panic(err)
}
k.AccumulateUSDXMintingRewards(ctx, rp)
}
for _, rp := range params.HardSupplyRewardPeriods {
err := k.AccumulateHardSupplyRewards(ctx, rp)
if err != nil {
panic(err)
}
k.AccumulateHardSupplyRewards(ctx, rp)
}
for _, rp := range params.HardBorrowRewardPeriods {
err := k.AccumulateHardBorrowRewards(ctx, rp)
if err != nil {
panic(err)
}
k.AccumulateHardBorrowRewards(ctx, rp)
}
for _, rp := range params.DelegatorRewardPeriods {
err := k.AccumulateDelegatorRewards(ctx, rp)
if err != nil {
panic(err)
}
k.AccumulateDelegatorRewards(ctx, rp)
}
for _, rp := range params.SwapRewardPeriods {
k.AccumulateSwapRewards(ctx, rp)

View File

@ -17,7 +17,6 @@ const (
AttributeKeyClaimedBy = types.AttributeKeyClaimedBy
AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod
AttributeValueCategory = types.AttributeValueCategory
BondDenom = types.BondDenom
DefaultParamspace = types.DefaultParamspace
DelegatorClaimType = types.DelegatorClaimType
EventTypeClaim = types.EventTypeClaim
@ -49,7 +48,6 @@ const (
var (
// function aliases
CalculateTimeElapsed = keeper.CalculateTimeElapsed
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
DefaultGenesisState = types.DefaultGenesisState
@ -73,6 +71,7 @@ var (
NewMsgClaimUSDXMintingRewardVVesting = types.NewMsgClaimUSDXMintingRewardVVesting
NewMultiRewardIndex = types.NewMultiRewardIndex
NewMultiRewardPeriod = types.NewMultiRewardPeriod
NewMultiRewardPeriodFromRewardPeriod = types.NewMultiRewardPeriodFromRewardPeriod
NewMultiplier = types.NewMultiplier
NewParams = types.NewParams
NewPeriod = types.NewPeriod
@ -86,6 +85,7 @@ var (
RegisterCodec = types.RegisterCodec
// variable aliases
BondDenom = types.BondDenom
DefaultActive = types.DefaultActive
DefaultClaimEnd = types.DefaultClaimEnd
DefaultDelegatorClaims = types.DefaultDelegatorClaims
@ -111,7 +111,6 @@ var (
ErrNoClaimsFound = types.ErrNoClaimsFound
ErrRewardPeriodNotFound = types.ErrRewardPeriodNotFound
ErrZeroClaim = types.ErrZeroClaim
GovDenom = types.GovDenom
HardBorrowRewardIndexesKeyPrefix = types.HardBorrowRewardIndexesKeyPrefix
HardLiquidityClaimKeyPrefix = types.HardLiquidityClaimKeyPrefix
HardSupplyRewardIndexesKeyPrefix = types.HardSupplyRewardIndexesKeyPrefix
@ -129,7 +128,6 @@ var (
PreviousHardSupplyRewardAccrualTimeKeyPrefix = types.PreviousHardSupplyRewardAccrualTimeKeyPrefix
PreviousSwapRewardAccrualTimeKeyPrefix = types.PreviousSwapRewardAccrualTimeKeyPrefix
PreviousUSDXMintingRewardAccrualTimeKeyPrefix = types.PreviousUSDXMintingRewardAccrualTimeKeyPrefix
PrincipalDenom = types.PrincipalDenom
SwapClaimKeyPrefix = types.SwapClaimKeyPrefix
SwapRewardIndexesKeyPrefix = types.SwapRewardIndexesKeyPrefix
USDXMintingClaimKeyPrefix = types.USDXMintingClaimKeyPrefix

View File

@ -10,6 +10,12 @@ import (
"github.com/kava-labs/kava/x/incentive/types"
)
const year = 365 * 24 * time.Hour
// EarliestValidAccumulationTime is how far behind the genesis time an accumulation time can be for it to be valid.
// It's a safety check to ensure rewards aren't accidentally accumulated for many years on the first block (eg since Jan 1970).
var EarliestValidAccumulationTime time.Duration = year
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, cdpKeeper types.CdpKeeper, gs types.GenesisState) {
@ -37,6 +43,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetUSDXMintingClaim(ctx, claim)
}
for _, gat := range gs.USDXRewardState.AccumulationTimes {
if err := ValidateAccumulationTime(gat.PreviousAccumulationTime, ctx.BlockTime()); err != nil {
panic(err.Error())
}
k.SetPreviousUSDXMintingAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
for _, mri := range gs.USDXRewardState.MultiRewardIndexes {
@ -52,12 +61,18 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetHardLiquidityProviderClaim(ctx, claim)
}
for _, gat := range gs.HardSupplyRewardState.AccumulationTimes {
if err := ValidateAccumulationTime(gat.PreviousAccumulationTime, ctx.BlockTime()); err != nil {
panic(err.Error())
}
k.SetPreviousHardSupplyRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
for _, mri := range gs.HardSupplyRewardState.MultiRewardIndexes {
k.SetHardSupplyRewardIndexes(ctx, mri.CollateralType, mri.RewardIndexes)
}
for _, gat := range gs.HardBorrowRewardState.AccumulationTimes {
if err := ValidateAccumulationTime(gat.PreviousAccumulationTime, ctx.BlockTime()); err != nil {
panic(err.Error())
}
k.SetPreviousHardBorrowRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
for _, mri := range gs.HardBorrowRewardState.MultiRewardIndexes {
@ -69,6 +84,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetDelegatorClaim(ctx, claim)
}
for _, gat := range gs.DelegatorRewardState.AccumulationTimes {
if err := ValidateAccumulationTime(gat.PreviousAccumulationTime, ctx.BlockTime()); err != nil {
panic(err.Error())
}
k.SetPreviousDelegatorRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
for _, mri := range gs.DelegatorRewardState.MultiRewardIndexes {
@ -80,6 +98,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetSwapClaim(ctx, claim)
}
for _, gat := range gs.SwapRewardState.AccumulationTimes {
if err := ValidateAccumulationTime(gat.PreviousAccumulationTime, ctx.BlockTime()); err != nil {
panic(err.Error())
}
k.SetSwapRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
for _, mri := range gs.SwapRewardState.MultiRewardIndexes {
@ -201,3 +222,15 @@ func getSwapGenesisRewardState(ctx sdk.Context, keeper keeper.Keeper) types.Gene
return types.NewGenesisRewardState(ats, mris)
}
func ValidateAccumulationTime(previousAccumulationTime, genesisTime time.Time) error {
if previousAccumulationTime.Before(genesisTime.Add(-1 * EarliestValidAccumulationTime)) {
return fmt.Errorf(
"found accumulation time '%s' more than '%s' behind genesis time '%s'",
previousAccumulationTime,
EarliestValidAccumulationTime,
genesisTime,
)
}
return nil
}

View File

@ -96,34 +96,6 @@ func (suite *GenesisTestSuite) SetupTest() {
suite.ctx = ctx
}
// Test to cover an bug where paid out claims would zero out rewards incorrectly, creating an invalid coins object.
// The invalid reward coins would fail the genesis state validation
func (suite *GenesisTestSuite) TestPaidOutClaimsPassValidateGenesis() {
hardHandler := hard.NewHandler(suite.app.GetHardKeeper())
_, err := hardHandler(suite.ctx, hard.NewMsgDeposit(suite.addrs[0], cs(c("bnb", 100_000_000))))
suite.Require().NoError(err)
suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{})
suite.ctx = suite.ctx.WithBlockTime(suite.genesisTime.Add(1 * 10 * time.Second))
suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{})
suite.app.EndBlocker(suite.ctx, abci.RequestEndBlock{})
suite.ctx = suite.ctx.WithBlockTime(suite.genesisTime.Add(2 * 10 * time.Second))
suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{})
_, err = hardHandler(suite.ctx, hard.NewMsgWithdraw(suite.addrs[0], cs(c("bnb", 100_000_000))))
suite.Require().NoError(err)
incentiveHandler := incentive.NewHandler(suite.keeper)
_, err = incentiveHandler(suite.ctx, incentive.NewMsgClaimHardReward(suite.addrs[0], string(incentive.Large), nil))
suite.Require().NoError(err)
genState := incentive.ExportGenesis(suite.ctx, suite.keeper)
suite.Require().NoError(
genState.Validate(),
)
}
func (suite *GenesisTestSuite) TestExportedGenesisMatchesImported() {
genesisTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
genesisState := incentive.NewGenesisState(
@ -219,7 +191,7 @@ func (suite *GenesisTestSuite) TestExportedGenesisMatchesImported() {
)
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1})
ctx := tApp.NewContext(true, abci.Header{Height: 0, Time: genesisTime})
// Incentive init genesis reads from the cdp keeper to check params are ok. So it needs to be initialized first.
// Then the cdp keeper reads from pricefeed keeper to check its params are ok. So it also need initialization.
@ -235,6 +207,84 @@ func (suite *GenesisTestSuite) TestExportedGenesisMatchesImported() {
suite.Equal(genesisState, exportedGenesisState)
}
func (suite *GenesisTestSuite) TestInitGenesisPanicsWhenAccumulationTimesToLongAgo() {
genesisTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
invalidRewardState := incentive.NewGenesisRewardState(
incentive.AccumulationTimes{
incentive.NewAccumulationTime("bnb", genesisTime.Add(-23*incentive.EarliestValidAccumulationTime).Add(-time.Nanosecond)),
},
incentive.MultiRewardIndexes{},
)
minimalParams := incentive.Params{
ClaimEnd: genesisTime.Add(5 * oneYear),
}
testCases := []struct {
genesisState incentive.GenesisState
}{
{
incentive.GenesisState{
Params: minimalParams,
USDXRewardState: invalidRewardState,
},
},
{
incentive.GenesisState{
Params: minimalParams,
HardSupplyRewardState: invalidRewardState,
},
},
{
incentive.GenesisState{
Params: minimalParams,
HardBorrowRewardState: invalidRewardState,
},
},
{
incentive.GenesisState{
Params: minimalParams,
DelegatorRewardState: invalidRewardState,
},
},
{
incentive.GenesisState{
Params: minimalParams,
SwapRewardState: invalidRewardState,
},
},
}
for _, tc := range testCases {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 0, Time: genesisTime})
// Incentive init genesis reads from the cdp keeper to check params are ok. So it needs to be initialized first.
// Then the cdp keeper reads from pricefeed keeper to check its params are ok. So it also need initialization.
tApp.InitializeFromGenesisStates(
NewCDPGenStateMulti(),
NewPricefeedGenStateMultiFromTime(genesisTime),
)
suite.PanicsWithValue(
"found accumulation time '1975-01-06 23:59:59.999999999 +0000 UTC' more than '8760h0m0s' behind genesis time '1998-01-01 00:00:00 +0000 UTC'",
func() {
incentive.InitGenesis(ctx, tApp.GetIncentiveKeeper(), tApp.GetSupplyKeeper(), tApp.GetCDPKeeper(), tc.genesisState)
},
)
}
}
func (suite *GenesisTestSuite) TestValidateAccumulationTime() {
genTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
err := incentive.ValidateAccumulationTime(
genTime.Add(-incentive.EarliestValidAccumulationTime).Add(-time.Nanosecond),
genTime,
)
suite.Error(err)
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

View File

@ -29,7 +29,8 @@ func TestRiskyCDPsAccumulateRewards(t *testing.T) {
WithSimpleUSDXRewardPeriod(collateralType, rewardsPerSecond)
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
tApp.InitializeFromGenesisStatesWithTime(
genesisTime,
authBuilder.BuildMarshalled(),
NewPricefeedGenStateMultiFromTime(genesisTime),
NewCDPGenStateMulti(),

View File

@ -319,7 +319,7 @@ func (k Keeper) GetHardSupplyRewardIndexes(ctx sdk.Context, denom string) (types
store := prefix.NewStore(ctx.KVStore(k.key), types.HardSupplyRewardIndexesKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.RewardIndexes{}, false
return nil, false
}
var rewardIndexes types.RewardIndexes
k.cdc.MustUnmarshalBinaryBare(bz, &rewardIndexes)
@ -366,7 +366,7 @@ func (k Keeper) GetHardBorrowRewardIndexes(ctx sdk.Context, denom string) (types
store := prefix.NewStore(ctx.KVStore(k.key), types.HardBorrowRewardIndexesKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.RewardIndexes{}, false
return nil, false
}
var rewardIndexes types.RewardIndexes
k.cdc.MustUnmarshalBinaryBare(bz, &rewardIndexes)
@ -406,7 +406,7 @@ func (k Keeper) GetDelegatorRewardIndexes(ctx sdk.Context, denom string) (types.
store := prefix.NewStore(ctx.KVStore(k.key), types.DelegatorRewardIndexesKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.RewardIndexes{}, false
return nil, false
}
var rewardIndexes types.RewardIndexes
k.cdc.MustUnmarshalBinaryBare(bz, &rewardIndexes)
@ -511,7 +511,7 @@ func (k Keeper) GetSwapRewardIndexes(ctx sdk.Context, poolID string) (types.Rewa
store := prefix.NewStore(ctx.KVStore(k.key), types.SwapRewardIndexesKeyPrefix)
bz := store.Get([]byte(poolID))
if bz == nil {
return types.RewardIndexes{}, false
return nil, false
}
var rewardIndexes types.RewardIndexes
k.cdc.MustUnmarshalBinaryBare(bz, &rewardIndexes)

View File

@ -10,70 +10,52 @@ import (
"github.com/kava-labs/kava/x/incentive/types"
)
// AccumulateHardBorrowRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateHardBorrowRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error {
// AccumulateHardBorrowRewards 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 Keeper) AccumulateHardBorrowRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
previousAccrualTime, found := k.GetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriod.RewardsPerSecond.IsZero() {
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
previousAccrualTime = ctx.BlockTime()
}
totalBorrowedCoins, foundTotalBorrowedCoins := k.hardKeeper.GetBorrowedCoins(ctx)
if !foundTotalBorrowedCoins {
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
totalBorrowed := totalBorrowedCoins.AmountOf(rewardPeriod.CollateralType).ToDec()
if totalBorrowed.IsZero() {
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
previousRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType)
indexes, found := k.GetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType)
if !found {
for _, rewardCoin := range rewardPeriod.RewardsPerSecond {
rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
previousRewardIndexes = append(previousRewardIndexes, rewardIndex)
}
k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes)
indexes = types.RewardIndexes{}
}
hardFactor, found := k.hardKeeper.GetBorrowInterestFactor(ctx, rewardPeriod.CollateralType)
acc := types.NewAccumulator(previousAccrualTime, indexes)
totalSource := k.getHardBorrowTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime())
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
if len(acc.Indexes) > 0 {
// the store panics when setting empty or nil indexes
k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes)
}
}
// getHardBorrowTotalSourceShares fetches the sum of all source shares for a borrow reward.
// In the case of hard borrow, this is the total borrowed divided by the borrow interest factor.
// This give the "pre interest" value of the total borrowed.
func (k Keeper) getHardBorrowTotalSourceShares(ctx sdk.Context, denom string) sdk.Dec {
totalBorrowedCoins, found := k.hardKeeper.GetBorrowedCoins(ctx)
if !found {
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
// assume no coins have been borrowed
totalBorrowedCoins = sdk.NewCoins()
}
totalBorrowed := totalBorrowedCoins.AmountOf(denom)
interestFactor, found := k.hardKeeper.GetBorrowInterestFactor(ctx, denom)
if !found {
// assume nothing has been borrowed so the factor starts at it's default value
interestFactor = sdk.OneDec()
}
newRewardIndexes := previousRewardIndexes
for _, rewardCoin := range rewardPeriod.RewardsPerSecond {
newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec())
previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom)
if !found {
previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
}
// Calculate new reward factor and update reward index
rewardFactor := newRewards.Mul(hardFactor).Quo(totalBorrowed)
newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor)
newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue)
i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom)
if found {
newRewardIndexes[i] = newRewardIndex
} else {
newRewardIndexes = append(newRewardIndexes, newRewardIndex)
}
}
k.SetHardBorrowRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes)
k.SetPreviousHardBorrowRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
// return borrowed/factor to get the "pre interest" value of the current total borrowed
return totalBorrowed.ToDec().Quo(interestFactor)
}
// InitializeHardBorrowReward initializes the borrow-side of a hard liquidity provider claim
@ -172,14 +154,14 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// CalculateRewards computes how much rewards should have accrued to a source (eg a user's hard borrowed btc amount)
// CalculateRewards computes how much rewards should have accrued to a reward source (eg a user's hard borrowed btc amount)
// between two index values.
//
// oldIndex is normally the index stored on a claim, newIndex the current global value, and rewardSource a hard borrowed/supplied amount.
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a hard borrowed/supplied amount.
//
// Returns an error if newIndexes does not contain all CollateralTypes from oldIndexes, or if any value of oldIndex.RewardFactor > newIndex.RewardFactor.
// It returns an error if newIndexes does not contain all CollateralTypes from oldIndexes, or if any value of oldIndex.RewardFactor > newIndex.RewardFactor.
// This should never happen, as it would mean that a global reward index has decreased in value, or that a global reward index has been deleted from state.
func (k Keeper) CalculateRewards(oldIndexes, newIndexes types.RewardIndexes, rewardSource sdk.Dec) (sdk.Coins, error) {
func (k Keeper) CalculateRewards(oldIndexes, newIndexes types.RewardIndexes, sourceShares sdk.Dec) (sdk.Coins, error) {
// check for missing CollateralType's
for _, oldIndex := range oldIndexes {
if newIndex, found := newIndexes.Get(oldIndex.CollateralType); !found {
@ -193,7 +175,7 @@ func (k Keeper) CalculateRewards(oldIndexes, newIndexes types.RewardIndexes, rew
oldFactor = sdk.ZeroDec()
}
rewardAmount, err := k.CalculateSingleReward(oldFactor, newIndex.RewardFactor, rewardSource)
rewardAmount, err := k.CalculateSingleReward(oldFactor, newIndex.RewardFactor, sourceShares)
if err != nil {
return nil, err
}
@ -205,18 +187,18 @@ func (k Keeper) CalculateRewards(oldIndexes, newIndexes types.RewardIndexes, rew
return reward, nil
}
// CalculateSingleReward computes how much rewards should have accrued to a source (eg a user's btcb-a cdp principal)
// CalculateSingleReward computes how much rewards should have accrued to a reward source (eg a user's btcb-a cdp principal)
// between two index values.
//
// oldIndex is normally the index stored on a claim, newIndex the current global value, and rewardSource a cdp principal amount.
// oldIndex is normally the index stored on a claim, newIndex the current global value, and sourceShares a cdp principal amount.
//
// Returns an error if oldIndex > newIndex. This should never happen, as it would mean that a global reward index has decreased in value,
// or that a global reward index has been deleted from state.
func (k Keeper) CalculateSingleReward(oldIndex, newIndex, rewardSource sdk.Dec) (sdk.Int, error) {
func (k Keeper) CalculateSingleReward(oldIndex, newIndex, sourceShares sdk.Dec) (sdk.Int, error) {
increase := newIndex.Sub(oldIndex)
if increase.IsNegative() {
return sdk.Int{}, sdkerrors.Wrapf(types.ErrDecreasingRewardFactor, "old: %v, new: %v", oldIndex, newIndex)
}
reward := increase.Mul(rewardSource).RoundInt()
reward := increase.Mul(sourceShares).RoundInt()
return reward, nil
}

View File

@ -0,0 +1,316 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
)
type AccumulateBorrowRewardsTests struct {
unitTester
}
func (suite *AccumulateBorrowRewardsTests) storedTimeEquals(denom string, expected time.Time) {
storedTime, found := suite.keeper.GetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom)
suite.True(found)
suite.Equal(expected, storedTime)
}
func (suite *AccumulateBorrowRewardsTests) storedIndexesEqual(denom string, expected types.RewardIndexes) {
storedIndexes, found := suite.keeper.GetHardBorrowRewardIndexes(suite.ctx, denom)
suite.Equal(found, expected != nil)
suite.Equal(expected, storedIndexes)
}
func TestAccumulateBorrowRewards(t *testing.T) {
suite.Run(t, new(AccumulateBorrowRewardsTests))
}
func (suite *AccumulateBorrowRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalBorrow(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
suite.storeGlobalBorrowIndexes(types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
})
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
newAccrualTime := previousAccrualTime.Add(1 * time.Hour)
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, newAccrualTime)
suite.storedIndexesEqual(denom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("7.22"),
},
{
CollateralType: "ukava",
RewardFactor: d("3.64"),
},
})
}
func (suite *AccumulateBorrowRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalBorrow(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalBorrowIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, previousAccrualTime)
expected, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expected)
}
func (suite *AccumulateBorrowRewardsTests) TestNoAccumulationWhenSourceSharesAreZero() {
denom := "bnb"
hardKeeper := newFakeHardKeeper() // zero total borrows
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalBorrowIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(7 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, firstAccrualTime)
expected, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expected)
}
func (suite *AccumulateBorrowRewardsTests) TestStateAddedWhenStateDoesNotExist() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalBorrow(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// 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(denom, firstAccrualTime)
suite.storedIndexesEqual(denom, nil)
secondAccrualTime := firstAccrualTime.Add(10 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// After the second accumulation both current block time and indexes should be stored.
suite.storedTimeEquals(denom, secondAccrualTime)
suite.storedIndexesEqual(denom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.01"),
},
})
}
func (suite *AccumulateBorrowRewardsTests) TestNoPanicWhenStateDoesNotExist() {
denom := "bnb"
hardKeeper := newFakeHardKeeper()
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
period := types.NewMultiRewardPeriod(
true,
denom,
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 source 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() {
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
})
suite.storedTimeEquals(denom, accrualTime)
suite.storedIndexesEqual(denom, nil)
}
func (suite *AccumulateBorrowRewardsTests) TestNoAccumulationWhenBeforeStartTime() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalBorrow(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalBorrowIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(10 * time.Second)
period := types.NewMultiRewardPeriod(
true,
denom,
firstAccrualTime.Add(time.Nanosecond), // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
// The accrual time should be updated, but the indexes unchanged
suite.storedTimeEquals(denom, firstAccrualTime)
expectedIndexes, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expectedIndexes)
}
func (suite *AccumulateBorrowRewardsTests) TestPanicWhenCurrentTimeLessThanPrevious() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalBorrow(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := time.Time{}
period := types.NewMultiRewardPeriod(
true,
denom,
time.Time{}, // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.Panics(func() {
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, period)
})
}

View File

@ -185,8 +185,7 @@ func (suite *BorrowRewardsTestSuite) TestAccumulateHardBorrowRewards() {
// Accumulate hard borrow rewards for the deposit denom
multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, tc.args.borrow.Denom)
suite.Require().True(found)
err = suite.keeper.AccumulateHardBorrowRewards(runCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardBorrowRewards(runCtx, multiRewardPeriod)
// Check that each expected reward index matches the current stored reward index for the denom
globalRewardIndexes, found := suite.keeper.GetHardBorrowRewardIndexes(runCtx, tc.args.borrow.Denom)
@ -555,8 +554,7 @@ func (suite *BorrowRewardsTestSuite) TestSynchronizeHardBorrowReward() {
// Accumulate hard borrow-side rewards
multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom)
if found {
err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod)
}
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
@ -666,15 +664,13 @@ func (suite *BorrowRewardsTestSuite) TestSynchronizeHardBorrowReward() {
// But new borrow denoms don't have their PreviousHardBorrowRewardAccrualTime set yet,
// so we need to call the accumulation method once to set the initial reward accrual time
if tc.args.borrow.Denom != tc.args.incentiveBorrowRewardDenom {
err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod)
}
// Now we can jump forward in time and accumulate rewards
updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err = suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardBorrowRewards(suite.ctx, multiRewardPeriod)
// After we've accumulated, run synchronize
borrow, found = suite.hardKeeper.GetBorrow(suite.ctx, userAddr)
@ -963,8 +959,7 @@ func (suite *BorrowRewardsTestSuite) TestSimulateHardBorrowRewardSynchronization
// Accumulate hard borrow-side rewards
multiRewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(blockCtx, tc.args.borrow.Denom)
suite.Require().True(found)
err := suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardBorrowRewards(blockCtx, multiRewardPeriod)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)

View File

@ -8,59 +8,39 @@ import (
"github.com/kava-labs/kava/x/incentive/types"
)
// AccumulateDelegatorRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateDelegatorRewards(ctx sdk.Context, rewardPeriods types.MultiRewardPeriod) error {
previousAccrualTime, found := k.GetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriods.CollateralType)
// AccumulateDelegatorRewards 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 Keeper) AccumulateDelegatorRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
previousAccrualTime, found := k.GetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriods.CollateralType, ctx.BlockTime())
return nil
}
timeElapsed := CalculateTimeElapsed(rewardPeriods.Start, rewardPeriods.End, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriods.RewardsPerSecond.IsZero() {
k.SetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriods.CollateralType, ctx.BlockTime())
return nil
previousAccrualTime = ctx.BlockTime()
}
totalBonded := k.stakingKeeper.TotalBondedTokens(ctx).ToDec()
if totalBonded.IsZero() {
k.SetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriods.CollateralType, ctx.BlockTime())
return nil
}
previousRewardIndexes, found := k.GetDelegatorRewardIndexes(ctx, rewardPeriods.CollateralType)
indexes, found := k.GetDelegatorRewardIndexes(ctx, rewardPeriod.CollateralType)
if !found {
for _, rewardCoin := range rewardPeriods.RewardsPerSecond {
rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
previousRewardIndexes = append(previousRewardIndexes, rewardIndex)
}
k.SetDelegatorRewardIndexes(ctx, rewardPeriods.CollateralType, previousRewardIndexes)
indexes = types.RewardIndexes{}
}
newRewardIndexes := previousRewardIndexes
for _, rewardCoin := range rewardPeriods.RewardsPerSecond {
newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec())
previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom)
if !found {
previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
}
acc := types.NewAccumulator(previousAccrualTime, indexes)
// Calculate new reward factor and update reward index
rewardFactor := newRewards.Quo(totalBonded)
newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor)
newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue)
i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom)
if found {
newRewardIndexes[i] = newRewardIndex
} else {
newRewardIndexes = append(newRewardIndexes, newRewardIndex)
}
totalSource := k.getDelegatorTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime())
k.SetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
if len(acc.Indexes) > 0 {
// the store panics when setting empty or nil indexes
k.SetDelegatorRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes)
}
k.SetDelegatorRewardIndexes(ctx, rewardPeriods.CollateralType, newRewardIndexes)
k.SetPreviousDelegatorRewardAccrualTime(ctx, rewardPeriods.CollateralType, ctx.BlockTime())
return nil
}
// getDelegatorTotalSourceShares fetches the sum of all source shares for a delegator reward.
// In the case of delegation, this is the total tokens staked to bonded validators.
func (k Keeper) getDelegatorTotalSourceShares(ctx sdk.Context, denom string) sdk.Dec {
totalBonded := k.stakingKeeper.TotalBondedTokens(ctx)
return totalBonded.ToDec()
}
// InitializeDelegatorReward initializes the reward index of a delegator claim

View File

@ -0,0 +1,309 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
)
type AccumulateDelegatorRewardsTests struct {
unitTester
}
func (suite *AccumulateDelegatorRewardsTests) storedTimeEquals(denom string, expected time.Time) {
storedTime, found := suite.keeper.GetPreviousDelegatorRewardAccrualTime(suite.ctx, denom)
suite.True(found)
suite.Equal(expected, storedTime)
}
func (suite *AccumulateDelegatorRewardsTests) storedIndexesEqual(denom string, expected types.RewardIndexes) {
storedIndexes, found := suite.keeper.GetDelegatorRewardIndexes(suite.ctx, denom)
suite.Equal(found, expected != nil)
suite.Equal(expected, storedIndexes)
}
func TestAccumulateDelegatorRewards(t *testing.T) {
suite.Run(t, new(AccumulateDelegatorRewardsTests))
}
func (suite *AccumulateDelegatorRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() {
stakingKeeper := newFakeStakingKeeper().addBondedTokens(1e6)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
suite.storeGlobalDelegatorIndexes(types.MultiRewardIndexes{
{
CollateralType: types.BondDenom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
})
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousDelegatorRewardAccrualTime(suite.ctx, types.BondDenom, previousAccrualTime)
newAccrualTime := previousAccrualTime.Add(1 * time.Hour)
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(types.BondDenom, newAccrualTime)
suite.storedIndexesEqual(types.BondDenom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("7.22"),
},
{
CollateralType: "ukava",
RewardFactor: d("3.64"),
},
})
}
func (suite *AccumulateDelegatorRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
stakingKeeper := newFakeStakingKeeper().addBondedTokens(1e6)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: types.BondDenom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalDelegatorIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousDelegatorRewardAccrualTime(suite.ctx, types.BondDenom, previousAccrualTime)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(types.BondDenom, previousAccrualTime)
expected, f := previousIndexes.Get(types.BondDenom)
suite.True(f)
suite.storedIndexesEqual(types.BondDenom, expected)
}
func (suite *AccumulateDelegatorRewardsTests) TestNoAccumulationWhenSourceSharesAreZero() {
stakingKeeper := newFakeStakingKeeper() // zero total bonded
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: types.BondDenom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalDelegatorIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousDelegatorRewardAccrualTime(suite.ctx, types.BondDenom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(7 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(types.BondDenom, firstAccrualTime)
expected, f := previousIndexes.Get(types.BondDenom)
suite.True(f)
suite.storedIndexesEqual(types.BondDenom, expected)
}
func (suite *AccumulateDelegatorRewardsTests) TestStateAddedWhenStateDoesNotExist() {
stakingKeeper := newFakeStakingKeeper().addBondedTokens(1e6)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// 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.BondDenom, firstAccrualTime)
suite.storedIndexesEqual(types.BondDenom, nil)
secondAccrualTime := firstAccrualTime.Add(10 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// After the second accumulation both current block time and indexes should be stored.
suite.storedTimeEquals(types.BondDenom, secondAccrualTime)
suite.storedIndexesEqual(types.BondDenom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.01"),
},
})
}
func (suite *AccumulateDelegatorRewardsTests) TestNoPanicWhenStateDoesNotExist() {
stakingKeeper := newFakeStakingKeeper()
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
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 source 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() {
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
})
suite.storedTimeEquals(types.BondDenom, accrualTime)
suite.storedIndexesEqual(types.BondDenom, nil)
}
func (suite *AccumulateDelegatorRewardsTests) TestNoAccumulationWhenBeforeStartTime() {
stakingKeeper := newFakeStakingKeeper().addBondedTokens(1e6)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: types.BondDenom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalDelegatorIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousDelegatorRewardAccrualTime(suite.ctx, types.BondDenom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(10 * time.Second)
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
firstAccrualTime.Add(time.Nanosecond), // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
// The accrual time should be updated, but the indexes unchanged
suite.storedTimeEquals(types.BondDenom, firstAccrualTime)
expectedIndexes, f := previousIndexes.Get(types.BondDenom)
suite.True(f)
suite.storedIndexesEqual(types.BondDenom, expectedIndexes)
}
func (suite *AccumulateDelegatorRewardsTests) TestPanicWhenCurrentTimeLessThanPrevious() {
stakingKeeper := newFakeStakingKeeper().addBondedTokens(1e6)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousDelegatorRewardAccrualTime(suite.ctx, types.BondDenom, previousAccrualTime)
firstAccrualTime := time.Time{}
period := types.NewMultiRewardPeriod(
true,
types.BondDenom,
time.Time{}, // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.Panics(func() {
suite.keeper.AccumulateDelegatorRewards(suite.ctx, period)
})
}

View File

@ -47,7 +47,7 @@ func (suite *InitializeDelegatorRewardTests) TestClaimIndexesAreSetWhenClaimDoes
func (suite *InitializeDelegatorRewardTests) TestClaimIsSyncedAndIndexesAreSetWhenClaimDoesExist() {
validatorAddress := arbitraryValidatorAddress()
sk := fakeStakingKeeper{
sk := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{{
ValidatorAddress: validatorAddress,
Shares: d("1000"),
@ -96,32 +96,3 @@ var arbitraryDelegatorRewardIndexes = types.MultiRewardIndexes{
},
),
}
type fakeStakingKeeper struct {
delegations stakingtypes.Delegations
validators stakingtypes.Validators
}
func (k fakeStakingKeeper) TotalBondedTokens(ctx sdk.Context) sdk.Int {
panic("unimplemented")
}
func (k fakeStakingKeeper) GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) []stakingtypes.Delegation {
return k.delegations
}
func (k fakeStakingKeeper) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (stakingtypes.Validator, bool) {
for _, val := range k.validators {
if val.GetOperator().Equals(addr) {
return val, true
}
}
return stakingtypes.Validator{}, false
}
func (k fakeStakingKeeper) GetValidatorDelegations(ctx sdk.Context, valAddr sdk.ValAddress) []stakingtypes.Delegation {
var delegations stakingtypes.Delegations
for _, d := range k.delegations {
if d.ValidatorAddress.Equals(valAddr) {
delegations = append(delegations, d)
}
}
return delegations
}

View File

@ -36,7 +36,7 @@ func (suite *SynchronizeDelegatorRewardTests) storeGlobalDelegatorFactor(multiRe
func (suite *SynchronizeDelegatorRewardTests) TestClaimIndexesAreUnchangedWhenGlobalFactorUnchanged() {
delegator := arbitraryAddress()
stakingKeeper := fakeStakingKeeper{} // use an empty staking keeper that returns no delegations
stakingKeeper := &fakeStakingKeeper{} // use an empty staking keeper that returns no delegations
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil)
claim := types.DelegatorClaim{
@ -58,7 +58,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestClaimIndexesAreUnchangedWhenGl
func (suite *SynchronizeDelegatorRewardTests) TestClaimIndexesAreUpdatedWhenGlobalFactorIncreased() {
delegator := arbitraryAddress()
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, fakeStakingKeeper{}, nil)
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, &fakeStakingKeeper{}, nil)
claim := types.DelegatorClaim{
BaseMultiClaim: types.BaseMultiClaim{
@ -85,7 +85,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestClaimIndexesAreUpdatedWhenGlob
func (suite *SynchronizeDelegatorRewardTests) TestRewardIsUnchangedWhenGlobalFactorUnchanged() {
delegator := arbitraryAddress()
validatorAddress := arbitraryValidatorAddress()
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
{
DelegatorAddress: delegator,
@ -130,7 +130,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestRewardIsUnchangedWhenGlobalFac
func (suite *SynchronizeDelegatorRewardTests) TestRewardIsIncreasedWhenNewRewardAdded() {
delegator := arbitraryAddress()
validatorAddress := arbitraryValidatorAddress()
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
{
DelegatorAddress: delegator,
@ -180,7 +180,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestRewardIsIncreasedWhenNewReward
func (suite *SynchronizeDelegatorRewardTests) TestRewardIsIncreasedWhenGlobalFactorIncreased() {
delegator := arbitraryAddress()
validatorAddress := arbitraryValidatorAddress()
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
{
DelegatorAddress: delegator,
@ -266,7 +266,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestGetDelegatedWhenValAddrIsNil()
// when valAddr is nil, get total delegated to bonded validators
delegator := arbitraryAddress()
validatorAddresses := generateValidatorAddresses(4)
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
//bonded
{
@ -309,7 +309,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestGetDelegatedWhenExcludingAVali
// when valAddr is x, get total delegated to bonded validators excluding those to x
delegator := arbitraryAddress()
validatorAddresses := generateValidatorAddresses(4)
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
//bonded
{
@ -352,7 +352,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestGetDelegatedWhenIncludingAVali
// when valAddr is x, get total delegated to bonded validators including those to x
delegator := arbitraryAddress()
validatorAddresses := generateValidatorAddresses(4)
stakingKeeper := fakeStakingKeeper{
stakingKeeper := &fakeStakingKeeper{
delegations: stakingtypes.Delegations{
//bonded
{

View File

@ -148,8 +148,7 @@ func (suite *DelegatorRewardsTestSuite) TestAccumulateDelegatorRewards() {
rewardPeriods, found := suite.keeper.GetDelegatorRewardPeriods(runCtx, tc.args.delegation.Denom)
suite.Require().True(found)
err = suite.keeper.AccumulateDelegatorRewards(runCtx, rewardPeriods)
suite.Require().NoError(err)
suite.keeper.AccumulateDelegatorRewards(runCtx, rewardPeriods)
rewardIndexes, _ := suite.keeper.GetDelegatorRewardIndexes(runCtx, tc.args.delegation.Denom)
suite.Require().Equal(tc.args.expectedRewardIndexes, rewardIndexes)
@ -269,8 +268,7 @@ func (suite *DelegatorRewardsTestSuite) TestSynchronizeDelegatorReward() {
rewardPeriods, found := suite.keeper.GetDelegatorRewardPeriods(blockCtx, tc.args.delegation.Denom)
suite.Require().True(found)
err := suite.keeper.AccumulateDelegatorRewards(blockCtx, rewardPeriods)
suite.Require().NoError(err)
suite.keeper.AccumulateDelegatorRewards(blockCtx, rewardPeriods)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
@ -385,8 +383,7 @@ func (suite *DelegatorRewardsTestSuite) TestSimulateDelegatorRewardSynchronizati
// Accumulate delegator rewards
rewardPeriods, found := suite.keeper.GetDelegatorRewardPeriods(blockCtx, tc.args.delegation.Denom)
suite.Require().True(found)
err := suite.keeper.AccumulateDelegatorRewards(blockCtx, rewardPeriods)
suite.Require().NoError(err)
suite.keeper.AccumulateDelegatorRewards(blockCtx, rewardPeriods)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)

View File

@ -2,8 +2,6 @@ package keeper
import (
"fmt"
"math"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -11,70 +9,52 @@ import (
"github.com/kava-labs/kava/x/incentive/types"
)
// AccumulateHardSupplyRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateHardSupplyRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) error {
// AccumulateHardSupplyRewards 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 Keeper) AccumulateHardSupplyRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) {
previousAccrualTime, found := k.GetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriod.RewardsPerSecond.IsZero() {
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
previousAccrualTime = ctx.BlockTime()
}
totalSuppliedCoins, foundTotalSuppliedCoins := k.hardKeeper.GetSuppliedCoins(ctx)
if !foundTotalSuppliedCoins {
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
totalSupplied := totalSuppliedCoins.AmountOf(rewardPeriod.CollateralType).ToDec()
if totalSupplied.IsZero() {
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
previousRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType)
indexes, found := k.GetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType)
if !found {
for _, rewardCoin := range rewardPeriod.RewardsPerSecond {
rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
previousRewardIndexes = append(previousRewardIndexes, rewardIndex)
}
k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, previousRewardIndexes)
indexes = types.RewardIndexes{}
}
hardFactor, found := k.hardKeeper.GetSupplyInterestFactor(ctx, rewardPeriod.CollateralType)
acc := types.NewAccumulator(previousAccrualTime, indexes)
totalSource := k.getHardSupplyTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime())
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
if len(acc.Indexes) > 0 {
// the store panics when setting empty or nil indexes
k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes)
}
}
// getHardSupplyTotalSourceShares fetches the sum of all source shares for a supply reward.
// In the case of hard supply, this is the total supplied divided by the supply interest factor.
// This give the "pre interest" value of the total supplied.
func (k Keeper) getHardSupplyTotalSourceShares(ctx sdk.Context, denom string) sdk.Dec {
totalSuppliedCoins, found := k.hardKeeper.GetSuppliedCoins(ctx)
if !found {
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
// assume no coins have been supplied
totalSuppliedCoins = sdk.NewCoins()
}
totalSupplied := totalSuppliedCoins.AmountOf(denom)
interestFactor, found := k.hardKeeper.GetSupplyInterestFactor(ctx, denom)
if !found {
// assume nothing has been borrowed so the factor starts at it's default value
interestFactor = sdk.OneDec()
}
newRewardIndexes := previousRewardIndexes
for _, rewardCoin := range rewardPeriod.RewardsPerSecond {
newRewards := rewardCoin.Amount.ToDec().Mul(timeElapsed.ToDec())
previousRewardIndex, found := previousRewardIndexes.GetRewardIndex(rewardCoin.Denom)
if !found {
previousRewardIndex = types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec())
}
// Calculate new reward factor and update reward index
rewardFactor := newRewards.Mul(hardFactor).Quo(totalSupplied)
newRewardFactorValue := previousRewardIndex.RewardFactor.Add(rewardFactor)
newRewardIndex := types.NewRewardIndex(rewardCoin.Denom, newRewardFactorValue)
i, found := newRewardIndexes.GetFactorIndex(rewardCoin.Denom)
if found {
newRewardIndexes[i] = newRewardIndex
} else {
newRewardIndexes = append(newRewardIndexes, newRewardIndex)
}
}
k.SetHardSupplyRewardIndexes(ctx, rewardPeriod.CollateralType, newRewardIndexes)
k.SetPreviousHardSupplyRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
// return supplied/factor to get the "pre interest" value of the current total supplied
return totalSupplied.ToDec().Quo(interestFactor)
}
// InitializeHardSupplyReward initializes the supply-side of a hard liquidity provider claim
@ -188,13 +168,6 @@ func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk
}
}
// ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object
func (k Keeper) ZeroHardLiquidityProviderClaim(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim {
claim.Reward = sdk.NewCoins()
k.SetHardLiquidityProviderClaim(ctx, claim)
return claim
}
// SimulateHardSynchronization calculates a user's outstanding hard rewards by simulating reward synchronization
func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim {
// 1. Simulate Hard supply-side rewards
@ -300,29 +273,6 @@ func (k Keeper) SimulateHardSynchronization(ctx sdk.Context, claim types.HardLiq
return claim
}
// CalculateTimeElapsed calculates the number of reward-eligible seconds that have passed since the previous
// time rewards were accrued, taking into account the end time of the reward period
func CalculateTimeElapsed(start, end, blockTime time.Time, previousAccrualTime time.Time) sdk.Int {
if (end.Before(blockTime) &&
(end.Before(previousAccrualTime) || end.Equal(previousAccrualTime))) ||
(start.After(blockTime)) ||
(start.Equal(blockTime)) {
return sdk.ZeroInt()
}
if start.After(previousAccrualTime) && start.Before(blockTime) {
previousAccrualTime = start
}
if end.Before(blockTime) {
return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven(
end.Sub(previousAccrualTime).Seconds(),
))))
}
return sdk.MaxInt(sdk.ZeroInt(), sdk.NewInt(int64(math.RoundToEven(
blockTime.Sub(previousAccrualTime).Seconds(),
))))
}
// Set setDifference: A - B
func setDifference(a, b []string) (diff []string) {
m := make(map[string]bool)

View File

@ -0,0 +1,316 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
)
type AccumulateSupplyRewardsTests struct {
unitTester
}
func (suite *AccumulateSupplyRewardsTests) storedTimeEquals(denom string, expected time.Time) {
storedTime, found := suite.keeper.GetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom)
suite.True(found)
suite.Equal(expected, storedTime)
}
func (suite *AccumulateSupplyRewardsTests) storedIndexesEqual(denom string, expected types.RewardIndexes) {
storedIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(suite.ctx, denom)
suite.Equal(found, expected != nil)
suite.Equal(expected, storedIndexes)
}
func TestAccumulateSupplyRewards(t *testing.T) {
suite.Run(t, new(AccumulateSupplyRewardsTests))
}
func (suite *AccumulateSupplyRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalSupply(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
suite.storeGlobalSupplyIndexes(types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
})
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
newAccrualTime := previousAccrualTime.Add(1 * time.Hour)
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, newAccrualTime)
suite.storedIndexesEqual(denom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("7.22"),
},
{
CollateralType: "ukava",
RewardFactor: d("3.64"),
},
})
}
func (suite *AccumulateSupplyRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalSupply(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalSupplyIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, previousAccrualTime)
expected, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expected)
}
func (suite *AccumulateSupplyRewardsTests) TestNoAccumulationWhenSourceSharesAreZero() {
denom := "bnb"
hardKeeper := newFakeHardKeeper() // zero total supplys
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalSupplyIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(7 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)), // same denoms as in global indexes
)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(denom, firstAccrualTime)
expected, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expected)
}
func (suite *AccumulateSupplyRewardsTests) TestStateAddedWhenStateDoesNotExist() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalSupply(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
period := types.NewMultiRewardPeriod(
true,
denom,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// 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(denom, firstAccrualTime)
suite.storedIndexesEqual(denom, nil)
secondAccrualTime := firstAccrualTime.Add(10 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// After the second accumulation both current block time and indexes should be stored.
suite.storedTimeEquals(denom, secondAccrualTime)
suite.storedIndexesEqual(denom, types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.01"),
},
})
}
func (suite *AccumulateSupplyRewardsTests) TestNoPanicWhenStateDoesNotExist() {
denom := "bnb"
hardKeeper := newFakeHardKeeper()
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
period := types.NewMultiRewardPeriod(
true,
denom,
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 source 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() {
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
})
suite.storedTimeEquals(denom, accrualTime)
suite.storedIndexesEqual(denom, nil)
}
func (suite *AccumulateSupplyRewardsTests) TestNoAccumulationWhenBeforeStartTime() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalSupply(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: denom,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalSupplyIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(10 * time.Second)
period := types.NewMultiRewardPeriod(
true,
denom,
firstAccrualTime.Add(time.Nanosecond), // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
// The accrual time should be updated, but the indexes unchanged
suite.storedTimeEquals(denom, firstAccrualTime)
expectedIndexes, f := previousIndexes.Get(denom)
suite.True(f)
suite.storedIndexesEqual(denom, expectedIndexes)
}
func (suite *AccumulateSupplyRewardsTests) TestPanicWhenCurrentTimeLessThanPrevious() {
denom := "bnb"
hardKeeper := newFakeHardKeeper().addTotalSupply(c(denom, 1e6), d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, hardKeeper, nil, nil, nil)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, denom, previousAccrualTime)
firstAccrualTime := time.Time{}
period := types.NewMultiRewardPeriod(
true,
denom,
time.Time{}, // start time after accrual time
distantFuture,
cs(c("hard", 2000), c("ukava", 1000)),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.Panics(func() {
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, period)
})
}

View File

@ -186,8 +186,7 @@ func (suite *SupplyRewardsTestSuite) TestAccumulateHardSupplyRewards() {
// Accumulate hard supply rewards for the deposit denom
multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, tc.args.deposit.Denom)
suite.Require().True(found)
err = suite.keeper.AccumulateHardSupplyRewards(runCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardSupplyRewards(runCtx, multiRewardPeriod)
// Check that each expected reward index matches the current stored reward index for the denom
globalRewardIndexes, found := suite.keeper.GetHardSupplyRewardIndexes(runCtx, tc.args.deposit.Denom)
@ -548,8 +547,8 @@ func (suite *SupplyRewardsTestSuite) TestSynchronizeHardSupplyReward() {
// Accumulate hard supply-side rewards
multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom)
if found {
err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod)
}
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
@ -658,15 +657,13 @@ func (suite *SupplyRewardsTestSuite) TestSynchronizeHardSupplyReward() {
// But new deposit denoms don't have their PreviousHardSupplyRewardAccrualTime set yet,
// so we need to call the accumulation method once to set the initial reward accrual time
if tc.args.deposit.Denom != tc.args.incentiveSupplyRewardDenom {
err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod)
}
// Now we can jump forward in time and accumulate rewards
updatedBlockTime = previousBlockTime.Add(time.Duration(int(time.Second) * tc.args.updatedTimeDuration))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err = suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardSupplyRewards(suite.ctx, multiRewardPeriod)
// After we've accumulated, run synchronize
deposit, found = suite.hardKeeper.GetDeposit(suite.ctx, userAddr)
@ -919,8 +916,7 @@ func (suite *SupplyRewardsTestSuite) TestSimulateHardSupplyRewardSynchronization
// Accumulate hard supply-side rewards
multiRewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(blockCtx, tc.args.deposit.Denom)
suite.Require().True(found)
err := suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateHardSupplyRewards(blockCtx, multiRewardPeriod)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)

View File

@ -24,12 +24,9 @@ func (k Keeper) AccumulateSwapRewards(ctx sdk.Context, rewardPeriod types.MultiR
acc := types.NewAccumulator(previousAccrualTime, indexes)
totalShares, found := k.swapKeeper.GetPoolShares(ctx, rewardPeriod.CollateralType)
if !found {
totalShares = sdk.ZeroInt()
}
totalSource := k.getSwapTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(rewardPeriod, totalShares.ToDec(), ctx.BlockTime())
acc.Accumulate(rewardPeriod, totalSource, ctx.BlockTime())
k.SetSwapRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
if len(acc.Indexes) > 0 {
@ -38,6 +35,16 @@ func (k Keeper) AccumulateSwapRewards(ctx sdk.Context, rewardPeriod types.MultiR
}
}
// getSwapTotalSourceShares fetches the sum of all source shares for a swap reward.
// In the case of swap, these are the total (swap module) shares in a particular pool.
func (k Keeper) getSwapTotalSourceShares(ctx sdk.Context, poolID string) sdk.Dec {
totalShares, found := k.swapKeeper.GetPoolShares(ctx, poolID)
if !found {
totalShares = sdk.ZeroInt()
}
return totalShares.ToDec()
}
// InitializeSwapReward creates a new claim with zero rewards and indexes matching the global indexes.
// If the claim already exists it just updates the indexes.
func (k Keeper) InitializeSwapReward(ctx sdk.Context, poolID string, owner sdk.AccAddress) {
@ -107,7 +114,7 @@ func (k Keeper) GetSynchronizedSwapClaim(ctx sdk.Context, owner sdk.AccAddress)
if !found {
return types.SwapClaim{}, false
}
for _, indexes := range claim.RewardIndexes {
for _, indexes := range claim.RewardIndexes { // TODO shouldn't this loop through global indexes, in case some have been recently added?
poolID := indexes.CollateralType
shares, found := k.swapKeeper.GetDepositorSharesAmount(ctx, owner, poolID)

View File

@ -4,7 +4,6 @@ import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
@ -14,15 +13,15 @@ type AccumulateSwapRewardsTests struct {
unitTester
}
func (suite *AccumulateSwapRewardsTests) checkStoredTimeEquals(poolID string, expected time.Time) {
func (suite *AccumulateSwapRewardsTests) storedTimeEquals(poolID string, expected time.Time) {
storedTime, found := suite.keeper.GetSwapRewardAccrualTime(suite.ctx, poolID)
suite.True(found)
suite.Equal(expected, storedTime)
}
func (suite *AccumulateSwapRewardsTests) checkStoredIndexesEqual(poolID string, expected types.RewardIndexes) {
func (suite *AccumulateSwapRewardsTests) storedIndexesEqual(poolID string, expected types.RewardIndexes) {
storedIndexes, found := suite.keeper.GetSwapRewardIndexes(suite.ctx, poolID)
suite.True(found)
suite.Equal(found, expected != nil)
suite.Equal(expected, storedIndexes)
}
@ -31,10 +30,11 @@ func TestAccumulateSwapRewards(t *testing.T) {
}
func (suite *AccumulateSwapRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() {
swapKeeper := &fakeSwapKeeper{i(1e6)}
pool := "btc:usdx"
swapKeeper := newFakeSwapKeeper().addPool(pool, i(1e6))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
pool := "btc/usdx"
suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{
{
CollateralType: pool,
@ -68,9 +68,8 @@ func (suite *AccumulateSwapRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
// check time and factors
suite.checkStoredTimeEquals(pool, newAccrualTime)
expectedIndexes := types.RewardIndexes{
suite.storedTimeEquals(pool, newAccrualTime)
suite.storedIndexesEqual(pool, types.RewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("7.22"),
@ -79,62 +78,16 @@ func (suite *AccumulateSwapRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreas
CollateralType: "ukava",
RewardFactor: d("3.64"),
},
}
suite.checkStoredIndexesEqual(pool, expectedIndexes)
}
func (suite *AccumulateSwapRewardsTests) TestLimitsOfAccumulationPrecision() {
swapKeeper := &fakeSwapKeeper{i(1e17)} // approximate shares in a $1B pool of 10^8 precision ~$1 asset
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
pool := "btc/usdx"
suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{
{
CollateralType: pool,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("0.0"),
},
},
},
})
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetSwapRewardAccrualTime(suite.ctx, pool, previousAccrualTime)
newAccrualTime := previousAccrualTime.Add(1 * time.Second) // 1 second is the smallest increment accrual happens over
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
period := types.NewMultiRewardPeriod(
true,
pool,
time.Unix(0, 0),
distantFuture,
cs(c("swap", 1)), // single unit of any denom is the smallest reward amount
)
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
// check time and factors
suite.checkStoredTimeEquals(pool, newAccrualTime)
expectedIndexes := types.RewardIndexes{
{
CollateralType: "swap",
// smallest reward amount over smallest accumulation duration does not go past 10^-18 decimal precision
RewardFactor: d("0.000000000000000010"),
},
}
suite.checkStoredIndexesEqual(pool, expectedIndexes)
}
func (suite *AccumulateSwapRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
swapKeeper := &fakeSwapKeeper{i(1e6)}
pool := "btc:usdx"
swapKeeper := newFakeSwapKeeper().addPool(pool, i(1e6))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
pool := "btc/usdx"
suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: pool,
RewardIndexes: types.RewardIndexes{
@ -148,7 +101,8 @@ func (suite *AccumulateSwapRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIn
},
},
},
})
}
suite.storeGlobalSwapIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetSwapRewardAccrualTime(suite.ctx, pool, previousAccrualTime)
@ -166,26 +120,63 @@ func (suite *AccumulateSwapRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIn
// check time and factors
suite.checkStoredTimeEquals(pool, previousAccrualTime)
suite.storedTimeEquals(pool, previousAccrualTime)
expected, f := previousIndexes.Get(pool)
suite.True(f)
suite.storedIndexesEqual(pool, expected)
}
expectedIndexes := types.RewardIndexes{
func (suite *AccumulateSwapRewardsTests) TestNoAccumulationWhenSourceSharesAreZero() {
pool := "btc:usdx"
swapKeeper := newFakeSwapKeeper() // no pools, so no source shares
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
CollateralType: pool,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.checkStoredIndexesEqual(pool, expectedIndexes)
suite.storeGlobalSwapIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetSwapRewardAccrualTime(suite.ctx, 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
)
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(pool, firstAccrualTime)
expected, f := previousIndexes.Get(pool)
suite.True(f)
suite.storedIndexesEqual(pool, expected)
}
func (suite *AccumulateSwapRewardsTests) TestStateAddedWhenStateDoesNotExist() {
swapKeeper := &fakeSwapKeeper{i(1e6)}
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
pool := "btc:usdx"
pool := "btc/usdx"
swapKeeper := newFakeSwapKeeper().addPool(pool, i(1e6))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
period := types.NewMultiRewardPeriod(
true,
@ -201,8 +192,9 @@ func (suite *AccumulateSwapRewardsTests) TestStateAddedWhenStateDoesNotExist() {
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
// After the first accumulation only the current block time should be stored.
// This indexes will be zero as no time has passed since the previous block because it didn't exist.
suite.checkStoredTimeEquals(pool, firstAccrualTime)
// The indexes will be empty as no time has passed since the previous block because it didn't exist.
suite.storedTimeEquals(pool, firstAccrualTime)
suite.storedIndexesEqual(pool, nil)
secondAccrualTime := firstAccrualTime.Add(10 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime)
@ -210,9 +202,8 @@ func (suite *AccumulateSwapRewardsTests) TestStateAddedWhenStateDoesNotExist() {
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
// After the second accumulation both current block time and indexes should be stored.
suite.checkStoredTimeEquals(pool, secondAccrualTime)
expectedIndexes := types.RewardIndexes{
suite.storedTimeEquals(pool, secondAccrualTime)
suite.storedIndexesEqual(pool, types.RewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("0.02"),
@ -221,14 +212,14 @@ func (suite *AccumulateSwapRewardsTests) TestStateAddedWhenStateDoesNotExist() {
CollateralType: "ukava",
RewardFactor: d("0.01"),
},
}
suite.checkStoredIndexesEqual(pool, expectedIndexes)
})
}
func (suite *AccumulateSwapRewardsTests) TestNoPanicWhenStateDoesNotExist() {
swapKeeper := &fakeSwapKeeper{i(0)}
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
pool := "btc/usdx"
func (suite *AccumulateSwapRewardsTests) TestNoPanicWhenStateDoesNotExist() {
pool := "btc:usdx"
swapKeeper := newFakeSwapKeeper()
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
period := types.NewMultiRewardPeriod(
true,
@ -248,20 +239,78 @@ func (suite *AccumulateSwapRewardsTests) TestNoPanicWhenStateDoesNotExist() {
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
})
suite.checkStoredTimeEquals(pool, accrualTime)
suite.storedTimeEquals(pool, accrualTime)
suite.storedIndexesEqual(pool, nil)
}
type fakeSwapKeeper struct {
poolShares sdk.Int
func (suite *AccumulateSwapRewardsTests) TestNoAccumulationWhenBeforeStartTime() {
pool := "btc:usdx"
swapKeeper := newFakeSwapKeeper().addPool(pool, i(1e6))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
previousIndexes := types.MultiRewardIndexes{
{
CollateralType: pool,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "swap",
RewardFactor: d("0.02"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.04"),
},
},
},
}
suite.storeGlobalSwapIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetSwapRewardAccrualTime(suite.ctx, 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)
suite.keeper.AccumulateSwapRewards(suite.ctx, period)
// The accrual time should be updated, but the indexes unchanged
suite.storedTimeEquals(pool, firstAccrualTime)
expectedIndexes, f := previousIndexes.Get(pool)
suite.True(f)
suite.storedIndexesEqual(pool, expectedIndexes)
}
func (k fakeSwapKeeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Int, bool) {
return k.poolShares, true
}
func (k fakeSwapKeeper) GetDepositorSharesAmount(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) {
// This is just to implement the swap keeper interface.
return sdk.Int{}, false
}
func (suite *AccumulateSwapRewardsTests) TestPanicWhenCurrentTimeLessThanPrevious() {
pool := "btc:usdx"
// note: amino panics when encoding times ≥ the start of year 10000.
var distantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)
swapKeeper := newFakeSwapKeeper().addPool(pool, i(1e6))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetSwapRewardAccrualTime(suite.ctx, 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.AccumulateSwapRewards(suite.ctx, period)
})
}

View File

@ -29,7 +29,7 @@ func (suite *InitializeSwapRewardTests) TestClaimAddedWhenClaimDoesNotExistAndNo
// When a claim doesn't exist, and a user deposits to a non-rewarded pool;
// then a claim is added with no rewards and no indexes
poolID := "base/quote"
poolID := "base:quote"
// no global indexes stored as this pool is not rewarded
@ -53,7 +53,7 @@ func (suite *InitializeSwapRewardTests) TestClaimAddedWhenClaimDoesNotExistAndRe
// When a claim doesn't exist, and a user deposits to a rewarded pool;
// then a claim is added with no rewards and indexes matching the global indexes
poolID := "base/quote"
poolID := "base:quote"
globalIndexes := types.MultiRewardIndexes{
{
@ -92,7 +92,7 @@ func (suite *InitializeSwapRewardTests) TestClaimUpdatedWhenClaimExistsAndNoRewa
},
}
newPoolID := "btcb/usdx"
newPoolID := "btcb:usdx"
claim := types.SwapClaim{
BaseMultiClaim: types.BaseMultiClaim{
@ -141,7 +141,7 @@ func (suite *InitializeSwapRewardTests) TestClaimUpdatedWhenClaimExistsAndReward
},
}
newPoolID := "btcb/usdx"
newPoolID := "btcb:usdx"
newIndexes := types.RewardIndexes{
{
CollateralType: "otherrewarddenom",

View File

@ -32,7 +32,7 @@ func (suite *SynchronizeSwapRewardTests) TestClaimUpdatedWhenGlobalIndexesHaveIn
// The user earns rewards for the time passed, and the claim indexes are updated
originalReward := arbitraryCoins()
poolID := "base/quote"
poolID := "base:quote"
claim := types.SwapClaim{
BaseMultiClaim: types.BaseMultiClaim{
@ -83,7 +83,7 @@ func (suite *SynchronizeSwapRewardTests) TestClaimUpdatedWhenGlobalIndexesHaveIn
func (suite *SynchronizeSwapRewardTests) TestClaimUnchangedWhenGlobalIndexesUnchanged() {
// It should be safe to call SynchronizeSwapReward multiple times
poolID := "base/quote"
poolID := "base:quote"
unchangingIndexes := types.MultiRewardIndexes{
{
CollateralType: poolID,
@ -211,7 +211,7 @@ func (suite *SynchronizeSwapRewardTests) TestClaimUpdatedWhenNewRewardDenomAdded
// Then the user earns rewards for the time since the reward was added, and the new indexes are added.
originalReward := arbitraryCoins()
poolID := "base/quote"
poolID := "base:quote"
claim := types.SwapClaim{
BaseMultiClaim: types.BaseMultiClaim{
@ -271,7 +271,7 @@ func (suite *SynchronizeSwapRewardTests) TestClaimUpdatedWhenGlobalIndexesIncrea
// When the claim is synced, but the user has no shares
// The user earns no rewards for the time passed, but the claim indexes are updated
poolID := "base/quote"
poolID := "base:quote"
claim := types.SwapClaim{
BaseMultiClaim: types.BaseMultiClaim{

View File

@ -9,42 +9,49 @@ import (
"github.com/kava-labs/kava/x/incentive/types"
)
// AccumulateUSDXMintingRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateUSDXMintingRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error {
// AccumulateUSDXMintingRewards 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 Keeper) AccumulateUSDXMintingRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) {
previousAccrualTime, found := k.GetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
previousAccrualTime = ctx.BlockTime()
}
timeElapsed := CalculateTimeElapsed(rewardPeriod.Start, rewardPeriod.End, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriod.RewardsPerSecond.Amount.IsZero() {
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rewardPeriod.CollateralType, types.PrincipalDenom).ToDec()
if totalPrincipal.IsZero() {
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)
cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
rewardFactor := newRewards.ToDec().Mul(cdpFactor).Quo(totalPrincipal)
previousRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType)
factor, found := k.GetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType)
if !found {
previousRewardFactor = sdk.ZeroDec()
factor = sdk.ZeroDec()
}
newRewardFactor := previousRewardFactor.Add(rewardFactor)
k.SetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor)
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
// wrap in RewardIndexes for compatibility with Accumulator
indexes := types.RewardIndexes{}.With(types.USDXMintingRewardDenom, factor)
acc := types.NewAccumulator(previousAccrualTime, indexes)
totalSource := k.getUSDXTotalSourceShares(ctx, rewardPeriod.CollateralType)
acc.Accumulate(types.NewMultiRewardPeriodFromRewardPeriod(rewardPeriod), totalSource, ctx.BlockTime())
k.SetPreviousUSDXMintingAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime)
factor, found = acc.Indexes.Get(types.USDXMintingRewardDenom)
if !found {
panic("could not find factor that should never be missing when accumulating usdx rewards")
}
k.SetUSDXMintingRewardFactor(ctx, rewardPeriod.CollateralType, factor)
}
// getUSDXTotalSourceShares fetches the sum of all source shares for a usdx minting reward.
// In the case of usdx minting, this is the total debt from all cdps of a particular type, divided by the cdp interest factor.
// This give the "pre interest" value of the total debt.
func (k Keeper) getUSDXTotalSourceShares(ctx sdk.Context, collateralType string) sdk.Dec {
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, collateralType, cdptypes.DefaultStableDenom)
cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, collateralType)
if !found {
// assume nothing has been borrowed so the factor starts at it's default value
cdpFactor = sdk.OneDec()
}
// return debt/factor to get the "pre interest" value of the current total debt
return totalPrincipal.ToDec().Quo(cdpFactor)
}
// InitializeUSDXMintingClaim creates or updates a claim such that no new rewards are accrued, but any existing rewards are not lost.

View File

@ -0,0 +1,234 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
)
type AccumulateUSDXRewardsTests struct {
usdxRewardsUnitTester
}
func (suite *AccumulateUSDXRewardsTests) storedTimeEquals(cType string, expected time.Time) {
storedTime, found := suite.keeper.GetPreviousUSDXMintingAccrualTime(suite.ctx, cType)
suite.True(found)
suite.Equal(expected, storedTime)
}
func (suite *AccumulateUSDXRewardsTests) storedIndexesEqual(cType string, expected sdk.Dec) {
storedIndexes, found := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, cType)
suite.True(found)
suite.Equal(expected, storedIndexes)
}
func TestAccumulateUSDXRewards(t *testing.T) {
suite.Run(t, new(AccumulateUSDXRewardsTests))
}
func (suite *AccumulateUSDXRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper().addTotalPrincipal(i(1e6)).addInterestFactor(d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
suite.storeGlobalUSDXIndexes(types.RewardIndexes{
{
CollateralType: cType,
RewardFactor: d("0.04"),
},
})
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, cType, previousAccrualTime)
newAccrualTime := previousAccrualTime.Add(1 * time.Hour)
suite.ctx = suite.ctx.WithBlockTime(newAccrualTime)
period := types.NewRewardPeriod(
true,
cType,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
c("ukava", 1000),
)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(cType, newAccrualTime)
suite.storedIndexesEqual(cType, d("3.64"))
}
func (suite *AccumulateUSDXRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper().addTotalPrincipal(i(1e6)).addInterestFactor(d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
previousIndexes := types.RewardIndexes{
{
CollateralType: cType,
RewardFactor: d("0.04"),
},
}
suite.storeGlobalUSDXIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, cType, previousAccrualTime)
suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime)
period := types.NewRewardPeriod(
true,
cType,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
c("ukava", 2000),
)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(cType, previousAccrualTime)
expected, f := previousIndexes.Get(cType)
suite.True(f)
suite.storedIndexesEqual(cType, expected)
}
func (suite *AccumulateUSDXRewardsTests) TestNoAccumulationWhenSourceSharesAreZero() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper() // zero total borrows
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
previousIndexes := types.RewardIndexes{
{
CollateralType: cType,
RewardFactor: d("0.04"),
},
}
suite.storeGlobalUSDXIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, cType, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(7 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
period := types.NewRewardPeriod(
true,
cType,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
c("ukava", 1000),
)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// check time and factors
suite.storedTimeEquals(cType, firstAccrualTime)
expected, f := previousIndexes.Get(cType)
suite.True(f)
suite.storedIndexesEqual(cType, expected)
}
func (suite *AccumulateUSDXRewardsTests) TestStateAddedWhenStateDoesNotExist() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper().addTotalPrincipal(i(1e6)).addInterestFactor(d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
period := types.NewRewardPeriod(
true,
cType,
time.Unix(0, 0), // ensure the test is within start and end times
distantFuture,
c("ukava", 1000),
)
firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// After the first accumulation the current block time should be stored and the factor will be zero.
suite.storedTimeEquals(cType, firstAccrualTime)
suite.storedIndexesEqual(cType, sdk.ZeroDec())
secondAccrualTime := firstAccrualTime.Add(10 * time.Second)
suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// After the second accumulation both current block time and indexes should be stored.
suite.storedTimeEquals(cType, secondAccrualTime)
suite.storedIndexesEqual(cType, d("0.01"))
}
func (suite *AccumulateUSDXRewardsTests) TestNoAccumulationWhenBeforeStartTime() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper().addTotalPrincipal(i(1e6)).addInterestFactor(d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
previousIndexes := types.RewardIndexes{
{
CollateralType: cType,
RewardFactor: d("0.04"),
},
}
suite.storeGlobalUSDXIndexes(previousIndexes)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, cType, previousAccrualTime)
firstAccrualTime := previousAccrualTime.Add(10 * time.Second)
period := types.NewRewardPeriod(
true,
cType,
firstAccrualTime.Add(time.Nanosecond), // start time after accrual time
distantFuture,
c("ukava", 1000),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
// The accrual time should be updated, but the indexes unchanged
suite.storedTimeEquals(cType, firstAccrualTime)
expected, f := previousIndexes.Get(cType)
suite.True(f)
suite.storedIndexesEqual(cType, expected)
}
func (suite *AccumulateUSDXRewardsTests) TestPanicWhenCurrentTimeLessThanPrevious() {
cType := "bnb-a"
cdpKeeper := newFakeCDPKeeper().addTotalPrincipal(i(1e6)).addInterestFactor(d("1"))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, cdpKeeper, nil, nil, nil, nil)
previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, cType, previousAccrualTime)
firstAccrualTime := time.Time{}
period := types.NewRewardPeriod(
true,
cType,
time.Time{}, // start time after accrual time
distantFuture,
c("ukava", 1000),
)
suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime)
suite.Panics(func() {
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, period)
})
}

View File

@ -117,8 +117,7 @@ func (suite *USDXRewardsTestSuite) TestAccumulateUSDXMintingRewards() {
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype)
suite.Require().True(found)
err := suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod)
rewardFactor, _ := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype)
suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor)
@ -191,8 +190,7 @@ func (suite *USDXRewardsTestSuite) TestSynchronizeUSDXMintingReward() {
blockCtx := suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype)
suite.Require().True(found)
err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
@ -278,8 +276,7 @@ func (suite *USDXRewardsTestSuite) TestSimulateUSDXMintingRewardSynchronization(
blockCtx := suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype)
suite.Require().True(found)
err := suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod)
suite.Require().NoError(err)
suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)

View File

@ -2,17 +2,21 @@ package keeper_test
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
db "github.com/tendermint/tm-db"
"github.com/kava-labs/kava/app"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
hardtypes "github.com/kava-labs/kava/x/hard/types"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
@ -94,6 +98,7 @@ func (suite *unitTester) storeSwapClaim(claim types.SwapClaim) {
suite.keeper.SetSwapClaim(suite.ctx, claim)
}
// fakeParamSubspace is a stub paramSpace to simplify keeper unit test setup.
type fakeParamSubspace struct {
params types.Params
}
@ -113,6 +118,197 @@ func (subspace *fakeParamSubspace) WithKeyTable(params.KeyTable) params.Subspace
return params.Subspace{}
}
// fakeSwapKeeper is a stub swap keeper.
// It can be used to return values to the incentive keeper without having to initialize a full swap keeper.
type fakeSwapKeeper struct {
poolShares map[string]sdk.Int
}
var _ types.SwapKeeper = newFakeSwapKeeper()
func newFakeSwapKeeper() *fakeSwapKeeper {
return &fakeSwapKeeper{
poolShares: map[string]sdk.Int{},
}
}
func (k *fakeSwapKeeper) addPool(id string, shares sdk.Int) *fakeSwapKeeper {
k.poolShares[id] = shares
return k
}
func (k *fakeSwapKeeper) GetPoolShares(_ sdk.Context, poolID string) (sdk.Int, bool) {
shares, ok := k.poolShares[poolID]
return shares, ok
}
func (k *fakeSwapKeeper) GetDepositorSharesAmount(_ sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) {
// This is just to implement the swap keeper interface.
return sdk.Int{}, false
}
// fakeHardKeeper is a stub hard keeper.
// It can be used to return values to the incentive keeper without having to initialize a full hard keeper.
type fakeHardKeeper struct {
borrows fakeHardState
deposits fakeHardState
}
type fakeHardState struct {
total sdk.Coins
interestFactors map[string]sdk.Dec
}
func newFakeHardState() fakeHardState {
return fakeHardState{
total: nil,
interestFactors: map[string]sdk.Dec{}, // initialize map to avoid panics on read
}
}
var _ types.HardKeeper = newFakeHardKeeper()
func newFakeHardKeeper() *fakeHardKeeper {
return &fakeHardKeeper{
borrows: newFakeHardState(),
deposits: newFakeHardState(),
}
}
func (k *fakeHardKeeper) addTotalBorrow(coin sdk.Coin, factor sdk.Dec) *fakeHardKeeper {
k.borrows.total = k.borrows.total.Add(coin)
k.borrows.interestFactors[coin.Denom] = factor
return k
}
func (k *fakeHardKeeper) addTotalSupply(coin sdk.Coin, factor sdk.Dec) *fakeHardKeeper {
k.deposits.total = k.deposits.total.Add(coin)
k.deposits.interestFactors[coin.Denom] = factor
return k
}
func (k *fakeHardKeeper) GetBorrowedCoins(_ sdk.Context) (sdk.Coins, bool) {
if k.borrows.total == nil {
return nil, false
}
return k.borrows.total, true
}
func (k *fakeHardKeeper) GetSuppliedCoins(_ sdk.Context) (sdk.Coins, bool) {
if k.deposits.total == nil {
return nil, false
}
return k.deposits.total, true
}
func (k *fakeHardKeeper) GetBorrowInterestFactor(_ sdk.Context, denom string) (sdk.Dec, bool) {
f, ok := k.borrows.interestFactors[denom]
return f, ok
}
func (k *fakeHardKeeper) GetSupplyInterestFactor(_ sdk.Context, denom string) (sdk.Dec, bool) {
f, ok := k.deposits.interestFactors[denom]
return f, ok
}
func (k *fakeHardKeeper) GetBorrow(_ sdk.Context, _ sdk.AccAddress) (hardtypes.Borrow, bool) {
panic("unimplemented")
}
func (k *fakeHardKeeper) GetDeposit(_ sdk.Context, _ sdk.AccAddress) (hardtypes.Deposit, bool) {
panic("unimplemented")
}
// fakeStakingKeeper is a stub staking keeper.
// It can be used to return values to the incentive keeper without having to initialize a full staking keeper.
type fakeStakingKeeper struct {
delegations stakingtypes.Delegations
validators stakingtypes.Validators
}
var _ types.StakingKeeper = newFakeStakingKeeper()
func newFakeStakingKeeper() *fakeStakingKeeper { return &fakeStakingKeeper{} }
func (k *fakeStakingKeeper) addBondedTokens(amount int64) *fakeStakingKeeper {
if len(k.validators) != 0 {
panic("cannot set total bonded if keeper already has validators set")
}
// add a validator with all the tokens
k.validators = append(k.validators, stakingtypes.Validator{
Status: sdk.Bonded,
Tokens: sdk.NewInt(amount),
})
return k
}
func (k *fakeStakingKeeper) TotalBondedTokens(_ sdk.Context) sdk.Int {
total := sdk.ZeroInt()
for _, val := range k.validators {
if val.GetStatus() == sdk.Bonded {
total = total.Add(val.GetBondedTokens())
}
}
return total
}
func (k *fakeStakingKeeper) GetDelegatorDelegations(_ sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) []stakingtypes.Delegation {
return k.delegations
}
func (k *fakeStakingKeeper) GetValidator(_ sdk.Context, addr sdk.ValAddress) (stakingtypes.Validator, bool) {
for _, val := range k.validators {
if val.GetOperator().Equals(addr) {
return val, true
}
}
return stakingtypes.Validator{}, false
}
func (k *fakeStakingKeeper) GetValidatorDelegations(_ sdk.Context, valAddr sdk.ValAddress) []stakingtypes.Delegation {
var delegations stakingtypes.Delegations
for _, d := range k.delegations {
if d.ValidatorAddress.Equals(valAddr) {
delegations = append(delegations, d)
}
}
return delegations
}
// fakeCDPKeeper is a stub cdp keeper.
// It can be used to return values to the incentive keeper without having to initialize a full cdp keeper.
type fakeCDPKeeper struct {
interestFactor *sdk.Dec
totalPrincipal sdk.Int
}
var _ types.CdpKeeper = newFakeCDPKeeper()
func newFakeCDPKeeper() *fakeCDPKeeper {
return &fakeCDPKeeper{
interestFactor: nil,
totalPrincipal: sdk.ZeroInt(),
}
}
func (k *fakeCDPKeeper) addInterestFactor(f sdk.Dec) *fakeCDPKeeper {
k.interestFactor = &f
return k
}
func (k *fakeCDPKeeper) addTotalPrincipal(p sdk.Int) *fakeCDPKeeper {
k.totalPrincipal = p
return k
}
func (k *fakeCDPKeeper) GetInterestFactor(_ sdk.Context, collateralType string) (sdk.Dec, bool) {
if k.interestFactor != nil {
return *k.interestFactor, true
}
return sdk.Dec{}, false
}
func (k *fakeCDPKeeper) GetTotalPrincipal(_ sdk.Context, collateralType string, principalDenom string) sdk.Int {
return k.totalPrincipal
}
func (k *fakeCDPKeeper) GetCdpByOwnerAndCollateralType(_ sdk.Context, owner sdk.AccAddress, collateralType string) (cdptypes.CDP, bool) {
return cdptypes.CDP{}, false
}
func (k *fakeCDPKeeper) GetCollateral(_ sdk.Context, collateralType string) (cdptypes.CollateralParam, bool) {
return cdptypes.CollateralParam{}, false
}
// Assorted Testing Data
// note: amino panics when encoding times ≥ the start of year 10000.
var distantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)
func arbitraryCoin() sdk.Coin {
return c("hard", 1e9)
}

View File

@ -12,15 +12,15 @@ Rewards target various user activity. For example, usdx borrowed from bnb CDPs,
Each second, the rewards accumulate at a rate set in the params, eg 100 ukava per second. These are then distributed to all users ratably based on their percentage involvement in the rewarded activity. For example if a user holds 1% of all funds deposited to the kava/usdx swap pool. They will receive 1% of the total rewards each second.
The number tracking a user's involvement is referred to as "reward source" in the code. And the total across all users the "reward source total".
The quantity tracking a user's involvement is referred to as "source shares" in the code. And the total across all users the "total source shares". The quotient then gives their percentage involvement, eg if a user borrowed 10,000 usdx, and there is 100,000 usdx borrowed by all users, then they will get 10% of rewards.
## Efficiency
Paying out rewards to every user every block would be slow and lead to long block times. Instead rewards are calculated much less frequently.
Every block a global tracker adds up total rewards paid out per unit of user involvement. For example, per unit of xrpb supplied to hard, or per share in a kava/usdx swap pool. A user's specific reward can then be calculated as needed based on their current deposit/shares/borrow.
Every block a global tracker adds up total rewards paid out per unit of user involvement. For example, per unit of xrpb supplied to hard, or per share in a kava/usdx swap pool. A user's specific reward can then be calculated as needed based on their current source shares.
User's rewards must be updated whenever their reward source changes. This happens through hooks into other modules that run before deposits/borrows/supplies etc.
Users' rewards must be updated whenever their source shares change. This happens through hooks into other modules that run before deposits/borrows/supplies etc.
## HARD Token distribution
@ -39,3 +39,7 @@ The exact multipliers will be voted by governance and can be changed via a gover
## USDX Minting Rewards
The incentive module is responsible for distribution of KAVA tokens to users who mint USDX. When governance adds a collateral type to be eligible for rewards, they set the rate (coins/second) at which rewards are given to users, the length of each reward period, the length of each claim period, and the amount of time reward coins must vest before users who claim them can transfer them. For the duration of a reward period, any user that has minted USDX using an eligible collateral type will ratably accumulate rewards in a `USDXMintingClaim` object. For example, if a user has minted 10% of all USDX for the duration of the reward period, they will earn 10% of all rewards for that period. When the reward period ends, the claim period begins immediately, at which point users can submit a message to claim their rewards. Rewards are time-locked, meaning that when a user claims rewards they will receive them as a vesting balance on their account. Vesting balances can be used to stake coins, but cannot be transferred until the vesting period ends. In addition to vesting, rewards can have multipliers that vary the number of tokens received. For example, a reward with a vesting period of 1 month may have a multiplier of 0.25, meaning that the user will receive 25% of the reward balance if they choose that vesting schedule.
## SWP Token Distribution
TODO

View File

@ -22,4 +22,4 @@ parent:
### Dependencies
This module uses hooks to update user rewards. Currently, `incentive` implements hooks from the `cdp`, `hard`, and `staking` (comsos-sdk) modules. All rewards are paid out from the `kavadist` module account.
This module uses hooks to update user rewards. Currently, `incentive` implements hooks from the `cdp`, `hard`, `swap`, and `staking` (comsos-sdk) modules. All rewards are paid out from the `kavadist` module account.

View File

@ -46,6 +46,8 @@ func (builder IncentiveGenesisBuilder) WithGenesisTime(time time.Time) Incentive
return builder
}
// WithInitializedBorrowRewardPeriod 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) WithInitializedBorrowRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
builder.Params.HardBorrowRewardPeriods = append(builder.Params.HardBorrowRewardPeriods, period)
@ -55,6 +57,7 @@ func (builder IncentiveGenesisBuilder) WithInitializedBorrowRewardPeriod(period
accumulationTimeForPeriod,
)
// TODO remove to better reflect real states
builder.HardBorrowRewardState.MultiRewardIndexes = builder.HardBorrowRewardState.MultiRewardIndexes.With(
period.CollateralType,
newZeroRewardIndexesFromCoins(period.RewardsPerSecond...),
@ -67,6 +70,8 @@ func (builder IncentiveGenesisBuilder) WithSimpleBorrowRewardPeriod(ctype string
return builder.WithInitializedBorrowRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond))
}
// WithInitializedSupplyRewardPeriod 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) WithInitializedSupplyRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
builder.Params.HardSupplyRewardPeriods = append(builder.Params.HardSupplyRewardPeriods, period)
@ -76,6 +81,7 @@ func (builder IncentiveGenesisBuilder) WithInitializedSupplyRewardPeriod(period
accumulationTimeForPeriod,
)
// TODO remove to better reflect real states
builder.HardSupplyRewardState.MultiRewardIndexes = builder.HardSupplyRewardState.MultiRewardIndexes.With(
period.CollateralType,
newZeroRewardIndexesFromCoins(period.RewardsPerSecond...),
@ -88,6 +94,8 @@ func (builder IncentiveGenesisBuilder) WithSimpleSupplyRewardPeriod(ctype string
return builder.WithInitializedSupplyRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond))
}
// WithInitializedDelegatorRewardPeriod 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) WithInitializedDelegatorRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
builder.Params.DelegatorRewardPeriods = append(builder.Params.DelegatorRewardPeriods, period)
@ -97,6 +105,7 @@ func (builder IncentiveGenesisBuilder) WithInitializedDelegatorRewardPeriod(peri
accumulationTimeForPeriod,
)
// TODO remove to better reflect real states
builder.DelegatorRewardState.MultiRewardIndexes = builder.DelegatorRewardState.MultiRewardIndexes.With(
period.CollateralType,
newZeroRewardIndexesFromCoins(period.RewardsPerSecond...),
@ -109,6 +118,8 @@ func (builder IncentiveGenesisBuilder) WithSimpleDelegatorRewardPeriod(ctype str
return builder.WithInitializedDelegatorRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond))
}
// WithInitializedSwapRewardPeriod 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) WithInitializedSwapRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
builder.Params.SwapRewardPeriods = append(builder.Params.SwapRewardPeriods, period)
@ -118,11 +129,6 @@ func (builder IncentiveGenesisBuilder) WithInitializedSwapRewardPeriod(period ty
accumulationTimeForPeriod,
)
builder.SwapRewardState.MultiRewardIndexes = builder.SwapRewardState.MultiRewardIndexes.With(
period.CollateralType,
newZeroRewardIndexesFromCoins(period.RewardsPerSecond...),
)
return builder
}
@ -130,6 +136,8 @@ func (builder IncentiveGenesisBuilder) WithSimpleSwapRewardPeriod(poolID string,
return builder.WithInitializedSwapRewardPeriod(builder.simpleRewardPeriod(poolID, rewardsPerSecond))
}
// WithInitializedUSDXRewardPeriod 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) WithInitializedUSDXRewardPeriod(period types.RewardPeriod) IncentiveGenesisBuilder {
builder.Params.USDXMintingRewardPeriods = append(builder.Params.USDXMintingRewardPeriods, period)
@ -139,6 +147,7 @@ func (builder IncentiveGenesisBuilder) WithInitializedUSDXRewardPeriod(period ty
accumulationTimeForPeriod,
)
// TODO remove to better reflect real states
builder.USDXRewardState.MultiRewardIndexes = builder.USDXRewardState.MultiRewardIndexes.With(
period.CollateralType,
newZeroRewardIndexesFromCoins(period.RewardsPerSecond),

View File

@ -29,10 +29,12 @@ func NewAccumulator(previousAccrual time.Time, indexes RewardIndexes) *Accumulat
// Rewards are not accrued for times outside of the start and end times of a reward period.
// If a period ends before currentTime, the PreviousAccrualTime is shortened to the end time. This allows accumulate to be called sequentially on consecutive reward periods.
//
// rewardSourceTotal is the total of all user reward sources. For example: total shares in a swap pool, total btcb supplied to hard, or total usdx borrowed from all bnb CDPs.
func (acc *Accumulator) Accumulate(period MultiRewardPeriod, rewardSourceTotal sdk.Dec, currentTime time.Time) {
// totalSourceShares is the sum of all users' source shares. For example:total btcb supplied to hard, total usdx borrowed from all bnb CDPs, or total shares in a swap pool.
func (acc *Accumulator) Accumulate(period MultiRewardPeriod, totalSourceShares sdk.Dec, currentTime time.Time) {
accumulationDuration := acc.getTimeElapsedWithinLimits(acc.PreviousAccumulationTime, currentTime, period.Start, period.End)
indexesIncrement := acc.calculateNewRewards(period.RewardsPerSecond, rewardSourceTotal, accumulationDuration)
indexesIncrement := acc.calculateNewRewards(period.RewardsPerSecond, totalSourceShares, accumulationDuration)
acc.Indexes = acc.Indexes.Add(indexesIncrement)
acc.PreviousAccumulationTime = minTime(period.End, currentTime)
@ -40,7 +42,7 @@ func (acc *Accumulator) Accumulate(period MultiRewardPeriod, rewardSourceTotal s
// getTimeElapsedWithinLimits returns the duration between start and end times, capped by min and max times.
// If the start and end range is outside the min to max time range then zero duration is returned.
func (acc *Accumulator) getTimeElapsedWithinLimits(start, end, limitMin, limitMax time.Time) time.Duration {
func (*Accumulator) getTimeElapsedWithinLimits(start, end, limitMin, limitMax time.Time) time.Duration {
if start.After(end) {
panic(fmt.Sprintf("start time (%s) cannot be after end time (%s)", start, end))
}
@ -54,19 +56,25 @@ func (acc *Accumulator) getTimeElapsedWithinLimits(start, end, limitMin, limitMa
return minTime(end, limitMax).Sub(maxTime(start, limitMin))
}
// calculateNewRewards calculates the amount to increase the global reward indexes for a given reward rate, duration, and source total.
// The total rewards to distribute in this block are given by reward rate * duration. This value divided by the source total to give
// total rewards per unit of source, which is what the indexes store.
// Note, duration is rounded to the nearest second to keep rewards calculation the same as in kava-7.
func (acc *Accumulator) calculateNewRewards(rewardsPerSecond sdk.Coins, rewardSourceTotal sdk.Dec, duration time.Duration) RewardIndexes {
if rewardSourceTotal.IsZero() {
// When the source total is zero, there is no users with deposits/borrows/delegations to pay out the current block's rewards to.
// calculateNewRewards calculates the amount to increase the global reward indexes by, for a given reward rate, duration, and number of source shares.
// The total rewards to distribute in this block are given by reward rate * duration. This value divided by the sum of all source shares to give
// total rewards per source share, which is what the indexes store.
// Note, duration is rounded to the nearest second to keep rewards calculation consistent with kava-7.
func (*Accumulator) calculateNewRewards(rewardsPerSecond sdk.Coins, totalSourceShares sdk.Dec, duration time.Duration) RewardIndexes {
if totalSourceShares.LTE(sdk.ZeroDec()) {
// When there is zero source shares, there is no users with deposits/borrows/delegations to pay out the current block's rewards to.
// So drop the rewards and pay out nothing.
return nil
}
durationSeconds := int64(math.RoundToEven(duration.Seconds()))
if durationSeconds <= 0 {
// If the duration is zero, there will be no increment.
// So return an empty increment instead of one full of zeros.
return nil
}
increment := newRewardIndexesFromCoins(rewardsPerSecond)
return increment.Mul(sdk.NewDec(durationSeconds)).Quo(rewardSourceTotal)
increment = increment.Mul(sdk.NewDec(durationSeconds)).Quo(totalSourceShares)
return increment
}
// minTime returns the earliest of two times.

View File

@ -104,7 +104,7 @@ func TestAccumulator(t *testing.T) {
type args struct {
rewardsPerSecond sdk.Coins
duration time.Duration
rewardSourceTotal sdk.Dec
totalSourceShares sdk.Dec
}
testcases := []struct {
name string
@ -116,7 +116,7 @@ func TestAccumulator(t *testing.T) {
args: args{
rewardsPerSecond: cs(c("hard", 1000), c("swap", 100)),
duration: 10 * time.Second,
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
},
expected: RewardIndexes{
{CollateralType: "hard", RewardFactor: d("10")},
@ -128,27 +128,39 @@ func TestAccumulator(t *testing.T) {
args: args{
rewardsPerSecond: cs(c("hard", 1000)),
duration: 10*time.Second + 500*time.Millisecond,
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
},
expected: RewardIndexes{
{CollateralType: "hard", RewardFactor: d("10")},
},
},
{
name: "when duration is zero the rewards are zero",
name: "reward indexes have enough precision for extreme params",
args: args{
rewardsPerSecond: cs(c("anydenom", 1)), // minimum possible rewards
duration: 1 * time.Second, // minimum possible duration (beyond zero as it's rounded)
totalSourceShares: d("100000000000000000"), // approximate shares in a $1B pool of 10^8 precision assets
},
expected: RewardIndexes{
// smallest reward amount over smallest accumulation duration does not go past 10^-18 decimal precision
{CollateralType: "anydenom", RewardFactor: d("0.000000000000000010")},
},
},
{
name: "when duration is zero there is no rewards",
args: args{
rewardsPerSecond: cs(c("hard", 1000)),
duration: 0,
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
},
expected: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0")}}, // TODO should this be nil?
expected: nil,
},
{
name: "when rewards per second are nil there is no rewards",
args: args{
rewardsPerSecond: cs(),
duration: 10 * time.Second,
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
},
expected: nil,
},
@ -157,7 +169,7 @@ func TestAccumulator(t *testing.T) {
args: args{
rewardsPerSecond: cs(c("hard", 1000)),
duration: 10 * time.Second,
rewardSourceTotal: d("0"),
totalSourceShares: d("0"),
},
expected: nil,
},
@ -166,7 +178,7 @@ func TestAccumulator(t *testing.T) {
args: args{
rewardsPerSecond: cs(),
duration: 0,
rewardSourceTotal: d("0"),
totalSourceShares: d("0"),
},
expected: nil,
},
@ -175,7 +187,7 @@ func TestAccumulator(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
acc := &Accumulator{}
indexes := acc.calculateNewRewards(tc.args.rewardsPerSecond, tc.args.rewardSourceTotal, tc.args.duration)
indexes := acc.calculateNewRewards(tc.args.rewardsPerSecond, tc.args.totalSourceShares, tc.args.duration)
require.Equal(t, tc.expected, indexes)
})
@ -185,7 +197,7 @@ func TestAccumulator(t *testing.T) {
type args struct {
accumulator Accumulator
period MultiRewardPeriod
rewardSourceTotal sdk.Dec
totalSourceShares sdk.Dec
currentTime time.Time
}
testcases := []struct {
@ -198,34 +210,40 @@ func TestAccumulator(t *testing.T) {
args: args{
accumulator: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0.1")}},
Indexes: RewardIndexes{
{CollateralType: "hard", RewardFactor: d("0.1")},
{CollateralType: "swap", RewardFactor: d("0.2")},
},
},
period: MultiRewardPeriod{
Start: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
RewardsPerSecond: cs(c("hard", 1000)),
},
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
currentTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC),
},
expected: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC),
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("5.1")}},
Indexes: RewardIndexes{
{CollateralType: "hard", RewardFactor: d("5.1")},
{CollateralType: "swap", RewardFactor: d("0.2")},
},
},
},
{
name: "nil reward indexes are treated as empty",
name: "empty reward indexes are added to correctly",
args: args{
accumulator: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
Indexes: nil,
Indexes: RewardIndexes{},
},
period: MultiRewardPeriod{
Start: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
RewardsPerSecond: cs(c("hard", 1000)),
},
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
currentTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC),
},
expected: Accumulator{
@ -233,6 +251,26 @@ func TestAccumulator(t *testing.T) {
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("5.0")}},
},
},
{
name: "empty reward indexes are unchanged when there's no rewards",
args: args{
accumulator: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
Indexes: RewardIndexes{},
},
period: MultiRewardPeriod{
Start: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
RewardsPerSecond: cs(),
},
totalSourceShares: d("1000"),
currentTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC),
},
expected: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC),
Indexes: RewardIndexes{},
},
},
{
name: "when a period is enclosed within block the accumulation time is set to the period end time",
args: args{
@ -245,7 +283,7 @@ func TestAccumulator(t *testing.T) {
End: time.Date(1998, 1, 1, 0, 0, 7, 0, time.UTC),
RewardsPerSecond: cs(c("hard", 1000)),
},
rewardSourceTotal: d("1000"),
totalSourceShares: d("1000"),
currentTime: time.Date(1998, 1, 1, 0, 0, 10, 0, time.UTC),
},
expected: Accumulator{
@ -253,11 +291,33 @@ func TestAccumulator(t *testing.T) {
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("2.1")}},
},
},
{
name: "accumulation duration is capped at param start when previous stored time is in the distant past",
// This could happend in the default time value time.Time{} was accidentally stored, or if a reward period was
// removed from the params, then added back a long time later.
args: args{
accumulator: Accumulator{
PreviousAccumulationTime: time.Time{},
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0.1")}},
},
period: MultiRewardPeriod{
Start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
RewardsPerSecond: cs(c("hard", 1000)),
},
totalSourceShares: d("1000"),
currentTime: time.Date(1998, 1, 1, 0, 0, 10, 0, time.UTC),
},
expected: Accumulator{
PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 10, 0, time.UTC),
Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("10.1")}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tc.args.accumulator.Accumulate(tc.args.period, tc.args.rewardSourceTotal, tc.args.currentTime)
tc.args.accumulator.Accumulate(tc.args.period, tc.args.totalSourceShares, tc.args.currentTime)
require.Equal(t, tc.expected, tc.args.accumulator)
})
}

View File

@ -13,8 +13,6 @@ const (
HardLiquidityProviderClaimType = "hard_liquidity_provider"
DelegatorClaimType = "delegator_claim"
SwapClaimType = "swap"
BondDenom = "ukava"
)
// Claim is an interface for handling common claim actions

View File

@ -33,6 +33,4 @@ var (
SwapClaimKeyPrefix = []byte{0x12} // prefix for keys that store swap claims
SwapRewardIndexesKeyPrefix = []byte{0x13} // prefix for key that stores swap reward indexes
PreviousSwapRewardAccrualTimeKeyPrefix = []byte{0x14} // prefix for key that stores the previous time swap rewards accrued
USDXMintingRewardDenom = "ukava"
)

View File

@ -9,10 +9,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
kavadistTypes "github.com/kava-labs/kava/x/kavadist/types"
)
@ -32,14 +30,17 @@ var (
KeySwapRewardPeriods = []byte("SwapRewardPeriods")
KeyClaimEnd = []byte("ClaimEnd")
KeyMultipliers = []byte("ClaimMultipliers")
DefaultActive = false
DefaultRewardPeriods = RewardPeriods{}
DefaultMultiRewardPeriods = MultiRewardPeriods{}
DefaultMultipliers = Multipliers{}
DefaultClaimEnd = tmtime.Canonical(time.Unix(1, 0))
GovDenom = cdptypes.DefaultGovDenom
PrincipalDenom = "usdx"
IncentiveMacc = kavadistTypes.ModuleName
DefaultActive = false
DefaultRewardPeriods = RewardPeriods{}
DefaultMultiRewardPeriods = MultiRewardPeriods{}
DefaultMultipliers = Multipliers{}
DefaultClaimEnd = tmtime.Canonical(time.Unix(1, 0))
BondDenom = "ukava"
USDXMintingRewardDenom = "ukava"
IncentiveMacc = kavadistTypes.ModuleName
)
// Params governance parameters for the incentive module
@ -210,6 +211,18 @@ func NewRewardPeriod(active bool, collateralType string, start time.Time, end ti
}
}
// NewMultiRewardPeriodFromRewardPeriod converts a RewardPeriod into a MultiRewardPeriod.
// It's useful for compatibility between single and multi denom rewards.
func NewMultiRewardPeriodFromRewardPeriod(period RewardPeriod) MultiRewardPeriod {
return NewMultiRewardPeriod(
period.Active,
period.CollateralType,
period.Start,
period.End,
sdk.NewCoins(period.RewardsPerSecond),
)
}
// Validate performs a basic check of a RewardPeriod fields.
func (rp RewardPeriod) Validate() error {
if rp.Start.Unix() <= 0 {
@ -219,8 +232,12 @@ func (rp RewardPeriod) Validate() error {
return errors.New("reward period end time cannot be 0")
}
if rp.Start.After(rp.End) {
// This is needed to ensure that the begin blocker accumulation does not panic.
return fmt.Errorf("end period time %s cannot be before start time %s", rp.End, rp.Start)
}
if rp.RewardsPerSecond.Denom != USDXMintingRewardDenom {
return fmt.Errorf("reward denom must be %s, got: %s", USDXMintingRewardDenom, rp.RewardsPerSecond.Denom)
}
if !rp.RewardsPerSecond.IsValid() {
return fmt.Errorf("invalid reward amount: %s", rp.RewardsPerSecond)
}
@ -291,6 +308,7 @@ func (mrp MultiRewardPeriod) Validate() error {
return errors.New("reward period end time cannot be 0")
}
if mrp.Start.After(mrp.End) {
// This is needed to ensure that the begin blocker accumulation does not panic.
return fmt.Errorf("end period time %s cannot be before start time %s", mrp.End, mrp.Start)
}
if !mrp.RewardsPerSecond.IsValid() {

View File

@ -1,6 +1,7 @@
package types_test
import (
"fmt"
"testing"
"time"
@ -31,6 +32,20 @@ var rewardMultiPeriodWithInvalidRewardsPerSecond = types.NewMultiRewardPeriod(
time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.Coins{sdk.Coin{Denom: "INVALID!@#😫", Amount: sdk.ZeroInt()}},
)
var validMultiRewardPeriod = types.NewMultiRewardPeriod(
true,
"bnb",
time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.NewCoins(sdk.NewInt64Coin("swap", 1e9)),
)
var validRewardPeriod = types.NewRewardPeriod(
true,
"bnb-a",
time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.NewInt64Coin(types.USDXMintingRewardDenom, 1e9),
)
func (suite *ParamTestSuite) TestParamValidation() {
type errArgs struct {
@ -93,7 +108,7 @@ func (suite *ParamTestSuite) TestParamValidation() {
},
errArgs{
expectPass: false,
contains: "invalid reward amount",
contains: fmt.Sprintf("reward denom must be %s", types.USDXMintingRewardDenom),
},
},
{
@ -176,6 +191,120 @@ func (suite *ParamTestSuite) TestParamValidation() {
}
}
func (suite *ParamTestSuite) TestRewardPeriods() {
suite.Run("Validate", func() {
type err struct {
pass bool
contains string
}
testCases := []struct {
name string
periods types.RewardPeriods
expect err
}{
{
name: "single period is valid",
periods: types.RewardPeriods{
validRewardPeriod,
},
expect: err{
pass: true,
},
},
{
name: "duplicated reward period is invalid",
periods: types.RewardPeriods{
validRewardPeriod,
validRewardPeriod,
},
expect: err{
contains: "duplicated reward period",
},
},
{
name: "invalid reward denom is invalid",
periods: types.RewardPeriods{
types.NewRewardPeriod(
true,
"bnb-a",
time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.NewInt64Coin("hard", 1e9),
),
},
expect: err{
contains: fmt.Sprintf("reward denom must be %s", types.USDXMintingRewardDenom),
},
},
}
for _, tc := range testCases {
err := tc.periods.Validate()
if tc.expect.pass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Contains(err.Error(), tc.expect.contains)
}
}
})
}
func (suite *ParamTestSuite) TestMultiRewardPeriods() {
suite.Run("Validate", func() {
type err struct {
pass bool
contains string
}
testCases := []struct {
name string
periods types.MultiRewardPeriods
expect err
}{
{
name: "single period is valid",
periods: types.MultiRewardPeriods{
validMultiRewardPeriod,
},
expect: err{
pass: true,
},
},
{
name: "duplicated reward period is invalid",
periods: types.MultiRewardPeriods{
validMultiRewardPeriod,
validMultiRewardPeriod,
},
expect: err{
contains: "duplicated reward period",
},
},
{
name: "invalid reward period is invalid",
periods: types.MultiRewardPeriods{
rewardMultiPeriodWithInvalidRewardsPerSecond,
},
expect: err{
contains: "invalid reward amount",
},
},
}
for _, tc := range testCases {
err := tc.periods.Validate()
if tc.expect.pass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Contains(err.Error(), tc.expect.contains)
}
}
})
}
func TestParamTestSuite(t *testing.T) {
suite.Run(t, new(ParamTestSuite))
}

View File

@ -1,11 +0,0 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// NewPeriod returns a new vesting period
func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
return vesting.Period{Amount: amount, Length: length}
}

View File

@ -5,6 +5,11 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// NewPeriod returns a new vesting period
func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
return vesting.Period{Amount: amount, Length: length}
}
// GetTotalVestingPeriodLength returns the summed length of all vesting periods
func GetTotalVestingPeriodLength(periods vesting.Periods) int64 {
length := int64(0)