From 44fc8d44c6b444e6cf2ac9b95519e2b731cc1dcd Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 14 Jan 2021 14:18:27 +0000 Subject: [PATCH 1/4] add usdx incentive calculation test --- x/incentive/keeper/integration_test.go | 76 ++++++++++++++++++ x/incentive/keeper/rewards_test.go | 106 +++++++++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/x/incentive/keeper/integration_test.go b/x/incentive/keeper/integration_test.go index cfbb974acf..75655e4dd1 100644 --- a/x/incentive/keeper/integration_test.go +++ b/x/incentive/keeper/integration_test.go @@ -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" ) @@ -141,3 +142,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)} +} diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index 8adb4dcbf5..d1a5ffb1b3 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -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" ) @@ -200,6 +203,100 @@ 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 genesis 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), + ) + fmt.Println("RUN INIT GENESIS") + + // Create a CDP + cdpKeeper := tApp.GetCDPKeeper() + err := cdpKeeper.AddCdp( + ctx, + addrs[0], + initialCollateral, + initialPrincipal, + ctype, + ) + require.NoError(t, err) + + // Calculate rewards + // Step through several blocks, calculating the cdp rewards naively + // 6307200 1/5th of a year + blockTimes := newRepeatingSliceInt(630720, 10) // super long blocks + expectedCDPReward := 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 + tApp.BeginBlocker(ctx, abci.RequestBeginBlock{}) + + // replicate incentive calculations + cdpBlockReward, err := calculateCDPBlockReward(ctx, cdpKeeper, addrs[0], ctype, sdk.NewInt(int64(bt)), rewardPeriod) + require.NoError(t, err) + expectedCDPReward = expectedCDPReward.Add(cdpBlockReward) + } + + // compute cdp reward using interest 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) + + require.Equalf(t, expectedCDPReward, claim.Reward, "expected: %s, actual %s", expectedCDPReward, claim.Reward) + +} + +// 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.Coin, 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.Coin{}, 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 + cdpBlockReward := sdk.NewCoin( + rewardPeriod.RewardsPerSecond.Denom, + newRewards.Mul(cdpDebt).ToDec().Quo(totalPrincipal).RoundInt(), + ) + return cdpBlockReward, nil +} + func (suite *KeeperTestSuite) SetupWithCDPGenState() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) @@ -214,3 +311,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 +} From ea53f9fbe3c230a3f59218a502bb9977ededf7f2 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 14 Jan 2021 14:33:12 +0000 Subject: [PATCH 2/4] update reward calculation --- x/incentive/keeper/rewards.go | 9 +++++++-- x/incentive/types/expected_keepers.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index f8edc4cdf4..0e5b27be20 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -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 { @@ -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 diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index 58d635f787..abc4367371 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -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 From 54feead60e0bd86c8d013db5304be81765b9cbbd Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 14 Jan 2021 16:12:19 +0000 Subject: [PATCH 3/4] add allowable error to test criteria --- x/incentive/keeper/rewards_test.go | 38 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index d1a5ffb1b3..30f6712761 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -220,7 +220,7 @@ func TestRewardCalculation(t *testing.T) { rewardsPerSecond, ) - // Setup app and genesis module params + // Setup app and module params _, addrs := app.GeneratePrivKeyAddressPairs(5) tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: initialTime}) @@ -243,24 +243,26 @@ func TestRewardCalculation(t *testing.T) { ) require.NoError(t, err) - // Calculate rewards - // Step through several blocks, calculating the cdp rewards naively - // 6307200 1/5th of a year - blockTimes := newRepeatingSliceInt(630720, 10) // super long blocks - expectedCDPReward := c(rewardPeriod.RewardsPerSecond.Denom, 0) + // 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 + // run cdp and incentive begin blockers to update factors tApp.BeginBlocker(ctx, abci.RequestBeginBlock{}) - // replicate incentive calculations + // 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) } - // compute cdp reward using interest factor + // calculate cdp reward using factor cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, addrs[0], ctype) require.True(t, found) incentiveKeeper := tApp.GetIncentiveKeeper() @@ -270,12 +272,16 @@ func TestRewardCalculation(t *testing.T) { claim, found := incentiveKeeper.GetClaim(ctx, addrs[0]) require.True(t, found) - require.Equalf(t, expectedCDPReward, claim.Reward, "expected: %s, actual %s", expectedCDPReward, claim.Reward) - + // 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.Coin, error) { +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) @@ -284,17 +290,13 @@ func calculateCDPBlockReward(ctx sdk.Context, cdpKeeper cdpkeeper.Keeper, owner // cdpDebt cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, owner, ctype) if !found { - return sdk.Coin{}, fmt.Errorf("couldn't find cdp for owner '%s' and collateral type '%s'", owner, ctype) + 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 - cdpBlockReward := sdk.NewCoin( - rewardPeriod.RewardsPerSecond.Denom, - newRewards.Mul(cdpDebt).ToDec().Quo(totalPrincipal).RoundInt(), - ) - return cdpBlockReward, nil + return newRewards.Mul(cdpDebt).ToDec().Quo(totalPrincipal), nil } func (suite *KeeperTestSuite) SetupWithCDPGenState() { From 4d45a83008d4ab4157a7bfcb43a69cd274c9de3c Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Mon, 18 Jan 2021 09:18:29 -0700 Subject: [PATCH 4/4] Update x/incentive/keeper/rewards_test.go --- x/incentive/keeper/rewards_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/x/incentive/keeper/rewards_test.go b/x/incentive/keeper/rewards_test.go index 30f6712761..2cad11544d 100644 --- a/x/incentive/keeper/rewards_test.go +++ b/x/incentive/keeper/rewards_test.go @@ -230,7 +230,6 @@ func TestRewardCalculation(t *testing.T) { NewCDPGenStateHighInterest(), NewIncentiveGenState(initialTime, initialTime.Add(oneYear), rewardPeriod), ) - fmt.Println("RUN INIT GENESIS") // Create a CDP cdpKeeper := tApp.GetCDPKeeper()