mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +00:00
Incentive refactor: hard rewards (#929)
* organise testing committee gen state * remove repeated test app initialization * minor fixes from linter in tests * move more setup to SetupApp * split up KeeperTestSuite for each reward type * simplify KeeperTestSuite * simplify PayoutKeeperSuite * simplify DelegatorRewardSuite * simplify SupplyRewardsSuite * simplify BorrowRewardsSuite * simplify USDXRewardsSuite * add auth genesis builder for easier test setup * migrate all incentive tests to auth builder * add incentive genesis builder for easier setup migrate hard incentive tests * migrate all tests to incentive builder * add hard genesis builder * small tidy ups * deduplicate initialTime from borrow tests * deduplicate initialtTime from supply tests * deduplicate initialTime from usdx and keeper tests * deduplicate initialTime in delgator tests * deduplicate genesis time in payout test * deduplicate test app initialization * make authGenesisBuilder available for all modules * remove unused pricefeed setup * export incentive genesis builder * remove commented out test cases * migrate cdp test to new test state builders * migrate vv payout tests to use new builders * add SynchronizeHardBorrowReward unit test * extract calculatReward method * tidy up unit test for borrow rewards * add helper method to RewardIndexes * user helper to extract logic from SyncBorrowReward * add Get methods to (Multi)RewardIndexes * replace params.Subspace in keeper to test easier * add unit tests for usdx minting * refactor InitializeUSDXMintingClaim * add unit tests for InitializeHardBorrowRewards * refactor SynchronizeUSDXMintingReward * add unit tests for UpdateHardBorrowIndexDenoms * change rewardSource type to Dec needed by delegation rewards * fix typo in test names * refactor UpdateHardBorrowIndexDenoms * update genesis test TODO to use auth builder * add skipped test for bug in usdx sync * extract common method for calculating rewards * doc comment tidy * add unit tests for delegator rewards * tidy up test files * remove old TODOs * reaarrange InitializeHardDelegatorReward to fit with other init reward functions * duplicate borrow unit tests to create supply tests * add tests for syncing with zero rewards per second * refactor SynchronizeHardDelegatorRewards * refactor supply rewards in same way as borrow * fix total delegation calculation bug * fix new usdx reward bug * fix new supply/borrow reward bug * remove working comment * standardize behaviour when global factors missing * improve documentation for CalculateRewards * standardize variable names * remove panic from calculateSingleReward * wip * Tidy up comments * remove wip comment
This commit is contained in:
parent
15598af3a3
commit
cf16029e77
@ -37,11 +37,10 @@ func (suite *GenesisTestSuite) SetupTest() {
|
|||||||
suite.genesisTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
suite.genesisTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
_, addrs := app.GeneratePrivKeyAddressPairs(3)
|
_, addrs := app.GeneratePrivKeyAddressPairs(3)
|
||||||
coins := []sdk.Coins{}
|
|
||||||
for j := 0; j < 3; j++ {
|
authBuilder := app.NewAuthGenesisBuilder().
|
||||||
coins = append(coins, cs(c("bnb", 10_000_000_000), c("ukava", 10_000_000_000)))
|
WithSimpleAccount(addrs[0], cs(c("bnb", 1e10), c("ukava", 1e10))).
|
||||||
}
|
WithSimpleModuleAccount(kavadist.KavaDistMacc, cs(c("hard", 1e15), c("ukava", 1e15)))
|
||||||
authGS := app.NewAuthGenState(addrs, coins)
|
|
||||||
|
|
||||||
loanToValue, _ := sdk.NewDecFromStr("0.6")
|
loanToValue, _ := sdk.NewDecFromStr("0.6")
|
||||||
borrowLimit := sdk.NewDec(1000000000000000)
|
borrowLimit := sdk.NewDec(1000000000000000)
|
||||||
@ -78,7 +77,7 @@ func (suite *GenesisTestSuite) SetupTest() {
|
|||||||
)
|
)
|
||||||
tApp.InitializeFromGenesisStatesWithTime(
|
tApp.InitializeFromGenesisStatesWithTime(
|
||||||
suite.genesisTime,
|
suite.genesisTime,
|
||||||
authGS,
|
authBuilder.BuildMarshalled(),
|
||||||
app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(incentiveGS)},
|
app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(incentiveGS)},
|
||||||
app.GenesisState{hard.ModuleName: hard.ModuleCdc.MustMarshalJSON(hardGS)},
|
app.GenesisState{hard.ModuleName: hard.ModuleCdc.MustMarshalJSON(hardGS)},
|
||||||
NewCDPGenStateMulti(),
|
NewCDPGenStateMulti(),
|
||||||
@ -87,14 +86,6 @@ func (suite *GenesisTestSuite) SetupTest() {
|
|||||||
|
|
||||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime})
|
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime})
|
||||||
|
|
||||||
// TODO add to auth gen state to tidy up test
|
|
||||||
err := tApp.GetSupplyKeeper().MintCoins(
|
|
||||||
ctx,
|
|
||||||
kavadist.KavaDistMacc,
|
|
||||||
cs(c("hard", 1_000_000_000_000_000), c("ukava", 1_000_000_000_000_000)),
|
|
||||||
)
|
|
||||||
suite.Require().NoError(err)
|
|
||||||
|
|
||||||
suite.addrs = addrs
|
suite.addrs = addrs
|
||||||
suite.keeper = keeper
|
suite.keeper = keeper
|
||||||
suite.app = tApp
|
suite.app = tApp
|
||||||
|
@ -81,6 +81,8 @@ When delegated tokens (to bonded validators) are changed:
|
|||||||
- jail: total bonded delegation decreases (tokens no longer bonded (after end blocker runs))
|
- jail: total bonded delegation decreases (tokens no longer bonded (after end blocker runs))
|
||||||
- validator becomes unbonded (ie when they drop out of the top 100)
|
- validator becomes unbonded (ie when they drop out of the top 100)
|
||||||
- total bonded delegation decreases (tokens no longer bonded)
|
- total bonded delegation decreases (tokens no longer bonded)
|
||||||
|
- validator becomes bonded (ie when they're promoted into the top 100)
|
||||||
|
- total bonded delegation increases (tokens become bonded)
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -262,9 +262,6 @@ func (builder IncentiveGenesisBuilder) WithSimpleBorrowRewardPeriod(ctype string
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
func (builder IncentiveGenesisBuilder) WithInitializedSupplyRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
|
func (builder IncentiveGenesisBuilder) WithInitializedSupplyRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder {
|
||||||
// TODO this could set the start/end times on the period according to builder.genesisTime
|
|
||||||
// Then they could be created by a different builder
|
|
||||||
|
|
||||||
builder.Params.HardSupplyRewardPeriods = append(builder.Params.HardSupplyRewardPeriods, period)
|
builder.Params.HardSupplyRewardPeriods = append(builder.Params.HardSupplyRewardPeriods, period)
|
||||||
|
|
||||||
accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime)
|
accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime)
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
|
||||||
|
|
||||||
"github.com/kava-labs/kava/x/incentive/types"
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
)
|
)
|
||||||
@ -18,24 +17,28 @@ type Keeper struct {
|
|||||||
cdpKeeper types.CdpKeeper
|
cdpKeeper types.CdpKeeper
|
||||||
hardKeeper types.HardKeeper
|
hardKeeper types.HardKeeper
|
||||||
key sdk.StoreKey
|
key sdk.StoreKey
|
||||||
paramSubspace subspace.Subspace
|
paramSubspace types.ParamSubspace
|
||||||
supplyKeeper types.SupplyKeeper
|
supplyKeeper types.SupplyKeeper
|
||||||
stakingKeeper types.StakingKeeper
|
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 types.ParamSubspace, sk types.SupplyKeeper,
|
||||||
cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper,
|
cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper,
|
||||||
) Keeper {
|
) Keeper {
|
||||||
|
|
||||||
|
if !paramstore.HasKeyTable() {
|
||||||
|
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
|
||||||
|
}
|
||||||
|
|
||||||
return Keeper{
|
return Keeper{
|
||||||
accountKeeper: ak,
|
accountKeeper: ak,
|
||||||
cdc: cdc,
|
cdc: cdc,
|
||||||
cdpKeeper: cdpk,
|
cdpKeeper: cdpk,
|
||||||
hardKeeper: hk,
|
hardKeeper: hk,
|
||||||
key: key,
|
key: key,
|
||||||
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
|
paramSubspace: paramstore,
|
||||||
supplyKeeper: sk,
|
supplyKeeper: sk,
|
||||||
stakingKeeper: stk,
|
stakingKeeper: stk,
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
"github.com/kava-labs/kava/x/incentive/types"
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
@ -85,14 +86,11 @@ func (k Keeper) InitializeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bor
|
|||||||
|
|
||||||
var borrowRewardIndexes types.MultiRewardIndexes
|
var borrowRewardIndexes types.MultiRewardIndexes
|
||||||
for _, coin := range borrow.Amount {
|
for _, coin := range borrow.Amount {
|
||||||
globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
|
globalRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
|
||||||
var multiRewardIndex types.MultiRewardIndex
|
if !found {
|
||||||
if foundGlobalRewardIndexes {
|
globalRewardIndexes = types.RewardIndexes{}
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes)
|
|
||||||
} else {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{})
|
|
||||||
}
|
}
|
||||||
borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex)
|
borrowRewardIndexes = borrowRewardIndexes.With(coin.Denom, globalRewardIndexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
claim.BorrowRewardIndexes = borrowRewardIndexes
|
claim.BorrowRewardIndexes = borrowRewardIndexes
|
||||||
@ -108,49 +106,34 @@ func (k Keeper) SynchronizeHardBorrowReward(ctx sdk.Context, borrow hardtypes.Bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, coin := range borrow.Amount {
|
for _, coin := range borrow.Amount {
|
||||||
globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
|
globalRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, coin.Denom)
|
||||||
if !foundGlobalRewardIndexes {
|
if !found {
|
||||||
|
// The global factor is only not found if
|
||||||
|
// - the borrowed denom has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit)
|
||||||
|
// - OR it was wrongly deleted from state (factors should never be removed while unsynced claims exist)
|
||||||
|
// If not found we could either skip this sync, or assume the global factor is zero.
|
||||||
|
// Skipping will avoid storing unnecessary factors in the claim for non rewarded denoms.
|
||||||
|
// And in the event a global factor is wrongly deleted, it will avoid this function panicking when calculating rewards.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
userMultiRewardIndex, foundUserMultiRewardIndex := claim.BorrowRewardIndexes.GetRewardIndex(coin.Denom)
|
userRewardIndexes, found := claim.BorrowRewardIndexes.Get(coin.Denom)
|
||||||
if !foundUserMultiRewardIndex {
|
if !found {
|
||||||
continue
|
// Normally the reward indexes should always be found.
|
||||||
|
// But if a denom was not rewarded then becomes rewarded (ie a reward period is added to params), then the indexes will be missing from claims for that borrowed denom.
|
||||||
|
// So given the reward period was just added, assume the starting value for any global reward indexes, which is an empty slice.
|
||||||
|
userRewardIndexes = types.RewardIndexes{}
|
||||||
}
|
}
|
||||||
|
|
||||||
userRewardIndexIndex, foundUserRewardIndexIndex := claim.BorrowRewardIndexes.GetRewardIndexIndex(coin.Denom)
|
newRewards, err := k.CalculateRewards(userRewardIndexes, globalRewardIndexes, coin.Amount.ToDec())
|
||||||
if !foundUserRewardIndexIndex {
|
if err != nil {
|
||||||
continue
|
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
|
||||||
|
// This panics if a global reward factor decreases or disappears between the old and new indexes.
|
||||||
|
panic(fmt.Sprintf("corrupted global reward indexes found: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, globalRewardIndex := range globalRewardIndexes {
|
claim.Reward = claim.Reward.Add(newRewards...)
|
||||||
userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType)
|
claim.BorrowRewardIndexes = claim.BorrowRewardIndexes.With(coin.Denom, globalRewardIndexes)
|
||||||
if !foundUserRewardIndex {
|
|
||||||
// User borrowed this coin type before it had rewards. When new rewards are added, legacy borrowers
|
|
||||||
// should immediately begin earning rewards. Enable users to do so by updating their claim with the global
|
|
||||||
// reward index denom and start their reward factor at 0.0
|
|
||||||
userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec())
|
|
||||||
userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex)
|
|
||||||
claim.BorrowRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
globalRewardFactor := globalRewardIndex.RewardFactor
|
|
||||||
userRewardFactor := userRewardIndex.RewardFactor
|
|
||||||
rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor)
|
|
||||||
if rewardsAccumulatedFactor.IsNegative() {
|
|
||||||
panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor))
|
|
||||||
}
|
|
||||||
|
|
||||||
newRewardsAmount := rewardsAccumulatedFactor.Mul(borrow.Amount.AmountOf(coin.Denom).ToDec()).RoundInt()
|
|
||||||
|
|
||||||
factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType)
|
|
||||||
if !foundFactorIndex { // should never trigger
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
claim.BorrowRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor
|
|
||||||
newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount)
|
|
||||||
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
@ -165,26 +148,22 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo
|
|||||||
borrowDenoms := getDenoms(borrow.Amount)
|
borrowDenoms := getDenoms(borrow.Amount)
|
||||||
borrowRewardIndexDenoms := claim.BorrowRewardIndexes.GetCollateralTypes()
|
borrowRewardIndexDenoms := claim.BorrowRewardIndexes.GetCollateralTypes()
|
||||||
|
|
||||||
uniqueBorrowDenoms := setDifference(borrowDenoms, borrowRewardIndexDenoms)
|
|
||||||
uniqueBorrowRewardDenoms := setDifference(borrowRewardIndexDenoms, borrowDenoms)
|
|
||||||
|
|
||||||
borrowRewardIndexes := claim.BorrowRewardIndexes
|
borrowRewardIndexes := claim.BorrowRewardIndexes
|
||||||
|
|
||||||
// Create a new multi-reward index in the claim for every new borrow denom
|
// Create a new multi-reward index in the claim for every new borrow denom
|
||||||
|
uniqueBorrowDenoms := setDifference(borrowDenoms, borrowRewardIndexDenoms)
|
||||||
|
|
||||||
for _, denom := range uniqueBorrowDenoms {
|
for _, denom := range uniqueBorrowDenoms {
|
||||||
_, foundUserRewardIndexes := claim.BorrowRewardIndexes.GetRewardIndex(denom)
|
globalBorrowRewardIndexes, found := k.GetHardBorrowRewardIndexes(ctx, denom)
|
||||||
if !foundUserRewardIndexes {
|
if !found {
|
||||||
globalBorrowRewardIndexes, foundGlobalBorrowRewardIndexes := k.GetHardBorrowRewardIndexes(ctx, denom)
|
globalBorrowRewardIndexes = types.RewardIndexes{}
|
||||||
var multiRewardIndex types.MultiRewardIndex
|
|
||||||
if foundGlobalBorrowRewardIndexes {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(denom, globalBorrowRewardIndexes)
|
|
||||||
} else {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{})
|
|
||||||
}
|
|
||||||
borrowRewardIndexes = append(borrowRewardIndexes, multiRewardIndex)
|
|
||||||
}
|
}
|
||||||
|
borrowRewardIndexes = borrowRewardIndexes.With(denom, globalBorrowRewardIndexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete multi-reward index from claim if the collateral type is no longer borrowed
|
// Delete multi-reward index from claim if the collateral type is no longer borrowed
|
||||||
|
uniqueBorrowRewardDenoms := setDifference(borrowRewardIndexDenoms, borrowDenoms)
|
||||||
|
|
||||||
for _, denom := range uniqueBorrowRewardDenoms {
|
for _, denom := range uniqueBorrowRewardDenoms {
|
||||||
borrowRewardIndexes = borrowRewardIndexes.RemoveRewardIndex(denom)
|
borrowRewardIndexes = borrowRewardIndexes.RemoveRewardIndex(denom)
|
||||||
}
|
}
|
||||||
@ -192,3 +171,52 @@ func (k Keeper) UpdateHardBorrowIndexDenoms(ctx sdk.Context, borrow hardtypes.Bo
|
|||||||
claim.BorrowRewardIndexes = borrowRewardIndexes
|
claim.BorrowRewardIndexes = borrowRewardIndexes
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CalculateRewards computes how much rewards should have accrued to a 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.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
// check for missing CollateralType's
|
||||||
|
for _, oldIndex := range oldIndexes {
|
||||||
|
if newIndex, found := newIndexes.Get(oldIndex.CollateralType); !found {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrDecreasingRewardFactor, "old: %v, new: %v", oldIndex, newIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var reward sdk.Coins
|
||||||
|
for _, newIndex := range newIndexes {
|
||||||
|
oldFactor, found := oldIndexes.Get(newIndex.CollateralType)
|
||||||
|
if !found {
|
||||||
|
oldFactor = sdk.ZeroDec()
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardAmount, err := k.CalculateSingleReward(oldFactor, newIndex.RewardFactor, rewardSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reward = reward.Add(
|
||||||
|
sdk.NewCoin(newIndex.CollateralType, rewardAmount),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return reward, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateSingleReward computes how much rewards should have accrued to a 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.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
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()
|
||||||
|
return reward, nil
|
||||||
|
}
|
||||||
|
89
x/incentive/keeper/rewards_borrow_init_test.go
Normal file
89
x/incentive/keeper/rewards_borrow_init_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeHardBorrowRewardTests runs unit tests for the keeper.InitializeHardBorrowReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.BorrowRewardIndexes)
|
||||||
|
// - global indexes in store
|
||||||
|
// - borrow function arg (only borrow.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets or creates a claim
|
||||||
|
type InitializeHardBorrowRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeHardBorrowReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(InitializeHardBorrowRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeHardBorrowRewardTests) TestClaimIndexesAreSetWhenClaimExists() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
// Indexes should always be empty when initialize is called.
|
||||||
|
// If initialize is called then the user must have repaid their borrow positions,
|
||||||
|
// which means UpdateHardBorrowIndexDenoms was called and should have remove indexes.
|
||||||
|
BorrowRewardIndexes: types.MultiRewardIndexes{},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *InitializeHardBorrowRewardTests) TestClaimIndexesAreSetWhenClaimDoesNotExist() {
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
owner := arbitraryAddress()
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, owner)
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *InitializeHardBorrowRewardTests) TestClaimIndexesAreSetEmptyForMissingIndexes() {
|
||||||
|
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
owner := arbitraryAddress()
|
||||||
|
// Borrow a denom that is not in the global indexes.
|
||||||
|
// This happens when a borrow denom has no rewards associated with it.
|
||||||
|
expectedIndexes := appendUniqueEmptyMultiRewardIndex(globalIndexes)
|
||||||
|
borrowedDenoms := extractCollateralTypes(expectedIndexes)
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(borrowedDenoms...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
528
x/incentive/keeper/rewards_borrow_sync_test.go
Normal file
528
x/incentive/keeper/rewards_borrow_sync_test.go
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SynchronizeHardBorrowRewardTests runs unit tests for the keeper.SynchronizeHardBorrowReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store (only claim.BorrowRewardIndexes, claim.Reward)
|
||||||
|
// - global indexes in store
|
||||||
|
// - borrow function arg (only borrow.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets a claim
|
||||||
|
type SynchronizeHardBorrowRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeHardBorrowReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(SynchronizeHardBorrowRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenGlobalIndexesHaveIncreased() {
|
||||||
|
// This is the normal case
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := increaseAllRewardFactors(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(claim.BorrowRewardIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUnchangedWhenGlobalIndexesUnchanged() {
|
||||||
|
// It should be safe to call SynchronizeHardBorrowReward multiple times
|
||||||
|
|
||||||
|
unchangingIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: unchangingIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalBorrowIndexes(unchangingIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(unchangingIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(unchangingIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardAdded() {
|
||||||
|
// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced;
|
||||||
|
// Then the new reward's index should be added to the claim.
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := appendUniqueMultiRewardIndex(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardDenomAdded() {
|
||||||
|
// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
|
||||||
|
// Then the new reward coin's index should be added to the claim.
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := appendUniqueRewardIndexToFirstItem(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenGlobalIndexesHaveIncreased() {
|
||||||
|
// This is the normal case
|
||||||
|
// Given some time has passed (meaning the global indexes have increased)
|
||||||
|
// When the claim is synced
|
||||||
|
// The user earns rewards for the time passed
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "borrowdenom",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarddenom",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalBorrowIndexes(types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "borrowdenom",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarddenom",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: cs(c("borrowdenom", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * borrow amount
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("rewarddenom", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardAdded() {
|
||||||
|
// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced
|
||||||
|
// Then the user earns rewards for the time since the reward was added
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "newlyrewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "otherreward",
|
||||||
|
// Indexes start at 0 when the reward is added by gov,
|
||||||
|
// so this represents the syncing happening some time later.
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: cs(c("rewarded", 1e9), c("newlyrewarded", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * borrow amount for each borrowed denom
|
||||||
|
// The old index for `newlyrewarded` isn't in the claim, so it's added starting at 0 for calculating the reward.
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("otherreward", 1_000_001_000_000), c("reward", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardDenomAdded() {
|
||||||
|
// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
|
||||||
|
// Then the user earns rewards for the time since the reward was added
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "borrowed",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "borrowed",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "otherreward",
|
||||||
|
// Indexes start at 0 when the reward is added by gov,
|
||||||
|
// so this represents the syncing happening some time later.
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: cs(c("borrowed", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * borrow amount for each borrowed denom
|
||||||
|
// The old index for `otherreward` isn't in the claim, so it's added starting at 0 for calculating the reward.
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("reward", 1_000_001_000_000), c("otherreward", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateRewards(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
err error
|
||||||
|
coins sdk.Coins
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
oldIndexes, newIndexes types.RewardIndexes
|
||||||
|
sourceAmount sdk.Dec
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when old and new indexes have same denoms, rewards are calculated correctly",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.000000001"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("1000.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.100000001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
// for each denom: (new - old) * sourceAmount
|
||||||
|
coins: cs(c("hard", 999999999999), c("ukava", 1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when new indexes have an extra denom, rewards are calculated as if it was 0 in old indexes",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.000000001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("1000.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.100000001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
// for each denom: (new - old) * sourceAmount
|
||||||
|
coins: cs(c("hard", 999999999999), c("ukava", 100000001)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when new indexes are smaller than old, an error is returned",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
err: types.ErrDecreasingRewardFactor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when old indexes have an extra denom, an error is returned",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
err: types.ErrDecreasingRewardFactor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when old and new indexes are 0, rewards are 0",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
coins: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when old and new indexes are empty, rewards are 0",
|
||||||
|
args: args{
|
||||||
|
oldIndexes: types.RewardIndexes{},
|
||||||
|
newIndexes: nil,
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
coins: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
coins, err := keeper.Keeper{}.CalculateRewards(tc.args.oldIndexes, tc.args.newIndexes, tc.args.sourceAmount)
|
||||||
|
if tc.expected.err != nil {
|
||||||
|
require.True(t, errors.Is(err, tc.expected.err))
|
||||||
|
} else {
|
||||||
|
require.Equal(t, tc.expected.coins, coins)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestCalculateSingleReward(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
err error
|
||||||
|
reward sdk.Int
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
oldIndex, newIndex sdk.Dec
|
||||||
|
sourceAmount sdk.Dec
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when new index is > old, rewards are calculated correctly",
|
||||||
|
args: args{
|
||||||
|
oldIndex: d("0.000000001"),
|
||||||
|
newIndex: d("1000.0"),
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
// (new - old) * sourceAmount
|
||||||
|
reward: i(999999999999),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when new index is < old, an error is returned",
|
||||||
|
args: args{
|
||||||
|
oldIndex: d("0.000000001"),
|
||||||
|
newIndex: d("0.0"),
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
err: types.ErrDecreasingRewardFactor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when old and new indexes are 0, rewards are 0",
|
||||||
|
args: args{
|
||||||
|
oldIndex: d("0.0"),
|
||||||
|
newIndex: d("0.0"),
|
||||||
|
sourceAmount: d("1000000000"),
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
reward: sdk.ZeroInt(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
reward, err := keeper.Keeper{}.CalculateSingleReward(tc.args.oldIndex, tc.args.newIndex, tc.args.sourceAmount)
|
||||||
|
if tc.expected.err != nil {
|
||||||
|
require.True(t, errors.Is(err, tc.expected.err))
|
||||||
|
} else {
|
||||||
|
require.Equal(t, tc.expected.reward, reward)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -61,7 +61,7 @@ func (suite *BorrowRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenes
|
|||||||
authBuilder.BuildMarshalled(),
|
authBuilder.BuildMarshalled(),
|
||||||
NewPricefeedGenStateMultiFromTime(suite.genesisTime),
|
NewPricefeedGenStateMultiFromTime(suite.genesisTime),
|
||||||
hardBuilder.BuildMarshalled(),
|
hardBuilder.BuildMarshalled(),
|
||||||
NewCommitteeGenesisState(suite.addrs[:2]), // TODO add committee members to suite,
|
NewCommitteeGenesisState(suite.addrs[:2]),
|
||||||
incentBuilder.BuildMarshalled(),
|
incentBuilder.BuildMarshalled(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -498,7 +498,6 @@ func (suite *BorrowRewardsTestSuite) TestSynchronizeHardBorrowReward() {
|
|||||||
updatedTimeDuration: 86400,
|
updatedTimeDuration: 86400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO test synchronize when there is a reward period with 0 rewardsPerSecond
|
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
suite.Run(tc.name, func() {
|
suite.Run(tc.name, func() {
|
||||||
|
119
x/incentive/keeper/rewards_borrow_update_test.go
Normal file
119
x/incentive/keeper/rewards_borrow_update_test.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateHardBorrowIndexDenomsTests runs unit tests for the keeper.UpdateHardBorrowIndexDenoms method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.BorrowRewardIndexes)
|
||||||
|
// - global indexes in store
|
||||||
|
// - borrow function arg (only borrow.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets a claim
|
||||||
|
type UpdateHardBorrowIndexDenomsTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateHardBorrowIndexDenoms(t *testing.T) {
|
||||||
|
suite.Run(t, new(UpdateHardBorrowIndexDenomsTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardBorrowIndexDenomsTests) TestClaimIndexesAreRemovedForDenomsNoLongerBorrowed() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
suite.storeGlobalBorrowIndexes(claim.BorrowRewardIndexes)
|
||||||
|
|
||||||
|
// remove one denom from the indexes already in the borrow
|
||||||
|
expectedIndexes := claim.BorrowRewardIndexes[1:]
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(expectedIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardBorrowIndexDenoms(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardBorrowIndexDenomsTests) TestClaimIndexesAreAddedForNewlyBorrowedDenoms() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
globalIndexes := appendUniqueMultiRewardIndex(claim.BorrowRewardIndexes)
|
||||||
|
suite.storeGlobalBorrowIndexes(globalIndexes)
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardBorrowIndexDenoms(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardBorrowIndexDenomsTests) TestClaimIndexesAreUnchangedWhenBorrowedDenomsUnchanged() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
// Set global indexes with same denoms but different values.
|
||||||
|
// UpdateHardBorrowIndexDenoms should ignore the new values.
|
||||||
|
suite.storeGlobalBorrowIndexes(increaseAllRewardFactors(claim.BorrowRewardIndexes))
|
||||||
|
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(claim.BorrowRewardIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardBorrowIndexDenoms(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(claim.BorrowRewardIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardBorrowIndexDenomsTests) TestEmptyClaimIndexesAreAddedForNewlyBorrowedButNotRewardedDenoms() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
suite.storeGlobalBorrowIndexes(claim.BorrowRewardIndexes)
|
||||||
|
|
||||||
|
// add a denom to the borrowed amount that is not in the global or claim's indexes
|
||||||
|
expectedIndexes := appendUniqueEmptyMultiRewardIndex(claim.BorrowRewardIndexes)
|
||||||
|
borrowedDenoms := extractCollateralTypes(expectedIndexes)
|
||||||
|
borrow := hardtypes.Borrow{
|
||||||
|
Borrower: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(borrowedDenoms...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardBorrowIndexDenoms(suite.ctx, borrow)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.BorrowRewardIndexes)
|
||||||
|
}
|
@ -45,23 +45,21 @@ func (k Keeper) AccumulateHardDelegatorRewards(ctx sdk.Context, rewardPeriod typ
|
|||||||
|
|
||||||
// InitializeHardDelegatorReward initializes the delegator reward index of a hard claim
|
// InitializeHardDelegatorReward initializes the delegator reward index of a hard claim
|
||||||
func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.AccAddress) {
|
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)
|
claim, found := k.GetHardLiquidityProviderClaim(ctx, delegator)
|
||||||
if !found {
|
if !found {
|
||||||
// Instantiate claim object
|
|
||||||
claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil)
|
claim = types.NewHardLiquidityProviderClaim(delegator, sdk.Coins{}, nil, nil, nil)
|
||||||
} else {
|
} else {
|
||||||
k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false)
|
k.SynchronizeHardDelegatorRewards(ctx, delegator, nil, false)
|
||||||
claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator)
|
claim, _ = k.GetHardLiquidityProviderClaim(ctx, delegator)
|
||||||
}
|
}
|
||||||
|
|
||||||
claim.DelegatorRewardIndexes = types.RewardIndexes{delegatorRewardIndexes}
|
globalRewardFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom)
|
||||||
|
if !found {
|
||||||
|
// If there's no global delegator reward factor, initialize the claim with a delegator factor of zero.
|
||||||
|
globalRewardFactor = sdk.ZeroDec()
|
||||||
|
}
|
||||||
|
|
||||||
|
claim.DelegatorRewardIndexes = types.RewardIndexes{types.NewRewardIndex(types.BondDenom, globalRewardFactor)}
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,23 +72,42 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delagatorFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom)
|
globalRewardFactor, found := k.GetHardDelegatorRewardFactor(ctx, types.BondDenom)
|
||||||
if !found {
|
if !found {
|
||||||
|
// The global factor is only not found if
|
||||||
|
// - the bond denom has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit)
|
||||||
|
// - OR it was wrongly deleted from state (factors should never be removed while unsynced claims exist)
|
||||||
|
// If not found we could either skip this sync, or assume the global factor is zero.
|
||||||
|
// Skipping will avoid storing unnecessary factors in the claim for non rewarded denoms.
|
||||||
|
// And in the event a global factor is wrongly deleted, it will avoid this function panicking when calculating rewards.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delegatorIndex, hasDelegatorRewardIndex := claim.HasDelegatorRewardIndex(types.BondDenom)
|
userRewardFactor, found := claim.DelegatorRewardIndexes.Get(types.BondDenom)
|
||||||
if !hasDelegatorRewardIndex {
|
if !found {
|
||||||
return
|
// Normally the factor should always be found, as it is added in InitializeHardDelegatorReward when a user delegates.
|
||||||
|
// However if there were no delegator rewards (ie no reward period in params) then a reward period is added, existing claims will not have the factor.
|
||||||
|
// So assume the factor is the starting value for any global factor: 0.
|
||||||
|
userRewardFactor = sdk.ZeroDec()
|
||||||
}
|
}
|
||||||
|
|
||||||
userRewardFactor := claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor
|
totalDelegated := k.GetTotalDelegated(ctx, delegator, valAddr, shouldIncludeValidator)
|
||||||
rewardsAccumulatedFactor := delagatorFactor.Sub(userRewardFactor)
|
|
||||||
if rewardsAccumulatedFactor.IsNegative() {
|
|
||||||
panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor))
|
|
||||||
}
|
|
||||||
claim.DelegatorRewardIndexes[delegatorIndex].RewardFactor = delagatorFactor
|
|
||||||
|
|
||||||
|
rewardsEarned, err := k.CalculateSingleReward(userRewardFactor, globalRewardFactor, totalDelegated)
|
||||||
|
if err != nil {
|
||||||
|
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
|
||||||
|
// This panics if a global reward factor decreases or disappears between the old and new indexes.
|
||||||
|
panic(fmt.Sprintf("corrupted global reward indexes found: %v", err))
|
||||||
|
}
|
||||||
|
newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned)
|
||||||
|
|
||||||
|
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
||||||
|
claim.DelegatorRewardIndexes = claim.DelegatorRewardIndexes.With(types.BondDenom, globalRewardFactor)
|
||||||
|
|
||||||
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) GetTotalDelegated(ctx sdk.Context, delegator sdk.AccAddress, valAddr sdk.ValAddress, shouldIncludeValidator bool) sdk.Dec {
|
||||||
totalDelegated := sdk.ZeroDec()
|
totalDelegated := sdk.ZeroDec()
|
||||||
|
|
||||||
delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delegator, 200)
|
delegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delegator, 200)
|
||||||
@ -100,14 +117,16 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if valAddr == nil {
|
if validator.OperatorAddress.Equals(valAddr) {
|
||||||
// Delegators don't accumulate rewards if their validator is unbonded
|
if shouldIncludeValidator {
|
||||||
if validator.GetStatus() != sdk.Bonded {
|
// do nothing, so the validator is included regardless of bonded status
|
||||||
|
} else {
|
||||||
|
// skip this validator
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !shouldIncludeValidator && validator.OperatorAddress.Equals(valAddr) {
|
// skip any not bonded validator
|
||||||
// ignore tokens delegated to the validator
|
if validator.GetStatus() != sdk.Bonded {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,10 +141,5 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A
|
|||||||
}
|
}
|
||||||
totalDelegated = totalDelegated.Add(delegatedTokens)
|
totalDelegated = totalDelegated.Add(delegatedTokens)
|
||||||
}
|
}
|
||||||
rewardsEarned := rewardsAccumulatedFactor.Mul(totalDelegated).RoundInt()
|
return totalDelegated
|
||||||
|
|
||||||
// Add rewards to delegator's hard claim
|
|
||||||
newRewardsCoin := sdk.NewCoin(types.HardLiquidityRewardDenom, rewardsEarned)
|
|
||||||
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
|
||||||
}
|
}
|
||||||
|
115
x/incentive/keeper/rewards_delegator_init_test.go
Normal file
115
x/incentive/keeper/rewards_delegator_init_test.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeHardDelegatorRewardTests runs unit tests for the keeper.InitializeHardDelegatorReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.DelegatorRewardIndexes)
|
||||||
|
// - global indexes in store
|
||||||
|
// - delegator function arg
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets or creates a claim
|
||||||
|
type InitializeHardDelegatorRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeHardDelegatorReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(InitializeHardDelegatorRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeHardDelegatorRewardTests) storeGlobalDelegatorFactor(rewardIndexes types.RewardIndexes) {
|
||||||
|
factor := rewardIndexes[0]
|
||||||
|
suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, factor.CollateralType, factor.RewardFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeHardDelegatorRewardTests) TestClaimIndexesAreSetWhenClaimDoesNotExist() {
|
||||||
|
globalIndex := arbitraryDelegatorRewardIndexes
|
||||||
|
suite.storeGlobalDelegatorFactor(globalIndex)
|
||||||
|
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
suite.keeper.InitializeHardDelegatorReward(suite.ctx, delegator)
|
||||||
|
|
||||||
|
syncedClaim, f := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, delegator)
|
||||||
|
suite.True(f)
|
||||||
|
suite.Equal(globalIndex, syncedClaim.DelegatorRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeHardDelegatorRewardTests) TestClaimIsSyncedAndIndexesAreSetWhenClaimDoesExist() {
|
||||||
|
validatorAddress := arbitraryValidatorAddress()
|
||||||
|
sk := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{{
|
||||||
|
ValidatorAddress: validatorAddress,
|
||||||
|
Shares: d("1000"),
|
||||||
|
}},
|
||||||
|
validators: stakingtypes.Validators{{
|
||||||
|
OperatorAddress: validatorAddress,
|
||||||
|
Status: sdk.Bonded,
|
||||||
|
Tokens: i(1000),
|
||||||
|
DelegatorShares: d("1000"),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, sk)
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: arbitraryDelegatorRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
// Set the global factor to a value different to one in claim so
|
||||||
|
// we can detect if it is overwritten.
|
||||||
|
globalIndex := increaseRewardFactors(claim.DelegatorRewardIndexes)
|
||||||
|
suite.storeGlobalDelegatorFactor(globalIndex)
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardDelegatorReward(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndex, syncedClaim.DelegatorRewardIndexes)
|
||||||
|
suite.Truef(syncedClaim.Reward.IsAllGT(claim.Reward), "'%s' not greater than '%s'", syncedClaim.Reward, claim.Reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
// arbitraryDelegatorRewardIndexes contains only one reward index as there is only every one bond denom
|
||||||
|
var arbitraryDelegatorRewardIndexes = types.RewardIndexes{
|
||||||
|
types.NewRewardIndex(types.BondDenom, d("0.2")),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
356
x/incentive/keeper/rewards_delegator_sync_test.go
Normal file
356
x/incentive/keeper/rewards_delegator_sync_test.go
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SynchronizeHardDelegatorRewardTests runs unit tests for the keeper.SynchronizeHardDelegatorReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.DelegatorRewardIndexes and claim.Reward)
|
||||||
|
// - global index in store
|
||||||
|
// - function args: delegator address, validator address, shouldIncludeValidator flag
|
||||||
|
// - delegator's delegations and the corresponding validators
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets or creates a claim
|
||||||
|
type SynchronizeHardDelegatorRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeHardDelegatorReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(SynchronizeHardDelegatorRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) storeGlobalDelegatorFactor(rewardIndexes types.RewardIndexes) {
|
||||||
|
factor := rewardIndexes[0]
|
||||||
|
suite.keeper.SetHardDelegatorRewardFactor(suite.ctx, factor.CollateralType, factor.RewardFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUnchangedWhenGlobalFactorUnchanged() {
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
|
||||||
|
stakingKeeper := fakeStakingKeeper{} // use an empty staking keeper that returns no delegations
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: delegator,
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: arbitraryDelegatorRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes)
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(claim.DelegatorRewardIndexes, syncedClaim.DelegatorRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUpdatedWhenGlobalFactorIncreased() {
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, fakeStakingKeeper{})
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: delegator,
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: arbitraryDelegatorRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := increaseRewardFactors(claim.DelegatorRewardIndexes)
|
||||||
|
suite.storeGlobalDelegatorFactor(globalIndexes)
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.DelegatorRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsUnchangedWhenGlobalFactorUnchanged() {
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddress := arbitraryValidatorAddress()
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddress,
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddress),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: delegator,
|
||||||
|
Reward: arbitraryCoins(),
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: types.RewardIndexes{{
|
||||||
|
CollateralType: types.BondDenom,
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes)
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
suite.Equal(claim.Reward, syncedClaim.Reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenNewRewardAdded() {
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddress := arbitraryValidatorAddress()
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddress,
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddress),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: delegator,
|
||||||
|
Reward: arbitraryCoins(),
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: types.RewardIndexes{},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
newGlobalIndexes := types.RewardIndexes{{
|
||||||
|
CollateralType: types.BondDenom,
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
}}
|
||||||
|
suite.storeGlobalDelegatorFactor(newGlobalIndexes)
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
suite.Equal(newGlobalIndexes, syncedClaim.DelegatorRewardIndexes)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c(types.HardLiquidityRewardDenom, 100)).Add(claim.Reward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenGlobalFactorIncreased() {
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddress := arbitraryValidatorAddress()
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddress,
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddress),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: delegator,
|
||||||
|
Reward: arbitraryCoins(),
|
||||||
|
},
|
||||||
|
DelegatorRewardIndexes: types.RewardIndexes{{
|
||||||
|
CollateralType: types.BondDenom,
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalDelegatorFactor(
|
||||||
|
types.RewardIndexes{
|
||||||
|
types.NewRewardIndex(types.BondDenom, d("0.2")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
suite.Equal(
|
||||||
|
cs(c(types.HardLiquidityRewardDenom, 100)).Add(claim.Reward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unslashedBondedValidator(address sdk.ValAddress) stakingtypes.Validator {
|
||||||
|
return stakingtypes.Validator{
|
||||||
|
OperatorAddress: address,
|
||||||
|
Status: sdk.Bonded,
|
||||||
|
|
||||||
|
// Set the tokens and shares equal so then
|
||||||
|
// a _delegator's_ token amount is equal to their shares amount
|
||||||
|
Tokens: i(1e12),
|
||||||
|
DelegatorShares: i(1e12).ToDec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func unslashedNotBondedValidator(address sdk.ValAddress) stakingtypes.Validator {
|
||||||
|
return stakingtypes.Validator{
|
||||||
|
OperatorAddress: address,
|
||||||
|
Status: sdk.Unbonding,
|
||||||
|
|
||||||
|
// Set the tokens and shares equal so then
|
||||||
|
// a _delegator's_ token amount is equal to their shares amount
|
||||||
|
Tokens: i(1e12),
|
||||||
|
DelegatorShares: i(1e12).ToDec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenValAddrIsNil() {
|
||||||
|
// when valAddr is nil, get total delegated to bonded validators
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddresses := generateValidatorAddresses(4)
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
//bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[0],
|
||||||
|
Shares: d("1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[1],
|
||||||
|
Shares: d("10"),
|
||||||
|
},
|
||||||
|
// not bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[2],
|
||||||
|
Shares: d("100"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[3],
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddresses[0]),
|
||||||
|
unslashedBondedValidator(validatorAddresses[1]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[2]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[3]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
suite.Equal(
|
||||||
|
d("11"), // delegation to bonded validators
|
||||||
|
suite.keeper.GetTotalDelegated(suite.ctx, delegator, nil, false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenExcludingAValidator() {
|
||||||
|
// when valAddr is x, get total delegated to bonded validators excluding those to x
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddresses := generateValidatorAddresses(4)
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
//bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[0],
|
||||||
|
Shares: d("1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[1],
|
||||||
|
Shares: d("10"),
|
||||||
|
},
|
||||||
|
// not bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[2],
|
||||||
|
Shares: d("100"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[3],
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddresses[0]),
|
||||||
|
unslashedBondedValidator(validatorAddresses[1]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[2]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[3]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
suite.Equal(
|
||||||
|
d("10"),
|
||||||
|
suite.keeper.GetTotalDelegated(suite.ctx, delegator, validatorAddresses[0], false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenIncludingAValidator() {
|
||||||
|
// when valAddr is x, get total delegated to bonded validators including those to x
|
||||||
|
delegator := arbitraryAddress()
|
||||||
|
validatorAddresses := generateValidatorAddresses(4)
|
||||||
|
stakingKeeper := fakeStakingKeeper{
|
||||||
|
delegations: stakingtypes.Delegations{
|
||||||
|
//bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[0],
|
||||||
|
Shares: d("1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[1],
|
||||||
|
Shares: d("10"),
|
||||||
|
},
|
||||||
|
// not bonded
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[2],
|
||||||
|
Shares: d("100"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DelegatorAddress: delegator,
|
||||||
|
ValidatorAddress: validatorAddresses[3],
|
||||||
|
Shares: d("1000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validators: stakingtypes.Validators{
|
||||||
|
unslashedBondedValidator(validatorAddresses[0]),
|
||||||
|
unslashedBondedValidator(validatorAddresses[1]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[2]),
|
||||||
|
unslashedNotBondedValidator(validatorAddresses[3]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper)
|
||||||
|
|
||||||
|
suite.Equal(
|
||||||
|
d("111"),
|
||||||
|
suite.keeper.GetTotalDelegated(suite.ctx, delegator, validatorAddresses[2], true),
|
||||||
|
)
|
||||||
|
}
|
@ -80,24 +80,20 @@ func (k Keeper) AccumulateHardSupplyRewards(ctx sdk.Context, rewardPeriod types.
|
|||||||
// InitializeHardSupplyReward initializes the supply-side of a hard liquidity provider claim
|
// InitializeHardSupplyReward initializes the supply-side of a hard liquidity provider claim
|
||||||
// by creating the claim and setting the supply reward factor index
|
// by creating the claim and setting the supply reward factor index
|
||||||
func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) {
|
func (k Keeper) InitializeHardSupplyReward(ctx sdk.Context, deposit hardtypes.Deposit) {
|
||||||
var supplyRewardIndexes types.MultiRewardIndexes
|
|
||||||
for _, coin := range deposit.Amount {
|
|
||||||
globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom)
|
|
||||||
var multiRewardIndex types.MultiRewardIndex
|
|
||||||
if foundGlobalRewardIndexes {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, globalRewardIndexes)
|
|
||||||
} else {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(coin.Denom, types.RewardIndexes{})
|
|
||||||
}
|
|
||||||
supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor)
|
claim, found := k.GetHardLiquidityProviderClaim(ctx, deposit.Depositor)
|
||||||
if !found {
|
if !found {
|
||||||
// Instantiate claim object
|
|
||||||
claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil)
|
claim = types.NewHardLiquidityProviderClaim(deposit.Depositor, sdk.Coins{}, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supplyRewardIndexes types.MultiRewardIndexes
|
||||||
|
for _, coin := range deposit.Amount {
|
||||||
|
globalRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, coin.Denom)
|
||||||
|
if !found {
|
||||||
|
globalRewardIndexes = types.RewardIndexes{}
|
||||||
|
}
|
||||||
|
supplyRewardIndexes = supplyRewardIndexes.With(coin.Denom, globalRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
claim.SupplyRewardIndexes = supplyRewardIndexes
|
claim.SupplyRewardIndexes = supplyRewardIndexes
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
@ -111,50 +107,34 @@ func (k Keeper) SynchronizeHardSupplyReward(ctx sdk.Context, deposit hardtypes.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, coin := range deposit.Amount {
|
for _, coin := range deposit.Amount {
|
||||||
globalRewardIndexes, foundGlobalRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, coin.Denom)
|
globalRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, coin.Denom)
|
||||||
if !foundGlobalRewardIndexes {
|
if !found {
|
||||||
|
// The global factor is only not found if
|
||||||
|
// - the supply denom has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit)
|
||||||
|
// - OR it was wrongly deleted from state (factors should never be removed while unsynced claims exist)
|
||||||
|
// If not found we could either skip this sync, or assume the global factor is zero.
|
||||||
|
// Skipping will avoid storing unnecessary factors in the claim for non rewarded denoms.
|
||||||
|
// And in the event a global factor is wrongly deleted, it will avoid this function panicking when calculating rewards.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
userMultiRewardIndex, foundUserMultiRewardIndex := claim.SupplyRewardIndexes.GetRewardIndex(coin.Denom)
|
userRewardIndexes, found := claim.SupplyRewardIndexes.Get(coin.Denom)
|
||||||
if !foundUserMultiRewardIndex {
|
if !found {
|
||||||
continue
|
// Normally the reward indexes should always be found.
|
||||||
|
// But if a denom was not rewarded then becomes rewarded (ie a reward period is added to params), then the indexes will be missing from claims for that supplied denom.
|
||||||
|
// So given the reward period was just added, assume the starting value for any global reward indexes, which is an empty slice.
|
||||||
|
userRewardIndexes = types.RewardIndexes{}
|
||||||
}
|
}
|
||||||
|
|
||||||
userRewardIndexIndex, foundUserRewardIndexIndex := claim.SupplyRewardIndexes.GetRewardIndexIndex(coin.Denom)
|
newRewards, err := k.CalculateRewards(userRewardIndexes, globalRewardIndexes, coin.Amount.ToDec())
|
||||||
if !foundUserRewardIndexIndex {
|
if err != nil {
|
||||||
continue
|
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
|
||||||
|
// This panics if a global reward factor decreases or disappears between the old and new indexes.
|
||||||
|
panic(fmt.Sprintf("corrupted global reward indexes found: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, globalRewardIndex := range globalRewardIndexes {
|
claim.Reward = claim.Reward.Add(newRewards...)
|
||||||
userRewardIndex, foundUserRewardIndex := userMultiRewardIndex.RewardIndexes.GetRewardIndex(globalRewardIndex.CollateralType)
|
claim.SupplyRewardIndexes = claim.SupplyRewardIndexes.With(coin.Denom, globalRewardIndexes)
|
||||||
if !foundUserRewardIndex {
|
|
||||||
// User deposited this coin type before it had rewards. When new rewards are added, legacy depositors
|
|
||||||
// should immediately begin earning rewards. Enable users to do so by updating their claim with the global
|
|
||||||
// reward index denom and start their reward factor at 0.0
|
|
||||||
userRewardIndex = types.NewRewardIndex(globalRewardIndex.CollateralType, sdk.ZeroDec())
|
|
||||||
userMultiRewardIndex.RewardIndexes = append(userMultiRewardIndex.RewardIndexes, userRewardIndex)
|
|
||||||
claim.SupplyRewardIndexes[userRewardIndexIndex] = userMultiRewardIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
globalRewardFactor := globalRewardIndex.RewardFactor
|
|
||||||
userRewardFactor := userRewardIndex.RewardFactor
|
|
||||||
rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor)
|
|
||||||
if rewardsAccumulatedFactor.IsNegative() {
|
|
||||||
panic(fmt.Sprintf("reward accumulation factor cannot be negative: %s", rewardsAccumulatedFactor))
|
|
||||||
}
|
|
||||||
|
|
||||||
newRewardsAmount := rewardsAccumulatedFactor.Mul(deposit.Amount.AmountOf(coin.Denom).ToDec()).RoundInt()
|
|
||||||
|
|
||||||
factorIndex, foundFactorIndex := userMultiRewardIndex.RewardIndexes.GetFactorIndex(globalRewardIndex.CollateralType)
|
|
||||||
if !foundFactorIndex { // should never trigger, as we basically do this check at the start of this loop
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
claim.SupplyRewardIndexes[userRewardIndexIndex].RewardIndexes[factorIndex].RewardFactor = globalRewardIndex.RewardFactor
|
|
||||||
|
|
||||||
newRewardsCoin := sdk.NewCoin(userRewardIndex.CollateralType, newRewardsAmount)
|
|
||||||
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
k.SetHardLiquidityProviderClaim(ctx, claim)
|
k.SetHardLiquidityProviderClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
@ -169,26 +149,22 @@ func (k Keeper) UpdateHardSupplyIndexDenoms(ctx sdk.Context, deposit hardtypes.D
|
|||||||
depositDenoms := getDenoms(deposit.Amount)
|
depositDenoms := getDenoms(deposit.Amount)
|
||||||
supplyRewardIndexDenoms := claim.SupplyRewardIndexes.GetCollateralTypes()
|
supplyRewardIndexDenoms := claim.SupplyRewardIndexes.GetCollateralTypes()
|
||||||
|
|
||||||
uniqueDepositDenoms := setDifference(depositDenoms, supplyRewardIndexDenoms)
|
|
||||||
uniqueSupplyRewardDenoms := setDifference(supplyRewardIndexDenoms, depositDenoms)
|
|
||||||
|
|
||||||
supplyRewardIndexes := claim.SupplyRewardIndexes
|
supplyRewardIndexes := claim.SupplyRewardIndexes
|
||||||
|
|
||||||
// Create a new multi-reward index in the claim for every new deposit denom
|
// Create a new multi-reward index in the claim for every new deposit denom
|
||||||
|
uniqueDepositDenoms := setDifference(depositDenoms, supplyRewardIndexDenoms)
|
||||||
|
|
||||||
for _, denom := range uniqueDepositDenoms {
|
for _, denom := range uniqueDepositDenoms {
|
||||||
_, foundUserRewardIndexes := claim.SupplyRewardIndexes.GetRewardIndex(denom)
|
globalSupplyRewardIndexes, found := k.GetHardSupplyRewardIndexes(ctx, denom)
|
||||||
if !foundUserRewardIndexes {
|
if !found {
|
||||||
globalSupplyRewardIndexes, foundGlobalSupplyRewardIndexes := k.GetHardSupplyRewardIndexes(ctx, denom)
|
globalSupplyRewardIndexes = types.RewardIndexes{}
|
||||||
var multiRewardIndex types.MultiRewardIndex
|
|
||||||
if foundGlobalSupplyRewardIndexes {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(denom, globalSupplyRewardIndexes)
|
|
||||||
} else {
|
|
||||||
multiRewardIndex = types.NewMultiRewardIndex(denom, types.RewardIndexes{})
|
|
||||||
}
|
|
||||||
supplyRewardIndexes = append(supplyRewardIndexes, multiRewardIndex)
|
|
||||||
}
|
}
|
||||||
|
supplyRewardIndexes = supplyRewardIndexes.With(denom, globalSupplyRewardIndexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete multi-reward index from claim if the collateral type is no longer deposited
|
// Delete multi-reward index from claim if the collateral type is no longer deposited
|
||||||
|
uniqueSupplyRewardDenoms := setDifference(supplyRewardIndexDenoms, depositDenoms)
|
||||||
|
|
||||||
for _, denom := range uniqueSupplyRewardDenoms {
|
for _, denom := range uniqueSupplyRewardDenoms {
|
||||||
supplyRewardIndexes = supplyRewardIndexes.RemoveRewardIndex(denom)
|
supplyRewardIndexes = supplyRewardIndexes.RemoveRewardIndex(denom)
|
||||||
}
|
}
|
||||||
|
89
x/incentive/keeper/rewards_supply_init_test.go
Normal file
89
x/incentive/keeper/rewards_supply_init_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeHardSupplyRewardTests runs unit tests for the keeper.InitializeHardSupplyReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.SupplyRewardIndexes)
|
||||||
|
// - global indexes in store
|
||||||
|
// - deposit function arg (only deposit.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets or creates a claim
|
||||||
|
type InitializeHardSupplyRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeHardSupplyReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(InitializeHardSupplyRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetWhenClaimExists() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
// Indexes should always be empty when initialize is called.
|
||||||
|
// If initialize is called then the user must have repaid their deposit positions,
|
||||||
|
// which means UpdateHardSupplyIndexDenoms was called and should have remove indexes.
|
||||||
|
SupplyRewardIndexes: types.MultiRewardIndexes{},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetWhenClaimDoesNotExist() {
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
owner := arbitraryAddress()
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, owner)
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetEmptyForMissingIndexes() {
|
||||||
|
|
||||||
|
globalIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
owner := arbitraryAddress()
|
||||||
|
// Supply a denom that is not in the global indexes.
|
||||||
|
// This happens when a deposit denom has no rewards associated with it.
|
||||||
|
expectedIndexes := appendUniqueEmptyMultiRewardIndex(globalIndexes)
|
||||||
|
depositedDenoms := extractCollateralTypes(expectedIndexes)
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(depositedDenoms...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.InitializeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
303
x/incentive/keeper/rewards_supply_sync_test.go
Normal file
303
x/incentive/keeper/rewards_supply_sync_test.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SynchronizeHardSupplyRewardTests runs unit tests for the keeper.SynchronizeHardSupplyReward method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store (only claim.SupplyRewardIndexes, claim.Reward)
|
||||||
|
// - global indexes in store
|
||||||
|
// - deposit function arg (only deposit.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets a claim
|
||||||
|
type SynchronizeHardSupplyRewardTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeHardSupplyReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(SynchronizeHardSupplyRewardTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenGlobalIndexesHaveIncreased() {
|
||||||
|
// This is the normal case
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := increaseAllRewardFactors(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(claim.SupplyRewardIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUnchangedWhenGlobalIndexesUnchanged() {
|
||||||
|
// It should be safe to call SynchronizeHardSupplyReward multiple times
|
||||||
|
|
||||||
|
unchangingIndexes := nonEmptyMultiRewardIndexes
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: unchangingIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalSupplyIndexes(unchangingIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(unchangingIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(unchangingIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardAdded() {
|
||||||
|
// When a new reward is added (via gov) for a hard deposit denom the user has already deposited, and the claim is synced;
|
||||||
|
// Then the new reward's index should be added to the claim.
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := appendUniqueMultiRewardIndex(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardDenomAdded() {
|
||||||
|
// When a new reward coin is added (via gov) to an already rewarded deposit denom (that the user has already deposited), and the claim is synced;
|
||||||
|
// Then the new reward coin's index should be added to the claim.
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := appendUniqueRewardIndexToFirstItem(nonEmptyMultiRewardIndexes)
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenGlobalIndexesHaveIncreased() {
|
||||||
|
// This is the normal case
|
||||||
|
// Given some time has passed (meaning the global indexes have increased)
|
||||||
|
// When the claim is synced
|
||||||
|
// The user earns rewards for the time passed
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "depositdenom",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarddenom",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalSupplyIndexes(types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "depositdenom",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarddenom",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: cs(c("depositdenom", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * deposit amount
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("rewarddenom", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenNewRewardAdded() {
|
||||||
|
// When a new reward is added (via gov) for a hard deposit denom the user has already deposited, and the claim is synced
|
||||||
|
// Then the user earns rewards for the time since the reward was added
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "rewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "newlyrewarded",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "otherreward",
|
||||||
|
// Indexes start at 0 when the reward is added by gov,
|
||||||
|
// so this represents the syncing happening some time later.
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: cs(c("rewarded", 1e9), c("newlyrewarded", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * deposit amount for each deposited denom
|
||||||
|
// The old index for `newlyrewarded` isn't in the claim, so it's added starting at 0 for calculating the reward.
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("otherreward", 1_000_001_000_000), c("reward", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenNewRewardDenomAdded() {
|
||||||
|
// When a new reward coin is added (via gov) to an already rewarded deposit denom (that the user has already deposited), and the claim is synced;
|
||||||
|
// Then the user earns rewards for the time since the reward was added
|
||||||
|
|
||||||
|
originalReward := arbitraryCoins()
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: originalReward,
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "deposited",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "deposited",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: d("2000.002"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "otherreward",
|
||||||
|
// Indexes start at 0 when the reward is added by gov,
|
||||||
|
// so this represents the syncing happening some time later.
|
||||||
|
RewardFactor: d("1000.001"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: cs(c("deposited", 1e9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit)
|
||||||
|
|
||||||
|
// new reward is (new index - old index) * deposit amount for each deposited denom
|
||||||
|
// The old index for `otherreward` isn't in the claim, so it's added starting at 0 for calculating the reward.
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(
|
||||||
|
cs(c("reward", 1_000_001_000_000), c("otherreward", 1_000_001_000_000)).Add(originalReward...),
|
||||||
|
syncedClaim.Reward,
|
||||||
|
)
|
||||||
|
}
|
@ -61,7 +61,7 @@ func (suite *SupplyRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenes
|
|||||||
authBuilder.BuildMarshalled(),
|
authBuilder.BuildMarshalled(),
|
||||||
NewPricefeedGenStateMultiFromTime(suite.genesisTime),
|
NewPricefeedGenStateMultiFromTime(suite.genesisTime),
|
||||||
hardBuilder.BuildMarshalled(),
|
hardBuilder.BuildMarshalled(),
|
||||||
NewCommitteeGenesisState(suite.addrs[:2]), // TODO add committee members to suite
|
NewCommitteeGenesisState(suite.addrs[:2]),
|
||||||
incentBuilder.BuildMarshalled(),
|
incentBuilder.BuildMarshalled(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -498,7 +498,6 @@ func (suite *SupplyRewardsTestSuite) TestSynchronizeHardSupplyReward() {
|
|||||||
updatedTimeDuration: 86400,
|
updatedTimeDuration: 86400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO test synchronize when there is a reward period with 0 rewardsPerSecond
|
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
suite.Run(tc.name, func() {
|
suite.Run(tc.name, func() {
|
||||||
|
119
x/incentive/keeper/rewards_supply_update_test.go
Normal file
119
x/incentive/keeper/rewards_supply_update_test.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateHardSupplyIndexDenomsTests runs unit tests for the keeper.UpdateHardSupplyIndexDenoms method
|
||||||
|
//
|
||||||
|
// inputs
|
||||||
|
// - claim in store if it exists (only claim.SupplyRewardIndexes)
|
||||||
|
// - global indexes in store
|
||||||
|
// - deposit function arg (only deposit.Amount)
|
||||||
|
//
|
||||||
|
// outputs
|
||||||
|
// - sets a claim
|
||||||
|
type UpdateHardSupplyIndexDenomsTests struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateHardSupplyIndexDenoms(t *testing.T) {
|
||||||
|
suite.Run(t, new(UpdateHardSupplyIndexDenomsTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreRemovedForDenomsNoLongerSupplied() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
suite.storeGlobalSupplyIndexes(claim.SupplyRewardIndexes)
|
||||||
|
|
||||||
|
// remove one denom from the indexes already in the deposit
|
||||||
|
expectedIndexes := claim.SupplyRewardIndexes[1:]
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(expectedIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardSupplyIndexDenoms(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreAddedForNewlySuppliedDenoms() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
globalIndexes := appendUniqueMultiRewardIndex(claim.SupplyRewardIndexes)
|
||||||
|
suite.storeGlobalSupplyIndexes(globalIndexes)
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(globalIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardSupplyIndexDenoms(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreUnchangedWhenSuppliedDenomsUnchanged() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
// Set global indexes with same denoms but different values.
|
||||||
|
// UpdateHardSupplyIndexDenoms should ignore the new values.
|
||||||
|
suite.storeGlobalSupplyIndexes(increaseAllRewardFactors(claim.SupplyRewardIndexes))
|
||||||
|
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(extractCollateralTypes(claim.SupplyRewardIndexes)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardSupplyIndexDenoms(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(claim.SupplyRewardIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UpdateHardSupplyIndexDenomsTests) TestEmptyClaimIndexesAreAddedForNewlySuppliedButNotRewardedDenoms() {
|
||||||
|
claim := types.HardLiquidityProviderClaim{
|
||||||
|
BaseMultiClaim: types.BaseMultiClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
SupplyRewardIndexes: nonEmptyMultiRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
suite.storeGlobalSupplyIndexes(claim.SupplyRewardIndexes)
|
||||||
|
|
||||||
|
// add a denom to the deposited amount that is not in the global or claim's indexes
|
||||||
|
expectedIndexes := appendUniqueEmptyMultiRewardIndex(claim.SupplyRewardIndexes)
|
||||||
|
depositedDenoms := extractCollateralTypes(expectedIndexes)
|
||||||
|
deposit := hardtypes.Deposit{
|
||||||
|
Depositor: claim.Owner,
|
||||||
|
Amount: arbitraryCoinsWithDenoms(depositedDenoms...),
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.keeper.UpdateHardSupplyIndexDenoms(suite.ctx, deposit)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.SupplyRewardIndexes)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package keeper
|
package keeper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||||
@ -55,66 +57,61 @@ func (k Keeper) InitializeUSDXMintingClaim(ctx sdk.Context, cdp cdptypes.CDP) {
|
|||||||
// this collateral type is not incentivized, do nothing
|
// this collateral type is not incentivized, do nothing
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type)
|
|
||||||
if !found {
|
|
||||||
rewardFactor = sdk.ZeroDec()
|
|
||||||
}
|
|
||||||
claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner)
|
claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner)
|
||||||
if !found { // this is the owner's first usdx minting reward claim
|
if !found { // this is the owner's first usdx minting reward claim
|
||||||
claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, rewardFactor)})
|
claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{})
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// the owner has an existing usdx minting reward claim
|
|
||||||
index, hasRewardIndex := claim.HasRewardIndex(cdp.Type)
|
|
||||||
if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type
|
|
||||||
claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, rewardFactor))
|
|
||||||
} else { // the owner has a previous usdx minting reward for this collateral type
|
|
||||||
claim.RewardIndexes[index] = types.NewRewardIndex(cdp.Type, rewardFactor)
|
|
||||||
}
|
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SynchronizeUSDXMintingReward updates the claim object by adding any accumulated rewards and updating the reward index value.
|
|
||||||
// this should be called before a cdp is modified, immediately after the 'SynchronizeInterest' method is called in the cdp module
|
|
||||||
func (k Keeper) SynchronizeUSDXMintingReward(ctx sdk.Context, cdp cdptypes.CDP) {
|
|
||||||
_, found := k.GetUSDXMintingRewardPeriod(ctx, cdp.Type)
|
|
||||||
if !found {
|
|
||||||
// this collateral type is not incentivized, do nothing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type)
|
globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type)
|
||||||
if !found {
|
if !found {
|
||||||
globalRewardFactor = sdk.ZeroDec()
|
globalRewardFactor = sdk.ZeroDec()
|
||||||
}
|
}
|
||||||
|
claim.RewardIndexes = claim.RewardIndexes.With(cdp.Type, globalRewardFactor)
|
||||||
|
|
||||||
|
k.SetUSDXMintingClaim(ctx, claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SynchronizeUSDXMintingReward updates the claim object by adding any accumulated rewards and updating the reward index value.
|
||||||
|
// this should be called before a cdp is modified.
|
||||||
|
func (k Keeper) SynchronizeUSDXMintingReward(ctx sdk.Context, cdp cdptypes.CDP) {
|
||||||
|
|
||||||
|
globalRewardFactor, found := k.GetUSDXMintingRewardFactor(ctx, cdp.Type)
|
||||||
|
if !found {
|
||||||
|
// The global factor is only not found if
|
||||||
|
// - the cdp collateral type has not started accumulating rewards yet (either there is no reward specified in params, or the reward start time hasn't been hit)
|
||||||
|
// - OR it was wrongly deleted from state (factors should never be removed while unsynced claims exist)
|
||||||
|
// If not found we could either skip this sync, or assume the global factor is zero.
|
||||||
|
// Skipping will avoid storing unnecessary factors in the claim for non rewarded denoms.
|
||||||
|
// And in the event a global factor is wrongly deleted, it will avoid this function panicking when calculating rewards.
|
||||||
|
return
|
||||||
|
}
|
||||||
claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner)
|
claim, found := k.GetUSDXMintingClaim(ctx, cdp.Owner)
|
||||||
if !found {
|
if !found {
|
||||||
claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, globalRewardFactor)})
|
claim = types.NewUSDXMintingClaim(
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
cdp.Owner,
|
||||||
return
|
sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()),
|
||||||
|
types.RewardIndexes{},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the owner has an existing usdx minting reward claim
|
userRewardFactor, found := claim.RewardIndexes.Get(cdp.Type)
|
||||||
index, hasRewardIndex := claim.HasRewardIndex(cdp.Type)
|
if !found {
|
||||||
if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type
|
// Normally the factor should always be found, as it is added when the cdp is created in InitializeUSDXMintingClaim.
|
||||||
claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, globalRewardFactor))
|
// However if a cdp type is not rewarded then becomes rewarded (ie a reward period is added to params), existing cdps will not have the factor in their claims.
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
// So assume the factor is the starting value for any global factor: 0.
|
||||||
return
|
userRewardFactor = sdk.ZeroDec()
|
||||||
}
|
}
|
||||||
userRewardFactor := claim.RewardIndexes[index].RewardFactor
|
|
||||||
rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor)
|
newRewardsAmount, err := k.CalculateSingleReward(userRewardFactor, globalRewardFactor, cdp.GetTotalPrincipal().Amount.ToDec())
|
||||||
if rewardsAccumulatedFactor.IsZero() {
|
if err != nil {
|
||||||
return
|
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
|
||||||
}
|
// This panics if a global reward factor decreases or disappears between the old and new indexes.
|
||||||
claim.RewardIndexes[index].RewardFactor = globalRewardFactor
|
panic(fmt.Sprintf("corrupted global reward indexes found: %v", err))
|
||||||
newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt()
|
|
||||||
if newRewardsAmount.IsZero() {
|
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount)
|
newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount)
|
||||||
|
|
||||||
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
claim.Reward = claim.Reward.Add(newRewardsCoin)
|
||||||
|
claim.RewardIndexes = claim.RewardIndexes.With(cdp.Type, globalRewardFactor)
|
||||||
|
|
||||||
k.SetUSDXMintingClaim(ctx, claim)
|
k.SetUSDXMintingClaim(ctx, claim)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
311
x/incentive/keeper/rewards_usdx_unit_test.go
Normal file
311
x/incentive/keeper/rewards_usdx_unit_test.go
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// usdxRewardsUnitTester contains common methods for running unit tests for keeper methods related to the USDX minting rewards
|
||||||
|
type usdxRewardsUnitTester struct {
|
||||||
|
unitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *usdxRewardsUnitTester) storeGlobalUSDXIndexes(indexes types.RewardIndexes) {
|
||||||
|
for _, ri := range indexes {
|
||||||
|
suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, ri.CollateralType, ri.RewardFactor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (suite *usdxRewardsUnitTester) storeClaim(claim types.USDXMintingClaim) {
|
||||||
|
suite.keeper.SetUSDXMintingClaim(suite.ctx, claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitializeUSDXMintingClaimTests struct {
|
||||||
|
usdxRewardsUnitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeUSDXMintingClaims(t *testing.T) {
|
||||||
|
suite.Run(t, new(InitializeUSDXMintingClaimTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeUSDXMintingClaimTests) TestClaimIndexIsSetWhenClaimDoesNotExist() {
|
||||||
|
collateralType := "bnb-a"
|
||||||
|
|
||||||
|
subspace := paramsWithSingleUSDXRewardPeriod(collateralType)
|
||||||
|
suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(arbitraryAddress(), collateralType).Build()
|
||||||
|
|
||||||
|
globalIndexes := types.RewardIndexes{{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
}}
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
suite.keeper.InitializeUSDXMintingClaim(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, f := suite.keeper.GetUSDXMintingClaim(suite.ctx, cdp.Owner)
|
||||||
|
suite.True(f)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.RewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InitializeUSDXMintingClaimTests) TestClaimIndexIsSetWhenClaimExists() {
|
||||||
|
collateralType := "bnb-a"
|
||||||
|
|
||||||
|
subspace := paramsWithSingleUSDXRewardPeriod(collateralType)
|
||||||
|
suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
},
|
||||||
|
RewardIndexes: types.RewardIndexes{{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.RewardIndexes{{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
}}
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, collateralType).Build()
|
||||||
|
|
||||||
|
suite.keeper.InitializeUSDXMintingClaim(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, cdp.Owner)
|
||||||
|
suite.Equal(globalIndexes, syncedClaim.RewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SynchronizeUSDXMintingRewardTests struct {
|
||||||
|
usdxRewardsUnitTester
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeUSDXMintingReward(t *testing.T) {
|
||||||
|
suite.Run(t, new(SynchronizeUSDXMintingRewardTests))
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestRewardUnchangedWhenGlobalIndexesUnchanged() {
|
||||||
|
unchangingRewardIndexes := nonEmptyRewardIndexes
|
||||||
|
collateralType := extractFirstCollateralType(unchangingRewardIndexes)
|
||||||
|
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: c(types.USDXMintingRewardDenom, 0),
|
||||||
|
},
|
||||||
|
RewardIndexes: unchangingRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
suite.storeGlobalUSDXIndexes(unchangingRewardIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, collateralType).WithPrincipal(i(1e12)).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(claim.Reward, syncedClaim.Reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestRewardIsIncrementedWhenGlobalIndexIncreased() {
|
||||||
|
collateralType := "bnb-a"
|
||||||
|
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: c(types.USDXMintingRewardDenom, 0),
|
||||||
|
},
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, collateralType).WithPrincipal(i(1e12)).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, claim.Owner)
|
||||||
|
// reward is ( new index - old index ) * cdp.TotalPrincipal
|
||||||
|
suite.Equal(c(types.USDXMintingRewardDenom, 1e11), syncedClaim.Reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestRewardIsIncrementedWhenNewRewardAddedAndClaimDoesNotExit() {
|
||||||
|
collateralType := "bnb-a"
|
||||||
|
|
||||||
|
globalIndexes := types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(arbitraryAddress(), collateralType).WithPrincipal(i(1e12)).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, cdp.Owner)
|
||||||
|
// The global index was not around when this cdp was created as it was not stored in a claim.
|
||||||
|
// Therefore it must have been added via params after.
|
||||||
|
// To include rewards since the params were updated, the old index should be assumed to be 0.
|
||||||
|
// reward is ( new index - old index ) * cdp.TotalPrincipal
|
||||||
|
suite.Equal(c(types.USDXMintingRewardDenom, 2e11), syncedClaim.Reward)
|
||||||
|
}
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestClaimIndexIsUpdatedWhenGlobalIndexIncreased() {
|
||||||
|
claimsRewardIndexes := nonEmptyRewardIndexes
|
||||||
|
collateralType := extractFirstCollateralType(claimsRewardIndexes)
|
||||||
|
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: c(types.USDXMintingRewardDenom, 0),
|
||||||
|
},
|
||||||
|
RewardIndexes: claimsRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := increaseRewardFactors(claimsRewardIndexes)
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, collateralType).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
// Only the claim's index for `collateralType` should have been changed
|
||||||
|
i, _ := globalIndexes.Get(collateralType)
|
||||||
|
expectedIndexes := claimsRewardIndexes.With(collateralType, i)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.RewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestClaimIndexIsUpdatedWhenNewRewardAddedAndClaimAlreadyExists() {
|
||||||
|
claimsRewardIndexes := types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "bnb-a",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "busd-b",
|
||||||
|
RewardFactor: d("0.4"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
newRewardIndex := types.NewRewardIndex("xrp-a", d("0.0001"))
|
||||||
|
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: c(types.USDXMintingRewardDenom, 0),
|
||||||
|
},
|
||||||
|
RewardIndexes: claimsRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
|
||||||
|
globalIndexes := increaseRewardFactors(claimsRewardIndexes)
|
||||||
|
globalIndexes = append(globalIndexes, newRewardIndex)
|
||||||
|
suite.storeGlobalUSDXIndexes(globalIndexes)
|
||||||
|
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, newRewardIndex.CollateralType).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, claim.Owner)
|
||||||
|
|
||||||
|
// Only the claim's index for `collateralType` should have been changed
|
||||||
|
expectedIndexes := claimsRewardIndexes.With(newRewardIndex.CollateralType, newRewardIndex.RewardFactor)
|
||||||
|
suite.Equal(expectedIndexes, syncedClaim.RewardIndexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SynchronizeUSDXMintingRewardTests) TestClaimIsUnchangedWhenGlobalFactorMissing() {
|
||||||
|
claimsRewardIndexes := nonEmptyRewardIndexes
|
||||||
|
claim := types.USDXMintingClaim{
|
||||||
|
BaseClaim: types.BaseClaim{
|
||||||
|
Owner: arbitraryAddress(),
|
||||||
|
Reward: c(types.USDXMintingRewardDenom, 0),
|
||||||
|
},
|
||||||
|
RewardIndexes: claimsRewardIndexes,
|
||||||
|
}
|
||||||
|
suite.storeClaim(claim)
|
||||||
|
// don't store any reward indexes
|
||||||
|
|
||||||
|
// create a cdp with collateral type that doesn't exist in the claim's indexes, and does not have a corresponding global factor
|
||||||
|
cdp := NewCDPBuilder(claim.Owner, "unrewardedcollateral").WithPrincipal(i(1e12)).Build()
|
||||||
|
|
||||||
|
suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp)
|
||||||
|
|
||||||
|
syncedClaim, _ := suite.keeper.GetUSDXMintingClaim(suite.ctx, claim.Owner)
|
||||||
|
suite.Equal(claim.RewardIndexes, syncedClaim.RewardIndexes)
|
||||||
|
suite.Equal(claim.Reward, syncedClaim.Reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cdpBuilder struct {
|
||||||
|
cdptypes.CDP
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCDPBuilder(owner sdk.AccAddress, collateralType string) cdpBuilder {
|
||||||
|
return cdpBuilder{
|
||||||
|
CDP: cdptypes.CDP{
|
||||||
|
Owner: owner,
|
||||||
|
Type: collateralType,
|
||||||
|
// The zero value of Principal and AccumulatedFees (type sdk.Coin) is invalid as the denom is ""
|
||||||
|
// Set them to the default denom, but with 0 amount.
|
||||||
|
Principal: c(cdptypes.DefaultStableDenom, 0),
|
||||||
|
AccumulatedFees: c(cdptypes.DefaultStableDenom, 0),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder cdpBuilder) Build() cdptypes.CDP { return builder.CDP }
|
||||||
|
|
||||||
|
func (builder cdpBuilder) WithPrincipal(principal sdk.Int) cdpBuilder {
|
||||||
|
builder.Principal = sdk.NewCoin(cdptypes.DefaultStableDenom, principal)
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonEmptyRewardIndexes = types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "bnb-a",
|
||||||
|
RewardFactor: d("0.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "busd-b",
|
||||||
|
RewardFactor: d("0.4"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramsWithSingleUSDXRewardPeriod(collateralType string) types.ParamSubspace {
|
||||||
|
return &fakeParamSubspace{
|
||||||
|
params: types.Params{
|
||||||
|
USDXMintingRewardPeriods: types.RewardPeriods{
|
||||||
|
{
|
||||||
|
CollateralType: collateralType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFirstCollateralType(indexes types.RewardIndexes) string {
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
panic("cannot extract a collateral type from 0 length RewardIndexes")
|
||||||
|
}
|
||||||
|
return indexes[0].CollateralType
|
||||||
|
}
|
248
x/incentive/keeper/unit_test.go
Normal file
248
x/incentive/keeper/unit_test.go
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"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"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/incentive/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTestContext sets up a basic context with an in-memory db
|
||||||
|
func NewTestContext(requiredStoreKeys ...sdk.StoreKey) sdk.Context {
|
||||||
|
memDB := db.NewMemDB()
|
||||||
|
cms := store.NewCommitMultiStore(memDB)
|
||||||
|
|
||||||
|
for _, key := range requiredStoreKeys {
|
||||||
|
cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
cms.LoadLatestVersion()
|
||||||
|
|
||||||
|
return sdk.NewContext(cms, abci.Header{}, false, log.NewNopLogger())
|
||||||
|
}
|
||||||
|
|
||||||
|
// unitTester is a wrapper around suite.Suite, with common functionality for keeper unit tests.
|
||||||
|
// It can be embedded in structs the same way as suite.Suite.
|
||||||
|
type unitTester struct {
|
||||||
|
suite.Suite
|
||||||
|
keeper keeper.Keeper
|
||||||
|
ctx sdk.Context
|
||||||
|
|
||||||
|
cdc *codec.Codec
|
||||||
|
incentiveStoreKey sdk.StoreKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) SetupSuite() {
|
||||||
|
suite.cdc = app.MakeCodec()
|
||||||
|
|
||||||
|
suite.incentiveStoreKey = sdk.NewKVStoreKey(types.StoreKey)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) SetupTest() {
|
||||||
|
suite.ctx = NewTestContext(suite.incentiveStoreKey)
|
||||||
|
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) TearDownTest() {
|
||||||
|
suite.keeper = keeper.Keeper{}
|
||||||
|
suite.ctx = sdk.Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) NewKeeper(paramSubspace types.ParamSubspace, sk types.SupplyKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper) keeper.Keeper {
|
||||||
|
return keeper.NewKeeper(suite.cdc, suite.incentiveStoreKey, paramSubspace, sk, cdpk, hk, ak, stk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) storeGlobalBorrowIndexes(indexes types.MultiRewardIndexes) {
|
||||||
|
for _, i := range indexes {
|
||||||
|
suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (suite *unitTester) storeGlobalSupplyIndexes(indexes types.MultiRewardIndexes) {
|
||||||
|
for _, i := range indexes {
|
||||||
|
suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *unitTester) storeClaim(claim types.HardLiquidityProviderClaim) {
|
||||||
|
suite.keeper.SetHardLiquidityProviderClaim(suite.ctx, claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeParamSubspace struct {
|
||||||
|
params types.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subspace *fakeParamSubspace) GetParamSet(_ sdk.Context, ps params.ParamSet) {
|
||||||
|
*(ps.(*types.Params)) = subspace.params
|
||||||
|
}
|
||||||
|
func (subspace *fakeParamSubspace) SetParamSet(_ sdk.Context, ps params.ParamSet) {
|
||||||
|
subspace.params = *(ps.(*types.Params))
|
||||||
|
}
|
||||||
|
func (subspace *fakeParamSubspace) HasKeyTable() bool {
|
||||||
|
// return true so the keeper does no try to set the key table, which does nothing
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (subspace *fakeParamSubspace) WithKeyTable(params.KeyTable) params.Subspace {
|
||||||
|
// return an non-functional subspace to satisfy the interface
|
||||||
|
return params.Subspace{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func arbitraryCoin() sdk.Coin {
|
||||||
|
return c("hard", 1e9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func arbitraryCoins() sdk.Coins {
|
||||||
|
return cs(c("btcb", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func arbitraryCoinsWithDenoms(denom ...string) sdk.Coins {
|
||||||
|
const arbitraryAmount = 1 // must be > 0 as sdk.Coins type only stores positive amounts
|
||||||
|
coins := sdk.NewCoins()
|
||||||
|
for _, d := range denom {
|
||||||
|
coins = coins.Add(sdk.NewInt64Coin(d, arbitraryAmount))
|
||||||
|
}
|
||||||
|
return coins
|
||||||
|
}
|
||||||
|
|
||||||
|
func arbitraryAddress() sdk.AccAddress {
|
||||||
|
_, addresses := app.GeneratePrivKeyAddressPairs(1)
|
||||||
|
return addresses[0]
|
||||||
|
}
|
||||||
|
func arbitraryValidatorAddress() sdk.ValAddress {
|
||||||
|
return generateValidatorAddresses(1)[0]
|
||||||
|
}
|
||||||
|
func generateValidatorAddresses(n int) []sdk.ValAddress {
|
||||||
|
_, addresses := app.GeneratePrivKeyAddressPairs(n)
|
||||||
|
var valAddresses []sdk.ValAddress
|
||||||
|
for _, a := range addresses {
|
||||||
|
valAddresses = append(valAddresses, sdk.ValAddress(a))
|
||||||
|
}
|
||||||
|
return valAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonEmptyMultiRewardIndexes = types.MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "bnb",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.02"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.04"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "btcb",
|
||||||
|
RewardIndexes: types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.4"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractCollateralTypes(indexes types.MultiRewardIndexes) []string {
|
||||||
|
var denoms []string
|
||||||
|
for _, ri := range indexes {
|
||||||
|
denoms = append(denoms, ri.CollateralType)
|
||||||
|
}
|
||||||
|
return denoms
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseAllRewardFactors(indexes types.MultiRewardIndexes) types.MultiRewardIndexes {
|
||||||
|
increasedIndexes := make(types.MultiRewardIndexes, len(indexes))
|
||||||
|
copy(increasedIndexes, indexes)
|
||||||
|
|
||||||
|
for i := range increasedIndexes {
|
||||||
|
increasedIndexes[i].RewardIndexes = increaseRewardFactors(increasedIndexes[i].RewardIndexes)
|
||||||
|
}
|
||||||
|
return increasedIndexes
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseRewardFactors(indexes types.RewardIndexes) types.RewardIndexes {
|
||||||
|
increasedIndexes := make(types.RewardIndexes, len(indexes))
|
||||||
|
copy(increasedIndexes, indexes)
|
||||||
|
|
||||||
|
for i := range increasedIndexes {
|
||||||
|
increasedIndexes[i].RewardFactor = increasedIndexes[i].RewardFactor.MulInt64(2)
|
||||||
|
}
|
||||||
|
return increasedIndexes
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueMultiRewardIndex(indexes types.MultiRewardIndexes) types.MultiRewardIndexes {
|
||||||
|
const uniqueDenom = "uniquedenom"
|
||||||
|
|
||||||
|
for _, mri := range indexes {
|
||||||
|
if mri.CollateralType == uniqueDenom {
|
||||||
|
panic(fmt.Sprintf("tried to add unique multi reward index with denom '%s', but denom already existed", uniqueDenom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(indexes, types.NewMultiRewardIndex(
|
||||||
|
uniqueDenom,
|
||||||
|
types.RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "hard",
|
||||||
|
RewardFactor: d("0.02"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "ukava",
|
||||||
|
RewardFactor: d("0.04"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueEmptyMultiRewardIndex(indexes types.MultiRewardIndexes) types.MultiRewardIndexes {
|
||||||
|
const uniqueDenom = "uniquedenom"
|
||||||
|
|
||||||
|
for _, mri := range indexes {
|
||||||
|
if mri.CollateralType == uniqueDenom {
|
||||||
|
panic(fmt.Sprintf("tried to add unique multi reward index with denom '%s', but denom already existed", uniqueDenom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(indexes, types.NewMultiRewardIndex(uniqueDenom, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueRewardIndexToFirstItem(indexes types.MultiRewardIndexes) types.MultiRewardIndexes {
|
||||||
|
newIndexes := make(types.MultiRewardIndexes, len(indexes))
|
||||||
|
copy(newIndexes, indexes)
|
||||||
|
|
||||||
|
newIndexes[0].RewardIndexes = appendUniqueRewardIndex(newIndexes[0].RewardIndexes)
|
||||||
|
return newIndexes
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueRewardIndex(indexes types.RewardIndexes) types.RewardIndexes {
|
||||||
|
const uniqueDenom = "uniquereward"
|
||||||
|
|
||||||
|
for _, mri := range indexes {
|
||||||
|
if mri.CollateralType == uniqueDenom {
|
||||||
|
panic(fmt.Sprintf("tried to add unique reward index with denom '%s', but denom already existed", uniqueDenom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(
|
||||||
|
indexes,
|
||||||
|
types.NewRewardIndex(uniqueDenom, d("0.02")),
|
||||||
|
)
|
||||||
|
}
|
@ -402,6 +402,30 @@ func (ris RewardIndexes) GetRewardIndex(denom string) (RewardIndex, bool) {
|
|||||||
return RewardIndex{}, false
|
return RewardIndex{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get fetches a RewardFactor by it's denom
|
||||||
|
func (ris RewardIndexes) Get(denom string) (sdk.Dec, bool) {
|
||||||
|
for _, ri := range ris {
|
||||||
|
if ri.CollateralType == denom {
|
||||||
|
return ri.RewardFactor, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sdk.Dec{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// With returns a copy of the indexes with a new reward factor added
|
||||||
|
func (ris RewardIndexes) With(denom string, factor sdk.Dec) RewardIndexes {
|
||||||
|
newIndexes := make(RewardIndexes, len(ris))
|
||||||
|
copy(newIndexes, ris)
|
||||||
|
|
||||||
|
for i, ri := range newIndexes {
|
||||||
|
if ri.CollateralType == denom {
|
||||||
|
newIndexes[i].RewardFactor = factor
|
||||||
|
return newIndexes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(newIndexes, NewRewardIndex(denom, factor))
|
||||||
|
}
|
||||||
|
|
||||||
// GetFactorIndex gets the index of a specific reward index inside the array by its index
|
// GetFactorIndex gets the index of a specific reward index inside the array by its index
|
||||||
func (ris RewardIndexes) GetFactorIndex(denom string) (int, bool) {
|
func (ris RewardIndexes) GetFactorIndex(denom string) (int, bool) {
|
||||||
for i, ri := range ris {
|
for i, ri := range ris {
|
||||||
@ -476,6 +500,16 @@ func (mris MultiRewardIndexes) GetRewardIndex(denom string) (MultiRewardIndex, b
|
|||||||
return MultiRewardIndex{}, false
|
return MultiRewardIndex{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get fetches a RewardIndexes by it's denom
|
||||||
|
func (mris MultiRewardIndexes) Get(denom string) (RewardIndexes, bool) {
|
||||||
|
for _, mri := range mris {
|
||||||
|
if mri.CollateralType == denom {
|
||||||
|
return mri.RewardIndexes, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// GetRewardIndexIndex fetches a specific reward index inside the array by its denom
|
// GetRewardIndexIndex fetches a specific reward index inside the array by its denom
|
||||||
func (mris MultiRewardIndexes) GetRewardIndexIndex(denom string) (int, bool) {
|
func (mris MultiRewardIndexes) GetRewardIndexIndex(denom string) (int, bool) {
|
||||||
for i, ri := range mris {
|
for i, ri := range mris {
|
||||||
@ -486,6 +520,19 @@ func (mris MultiRewardIndexes) GetRewardIndexIndex(denom string) (int, bool) {
|
|||||||
return -1, false
|
return -1, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With returns a copy of the indexes with a new RewardIndexes added
|
||||||
|
func (mris MultiRewardIndexes) With(denom string, indexes RewardIndexes) MultiRewardIndexes {
|
||||||
|
newIndexes := mris.copy()
|
||||||
|
|
||||||
|
for i, mri := range newIndexes {
|
||||||
|
if mri.CollateralType == denom {
|
||||||
|
newIndexes[i].RewardIndexes = indexes
|
||||||
|
return newIndexes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(newIndexes, NewMultiRewardIndex(denom, indexes))
|
||||||
|
}
|
||||||
|
|
||||||
// GetCollateralTypes returns a slice of containing all collateral types
|
// GetCollateralTypes returns a slice of containing all collateral types
|
||||||
func (mris MultiRewardIndexes) GetCollateralTypes() []string {
|
func (mris MultiRewardIndexes) GetCollateralTypes() []string {
|
||||||
var collateralTypes []string
|
var collateralTypes []string
|
||||||
@ -499,7 +546,9 @@ func (mris MultiRewardIndexes) GetCollateralTypes() []string {
|
|||||||
func (mris MultiRewardIndexes) RemoveRewardIndex(denom string) MultiRewardIndexes {
|
func (mris MultiRewardIndexes) RemoveRewardIndex(denom string) MultiRewardIndexes {
|
||||||
for i, ri := range mris {
|
for i, ri := range mris {
|
||||||
if ri.CollateralType == denom {
|
if ri.CollateralType == denom {
|
||||||
return append(mris[:i], mris[i+1:]...)
|
// copy the slice and underlying array to avoid altering the original
|
||||||
|
copy := mris.copy()
|
||||||
|
return append(copy[:i], copy[i+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mris
|
return mris
|
||||||
@ -514,3 +563,10 @@ func (mris MultiRewardIndexes) Validate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy returns a copy of the slice and underlying array
|
||||||
|
func (mris MultiRewardIndexes) copy() MultiRewardIndexes {
|
||||||
|
newIndexes := make(MultiRewardIndexes, len(mris))
|
||||||
|
copy(newIndexes, mris)
|
||||||
|
return newIndexes
|
||||||
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/tendermint/tendermint/crypto"
|
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tendermint/tendermint/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClaimsValidate(t *testing.T) {
|
func TestClaimsValidate(t *testing.T) {
|
||||||
@ -72,3 +71,295 @@ func TestClaimsValidate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRewardIndexes(t *testing.T) {
|
||||||
|
t.Run("With", func(t *testing.T) {
|
||||||
|
var arbitraryDec = sdk.MustNewDecFromStr("0.1")
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
denom string
|
||||||
|
factor sdk.Dec
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
rewardIndexes RewardIndexes
|
||||||
|
args args
|
||||||
|
expected RewardIndexes
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when index is not present, it's added and original isn't overwritten",
|
||||||
|
rewardIndexes: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
denom: "otherdenom",
|
||||||
|
factor: arbitraryDec,
|
||||||
|
},
|
||||||
|
expected: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec),
|
||||||
|
NewRewardIndex("otherdenom", arbitraryDec),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when index is present, it's updated and original isn't overwritten",
|
||||||
|
rewardIndexes: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
denom: "denom",
|
||||||
|
factor: arbitraryDec.MulInt64(2),
|
||||||
|
},
|
||||||
|
expected: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec.MulInt64(2)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
newIndexes := tc.rewardIndexes.With(tc.args.denom, tc.args.factor)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected, newIndexes)
|
||||||
|
require.NotEqual(t, tc.rewardIndexes, newIndexes) // check original slice not modified
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Get", func(t *testing.T) {
|
||||||
|
var arbitraryDec = sdk.MustNewDecFromStr("0.1")
|
||||||
|
|
||||||
|
type expected struct {
|
||||||
|
factor sdk.Dec
|
||||||
|
found bool
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
rewardIndexes RewardIndexes
|
||||||
|
arg_denom string
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when index is present, it is found and returned",
|
||||||
|
rewardIndexes: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec),
|
||||||
|
},
|
||||||
|
arg_denom: "denom",
|
||||||
|
expected: expected{
|
||||||
|
factor: arbitraryDec,
|
||||||
|
found: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when index is not present, it is not found",
|
||||||
|
rewardIndexes: RewardIndexes{
|
||||||
|
NewRewardIndex("denom", arbitraryDec),
|
||||||
|
},
|
||||||
|
arg_denom: "notpresent",
|
||||||
|
expected: expected{
|
||||||
|
found: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
factor, found := tc.rewardIndexes.Get(tc.arg_denom)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected.found, found)
|
||||||
|
require.Equal(t, tc.expected.factor, factor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiRewardIndexes(t *testing.T) {
|
||||||
|
arbitraryRewardIndexes := RewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "reward",
|
||||||
|
RewardFactor: sdk.MustNewDecFromStr("0.1"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get", func(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
rewardIndexes RewardIndexes
|
||||||
|
found bool
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
multiRewardIndexes MultiRewardIndexes
|
||||||
|
arg_denom string
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when indexes are present, they are found and returned",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arg_denom: "denom",
|
||||||
|
expected: expected{
|
||||||
|
found: true,
|
||||||
|
rewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when indexes are not present, they are not found",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arg_denom: "notpresent",
|
||||||
|
expected: expected{
|
||||||
|
found: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rewardIndexes, found := tc.multiRewardIndexes.Get(tc.arg_denom)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected.found, found)
|
||||||
|
require.Equal(t, tc.expected.rewardIndexes, rewardIndexes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("With", func(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
denom string
|
||||||
|
rewardIndexes RewardIndexes
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
multiRewardIndexes MultiRewardIndexes
|
||||||
|
args args
|
||||||
|
expected MultiRewardIndexes
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when indexes are not present, add them and do not update original",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
denom: "otherdenom",
|
||||||
|
rewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
expected: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "otherdenom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when indexes are present, update them and do not update original",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
denom: "denom",
|
||||||
|
rewardIndexes: appendUniqueRewardIndex(arbitraryRewardIndexes),
|
||||||
|
},
|
||||||
|
expected: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: appendUniqueRewardIndex(arbitraryRewardIndexes),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
oldIndexes := tc.multiRewardIndexes.copy()
|
||||||
|
|
||||||
|
newIndexes := tc.multiRewardIndexes.With(tc.args.denom, tc.args.rewardIndexes)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected, newIndexes)
|
||||||
|
require.Equal(t, oldIndexes, tc.multiRewardIndexes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("RemoveRewardIndex", func(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
multiRewardIndexes MultiRewardIndexes
|
||||||
|
arg_denom string
|
||||||
|
expected MultiRewardIndexes
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when indexes are not present, do nothing",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arg_denom: "notpresent",
|
||||||
|
expected: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when indexes are present, remove them and do not update original",
|
||||||
|
multiRewardIndexes: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "denom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CollateralType: "otherdenom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arg_denom: "denom",
|
||||||
|
expected: MultiRewardIndexes{
|
||||||
|
{
|
||||||
|
CollateralType: "otherdenom",
|
||||||
|
RewardIndexes: arbitraryRewardIndexes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
oldIndexes := tc.multiRewardIndexes.copy()
|
||||||
|
|
||||||
|
newIndexes := tc.multiRewardIndexes.RemoveRewardIndex(tc.arg_denom)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected, newIndexes)
|
||||||
|
require.Equal(t, oldIndexes, tc.multiRewardIndexes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueRewardIndex(indexes RewardIndexes) RewardIndexes {
|
||||||
|
const uniqueDenom = "uniquereward"
|
||||||
|
|
||||||
|
for _, mri := range indexes {
|
||||||
|
if mri.CollateralType == uniqueDenom {
|
||||||
|
panic(fmt.Sprintf("tried to add unique reward index with denom '%s', but denom already existed", uniqueDenom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(
|
||||||
|
indexes,
|
||||||
|
NewRewardIndex(uniqueDenom, sdk.MustNewDecFromStr("0.02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -19,4 +19,5 @@ var (
|
|||||||
ErrClaimExpired = sdkerrors.Register(ModuleName, 10, "claim has expired")
|
ErrClaimExpired = sdkerrors.Register(ModuleName, 10, "claim has expired")
|
||||||
ErrInvalidClaimType = sdkerrors.Register(ModuleName, 11, "invalid claim type")
|
ErrInvalidClaimType = sdkerrors.Register(ModuleName, 11, "invalid claim type")
|
||||||
ErrInvalidClaimOwner = sdkerrors.Register(ModuleName, 12, "invalid claim owner")
|
ErrInvalidClaimOwner = sdkerrors.Register(ModuleName, 12, "invalid claim owner")
|
||||||
|
ErrDecreasingRewardFactor = sdkerrors.Register(ModuleName, 13, "found new reward factor less than an old reward factor")
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params"
|
||||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
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"
|
||||||
|
|
||||||
@ -10,6 +11,14 @@ import (
|
|||||||
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
hardtypes "github.com/kava-labs/kava/x/hard/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParamSubspace defines the expected Subspace interfacace
|
||||||
|
type ParamSubspace interface {
|
||||||
|
GetParamSet(sdk.Context, params.ParamSet)
|
||||||
|
SetParamSet(sdk.Context, params.ParamSet)
|
||||||
|
WithKeyTable(params.KeyTable) params.Subspace
|
||||||
|
HasKeyTable() bool
|
||||||
|
}
|
||||||
|
|
||||||
// SupplyKeeper defines the expected supply keeper for module accounts
|
// SupplyKeeper defines the expected supply keeper for module accounts
|
||||||
type SupplyKeeper interface {
|
type SupplyKeeper interface {
|
||||||
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
|
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
|
||||||
|
Loading…
Reference in New Issue
Block a user