From 73bc32a183dd7935fed80960b8b6f62a1134dc98 Mon Sep 17 00:00:00 2001 From: Derrick Lee Date: Wed, 19 Oct 2022 16:13:37 -0700 Subject: [PATCH] Add incentive earn tests with real keepers (#1354) * Update incentive test to use beginblocker instead manual accumulation * Update integration test suite * Add base integration test, wip staking reward calculation * Get actual staking reward amounts from BeginBlocker events, calculate expected indexes * Simplify event parsing * Add initial earn accum test with real keepers * Add the rest of the accum integration tests with real keepers * Check if delegation rewards are zero before transferring * Update staking integration test to use updated methods --- x/incentive/keeper/keeper_utils_test.go | 48 ++ x/incentive/keeper/msg_server_earn_test.go | 96 ++- x/incentive/keeper/msg_server_swap_test.go | 7 +- x/incentive/keeper/rewards_borrow_test.go | 2 +- .../rewards_earn_accum_integration_test.go | 657 ++++++++++++++++++ .../rewards_earn_staking_integration_test.go | 193 +++++ x/incentive/keeper/rewards_supply_test.go | 2 +- x/incentive/keeper/rewards_usdx_test.go | 6 +- x/incentive/testutil/builder.go | 41 +- x/incentive/testutil/earn_builder.go | 40 ++ x/incentive/testutil/integration.go | 291 +++++++- x/incentive/testutil/mint_builder.go | 68 ++ x/incentive/testutil/staking_builder.go | 38 + x/liquid/keeper/claim.go | 4 + 14 files changed, 1396 insertions(+), 97 deletions(-) create mode 100644 x/incentive/keeper/keeper_utils_test.go create mode 100644 x/incentive/keeper/rewards_earn_accum_integration_test.go create mode 100644 x/incentive/keeper/rewards_earn_staking_integration_test.go create mode 100644 x/incentive/testutil/earn_builder.go create mode 100644 x/incentive/testutil/mint_builder.go create mode 100644 x/incentive/testutil/staking_builder.go diff --git a/x/incentive/keeper/keeper_utils_test.go b/x/incentive/keeper/keeper_utils_test.go new file mode 100644 index 00000000..2dac8ae2 --- /dev/null +++ b/x/incentive/keeper/keeper_utils_test.go @@ -0,0 +1,48 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/types" +) + +// TestKeeper is a test wrapper for the keeper which contains useful methods for testing +type TestKeeper struct { + keeper.Keeper +} + +func (keeper TestKeeper) storeGlobalBorrowIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetHardBorrowRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) storeGlobalSupplyIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetHardSupplyRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) storeGlobalDelegatorIndexes(ctx sdk.Context, multiRewardIndexes types.MultiRewardIndexes) { + // Hardcoded to use bond denom + multiRewardIndex, _ := multiRewardIndexes.GetRewardIndex(types.BondDenom) + keeper.SetDelegatorRewardIndexes(ctx, types.BondDenom, multiRewardIndex.RewardIndexes) +} + +func (keeper TestKeeper) storeGlobalSwapIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetSwapRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) storeGlobalSavingsIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetSavingsRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} + +func (keeper TestKeeper) storeGlobalEarnIndexes(ctx sdk.Context, indexes types.MultiRewardIndexes) { + for _, i := range indexes { + keeper.SetEarnRewardIndexes(ctx, i.CollateralType, i.RewardIndexes) + } +} diff --git a/x/incentive/keeper/msg_server_earn_test.go b/x/incentive/keeper/msg_server_earn_test.go index 0e160627..aab483f5 100644 --- a/x/incentive/keeper/msg_server_earn_test.go +++ b/x/incentive/keeper/msg_server_earn_test.go @@ -6,8 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" + "github.com/cosmos/cosmos-sdk/x/distribution" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/mint" earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive" "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" liquidtypes "github.com/kava-labs/kava/x/liquid/types" @@ -25,20 +28,26 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { WithSimpleAccount(validatorAddr1, cs(c("ukava", 1e12))). WithSimpleAccount(validatorAddr2, cs(c("ukava", 1e12))) - incentBuilder := suite.incentiveBuilder() + incentBuilder := suite.incentiveBuilder(). + WithSimpleEarnRewardPeriod("bkava", cs()) savingsBuilder := testutil.NewSavingsGenesisBuilder(). WithSupportedDenoms("bkava") - earnBuilder := suite.earnBuilder(). - WithVault(earntypes.AllowedVault{ + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ Denom: "bkava", Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, IsPrivateVault: false, AllowedDepositors: nil, }) - suite.SetupWithGenState(authBuilder, incentBuilder, earnBuilder, savingsBuilder) + suite.SetupWithGenState( + authBuilder, + incentBuilder, + earnBuilder, + savingsBuilder, + ) // ak := suite.App.GetAccountKeeper() // bk := suite.App.GetBankKeeper() @@ -48,6 +57,11 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { dk := suite.App.GetDistrKeeper() ik := suite.App.GetIncentiveKeeper() + iParams := ik.GetParams(suite.Ctx) + period, found := iParams.EarnRewardPeriods.GetMultiRewardPeriod("bkava") + suite.Require().True(found) + suite.Require().Equal("bkava", period.CollateralType) + // Use ukava for mint denom mParams := mk.GetParams(suite.Ctx) mParams.MintDenom = "ukava" @@ -84,13 +98,13 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { suite.Require().NoError(err) // Mint liquid tokens - err = suite.DeliverMsgMintDerivative(userAddr1, valAddr1, c("ukava", 1e9)) + _, err = suite.DeliverMsgMintDerivative(userAddr1, valAddr1, c("ukava", 1e9)) suite.Require().NoError(err) - err = suite.DeliverMsgMintDerivative(userAddr2, valAddr1, c("ukava", 99e9)) + _, err = suite.DeliverMsgMintDerivative(userAddr2, valAddr1, c("ukava", 99e9)) suite.Require().NoError(err) - err = suite.DeliverMsgMintDerivative(userAddr2, valAddr2, c("ukava", 99e9)) + _, err = suite.DeliverMsgMintDerivative(userAddr2, valAddr2, c("ukava", 99e9)) suite.Require().NoError(err) // Deposit liquid tokens to earn @@ -118,18 +132,29 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { Power: 100, } + // Query for next block to get staking rewards suite.Ctx = suite.Ctx. WithBlockHeight(suite.Ctx.BlockHeight() + 1). - WithBlockTime(suite.Ctx.BlockTime().Add(1 * time.Hour)) - // Accumulate some staking rewards - _ = suite.App.BeginBlocker(suite.Ctx, abci.RequestBeginBlock{ - LastCommitInfo: abci.LastCommitInfo{ - Votes: []abci.VoteInfo{{ - Validator: val, - SignedLastBlock: true, - }}, + WithBlockTime(suite.Ctx.BlockTime().Add(7 * time.Second)) + + // Mint tokens + mint.BeginBlocker( + suite.Ctx, + suite.App.GetMintKeeper(), + ) + // Distribute to validators, block needs votes + distribution.BeginBlocker( + suite.Ctx, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: true, + }}, + }, }, - }) + dk, + ) liquidMacc := suite.App.GetAccountKeeper().GetModuleAccount(suite.Ctx, liquidtypes.ModuleAccountName) delegation, found := sk.GetDelegation(suite.Ctx, liquidMacc.GetAddress(), valAddr1) @@ -137,18 +162,25 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { // Get amount of rewards endingPeriod := dk.IncrementValidatorPeriod(suite.Ctx, validator1) - delegationRewards := dk.CalculateDelegationRewards(suite.Ctx, validator1, delegation, endingPeriod) - // Accumulate rewards - claim rewards - rewardPeriod := types.NewMultiRewardPeriod( - true, - "bkava", // reward period is set for "bkava" to apply to all vaults - time.Unix(0, 0), // ensure the test is within start and end times - distantFuture, - cs(), // no incentives, so only the staking rewards are distributed + // Zero rewards since this block is the same as the block it was last claimed + + // This needs to run **after** staking rewards are minted/distributed in + // x/mint + x/distribution but **before** the x/incentive BeginBlocker. + + // Order of operations: + // 1. x/mint + x/distribution BeginBlocker + // 2. CalculateDelegationRewards + // 3. x/incentive BeginBlocker to claim staking rewards + delegationRewards := dk.CalculateDelegationRewards(suite.Ctx, validator1, delegation, endingPeriod) + suite.Require().False(delegationRewards.IsZero(), "expected non-zero delegation rewards") + + // Claim staking rewards via incentive. + // Block height was updated earlier. + incentive.BeginBlocker( + suite.Ctx, + ik, ) - err = ik.AccumulateEarnRewards(suite.Ctx, rewardPeriod) - suite.Require().NoError(err) preClaimBal1 := suite.GetBalance(userAddr1) preClaimBal2 := suite.GetBalance(userAddr2) @@ -171,15 +203,15 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { // User 2 gets 99% of rewards stakingRewards1 := delegationRewards. AmountOf("ukava"). - QuoInt64(100). + Quo(sdk.NewDec(100)). RoundInt() suite.BalanceEquals(userAddr1, preClaimBal1.Add(sdk.NewCoin("ukava", stakingRewards1))) // Total * 99 / 100 stakingRewards2 := delegationRewards. AmountOf("ukava"). - MulInt64(99). - QuoInt64(100). + Mul(sdk.NewDec(99)). + Quo(sdk.NewDec(100)). TruncateInt() suite.BalanceEquals(userAddr2, preClaimBal2.Add(sdk.NewCoin("ukava", stakingRewards2))) @@ -189,9 +221,3 @@ func (suite *HandlerTestSuite) TestEarnLiquidClaim() { suite.EarnRewardEquals(userAddr1, cs()) suite.EarnRewardEquals(userAddr2, cs()) } - -// earnBuilder returns a new earn genesis builder with a genesis time and multipliers set -func (suite *HandlerTestSuite) earnBuilder() testutil.EarnGenesisBuilder { - return testutil.NewEarnGenesisBuilder(). - WithGenesisTime(suite.genesisTime) -} diff --git a/x/incentive/keeper/msg_server_swap_test.go b/x/incentive/keeper/msg_server_swap_test.go index 92a2fba8..7a5b9a12 100644 --- a/x/incentive/keeper/msg_server_swap_test.go +++ b/x/incentive/keeper/msg_server_swap_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" "github.com/stretchr/testify/suite" @@ -46,11 +45,7 @@ func (suite *HandlerTestSuite) SetupApp() { suite.Ctx = suite.App.NewContext(true, tmproto.Header{Height: 1, Time: suite.genesisTime}) } -type genesisBuilder interface { - BuildMarshalled(cdc codec.JSONCodec) app.GenesisState -} - -func (suite *HandlerTestSuite) SetupWithGenState(builders ...genesisBuilder) { +func (suite *HandlerTestSuite) SetupWithGenState(builders ...testutil.GenesisBuilder) { suite.SetupApp() builtGenStates := []app.GenesisState{ diff --git a/x/incentive/keeper/rewards_borrow_test.go b/x/incentive/keeper/rewards_borrow_test.go index 27674779..de089a98 100644 --- a/x/incentive/keeper/rewards_borrow_test.go +++ b/x/incentive/keeper/rewards_borrow_test.go @@ -56,8 +56,8 @@ func (suite *BorrowIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncin WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6))) // only borrow rewards suite.SetApp() + suite.WithGenesisTime(suite.genesisTime) suite.StartChain( - suite.genesisTime, NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewHardGenStateMulti(suite.genesisTime).BuildMarshalled(suite.App.AppCodec()), authBulder.BuildMarshalled(suite.App.AppCodec()), diff --git a/x/incentive/keeper/rewards_earn_accum_integration_test.go b/x/incentive/keeper/rewards_earn_accum_integration_test.go new file mode 100644 index 00000000..51e3dfa0 --- /dev/null +++ b/x/incentive/keeper/rewards_earn_accum_integration_test.go @@ -0,0 +1,657 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" +) + +type AccumulateEarnRewardsIntegrationTests struct { + testutil.IntegrationTester + + keeper TestKeeper + userAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestAccumulateEarnRewardsIntegrationTests(t *testing.T) { + suite.Run(t, new(AccumulateEarnRewardsIntegrationTests)) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + suite.userAddrs = addrs[0:2] + suite.valAddrs = []sdk.ValAddress{ + sdk.ValAddress(addrs[2]), + sdk.ValAddress(addrs[3]), + } + + // Setup app with test state + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(addrs[0], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[1], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[2], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[3], cs(c("ukava", 1e12))) + + incentiveBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.GenesisTime). + WithSimpleEarnRewardPeriod("bkava", cs()) + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + stakingBuilder := testutil.NewStakingGenesisBuilder() + + mintBuilder := testutil.NewMintGenesisBuilder(). + WithInflationMax(sdk.OneDec()). + WithInflationMin(sdk.OneDec()). + WithMinter(sdk.OneDec(), sdk.ZeroDec()). + WithMintDenom("ukava") + + suite.StartChainWithBuilders( + authBuilder, + incentiveBuilder, + savingsBuilder, + earnBuilder, + stakingBuilder, + mintBuilder, + ) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUpdatedWhenBlockTimeHasIncreased() { + suite.AddIncentiveEarnMultiRewardPeriod( + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 800000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 200000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + + suite.keeper.storeGlobalEarnIndexes(suite.Ctx, globalIndexes) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 0") + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + + // check time and factors + + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + stakingRewardIndexes0 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + stakingRewardIndexes1 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + suite.StoredEarnIndexesEqual(derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64").Add(stakingRewardIndexes0), + }, + }) + suite.StoredEarnIndexesEqual(derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64").Add(stakingRewardIndexes1), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUpdatedWhenBlockTimeHasIncreased_partialDeposit() { + suite.AddIncentiveEarnMultiRewardPeriod( + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + // 800000bkava0 minted, 700000 deposited + // 200000bkava1 minted, 100000 deposited + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 800000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 200000)) + suite.NoError(err) + + depositAmount0 := c(derivative0.Denom, 700000) + depositAmount1 := c(derivative1.Denom, 100000) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], depositAmount0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], depositAmount1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + + suite.keeper.storeGlobalEarnIndexes(suite.Ctx, globalIndexes) + + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 0") + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + + // check time and factors + + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + // Divided by deposit amounts, not bank supply amounts + stakingRewardIndexes0 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(depositAmount0.Amount.ToDec()) + + stakingRewardIndexes1 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(depositAmount1.Amount.ToDec()) + + // Slightly increased rewards due to less bkava deposited + suite.StoredEarnIndexesEqual(derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("8.248571428571428571"), + }, + { + CollateralType: "ukava", + RewardFactor: d("4.154285714285714285").Add(stakingRewardIndexes0), + }, + }) + + suite.StoredEarnIndexesEqual(derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("14.42"), + }, + { + CollateralType: "ukava", + RewardFactor: d("7.24").Add(stakingRewardIndexes1), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() { + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.storeGlobalEarnIndexes(suite.Ctx, previousIndexes) + + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative1.Denom, suite.Ctx.BlockTime()) + + period := types.NewMultiRewardPeriod( + true, + "bkava", + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + // Must manually accumulate rewards as BeginBlockers only run when the block time increases + // This does not run any x/mint or x/distribution BeginBlockers + suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + + // check time and factors + + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + expected, f := previousIndexes.Get(derivative0.Denom) + suite.True(f) + suite.StoredEarnIndexesEqual(derivative0.Denom, expected) + + expected, f = previousIndexes.Get(derivative1.Denom) + suite.True(f) + suite.StoredEarnIndexesEqual(derivative1.Denom, expected) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestNoAccumulationWhenSourceSharesAreZero() { + suite.AddIncentiveEarnMultiRewardPeriod( + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + // No earn deposits + + previousIndexes := types.MultiRewardIndexes{ + { + CollateralType: derivative0.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + CollateralType: derivative1.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "earn", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + } + suite.keeper.storeGlobalEarnIndexes(suite.Ctx, previousIndexes) + + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative0.Denom, suite.Ctx.BlockTime()) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, derivative1.Denom, suite.Ctx.BlockTime()) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, _ = suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + // check time and factors + + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + expected, f := previousIndexes.Get(derivative0.Denom) + suite.True(f) + suite.StoredEarnIndexesEqual(derivative0.Denom, expected) + + expected, f = previousIndexes.Get(derivative1.Denom) + suite.True(f) + suite.StoredEarnIndexesEqual(derivative1.Denom, expected) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestStateAddedWhenStateDoesNotExist() { + suite.AddIncentiveEarnMultiRewardPeriod( + types.NewMultiRewardPeriod( + true, + "bkava", // reward period is set for "bkava" to apply to all vaults + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("earn", 2000), c("ukava", 1000)), // same denoms as in global indexes + ), + ) + + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], derivative0, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[1], derivative1, earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + val0 := suite.GetAbciValidator(suite.valAddrs[0]) + val1 := suite.GetAbciValidator(suite.valAddrs[1]) + + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + // After the second accumulation both current block time and indexes should be stored. + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + validatorRewards0, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + firstStakingRewardIndexes0 := validatorRewards0[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + firstStakingRewardIndexes1 := validatorRewards0[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + // After the first accumulation only the current block time should be stored. + // The indexes will be empty as no time has passed since the previous block because it didn't exist. + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + // First accumulation can have staking rewards, but no other rewards + suite.StoredEarnIndexesEqual(derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: firstStakingRewardIndexes0, + }, + }) + suite.StoredEarnIndexesEqual(derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: firstStakingRewardIndexes1, + }, + }) + + _, resBeginBlock = suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: val0, + SignedLastBlock: true, + }, + { + Validator: val1, + SignedLastBlock: true, + }, + }, + }, + }, + ) + + // After the second accumulation both current block time and indexes should be stored. + suite.StoredEarnTimeEquals(derivative0.Denom, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(derivative1.Denom, suite.Ctx.BlockTime()) + + validatorRewards1, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + secondStakingRewardIndexes0 := validatorRewards1[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative0.Amount.ToDec()) + + secondStakingRewardIndexes1 := validatorRewards1[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(derivative1.Amount.ToDec()) + + // Second accumulation has both staking rewards and incentive rewards + // ukava incentive rewards: 3600 * 1000 / (2 * 1000000) == 1.8 + suite.StoredEarnIndexesEqual(derivative0.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + // Incentive rewards + both staking rewards + RewardFactor: d("1.8").Add(firstStakingRewardIndexes0).Add(secondStakingRewardIndexes0), + }, + { + CollateralType: "earn", + RewardFactor: d("3.6"), + }, + }) + suite.StoredEarnIndexesEqual(derivative1.Denom, types.RewardIndexes{ + { + CollateralType: "ukava", + // Incentive rewards + both staking rewards + RewardFactor: d("1.8").Add(firstStakingRewardIndexes1).Add(secondStakingRewardIndexes1), + }, + { + CollateralType: "earn", + RewardFactor: d("3.6"), + }, + }) +} + +func (suite *AccumulateEarnRewardsIntegrationTests) TestNoPanicWhenStateDoesNotExist() { + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], c("ukava", 1000000)) + suite.NoError(err) + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[1], suite.valAddrs[1], c("ukava", 1000000)) + suite.NoError(err) + + period := types.NewMultiRewardPeriod( + true, + "bkava", + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(), + ) + + // Accumulate with no earn shares and no rewards per second will result in no increment to the indexes. + // No increment and no previous indexes stored, results in an updated of nil. Setting this in the state panics. + // Check there is no panic. + suite.NotPanics(func() { + // This does not update any state, as there are no bkava vaults + // to iterate over, denoms are unknown + suite.keeper.AccumulateEarnRewards(suite.Ctx, period) + }) + + // Times are not stored for vaults with no state + suite.StoredEarnTimeEquals(derivative0.Denom, time.Time{}) + suite.StoredEarnTimeEquals(derivative1.Denom, time.Time{}) + suite.StoredEarnIndexesEqual(derivative0.Denom, nil) + suite.StoredEarnIndexesEqual(derivative1.Denom, nil) +} diff --git a/x/incentive/keeper/rewards_earn_staking_integration_test.go b/x/incentive/keeper/rewards_earn_staking_integration_test.go new file mode 100644 index 00000000..f909bc42 --- /dev/null +++ b/x/incentive/keeper/rewards_earn_staking_integration_test.go @@ -0,0 +1,193 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" +) + +type EarnStakingRewardsIntegrationTestSuite struct { + testutil.IntegrationTester + + keeper TestKeeper + userAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestEarnStakingRewardsIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(EarnStakingRewardsIntegrationTestSuite)) +} + +func (suite *EarnStakingRewardsIntegrationTestSuite) SetupTest() { + suite.IntegrationTester.SetupTest() + + suite.keeper = TestKeeper{ + Keeper: suite.App.GetIncentiveKeeper(), + } + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + suite.userAddrs = addrs[0:2] + suite.valAddrs = []sdk.ValAddress{ + sdk.ValAddress(addrs[2]), + sdk.ValAddress(addrs[3]), + } + + // Setup app with test state + authBuilder := app.NewAuthBankGenesisBuilder(). + WithSimpleAccount(addrs[0], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[1], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[2], cs(c("ukava", 1e12))). + WithSimpleAccount(addrs[3], cs(c("ukava", 1e12))) + + incentiveBuilder := testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.GenesisTime). + WithSimpleEarnRewardPeriod("bkava", cs()) + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := testutil.NewEarnGenesisBuilder(). + WithAllowedVaults(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + stakingBuilder := testutil.NewStakingGenesisBuilder() + + mintBuilder := testutil.NewMintGenesisBuilder(). + WithInflationMax(sdk.OneDec()). + WithInflationMin(sdk.OneDec()). + WithMinter(sdk.OneDec(), sdk.ZeroDec()). + WithMintDenom("ukava") + + suite.StartChainWithBuilders( + authBuilder, + incentiveBuilder, + savingsBuilder, + earnBuilder, + stakingBuilder, + mintBuilder, + ) +} + +func (suite *EarnStakingRewardsIntegrationTestSuite) TestStakingRewardsDistributed() { + // derivative 1: 8 total staked, 7 to earn, 1 not in earn + // derivative 2: 2 total staked, 1 to earn, 1 not in earn + userMintAmount0 := c("ukava", 8e9) + userMintAmount1 := c("ukava", 2e9) + + userDepositAmount0 := i(7e9) + userDepositAmount1 := i(1e9) + + // Create two validators + derivative0, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[0], userMintAmount0) + suite.Require().NoError(err) + + derivative1, err := suite.MintLiquidAnyValAddr(suite.userAddrs[0], suite.valAddrs[1], userMintAmount1) + suite.Require().NoError(err) + + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], sdk.NewCoin(derivative0.Denom, userDepositAmount0), earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + err = suite.DeliverEarnMsgDeposit(suite.userAddrs[0], sdk.NewCoin(derivative1.Denom, userDepositAmount1), earntypes.STRATEGY_TYPE_SAVINGS) + suite.NoError(err) + + // Get derivative denoms + lq := suite.App.GetLiquidKeeper() + vaultDenom1 := lq.GetLiquidStakingTokenDenom(suite.valAddrs[0]) + vaultDenom2 := lq.GetLiquidStakingTokenDenom(suite.valAddrs[1]) + + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.Ctx = suite.Ctx.WithBlockTime(previousAccrualTime) + + initialVault1RewardFactor := d("0.04") + initialVault2RewardFactor := d("0.04") + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: vaultDenom1, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault1RewardFactor, + }, + }, + }, + { + CollateralType: vaultDenom2, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault2RewardFactor, + }, + }, + }, + } + + suite.keeper.storeGlobalEarnIndexes(suite.Ctx, globalIndexes) + + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, vaultDenom1, suite.Ctx.BlockTime()) + suite.keeper.SetEarnRewardAccrualTime(suite.Ctx, vaultDenom2, suite.Ctx.BlockTime()) + + val := suite.GetAbciValidator(suite.valAddrs[0]) + + // Mint tokens, distribute to validators, claim staking rewards + // 1 hour later + _, resBeginBlock := suite.NextBlockAfterWithReq( + 1*time.Hour, + abci.RequestEndBlock{}, + abci.RequestBeginBlock{ + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: true, + }}, + }, + }, + ) + + // check time and factors + suite.StoredEarnTimeEquals(vaultDenom1, suite.Ctx.BlockTime()) + suite.StoredEarnTimeEquals(vaultDenom2, suite.Ctx.BlockTime()) + + validatorRewards, _ := suite.GetBeginBlockClaimedStakingRewards(resBeginBlock) + + suite.Require().Contains(validatorRewards, suite.valAddrs[0].String(), "there should be claim events for validator 1") + suite.Require().Contains(validatorRewards, suite.valAddrs[1].String(), "there should be claim events for validator 2") + + // Total staking rewards / total source shares (**deposited in earn** not total minted) + // types.RewardIndexes.Quo() uses Dec.Quo() which uses bankers rounding. + // So we need to use Dec.Quo() to also round vs Dec.QuoInt() which truncates + expectedIndexes1 := validatorRewards[suite.valAddrs[0].String()]. + AmountOf("ukava"). + ToDec(). + Quo(userDepositAmount0.ToDec()) + + expectedIndexes2 := validatorRewards[suite.valAddrs[1].String()]. + AmountOf("ukava"). + ToDec(). + Quo(userDepositAmount1.ToDec()) + + // Only contains staking rewards + suite.StoredEarnIndexesEqual(vaultDenom1, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault1RewardFactor.Add(expectedIndexes1), + }, + }) + + suite.StoredEarnIndexesEqual(vaultDenom2, types.RewardIndexes{ + { + CollateralType: "ukava", + RewardFactor: initialVault2RewardFactor.Add(expectedIndexes2), + }, + }) +} diff --git a/x/incentive/keeper/rewards_supply_test.go b/x/incentive/keeper/rewards_supply_test.go index bc2b9190..b4775f58 100644 --- a/x/incentive/keeper/rewards_supply_test.go +++ b/x/incentive/keeper/rewards_supply_test.go @@ -57,8 +57,8 @@ func (suite *SupplyIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncin suite.SetApp() + suite.WithGenesisTime(suite.genesisTime) suite.StartChain( - suite.genesisTime, NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewHardGenStateMulti(suite.genesisTime).BuildMarshalled(suite.App.AppCodec()), authBulder.BuildMarshalled(suite.App.AppCodec()), diff --git a/x/incentive/keeper/rewards_usdx_test.go b/x/incentive/keeper/rewards_usdx_test.go index 02f1bb6f..e7561f09 100644 --- a/x/incentive/keeper/rewards_usdx_test.go +++ b/x/incentive/keeper/rewards_usdx_test.go @@ -63,8 +63,8 @@ func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncing( WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() + suite.WithGenesisTime(suite.genesisTime) suite.StartChain( - suite.genesisTime, NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), authBulder.BuildMarshalled(suite.App.AppCodec()), @@ -121,8 +121,8 @@ func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsWithoutSyncin WithSimpleUSDXRewardPeriod(collateralType, c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() + suite.WithGenesisTime(suite.genesisTime) suite.StartChain( - suite.genesisTime, authBuilder.BuildMarshalled(suite.App.AppCodec()), NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), @@ -167,8 +167,8 @@ func (suite *USDXIntegrationTests) TestReinstatingRewardParamsDoesNotTriggerOver WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() + suite.WithGenesisTime(suite.genesisTime) suite.StartChain( - suite.genesisTime, authBuilder.BuildMarshalled(suite.App.AppCodec()), NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), diff --git a/x/incentive/testutil/builder.go b/x/incentive/testutil/builder.go index 4b72f3b2..3190bedd 100644 --- a/x/incentive/testutil/builder.go +++ b/x/incentive/testutil/builder.go @@ -7,7 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/app" - earntypes "github.com/kava-labs/kava/x/earn/types" hardtypes "github.com/kava-labs/kava/x/hard/types" "github.com/kava-labs/kava/x/incentive/types" savingstypes "github.com/kava-labs/kava/x/savings/types" @@ -17,6 +16,10 @@ const ( oneYear time.Duration = time.Hour * 24 * 365 ) +type GenesisBuilder interface { + BuildMarshalled(cdc codec.JSONCodec) app.GenesisState +} + // IncentiveGenesisBuilder is a tool for creating an incentive genesis state. // Helper methods add values onto a default genesis state. // All methods are immutable and return updated copies of the builder. @@ -302,42 +305,6 @@ func (builder IncentiveGenesisBuilder) WithSimpleSavingsRewardPeriod(ctype strin return builder.WithInitializedSavingsRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond)) } -// EarnGenesisBuilder is a tool for creating a earn genesis state. -// Helper methods add values onto a default genesis state. -// All methods are immutable and return updated copies of the builder. -type EarnGenesisBuilder struct { - earntypes.GenesisState - genesisTime time.Time -} - -func NewEarnGenesisBuilder() EarnGenesisBuilder { - return EarnGenesisBuilder{ - GenesisState: earntypes.DefaultGenesisState(), - } -} - -func (builder EarnGenesisBuilder) Build() earntypes.GenesisState { - return builder.GenesisState -} - -func (builder EarnGenesisBuilder) BuildMarshalled(cdc codec.JSONCodec) app.GenesisState { - built := builder.Build() - - return app.GenesisState{ - earntypes.ModuleName: cdc.MustMarshalJSON(&built), - } -} - -func (builder EarnGenesisBuilder) WithGenesisTime(genTime time.Time) EarnGenesisBuilder { - builder.genesisTime = genTime - return builder -} - -func (builder EarnGenesisBuilder) WithVault(vault earntypes.AllowedVault) EarnGenesisBuilder { - builder.Params.AllowedVaults = append(builder.Params.AllowedVaults, vault) - return builder -} - // SavingsGenesisBuilder is a tool for creating a savings genesis state. // Helper methods add values onto a default genesis state. // All methods are immutable and return updated copies of the builder. diff --git a/x/incentive/testutil/earn_builder.go b/x/incentive/testutil/earn_builder.go new file mode 100644 index 00000000..010ab057 --- /dev/null +++ b/x/incentive/testutil/earn_builder.go @@ -0,0 +1,40 @@ +package testutil + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/kava-labs/kava/app" + + earntypes "github.com/kava-labs/kava/x/earn/types" +) + +// EarnGenesisBuilder is a tool for creating a earn genesis state. +// Helper methods add values onto a default genesis state. +// All methods are immutable and return updated copies of the builder. +type EarnGenesisBuilder struct { + earntypes.GenesisState +} + +var _ GenesisBuilder = (*EarnGenesisBuilder)(nil) + +func NewEarnGenesisBuilder() EarnGenesisBuilder { + return EarnGenesisBuilder{ + GenesisState: earntypes.DefaultGenesisState(), + } +} + +func (builder EarnGenesisBuilder) Build() earntypes.GenesisState { + return builder.GenesisState +} + +func (builder EarnGenesisBuilder) BuildMarshalled(cdc codec.JSONCodec) app.GenesisState { + built := builder.Build() + + return app.GenesisState{ + earntypes.ModuleName: cdc.MustMarshalJSON(&built), + } +} + +func (builder EarnGenesisBuilder) WithAllowedVaults(vault ...earntypes.AllowedVault) EarnGenesisBuilder { + builder.Params.AllowedVaults = append(builder.Params.AllowedVaults, vault...) + return builder +} diff --git a/x/incentive/testutil/integration.go b/x/incentive/testutil/integration.go index dc01d9c9..0bf1054d 100644 --- a/x/incentive/testutil/integration.go +++ b/x/incentive/testutil/integration.go @@ -10,6 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -30,6 +31,8 @@ import ( "github.com/kava-labs/kava/x/incentive/types" liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper" liquidtypes "github.com/kava-labs/kava/x/liquid/types" + routerkeeper "github.com/kava-labs/kava/x/router/keeper" + routertypes "github.com/kava-labs/kava/x/router/types" swapkeeper "github.com/kava-labs/kava/x/swap/keeper" swaptypes "github.com/kava-labs/kava/x/swap/types" ) @@ -40,42 +43,98 @@ type IntegrationTester struct { suite.Suite App app.TestApp Ctx sdk.Context + + GenesisTime time.Time } func (suite *IntegrationTester) SetupSuite() { config := sdk.GetConfig() app.SetBech32AddressPrefixes(config) + + // Default genesis time, can be overridden with WithGenesisTime + suite.GenesisTime = time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) } func (suite *IntegrationTester) SetApp() { suite.App = app.NewTestApp() } -func (suite *IntegrationTester) StartChain(genesisTime time.Time, genesisStates ...app.GenesisState) { +func (suite *IntegrationTester) SetupTest() { + suite.SetApp() +} + +func (suite *IntegrationTester) WithGenesisTime(genesisTime time.Time) { + suite.GenesisTime = genesisTime +} + +func (suite *IntegrationTester) StartChainWithBuilders(builders ...GenesisBuilder) { + var builtGenStates []app.GenesisState + for _, builder := range builders { + builtGenStates = append(builtGenStates, builder.BuildMarshalled(suite.App.AppCodec())) + } + + suite.StartChain(builtGenStates...) +} + +func (suite *IntegrationTester) StartChain(genesisStates ...app.GenesisState) { suite.App.InitializeFromGenesisStatesWithTimeAndChainID( - genesisTime, + suite.GenesisTime, testChainID, genesisStates..., ) - suite.Ctx = suite.App.NewContext(false, tmproto.Header{Height: 1, Time: genesisTime, ChainID: testChainID}) + suite.Ctx = suite.App.NewContext(false, tmproto.Header{ + Height: 1, + Time: suite.GenesisTime, + ChainID: testChainID, + }) } -func (suite *IntegrationTester) NextBlockAt(blockTime time.Time) { +func (suite *IntegrationTester) NextBlockAfter(blockDuration time.Duration) { + suite.NextBlockAfterWithReq( + blockDuration, + abcitypes.RequestEndBlock{}, + abcitypes.RequestBeginBlock{}, + ) +} + +func (suite *IntegrationTester) NextBlockAfterWithReq( + blockDuration time.Duration, + reqEnd abcitypes.RequestEndBlock, + reqBegin abcitypes.RequestBeginBlock, +) (abcitypes.ResponseEndBlock, abcitypes.ResponseBeginBlock) { + return suite.NextBlockAtWithRequest( + suite.Ctx.BlockTime().Add(blockDuration), + reqEnd, + reqBegin, + ) +} + +func (suite *IntegrationTester) NextBlockAt( + blockTime time.Time, +) (abcitypes.ResponseEndBlock, abcitypes.ResponseBeginBlock) { + return suite.NextBlockAtWithRequest( + blockTime, + abcitypes.RequestEndBlock{}, + abcitypes.RequestBeginBlock{}, + ) +} + +func (suite *IntegrationTester) NextBlockAtWithRequest( + blockTime time.Time, + reqEnd abcitypes.RequestEndBlock, + reqBegin abcitypes.RequestBeginBlock, +) (abcitypes.ResponseEndBlock, abcitypes.ResponseBeginBlock) { if !suite.Ctx.BlockTime().Before(blockTime) { panic(fmt.Sprintf("new block time %s must be after current %s", blockTime, suite.Ctx.BlockTime())) } blockHeight := suite.Ctx.BlockHeight() + 1 - _ = suite.App.EndBlocker(suite.Ctx, abcitypes.RequestEndBlock{}) - + responseEndBlock := suite.App.EndBlocker(suite.Ctx, reqEnd) suite.Ctx = suite.Ctx.WithBlockTime(blockTime).WithBlockHeight(blockHeight).WithChainID(testChainID) + responseBeginBlock := suite.App.BeginBlocker(suite.Ctx, reqBegin) // height and time in RequestBeginBlock are ignored by module begin blockers - _ = suite.App.BeginBlocker(suite.Ctx, abcitypes.RequestBeginBlock{}) // height and time in RequestBeginBlock are ignored by module begin blockers -} - -func (suite *IntegrationTester) NextBlockAfter(blockDuration time.Duration) { - suite.NextBlockAt(suite.Ctx.BlockTime().Add(blockDuration)) + return responseEndBlock, responseBeginBlock } func (suite *IntegrationTester) DeliverIncentiveMsg(msg sdk.Msg) error { @@ -101,6 +160,46 @@ func (suite *IntegrationTester) DeliverIncentiveMsg(msg sdk.Msg) error { return err } +// MintLiquidAnyValAddr mints liquid tokens with the given validator address, +// creating the validator if it does not already exist. +// **Note:** This will increment the block height/time and run the End and Begin +// blockers! +func (suite *IntegrationTester) MintLiquidAnyValAddr( + owner sdk.AccAddress, + validator sdk.ValAddress, + amount sdk.Coin, +) (sdk.Coin, error) { + // Check if validator already created + _, found := suite.App.GetStakingKeeper().GetValidator(suite.Ctx, validator) + if !found { + // Create validator + if err := suite.DeliverMsgCreateValidator(validator, sdk.NewCoin("ukava", sdk.NewInt(1e9))); err != nil { + return sdk.Coin{}, err + } + + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + } + + // Delegate and mint liquid tokens + return suite.DeliverMsgDelegateMint(owner, validator, amount) +} + +func (suite *IntegrationTester) GetAbciValidator(valAddr sdk.ValAddress) abcitypes.Validator { + sk := suite.App.GetStakingKeeper() + + val, found := sk.GetValidator(suite.Ctx, valAddr) + suite.Require().True(found) + + pk, err := val.ConsPubKey() + suite.Require().NoError(err) + + return abcitypes.Validator{ + Address: pk.Address(), + Power: val.GetConsensusPower(sk.PowerReduction(suite.Ctx)), + } +} + func (suite *IntegrationTester) DeliverMsgCreateValidator(address sdk.ValAddress, selfDelegation sdk.Coin) error { msg, err := stakingtypes.NewMsgCreateValidator( address, @@ -205,12 +304,17 @@ func (suite *IntegrationTester) DeliverMsgMintDerivative( sender sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin, -) error { +) (sdk.Coin, error) { msg := liquidtypes.NewMsgMintDerivative(sender, validator, amount) msgServer := liquidkeeper.NewMsgServerImpl(suite.App.GetLiquidKeeper()) - _, err := msgServer.MintDerivative(sdk.WrapSDKContext(suite.Ctx), &msg) - return err + res, err := msgServer.MintDerivative(sdk.WrapSDKContext(suite.Ctx), &msg) + if err != nil { + // Instead of returning res.Received, as res will be nil if there is an error + return sdk.Coin{}, err + } + + return res.Received, err } func (suite *IntegrationTester) DeliverEarnMsgDeposit( @@ -300,6 +404,9 @@ func (suite *IntegrationTester) VestingPeriodsEqual(address sdk.AccAddress, expe suite.Equal(expectedPeriods, vacc.VestingPeriods) } +// ----------------------------------------------------------------------------- +// x/incentive + func (suite *IntegrationTester) SwapRewardEquals(owner sdk.AccAddress, expected sdk.Coins) { claim, found := suite.App.GetIncentiveKeeper().GetSwapClaim(suite.Ctx, owner) suite.Require().Truef(found, "expected swap claim to be found for %s", owner) @@ -338,3 +445,159 @@ func (suite *IntegrationTester) AddTestAddrsFromPubKeys(ctx sdk.Context, pubKeys suite.App.FundAccount(ctx, sdk.AccAddress(pk.Address()), initCoins) } } + +func (suite *IntegrationTester) StoredEarnTimeEquals(denom string, expected time.Time) { + storedTime, found := suite.App.GetIncentiveKeeper().GetEarnRewardAccrualTime(suite.Ctx, denom) + suite.Equal(found, expected != time.Time{}, "expected time is %v but time found = %v", expected, found) + if found { + suite.Equal(expected, storedTime) + } else { + suite.Empty(storedTime) + } +} + +func (suite *IntegrationTester) StoredEarnIndexesEqual(denom string, expected types.RewardIndexes) { + storedIndexes, found := suite.App.GetIncentiveKeeper().GetEarnRewardIndexes(suite.Ctx, denom) + suite.Equal(found, expected != nil) + + if found { + suite.Equal(expected, storedIndexes) + } else { + // Can't compare Equal for types.RewardIndexes(nil) vs types.RewardIndexes{} + suite.Empty(storedIndexes) + } +} + +func (suite *IntegrationTester) AddIncentiveEarnMultiRewardPeriod(period types.MultiRewardPeriod) { + ik := suite.App.GetIncentiveKeeper() + params := ik.GetParams(suite.Ctx) + + for i, reward := range params.EarnRewardPeriods { + if reward.CollateralType == period.CollateralType { + // Replace existing reward period if the collateralType exists. + // Params are invalid if there are multiple reward periods for the + // same collateral type. + params.EarnRewardPeriods[i] = period + ik.SetParams(suite.Ctx, params) + return + } + } + + params.EarnRewardPeriods = append(params.EarnRewardPeriods, period) + + suite.NoError(params.Validate()) + ik.SetParams(suite.Ctx, params) +} + +// ----------------------------------------------------------------------------- +// x/router + +func (suite *IntegrationTester) DeliverRouterMsgDelegateMintDeposit( + depositor sdk.AccAddress, + validator sdk.ValAddress, + amount sdk.Coin, +) error { + msg := routertypes.MsgDelegateMintDeposit{ + Depositor: depositor.String(), + Validator: validator.String(), + Amount: amount, + } + msgServer := routerkeeper.NewMsgServerImpl(suite.App.GetRouterKeeper()) + + _, err := msgServer.DelegateMintDeposit(sdk.WrapSDKContext(suite.Ctx), &msg) + return err +} + +func (suite *IntegrationTester) DeliverRouterMsgMintDeposit( + depositor sdk.AccAddress, + validator sdk.ValAddress, + amount sdk.Coin, +) error { + msg := routertypes.MsgMintDeposit{ + Depositor: depositor.String(), + Validator: validator.String(), + Amount: amount, + } + msgServer := routerkeeper.NewMsgServerImpl(suite.App.GetRouterKeeper()) + + _, err := msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), &msg) + return err +} + +func (suite *IntegrationTester) DeliverMsgDelegateMint( + delegator sdk.AccAddress, + validator sdk.ValAddress, + amount sdk.Coin, +) (sdk.Coin, error) { + if err := suite.DeliverMsgDelegate(delegator, validator, amount); err != nil { + return sdk.Coin{}, err + } + + return suite.DeliverMsgMintDerivative(delegator, validator, amount) +} + +// ----------------------------------------------------------------------------- +// x/distribution + +func (suite *IntegrationTester) GetBeginBlockClaimedStakingRewards( + resBeginBlock abcitypes.ResponseBeginBlock, +) (validatorRewards map[string]sdk.Coins, totalRewards sdk.Coins) { + // Events emitted in BeginBlocker are in the ResponseBeginBlock, not in + // ctx.EventManager().Events() as BeginBlock is called with a NewEventManager() + // cosmos-sdk/types/module/module.go: func(m *Manager) BeginBlock(...) + + // We also need to parse the events to get the rewards as querying state will + // always contain 0 rewards -- rewards are always claimed right after + // mint+distribution in BeginBlocker which resets distribution state back to + // 0 for reward amounts + blockRewardsClaimed := make(map[string]sdk.Coins) + for _, event := range resBeginBlock.Events { + if event.Type != distributiontypes.EventTypeWithdrawRewards { + continue + } + + // Example event attributes, amount can be empty for no rewards + // + // Event: withdraw_rewards + // - amount: + // - validator: kavavaloper1em2mlkrkx0qsa6327tgvl3g0fh8a95hjnqvrwh + // Event: withdraw_rewards + // - amount: 523909ukava + // - validator: kavavaloper1nmgpgr8l4t8pw9zqx9cltuymvz85wmw9sy8kjy + attrsMap := attrsToMap(event.Attributes) + + validator, found := attrsMap[distributiontypes.AttributeKeyValidator] + suite.Require().Truef(found, "expected validator attribute to be found in event %s", event) + + amountStr, found := attrsMap[sdk.AttributeKeyAmount] + suite.Require().Truef(found, "expected amount attribute to be found in event %s", event) + + amount := sdk.NewCoins() + + // Only parse amount if it is not empty + if len(amountStr) > 0 { + parsedAmt, err := sdk.ParseCoinNormalized(amountStr) + suite.Require().NoError(err) + amount = amount.Add(parsedAmt) + } + + blockRewardsClaimed[validator] = amount + } + + totalClaimedRewards := sdk.NewCoins() + for _, amount := range blockRewardsClaimed { + totalClaimedRewards = totalClaimedRewards.Add(amount...) + } + + return blockRewardsClaimed, totalClaimedRewards +} + +func attrsToMap(attrs []abcitypes.EventAttribute) map[string]string { + out := make(map[string]string) + + for _, attr := range attrs { + out[string(attr.Key)] = string(attr.Value) + } + + return out +} diff --git a/x/incentive/testutil/mint_builder.go b/x/incentive/testutil/mint_builder.go new file mode 100644 index 00000000..9c7bde74 --- /dev/null +++ b/x/incentive/testutil/mint_builder.go @@ -0,0 +1,68 @@ +package testutil + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +// MintGenesisBuilder is a tool for creating a mint genesis state. +// Helper methods add values onto a default genesis state. +// All methods are immutable and return updated copies of the builder. +type MintGenesisBuilder struct { + minttypes.GenesisState +} + +var _ GenesisBuilder = (*MintGenesisBuilder)(nil) + +func NewMintGenesisBuilder() MintGenesisBuilder { + gen := minttypes.DefaultGenesisState() + gen.Params.MintDenom = "ukava" + + return MintGenesisBuilder{ + GenesisState: *gen, + } +} + +func (builder MintGenesisBuilder) Build() minttypes.GenesisState { + return builder.GenesisState +} + +func (builder MintGenesisBuilder) BuildMarshalled(cdc codec.JSONCodec) app.GenesisState { + built := builder.Build() + + return app.GenesisState{ + minttypes.ModuleName: cdc.MustMarshalJSON(&built), + } +} + +func (builder MintGenesisBuilder) WithMinter( + inflation sdk.Dec, + annualProvisions sdk.Dec, +) MintGenesisBuilder { + builder.Minter = minttypes.NewMinter(inflation, annualProvisions) + return builder +} + +func (builder MintGenesisBuilder) WithInflationMax( + inflationMax sdk.Dec, +) MintGenesisBuilder { + builder.Params.InflationMax = inflationMax + return builder +} + +func (builder MintGenesisBuilder) WithInflationMin( + inflationMin sdk.Dec, +) MintGenesisBuilder { + builder.Params.InflationMin = inflationMin + return builder +} + +func (builder MintGenesisBuilder) WithMintDenom( + mintDenom string, +) MintGenesisBuilder { + builder.Params.MintDenom = mintDenom + return builder +} diff --git a/x/incentive/testutil/staking_builder.go b/x/incentive/testutil/staking_builder.go new file mode 100644 index 00000000..7282fff6 --- /dev/null +++ b/x/incentive/testutil/staking_builder.go @@ -0,0 +1,38 @@ +package testutil + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/kava-labs/kava/app" + + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// StakingGenesisBuilder is a tool for creating a staking genesis state. +// Helper methods add values onto a default genesis state. +// All methods are immutable and return updated copies of the builder. +type StakingGenesisBuilder struct { + stakingtypes.GenesisState +} + +var _ GenesisBuilder = (*StakingGenesisBuilder)(nil) + +func NewStakingGenesisBuilder() StakingGenesisBuilder { + gen := stakingtypes.DefaultGenesisState() + gen.Params.BondDenom = "ukava" + + return StakingGenesisBuilder{ + GenesisState: *gen, + } +} + +func (builder StakingGenesisBuilder) Build() stakingtypes.GenesisState { + return builder.GenesisState +} + +func (builder StakingGenesisBuilder) BuildMarshalled(cdc codec.JSONCodec) app.GenesisState { + built := builder.Build() + + return app.GenesisState{ + stakingtypes.ModuleName: cdc.MustMarshalJSON(&built), + } +} diff --git a/x/liquid/keeper/claim.go b/x/liquid/keeper/claim.go index 7c115da9..89b8827e 100644 --- a/x/liquid/keeper/claim.go +++ b/x/liquid/keeper/claim.go @@ -29,6 +29,10 @@ func (k Keeper) CollectStakingRewards( return nil, err } + if rewards.IsZero() { + return rewards, nil + } + err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, destinationModAccount, rewards) if err != nil { return nil, err