Skip to content

Commit

Permalink
Add usdx incentives calculation test (#765)
Browse files Browse the repository at this point in the history
* add usdx incentive calculation test

* update reward calculation

* add allowable error to test criteria

* Update x/incentive/keeper/rewards_test.go

Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>
  • Loading branch information
rhuairahrighairidh and karzak authored Jan 18, 2021
1 parent b93d6b3 commit 067ecde
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 3 deletions.
76 changes: 76 additions & 0 deletions x/incentive/keeper/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/pricefeed"
)

Expand Down Expand Up @@ -138,3 +139,78 @@ func NewPricefeedGenStateMulti() app.GenesisState {
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
}

func NewIncentiveGenState(previousAccumTime, endTime time.Time, rewardPeriods ...incentive.RewardPeriod) app.GenesisState {
var accumulationTimes incentive.GenesisAccumulationTimes
for _, rp := range rewardPeriods {
accumulationTimes = append(
accumulationTimes,
incentive.NewGenesisAccumulationTime(
rp.CollateralType,
previousAccumTime,
sdk.ZeroDec(),
),
)
}
genesis := incentive.NewGenesisState(
incentive.NewParams(
rewardPeriods,
incentive.Multipliers{
incentive.NewMultiplier(incentive.Small, 1, d("0.25")),
incentive.NewMultiplier(incentive.Large, 12, d("1.0")),
},
endTime,
),
accumulationTimes,
incentive.USDXMintingClaims{},
)
return app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(genesis)}
}

func NewCDPGenStateHighInterest() app.GenesisState {
oneYear := time.Hour * 24 * 365
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: oneYear * 100, // never run savings distribution
CollateralParams: cdp.CollateralParams{
{
Denom: "bnb",
Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000051034942716"), // 500% APR
LiquidationPenalty: d("0.05"),
AuctionSize: i(50000000000),
Prefix: 0x22,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
ConversionFactor: i(8),
},
},
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("bnb-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
9 changes: 7 additions & 2 deletions x/incentive/keeper/rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ func (k Keeper) AccumulateRewards(ctx sdk.Context, rewardPeriod types.RewardPeri
return nil
}
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)
rewardFactor := newRewards.ToDec().Quo(totalPrincipal)
cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
rewardFactor := newRewards.ToDec().Mul(cdpFactor).Quo(totalPrincipal)

previousRewardFactor, found := k.GetRewardFactor(ctx, rewardPeriod.CollateralType)
if !found {
Expand Down Expand Up @@ -110,7 +115,7 @@ func (k Keeper) SynchronizeReward(ctx sdk.Context, cdp cdptypes.CDP) {
return
}
claim.RewardIndexes[index].RewardFactor = globalRewardFactor
newRewardsAmount := rewardsAccumulatedFactor.Mul(cdp.GetTotalPrincipal().Amount.ToDec()).RoundInt()
newRewardsAmount := cdp.GetTotalPrincipal().Amount.ToDec().Quo(cdp.InterestFactor).Mul(rewardsAccumulatedFactor).RoundInt()
if newRewardsAmount.IsZero() {
k.SetClaim(ctx, claim)
return
Expand Down
107 changes: 107 additions & 0 deletions x/incentive/keeper/rewards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package keeper_test

import (
"fmt"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"

"github.com/kava-labs/kava/app"
cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/types"
)
Expand Down Expand Up @@ -200,6 +203,101 @@ func (suite *KeeperTestSuite) TestSyncRewards() {

}

func TestRewardCalculation(t *testing.T) {

// Test Params
ctype := "bnb-a"
initialTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
rewardsPerSecond := c("ukava", 122_354)
initialCollateral := c("bnb", 10_000_000_000)
initialPrincipal := c("usdx", 100_000_000)
oneYear := time.Hour * 24 * 365
rewardPeriod := types.NewRewardPeriod(
true,
ctype,
initialTime,
initialTime.Add(4*oneYear),
rewardsPerSecond,
)

// Setup app and module params
_, addrs := app.GeneratePrivKeyAddressPairs(5)
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: initialTime})
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs[:1], []sdk.Coins{cs(initialCollateral)}),
NewPricefeedGenStateMulti(),
NewCDPGenStateHighInterest(),
NewIncentiveGenState(initialTime, initialTime.Add(oneYear), rewardPeriod),
)

// Create a CDP
cdpKeeper := tApp.GetCDPKeeper()
err := cdpKeeper.AddCdp(
ctx,
addrs[0],
initialCollateral,
initialPrincipal,
ctype,
)
require.NoError(t, err)

// Calculate expected cdp reward using iteration

// Use 10 blocks, each a very long 630720s, to total 6307200s or 1/5th of a year
// The cdp stability fee is set to the max value 500%, so this time ensures the debt increases a significant amount (doubles)
// High stability fees increase the chance of catching calculation bugs.
blockTimes := newRepeatingSliceInt(630720, 10)
expectedCDPReward := sdk.ZeroDec() //c(rewardPeriod.RewardsPerSecond.Denom, 0)
for _, bt := range blockTimes {
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Duration(int(time.Second) * bt)))

// run cdp and incentive begin blockers to update factors
tApp.BeginBlocker(ctx, abci.RequestBeginBlock{})

// calculate expected cdp reward
cdpBlockReward, err := calculateCDPBlockReward(ctx, cdpKeeper, addrs[0], ctype, sdk.NewInt(int64(bt)), rewardPeriod)
require.NoError(t, err)
expectedCDPReward = expectedCDPReward.Add(cdpBlockReward)
}

// calculate cdp reward using factor
cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, addrs[0], ctype)
require.True(t, found)
incentiveKeeper := tApp.GetIncentiveKeeper()
require.NotPanics(t, func() {
incentiveKeeper.SynchronizeReward(ctx, cdp)
})
claim, found := incentiveKeeper.GetClaim(ctx, addrs[0])
require.True(t, found)

// Compare two methods of calculation
relativeError := expectedCDPReward.Sub(claim.Reward.Amount.ToDec()).Quo(expectedCDPReward).Abs()
maxError := d("0.0001")
require.Truef(t, relativeError.LT(maxError),
"percent diff %s > %s , expected: %s, actual %s,", relativeError, maxError, expectedCDPReward, claim.Reward.Amount,
)
}

// calculateCDPBlockReward computes the reward that should be distributed to a cdp for the current block.
func calculateCDPBlockReward(ctx sdk.Context, cdpKeeper cdpkeeper.Keeper, owner sdk.AccAddress, ctype string, timeElapsed sdk.Int, rewardPeriod types.RewardPeriod) (sdk.Dec, error) {
// Calculate total rewards to distribute this block
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)

// Calculate cdp's share of total debt
totalPrincipal := cdpKeeper.GetTotalPrincipal(ctx, ctype, types.PrincipalDenom).ToDec()
// cdpDebt
cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, owner, ctype)
if !found {
return sdk.Dec{}, fmt.Errorf("couldn't find cdp for owner '%s' and collateral type '%s'", owner, ctype)
}
accumulatedInterest := cdpKeeper.CalculateNewInterest(ctx, cdp)
cdpDebt := cdp.Principal.Add(cdp.AccumulatedFees).Add(accumulatedInterest).Amount

// Calculate cdp's reward
return newRewards.Mul(cdpDebt).ToDec().Quo(totalPrincipal), nil
}

func (suite *KeeperTestSuite) SetupWithCDPGenState() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
Expand All @@ -214,3 +312,12 @@ func (suite *KeeperTestSuite) SetupWithCDPGenState() {
suite.keeper = keeper
suite.addrs = addrs
}

// newRepeatingSliceInt creates a slice of the specified length containing a single repeating element.
func newRepeatingSliceInt(element int, length int) []int {
slice := make([]int, length)
for i := 0; i < length; i++ {
slice[i] = element
}
return slice
}
2 changes: 1 addition & 1 deletion x/incentive/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ type SupplyKeeper interface {

// CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface {
IterateCdpsByCollateralType(ctx sdk.Context, collateralType string, cb func(cdp cdptypes.CDP) (stop bool))
GetTotalPrincipal(ctx sdk.Context, collateralType string, principalDenom string) (total sdk.Int)
GetCdpByOwnerAndCollateralType(ctx sdk.Context, owner sdk.AccAddress, collateralType string) (cdptypes.CDP, bool)
GetInterestFactor(ctx sdk.Context, collateralType string) (sdk.Dec, bool)
}

// AccountKeeper defines the expected keeper interface for interacting with account
Expand Down

0 comments on commit 067ecde

Please sign in to comment.