Hard: Kava delegators earn HARD rewards via the Incentive module (#776)

* add staking keeper to incentive module

* update hard with delegator methods

* add delegator methods to incentive

* implement delegator hook scaffolds

* implement hard delegator reward accumulation

* update claim names to delegator

* stakingKeeper expected keeper methods

* accumulate delegator rewards

* initialize delegator reward

* synchronize delegator reward

* add TODO comments to rewards

* implement staking hooks interface

* initial revisions

* remove outdated TODO

* update methods for test compatibility

* update method names for test compatibility

* implement initial accumulate delegator reward test

* attempt validator set up in staking module

* initial synchronize delegator reward test

* delegator accumulation test passing

* synchronize delegator rewards test (not passing)

* synchronize delegator rewards passing

* revisions
This commit is contained in:
Denali Marsh 2021-01-25 13:58:12 +01:00 committed by GitHub
parent dc330d02bf
commit 72a6df17fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 526 additions and 55 deletions

View File

@ -382,6 +382,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
&cdpKeeper, &cdpKeeper,
&hardKeeper, &hardKeeper,
app.accountKeeper, app.accountKeeper,
&stakingKeeper,
) )
app.issuanceKeeper = issuance.NewKeeper( app.issuanceKeeper = issuance.NewKeeper(
app.cdc, app.cdc,
@ -394,7 +395,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
// register the staking hooks // register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
app.stakingKeeper = *stakingKeeper.SetHooks( app.stakingKeeper = *stakingKeeper.SetHooks(
staking.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks())) staking.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks(), app.incentiveKeeper.Hooks()))
app.cdpKeeper = *cdpKeeper.SetHooks(cdp.NewMultiCDPHooks(app.incentiveKeeper.Hooks())) app.cdpKeeper = *cdpKeeper.SetHooks(cdp.NewMultiCDPHooks(app.incentiveKeeper.Hooks()))

View File

@ -44,6 +44,7 @@ var (
TotalReservesPrefix = []byte{0x10} // denom -> sdk.Coin TotalReservesPrefix = []byte{0x10} // denom -> sdk.Coin
BorrowInterestFactorPrefix = []byte{0x11} // denom -> sdk.Dec BorrowInterestFactorPrefix = []byte{0x11} // denom -> sdk.Dec
SupplyInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec SupplyInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec
DelegatorInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec
LtvIndexPrefix = []byte{0x13} LtvIndexPrefix = []byte{0x13}
sep = []byte(":") sep = []byte(":")
) )

View File

@ -2,6 +2,8 @@ package keeper
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types" cdptypes "github.com/kava-labs/kava/x/cdp/types"
hardtypes "github.com/kava-labs/kava/x/hard/types" hardtypes "github.com/kava-labs/kava/x/hard/types"
) )
@ -13,10 +15,13 @@ type Hooks struct {
var _ cdptypes.CDPHooks = Hooks{} var _ cdptypes.CDPHooks = Hooks{}
var _ hardtypes.HARDHooks = Hooks{} var _ hardtypes.HARDHooks = Hooks{}
var _ stakingtypes.StakingHooks = Hooks{}
// Hooks create new incentive hooks // Hooks create new incentive hooks
func (k Keeper) Hooks() Hooks { return Hooks{k} } func (k Keeper) Hooks() Hooks { return Hooks{k} }
// ------------------- Cdp Module Hooks -------------------
// AfterCDPCreated function that runs after a cdp is created // AfterCDPCreated function that runs after a cdp is created
func (h Hooks) AfterCDPCreated(ctx sdk.Context, cdp cdptypes.CDP) { func (h Hooks) AfterCDPCreated(ctx sdk.Context, cdp cdptypes.CDP) {
h.k.InitializeUSDXMintingClaim(ctx, cdp) h.k.InitializeUSDXMintingClaim(ctx, cdp)
@ -29,6 +34,8 @@ func (h Hooks) BeforeCDPModified(ctx sdk.Context, cdp cdptypes.CDP) {
h.k.SynchronizeUSDXMintingReward(ctx, cdp) h.k.SynchronizeUSDXMintingReward(ctx, cdp)
} }
// ------------------- Hard Module Hooks -------------------
// AfterDepositCreated function that runs after a deposit is created // AfterDepositCreated function that runs after a deposit is created
func (h Hooks) AfterDepositCreated(ctx sdk.Context, deposit hardtypes.Deposit) { func (h Hooks) AfterDepositCreated(ctx sdk.Context, deposit hardtypes.Deposit) {
h.k.InitializeHardSupplyReward(ctx, deposit) h.k.InitializeHardSupplyReward(ctx, deposit)
@ -58,3 +65,46 @@ func (h Hooks) BeforeBorrowModified(ctx sdk.Context, borrow hardtypes.Borrow) {
func (h Hooks) AfterBorrowModified(ctx sdk.Context, borrow hardtypes.Borrow) { func (h Hooks) AfterBorrowModified(ctx sdk.Context, borrow hardtypes.Borrow) {
h.k.UpdateHardBorrowIndexDenoms(ctx, borrow) h.k.UpdateHardBorrowIndexDenoms(ctx, borrow)
} }
// ------------------- Staking Module Hooks -------------------
// BeforeDelegationCreated runs before a delegation is created
func (h Hooks) BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) {
h.k.InitializeHardDelegatorReward(ctx, delAddr)
}
// BeforeDelegationSharesModified runs before an existing delegation is modified
func (h Hooks) BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) {
h.k.SynchronizeHardDelegatorRewards(ctx, delAddr)
}
// NOTE: following hooks are just implemented to ensure StakingHooks interface compliance
// BeforeValidatorSlashed is called before a validator is slashed
func (h Hooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) {}
// AfterValidatorBeginUnbonding is called after a validator begins unbonding
func (h Hooks) AfterValidatorBeginUnbonding(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) {
}
// AfterValidatorBonded is called after a validator is bonded
func (h Hooks) AfterValidatorBonded(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) {
}
// AfterDelegationModified runs after a delegation is modified
func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) {
}
// BeforeDelegationRemoved runs directly before a delegation is deleted
func (h Hooks) BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) {
}
// AfterValidatorCreated runs after a validator is created
func (h Hooks) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) {}
// BeforeValidatorModified runs before a validator is modified
func (h Hooks) BeforeValidatorModified(ctx sdk.Context, valAddr sdk.ValAddress) {}
// AfterValidatorRemoved runs after a validator is removed
func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) {
}

View File

@ -20,12 +20,13 @@ type Keeper struct {
key sdk.StoreKey key sdk.StoreKey
paramSubspace subspace.Subspace paramSubspace subspace.Subspace
supplyKeeper types.SupplyKeeper supplyKeeper types.SupplyKeeper
stakingKeeper types.StakingKeeper
} }
// NewKeeper creates a new keeper // NewKeeper creates a new keeper
func NewKeeper( func NewKeeper(
cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper, cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper,
cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper,
) Keeper { ) Keeper {
return Keeper{ return Keeper{
@ -36,6 +37,7 @@ func NewKeeper(
key: key, key: key,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
supplyKeeper: sk, supplyKeeper: sk,
stakingKeeper: stk,
} }
} }
@ -276,3 +278,20 @@ func (k Keeper) SetPreviousHardBorrowRewardAccrualTime(ctx sdk.Context, denom st
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousHardBorrowRewardAccrualTimeKeyPrefix) store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousHardBorrowRewardAccrualTimeKeyPrefix)
store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(blockTime)) store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(blockTime))
} }
// GetPreviousHardDelegatorRewardAccrualTime returns the last time a denom accrued Hard protocol delegator rewards
func (k Keeper) GetPreviousHardDelegatorRewardAccrualTime(ctx sdk.Context, denom string) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousHardDelegatorRewardAccrualTimeKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryBare(bz, &blockTime)
return blockTime, true
}
// SetPreviousHardDelegatorRewardAccrualTime sets the last time a denom accrued Hard protocol delegator rewards
func (k Keeper) SetPreviousHardDelegatorRewardAccrualTime(ctx sdk.Context, denom string, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousHardDelegatorRewardAccrualTimeKeyPrefix)
store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(blockTime))
}

View File

@ -9,6 +9,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting" "github.com/cosmos/cosmos-sdk/x/auth/vesting"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
@ -26,16 +27,20 @@ type KeeperTestSuite struct {
keeper keeper.Keeper keeper keeper.Keeper
hardKeeper hardkeeper.Keeper hardKeeper hardkeeper.Keeper
stakingKeeper stakingkeeper.Keeper
app app.TestApp app app.TestApp
ctx sdk.Context ctx sdk.Context
addrs []sdk.AccAddress addrs []sdk.AccAddress
validatorAddrs []sdk.ValAddress
} }
// The default state used by each test // The default state used by each test
func (suite *KeeperTestSuite) SetupTest() { func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates() tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(5) _, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIncentiveKeeper() keeper := tApp.GetIncentiveKeeper()
suite.app = tApp suite.app = tApp

View File

@ -53,6 +53,17 @@ func (k Keeper) GetHardBorrowRewardPeriod(ctx sdk.Context, denom string) (types.
return types.RewardPeriod{}, false return types.RewardPeriod{}, false
} }
// GetHardDelegatorRewardPeriod returns the reward period with the specified collateral type if it's found in the params
func (k Keeper) GetHardDelegatorRewardPeriod(ctx sdk.Context, denom string) (types.RewardPeriod, bool) {
params := k.GetParams(ctx)
for _, rp := range params.HardDelegatorRewardPeriods {
if rp.CollateralType == denom {
return rp, true
}
}
return types.RewardPeriod{}, false
}
// GetMultiplier returns the multiplier with the specified name if it's found in the params // GetMultiplier returns the multiplier with the specified name if it's found in the params
func (k Keeper) GetMultiplier(ctx sdk.Context, name types.MultiplierName) (types.Multiplier, bool) { func (k Keeper) GetMultiplier(ctx sdk.Context, name types.MultiplierName) (types.Multiplier, bool) {
params := k.GetParams(ctx) params := k.GetParams(ctx)

View File

@ -54,7 +54,7 @@ func (suite *KeeperTestSuite) TestPayoutClaim() {
multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
multiplier: types.MultiplierName("large"), multiplier: types.MultiplierName("large"),
timeElapsed: 86400, timeElapsed: 86400,
expectedBalance: cs(c("usdx", 10000000000), c("ukava", 10571385600)), expectedBalance: cs(c("usdx", 10000000000), c("ukava", 10576385600)),
expectedPeriods: vesting.Periods{vesting.Period{Length: 31536000, Amount: cs(c("ukava", 10571385600))}}, expectedPeriods: vesting.Periods{vesting.Period{Length: 31536000, Amount: cs(c("ukava", 10571385600))}},
isPeriodicVestingAccount: true, isPeriodicVestingAccount: true,
}, },

View File

@ -400,6 +400,124 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo
k.SetHardLiquidityProviderClaim(ctx, claim) k.SetHardLiquidityProviderClaim(ctx, claim)
} }
// SynchronizeHardDelegatorRewards updates the claim object by adding any accumulated rewards
func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.AccAddress) {
claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator)
if !found {
return
}
delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom)
if !found {
return
}
delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom)
if !hasDelegatorRewardIndex {
return
}
userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor
rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor)
if rewardsAccumulatedFactor.IsZero() {
return
}
claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor
totalDelegated := sdk.ZeroDec()
// TODO: set reasonable max limit on delegation iteration
maxUInt := ^uint16(0)
delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delegator, maxUInt)
for _, delegation := range delegations {
validator, found := k.stakingKeeper.GetValidator(ctx, delegation.GetValidatorAddr())
if !found {
continue
}
// Delegators don't accumulate rewards if their validator is unbonded/slashed
if validator.GetStatus() != sdk.Bonded {
continue
}
if validator.GetTokens().IsZero() {
continue
}
delegatedTokens := validator.TokensFromShares(delegation.GetShares())
if delegatedTokens.IsZero() || delegatedTokens.IsNegative() {
continue
}
totalDelegated = totalDelegated.Add(delegatedTokens)
}
rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt()
if rewardsEarned.IsZero() || rewardsEarned.IsNegative() {
return
}
// Add rewards to delegator's hard claim
newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned)
claim.Reward = claim.Reward.Add(newRewardsCoin)
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// AccumulateHardDelegatorRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateHardDelegatorRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error {
previousAccrualTime, found := k.GetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
timeElapsed := CalculateTimeElapsed(rewardPeriod, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriod.RewardsPerSecond.Amount.IsZero() {
k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
totalBonded := k.stakingKeeper.TotalBondedTokens(ctx).ToDec()
if totalBonded.IsZero() {
k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)
rewardFactor := newRewards.ToDec().Quo(totalBonded)
previousRewardFactor, found := k.GetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType)
if !found {
previousRewardFactor = sdk.ZeroDec()
}
newRewardFactor := previousRewardFactor.Add(rewardFactor)
k.SetHardDelegatorRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor)
k.SetPreviousHardDelegatorRewardAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
// InitializeHardDelegatorReward initializes the delegator reward index of a hard claim
func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.AccAddress) {
delegatorFactor, foundDelegatorFactor := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom)
if !foundDelegatorFactor { // Should always be found...
delegatorFactor = sdk.ZeroDec()
}
delegatorRewardIndexes := types.NewRewardIndex(types.BondDenom, delegatorFactor)
claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator)
if !found {
// Instantiate claim object
claim = types.NewHardLiquidityProviderClaim(delegator,
sdk.NewCoin(types.HardLiquidityRewardDenom, sdk.ZeroInt()),
nil, nil, nil)
}
claim.DelegatorRewardIndexes = types.RewardIndexes{delegatorRewardIndexes}
k.SetHardLiquidityProviderClaim(ctx, claim)
}
// ZeroClaim zeroes out the claim object's rewards and returns the updated claim object // ZeroClaim zeroes out the claim object's rewards and returns the updated claim object
func (k Keeper) ZeroClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { func (k Keeper) ZeroClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim {
claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt()) claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt())

View File

@ -4,8 +4,9 @@ import (
"time" "time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/ed25519"
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
@ -866,36 +867,292 @@ func (suite *KeeperTestSuite) TestUpdateHardBorrowIndexDenoms() {
} }
} }
func (suite *KeeperTestSuite) TestAccumulateHardDelegatorRewards() {
type args struct {
delegation sdk.Coin
rewardsPerSecond sdk.Coin
initialTime time.Time
timeElapsed int
expectedRewardFactor sdk.Dec
}
type test struct {
name string
args args
}
testCases := []test{
{
"7 seconds",
args{
delegation: c("ukava", 1_000_000),
rewardsPerSecond: c("hard", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
timeElapsed: 7,
expectedRewardFactor: d("0.428239000000000000"),
},
},
{
"1 day",
args{
delegation: c("ukava", 1_000_000),
rewardsPerSecond: c("hard", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
timeElapsed: 86400,
expectedRewardFactor: d("5285.692800000000000000"),
},
},
{
"0 seconds",
args{
delegation: c("ukava", 1_000_000),
rewardsPerSecond: c("hard", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
timeElapsed: 0,
expectedRewardFactor: d("0.0"),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupWithGenState()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// Mint coins to hard module account
supplyKeeper := suite.app.GetSupplyKeeper()
hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000)))
supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins)
// Set up incentive state
params := types.NewParams(
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
tc.args.initialTime.Add(time.Hour*24*365*5),
)
suite.keeper.SetParams(suite.ctx, params)
suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime)
suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec())
// Set up hard state (interest factor for the relevant denom)
suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime)
err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err)
suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err)
staking.EndBlocker(suite.ctx, suite.stakingKeeper)
// Set up chain context at future time
runAtTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
runCtx := suite.ctx.WithBlockTime(runAtTime)
// Run Hard begin blocker in order to update the denom's index factor
hard.BeginBlocker(runCtx, suite.hardKeeper)
rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(runCtx, tc.args.delegation.Denom)
suite.Require().True(found)
err = suite.keeper.AccumulateHardDelegatorRewards(runCtx, rewardPeriod)
suite.Require().NoError(err)
rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(runCtx, tc.args.delegation.Denom)
suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor)
})
}
}
func (suite *KeeperTestSuite) TestSynchronizeHardDelegatorReward() {
type args struct {
delegation sdk.Coin
rewardsPerSecond sdk.Coin
initialTime time.Time
blockTimes []int
expectedRewardFactor sdk.Dec
expectedRewards sdk.Coin
}
type test struct {
name string
args args
}
testCases := []test{
{
"10 blocks",
args{
delegation: c("ukava", 1_000_000),
rewardsPerSecond: c("hard", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
expectedRewardFactor: d("6.117700000000000000"),
expectedRewards: c("hard", 6117700),
},
},
{
"10 blocks - long block time",
args{
delegation: c("ukava", 1_000_000),
rewardsPerSecond: c("hard", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400},
expectedRewardFactor: d("52856.928000000000000000"),
expectedRewards: c("hard", 52856928000),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupWithGenState()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// Mint coins to hard module account
supplyKeeper := suite.app.GetSupplyKeeper()
hardMaccCoins := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(200000000)))
supplyKeeper.MintCoins(suite.ctx, hardtypes.ModuleAccountName, hardMaccCoins)
// setup incentive state
params := types.NewParams(
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.delegation.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
tc.args.initialTime.Add(time.Hour*24*365*5),
)
suite.keeper.SetParams(suite.ctx, params)
suite.keeper.SetPreviousHardDelegatorRewardAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime)
suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom, sdk.ZeroDec())
// Set up hard state (interest factor for the relevant denom)
suite.hardKeeper.SetPreviousAccrualTime(suite.ctx, tc.args.delegation.Denom, tc.args.initialTime)
// Delegator delegates
err := suite.deliverMsgCreateValidator(suite.ctx, suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err)
suite.deliverMsgDelegate(suite.ctx, suite.addrs[0], suite.validatorAddrs[0], tc.args.delegation)
suite.Require().NoError(err)
staking.EndBlocker(suite.ctx, suite.stakingKeeper)
// Check that Staking hooks initialized a HardLiquidityProviderClaim
claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0])
suite.Require().True(found)
suite.Require().Equal(sdk.ZeroDec(), claim.DelegatorRewardIndexes[0].RewardFactor)
// Run accumulator at several intervals
var timeElapsed int
previousBlockTime := suite.ctx.BlockTime()
for _, t := range tc.args.blockTimes {
timeElapsed += t
updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t))
previousBlockTime = updatedBlockTime
blockCtx := suite.ctx.WithBlockTime(updatedBlockTime)
// Run Hard begin blocker for each block ctx to update denom's interest factor
hard.BeginBlocker(blockCtx, suite.hardKeeper)
rewardPeriod, found := suite.keeper.GetHardDelegatorRewardPeriod(blockCtx, tc.args.delegation.Denom)
suite.Require().True(found)
err := suite.keeper.AccumulateHardDelegatorRewards(blockCtx, rewardPeriod)
suite.Require().NoError(err)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
// After we've accumulated, run synchronize
suite.Require().NotPanics(func() {
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, suite.addrs[0])
})
// Check that reward factor and claim have been updated as expected
rewardFactor, found := suite.keeper.GetHardDelegatorRewardFactor(suite.ctx, tc.args.delegation.Denom)
suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor)
claim, found = suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[0])
suite.Require().True(found)
suite.Require().Equal(tc.args.expectedRewardFactor, claim.DelegatorRewardIndexes[0].RewardFactor)
suite.Require().Equal(tc.args.expectedRewards, claim.Reward)
})
}
}
func (suite *KeeperTestSuite) SetupWithGenState() { func (suite *KeeperTestSuite) SetupWithGenState() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
_, allAddrs := app.GeneratePrivKeyAddressPairs(10)
suite.addrs = allAddrs[:5]
for _, a := range allAddrs[5:] {
suite.validatorAddrs = append(suite.validatorAddrs, sdk.ValAddress(a))
}
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(5)
authGS := app.NewAuthGenState(
[]sdk.AccAddress{addrs[3]},
[]sdk.Coins{
sdk.NewCoins(
sdk.NewCoin("bnb", sdk.NewInt(1000000000000000)),
sdk.NewCoin("ukava", sdk.NewInt(1000000000000000)),
sdk.NewCoin("btcb", sdk.NewInt(1000000000000000)),
sdk.NewCoin("xrp", sdk.NewInt(1000000000000000)),
),
},
)
tApp.InitializeFromGenesisStates( tApp.InitializeFromGenesisStates(
authGS, coinsAuthGenState(allAddrs, cs(c("ukava", 5_000_000))),
stakingGenesisState(),
NewPricefeedGenStateMulti(), NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(), NewCDPGenStateMulti(),
NewHardGenStateMulti(), NewHardGenStateMulti(),
) )
keeper := tApp.GetIncentiveKeeper()
hardKeeper := tApp.GetHardKeeper()
suite.app = tApp suite.app = tApp
suite.ctx = ctx suite.ctx = ctx
suite.keeper = keeper suite.keeper = tApp.GetIncentiveKeeper()
suite.hardKeeper = hardKeeper suite.hardKeeper = tApp.GetHardKeeper()
suite.addrs = addrs suite.stakingKeeper = tApp.GetStakingKeeper()
}
func coinsAuthGenState(addresses []sdk.AccAddress, coins sdk.Coins) app.GenesisState {
coinsList := []sdk.Coins{}
for range addresses {
coinsList = append(coinsList, coins)
}
// Load up our primary user address
if len(addresses) >= 4 {
coinsList[3] = sdk.NewCoins(
sdk.NewCoin("bnb", sdk.NewInt(1000000000000000)),
sdk.NewCoin("ukava", sdk.NewInt(1000000000000000)),
sdk.NewCoin("btcb", sdk.NewInt(1000000000000000)),
sdk.NewCoin("xrp", sdk.NewInt(1000000000000000)),
)
}
return app.NewAuthGenState(addresses, coinsList)
}
func stakingGenesisState() app.GenesisState {
genState := staking.DefaultGenesisState()
genState.Params.BondDenom = "ukava"
return app.GenesisState{
staking.ModuleName: staking.ModuleCdc.MustMarshalJSON(genState),
}
}
func (suite *KeeperTestSuite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error {
msg := staking.NewMsgCreateValidator(
address,
ed25519.GenPrivKey().PubKey(),
selfDelegation,
staking.Description{},
staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
sdk.NewInt(1_000_000),
)
handleStakingMsg := staking.NewHandler(suite.stakingKeeper)
_, err := handleStakingMsg(ctx, msg)
return err
}
func (suite *KeeperTestSuite) deliverMsgDelegate(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) error {
msg := staking.NewMsgDelegate(
delegator,
validator,
amount,
)
handleStakingMsg := staking.NewHandler(suite.stakingKeeper)
_, err := handleStakingMsg(ctx, msg)
return err
} }

View File

@ -11,6 +11,7 @@ import (
const ( const (
USDXMintingClaimType = "usdx_minting" USDXMintingClaimType = "usdx_minting"
HardLiquidityProviderClaimType = "hard_liquidity_provider" HardLiquidityProviderClaimType = "hard_liquidity_provider"
BondDenom = "ukava"
) )
// Claim is an interface for handling common claim actions // Claim is an interface for handling common claim actions
@ -125,12 +126,12 @@ type HardLiquidityProviderClaim struct {
BaseClaim `json:"base_claim" yaml:"base_claim"` BaseClaim `json:"base_claim" yaml:"base_claim"`
SupplyRewardIndexes RewardIndexes `json:"supply_reward_indexes" yaml:"supply_reward_indexes"` SupplyRewardIndexes RewardIndexes `json:"supply_reward_indexes" yaml:"supply_reward_indexes"`
BorrowRewardIndexes RewardIndexes `json:"borrow_reward_indexes" yaml:"borrow_reward_indexes"` BorrowRewardIndexes RewardIndexes `json:"borrow_reward_indexes" yaml:"borrow_reward_indexes"`
DelegationRewardIndexes RewardIndexes `json:"delegation_reward_indexes" yaml:"delegation_reward_indexes"` DelegatorRewardIndexes RewardIndexes `json:"delegator_reward_indexes" yaml:"delegator_reward_indexes"`
} }
// NewHardLiquidityProviderClaim returns a new HardLiquidityProviderClaim // NewHardLiquidityProviderClaim returns a new HardLiquidityProviderClaim
func NewHardLiquidityProviderClaim(owner sdk.AccAddress, reward sdk.Coin, supplyRewardIndexes, func NewHardLiquidityProviderClaim(owner sdk.AccAddress, reward sdk.Coin, supplyRewardIndexes,
borrowRewardIndexes, delegationRewardIndexes RewardIndexes) HardLiquidityProviderClaim { borrowRewardIndexes, delegatorRewardIndexes RewardIndexes) HardLiquidityProviderClaim {
return HardLiquidityProviderClaim{ return HardLiquidityProviderClaim{
BaseClaim: BaseClaim{ BaseClaim: BaseClaim{
Owner: owner, Owner: owner,
@ -138,7 +139,7 @@ func NewHardLiquidityProviderClaim(owner sdk.AccAddress, reward sdk.Coin, supply
}, },
SupplyRewardIndexes: supplyRewardIndexes, SupplyRewardIndexes: supplyRewardIndexes,
BorrowRewardIndexes: borrowRewardIndexes, BorrowRewardIndexes: borrowRewardIndexes,
DelegationRewardIndexes: delegationRewardIndexes, DelegatorRewardIndexes: delegatorRewardIndexes,
} }
} }
@ -155,7 +156,7 @@ func (c HardLiquidityProviderClaim) Validate() error {
return err return err
} }
if err := c.DelegationRewardIndexes.Validate(); err != nil { if err := c.DelegatorRewardIndexes.Validate(); err != nil {
return err return err
} }
@ -167,8 +168,8 @@ func (c HardLiquidityProviderClaim) String() string {
return fmt.Sprintf(`%s return fmt.Sprintf(`%s
Supply Reward Indexes: %s, Supply Reward Indexes: %s,
Borrow Reward Indexes: %s, Borrow Reward Indexes: %s,
Delegation Reward Indexes: %s, Delegator Reward Indexes: %s,
`, c.BaseClaim, c.SupplyRewardIndexes, c.BorrowRewardIndexes, c.DelegationRewardIndexes) `, c.BaseClaim, c.SupplyRewardIndexes, c.BorrowRewardIndexes, c.DelegatorRewardIndexes)
} }
// HasSupplyRewardIndex check if a claim has a supply reward index for the input collateral type // HasSupplyRewardIndex check if a claim has a supply reward index for the input collateral type
@ -191,9 +192,9 @@ func (c HardLiquidityProviderClaim) HasBorrowRewardIndex(denom string) (int64, b
return 0, false return 0, false
} }
// HasDelegationRewardIndex check if a claim has a delegation reward index for the input collateral type // HasDelegatorRewardIndex check if a claim has a delegator reward index for the input collateral type
func (c HardLiquidityProviderClaim) HasDelegationRewardIndex(collateralType string) (int64, bool) { func (c HardLiquidityProviderClaim) HasDelegatorRewardIndex(collateralType string) (int64, bool) {
for index, ri := range c.SupplyRewardIndexes { for index, ri := range c.DelegatorRewardIndexes {
if ri.CollateralType == collateralType { if ri.CollateralType == collateralType {
return int64(index), true return int64(index), true
} }

View File

@ -3,6 +3,7 @@ package types
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
cdptypes "github.com/kava-labs/kava/x/cdp/types" cdptypes "github.com/kava-labs/kava/x/cdp/types"
@ -15,6 +16,13 @@ type SupplyKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
} }
// StakingKeeper defines the expected staking keeper for module accounts
type StakingKeeper interface {
GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) (delegations []stakingtypes.Delegation)
GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool)
TotalBondedTokens(ctx sdk.Context) sdk.Int
}
// CdpKeeper defines the expected cdp keeper for interacting with cdps // CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface { type CdpKeeper interface {
GetInterestFactor(ctx sdk.Context, collateralType string) (sdk.Dec, bool) GetInterestFactor(ctx sdk.Context, collateralType string) (sdk.Dec, bool)