From e8242ace80c58c101d7caaa3e9871bc2aed475b7 Mon Sep 17 00:00:00 2001 From: Derrick Lee Date: Wed, 12 Oct 2022 14:35:09 -0700 Subject: [PATCH] fix: Use correct share values for earn incentive hooks (#1347) * Call earn hooks with correct share values User's shares instead of total vault shares * Use different amounts for different accounts * Add claim test with staking claims * Remove logging statements * Remove log * Pass suite.T() to mock earn hooks * Use begin blocker for staking reward distribution * Remove unused beginblocker * Remove log --- x/earn/keeper/deposit.go | 4 +- x/earn/keeper/hooks_test.go | 285 ++++++++++++++++++--- x/earn/keeper/withdraw.go | 4 +- x/incentive/keeper/msg_server_earn_test.go | 197 ++++++++++++++ x/incentive/testutil/builder.go | 92 +++++++ x/incentive/testutil/integration.go | 46 ++++ 6 files changed, 590 insertions(+), 38 deletions(-) create mode 100644 x/incentive/keeper/msg_server_earn_test.go diff --git a/x/earn/keeper/deposit.go b/x/earn/keeper/deposit.go index 143f2224..5598e5c1 100644 --- a/x/earn/keeper/deposit.go +++ b/x/earn/keeper/deposit.go @@ -79,8 +79,8 @@ func (k *Keeper) Deposit( isNew := vaultShareRecord.Shares.AmountOf(amount.Denom).IsZero() if !isNew { - // If deposits for this vault already exists - k.BeforeVaultDepositModified(ctx, amount.Denom, depositor, vaultRecord.TotalShares.Amount) + // If deposits for this vault already exists, call hook with user's existing shares + k.BeforeVaultDepositModified(ctx, amount.Denom, depositor, vaultShareRecord.Shares.AmountOf(amount.Denom)) } // Increment VaultRecord total shares and account shares diff --git a/x/earn/keeper/hooks_test.go b/x/earn/keeper/hooks_test.go index 63ef4aa4..462f64d0 100644 --- a/x/earn/keeper/hooks_test.go +++ b/x/earn/keeper/hooks_test.go @@ -27,13 +27,16 @@ func TestHookTestSuite(t *testing.T) { func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { suite.Keeper.ClearHooks() - earnHooks := &mocks.EarnHooks{} + earnHooks := mocks.NewEarnHooks(suite.T()) suite.Keeper.SetHooks(earnHooks) vault1Denom := "usdx" vault2Denom := "ukava" - deposit1Amount := sdk.NewInt64Coin(vault1Denom, 100) - deposit2Amount := sdk.NewInt64Coin(vault2Denom, 100) + acc1deposit1Amount := sdk.NewInt64Coin(vault1Denom, 100) + acc1deposit2Amount := sdk.NewInt64Coin(vault2Denom, 200) + + acc2deposit1Amount := sdk.NewInt64Coin(vault1Denom, 200) + acc2deposit2Amount := sdk.NewInt64Coin(vault2Denom, 300) suite.CreateVault(vault1Denom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil) suite.CreateVault(vault2Denom, types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil) @@ -43,19 +46,24 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { sdk.NewInt64Coin(vault2Denom, 1000), ), 0) + acc2 := suite.CreateAccount(sdk.NewCoins( + sdk.NewInt64Coin(vault1Denom, 1000), + sdk.NewInt64Coin(vault2Denom, 1000), + ), 1) + // first deposit creates vault - calls AfterVaultDepositCreated with initial shares // shares are 1:1 earnHooks.On( "AfterVaultDepositCreated", suite.Ctx, - deposit1Amount.Denom, + acc1deposit1Amount.Denom, acc.GetAddress(), - deposit1Amount.Amount.ToDec(), + acc1deposit1Amount.Amount.ToDec(), ).Once() err := suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit1Amount, + acc1deposit1Amount, types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) @@ -65,14 +73,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit1Amount.Denom, + acc1deposit1Amount.Denom, acc.GetAddress(), - deposit1Amount.Amount.ToDec(), + acc1deposit1Amount.Amount.ToDec(), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit1Amount, + acc1deposit1Amount, types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) @@ -89,14 +97,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit1Amount.Denom, + acc1deposit1Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit1Amount.Denom), + shareRecord.AmountOf(acc1deposit1Amount.Denom), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit1Amount, + acc1deposit1Amount, types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) @@ -105,14 +113,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "AfterVaultDepositCreated", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - deposit2Amount.Amount.ToDec(), + acc1deposit2Amount.Amount.ToDec(), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) @@ -121,14 +129,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - deposit2Amount.Amount.ToDec(), + acc1deposit2Amount.Amount.ToDec(), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) @@ -144,14 +152,131 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit2Amount.Denom), + shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) + + // ------------------------------------------------------------ + // Second account deposits + + // first deposit by user - calls AfterVaultDepositCreated with user's shares + // not total shares + earnHooks.On( + "AfterVaultDepositCreated", + suite.Ctx, + acc2deposit1Amount.Denom, + acc2.GetAddress(), + acc2deposit1Amount.Amount.ToDec(), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit1Amount, + types.STRATEGY_TYPE_HARD, + ) + suite.Require().NoError(err) + + // second deposit adds to vault - calls BeforeVaultDepositModified + // shares given are the initial shares, not new the shares added to the vault + // and not the total vault shares + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit1Amount.Denom, + acc2.GetAddress(), + acc2deposit1Amount.Amount.ToDec(), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit1Amount, + types.STRATEGY_TYPE_HARD, + ) + suite.Require().NoError(err) + + // get the shares from the store from the last deposit + shareRecord2, found := suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // third deposit adds to vault - calls BeforeVaultDepositModified + // shares given are the shares added in previous deposit, not the shares added to the vault now + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit1Amount.Denom, + acc2.GetAddress(), + shareRecord2.AmountOf(acc2deposit1Amount.Denom), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit1Amount, + types.STRATEGY_TYPE_HARD, + ) + suite.Require().NoError(err) + + // new deposit denom into vault creates the deposit and calls AfterVaultDepositCreated + earnHooks.On( + "AfterVaultDepositCreated", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + acc2deposit2Amount.Amount.ToDec(), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) + + // second deposit into vault calls BeforeVaultDepositModified with initial shares given + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + acc2deposit2Amount.Amount.ToDec(), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) + + // get the shares from the store from the last deposit + shareRecord2, found = suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // third deposit into vault calls BeforeVaultDepositModified with shares from last deposit + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + shareRecord2.AmountOf(acc2deposit2Amount.Denom), + ).Once() + err = suite.Keeper.Deposit( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) @@ -168,15 +293,15 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit1Amount.Denom, + acc1deposit1Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit1Amount.Denom), + shareRecord.AmountOf(acc1deposit1Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), // 3 deposits, multiply original deposit amount by 3 - sdk.NewCoin(deposit1Amount.Denom, deposit1Amount.Amount.MulRaw(3)), + sdk.NewCoin(acc1deposit1Amount.Denom, acc1deposit1Amount.Amount.MulRaw(3)), types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) @@ -192,14 +317,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit2Amount.Denom), + shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) @@ -215,14 +340,14 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit2Amount.Denom), + shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) @@ -238,19 +363,111 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, - deposit2Amount.Denom, + acc1deposit2Amount.Denom, acc.GetAddress(), - shareRecord.AmountOf(deposit2Amount.Denom), + shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), - deposit2Amount, + acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) - earnHooks.AssertExpectations(suite.T()) + // ------------------------------------------------------------ + // withdraw from acc2 + shareRecord, found = suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // all shares given to BeforeVaultDepositModified + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit1Amount.Denom, + acc2.GetAddress(), + shareRecord.AmountOf(acc2deposit1Amount.Denom), + ).Once() + _, err = suite.Keeper.Withdraw( + suite.Ctx, + acc2.GetAddress(), + // 3 deposits, multiply original deposit amount by 3 + sdk.NewCoin(acc2deposit1Amount.Denom, acc2deposit1Amount.Amount.MulRaw(3)), + types.STRATEGY_TYPE_HARD, + ) + suite.Require().NoError(err) + + // test hooks on partial withdraw + shareRecord2, found = suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // all shares given to before deposit modified even with partial withdraw + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + shareRecord2.AmountOf(acc2deposit2Amount.Denom), + ).Once() + _, err = suite.Keeper.Withdraw( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) + + // test hooks on second partial withdraw + shareRecord2, found = suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // all shares given to before deposit modified even with partial withdraw + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + shareRecord2.AmountOf(acc2deposit2Amount.Denom), + ).Once() + _, err = suite.Keeper.Withdraw( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) + + // test hooks withdraw all remaining shares + shareRecord2, found = suite.Keeper.GetVaultAccountShares( + suite.Ctx, + acc2.GetAddress(), + ) + suite.Require().True(found) + + // all shares given to before deposit modified even with partial withdraw + earnHooks.On( + "BeforeVaultDepositModified", + suite.Ctx, + acc2deposit2Amount.Denom, + acc2.GetAddress(), + shareRecord2.AmountOf(acc2deposit2Amount.Denom), + ).Once() + _, err = suite.Keeper.Withdraw( + suite.Ctx, + acc2.GetAddress(), + acc2deposit2Amount, + types.STRATEGY_TYPE_SAVINGS, + ) + suite.Require().NoError(err) } func (suite *hookTestSuite) TestHooks_NoPanicsOnNilHooks() { diff --git a/x/earn/keeper/withdraw.go b/x/earn/keeper/withdraw.go index 122e570b..db43aae3 100644 --- a/x/earn/keeper/withdraw.go +++ b/x/earn/keeper/withdraw.go @@ -126,8 +126,8 @@ func (k *Keeper) Withdraw( withdrawShares = vaultShareRecord.Shares.GetShare(withdrawAmount.Denom) } - // Call hook before record is modified - k.BeforeVaultDepositModified(ctx, wantAmount.Denom, from, vaultRecord.TotalShares.Amount) + // Call hook before record is modified with the user's current shares + k.BeforeVaultDepositModified(ctx, wantAmount.Denom, from, accCurrentShares) // Decrement VaultRecord and VaultShareRecord supplies - must delete same // amounts diff --git a/x/incentive/keeper/msg_server_earn_test.go b/x/incentive/keeper/msg_server_earn_test.go new file mode 100644 index 00000000..0e160627 --- /dev/null +++ b/x/incentive/keeper/msg_server_earn_test.go @@ -0,0 +1,197 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + 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" + liquidtypes "github.com/kava-labs/kava/x/liquid/types" +) + +func (suite *HandlerTestSuite) TestEarnLiquidClaim() { + userAddr1, userAddr2, validatorAddr1, validatorAddr2 := suite.addrs[0], suite.addrs[1], suite.addrs[2], suite.addrs[3] + + valAddr1 := sdk.ValAddress(validatorAddr1) + valAddr2 := sdk.ValAddress(validatorAddr2) + + authBuilder := suite.authBuilder(). + WithSimpleAccount(userAddr1, cs(c("ukava", 1e12))). + WithSimpleAccount(userAddr2, cs(c("ukava", 1e12))). + WithSimpleAccount(validatorAddr1, cs(c("ukava", 1e12))). + WithSimpleAccount(validatorAddr2, cs(c("ukava", 1e12))) + + incentBuilder := suite.incentiveBuilder() + + savingsBuilder := testutil.NewSavingsGenesisBuilder(). + WithSupportedDenoms("bkava") + + earnBuilder := suite.earnBuilder(). + WithVault(earntypes.AllowedVault{ + Denom: "bkava", + Strategies: earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, + IsPrivateVault: false, + AllowedDepositors: nil, + }) + + suite.SetupWithGenState(authBuilder, incentBuilder, earnBuilder, savingsBuilder) + + // ak := suite.App.GetAccountKeeper() + // bk := suite.App.GetBankKeeper() + sk := suite.App.GetStakingKeeper() + lq := suite.App.GetLiquidKeeper() + mk := suite.App.GetMintKeeper() + dk := suite.App.GetDistrKeeper() + ik := suite.App.GetIncentiveKeeper() + + // Use ukava for mint denom + mParams := mk.GetParams(suite.Ctx) + mParams.MintDenom = "ukava" + mk.SetParams(suite.Ctx, mParams) + + bkavaDenom1 := lq.GetLiquidStakingTokenDenom(valAddr1) + bkavaDenom2 := lq.GetLiquidStakingTokenDenom(valAddr2) + + err := suite.App.FundModuleAccount(suite.Ctx, distrtypes.ModuleName, cs(c("ukava", 1e12))) + suite.NoError(err) + + // Create validators + err = suite.DeliverMsgCreateValidator(valAddr1, c("ukava", 1e9)) + suite.Require().NoError(err) + + err = suite.DeliverMsgCreateValidator(valAddr2, c("ukava", 1e9)) + suite.Require().NoError(err) + + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + // Now the delegation is bonded, accumulate some delegator rewards + suite.NextBlockAfter(7 * time.Second) + + // Create delegations from users + // User 1: 1e9 ukava to validator 1 + // User 2: 99e9 ukava to validator 1 AND 2 + err = suite.DeliverMsgDelegate(userAddr1, valAddr1, c("ukava", 1e9)) + suite.Require().NoError(err) + + err = suite.DeliverMsgDelegate(userAddr2, valAddr1, c("ukava", 99e9)) + suite.Require().NoError(err) + + err = suite.DeliverMsgDelegate(userAddr2, valAddr2, c("ukava", 99e9)) + suite.Require().NoError(err) + + // Mint liquid tokens + err = suite.DeliverMsgMintDerivative(userAddr1, valAddr1, c("ukava", 1e9)) + suite.Require().NoError(err) + + err = suite.DeliverMsgMintDerivative(userAddr2, valAddr1, c("ukava", 99e9)) + suite.Require().NoError(err) + + err = suite.DeliverMsgMintDerivative(userAddr2, valAddr2, c("ukava", 99e9)) + suite.Require().NoError(err) + + // Deposit liquid tokens to earn + err = suite.DeliverEarnMsgDeposit(userAddr1, c(bkavaDenom1, 1e9), earntypes.STRATEGY_TYPE_SAVINGS) + suite.Require().NoError(err) + + err = suite.DeliverEarnMsgDeposit(userAddr2, c(bkavaDenom1, 99e9), earntypes.STRATEGY_TYPE_SAVINGS) + suite.Require().NoError(err) + err = suite.DeliverEarnMsgDeposit(userAddr2, c(bkavaDenom2, 99e9), earntypes.STRATEGY_TYPE_SAVINGS) + suite.Require().NoError(err) + + // BeginBlocker to update minter annual provisions as it starts at 0 which results in no minted coins + _ = suite.App.BeginBlocker(suite.Ctx, abci.RequestBeginBlock{}) + + // DeliverMsgCreateValidator uses a generated pubkey, so we need to fetch + // the validator to get the correct pubkey + validator1, found := sk.GetValidator(suite.Ctx, valAddr1) + suite.Require().True(found) + + pk, err := validator1.ConsPubKey() + suite.Require().NoError(err) + + val := abci.Validator{ + Address: pk.Address(), + Power: 100, + } + + 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, + }}, + }, + }) + + liquidMacc := suite.App.GetAccountKeeper().GetModuleAccount(suite.Ctx, liquidtypes.ModuleAccountName) + delegation, found := sk.GetDelegation(suite.Ctx, liquidMacc.GetAddress(), valAddr1) + suite.Require().True(found) + + // 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 + ) + err = ik.AccumulateEarnRewards(suite.Ctx, rewardPeriod) + suite.Require().NoError(err) + + preClaimBal1 := suite.GetBalance(userAddr1) + preClaimBal2 := suite.GetBalance(userAddr2) + + // Claim ukava staking rewards + denomsToClaim := map[string]string{"ukava": "large"} + selections := types.NewSelectionsFromMap(denomsToClaim) + + msg1 := types.NewMsgClaimEarnReward(userAddr1.String(), selections) + msg2 := types.NewMsgClaimEarnReward(userAddr2.String(), selections) + + err = suite.DeliverIncentiveMsg(&msg1) + suite.Require().NoError(err) + + err = suite.DeliverIncentiveMsg(&msg2) + suite.Require().NoError(err) + + // Check rewards were paid out + // User 1 gets 1% of rewards + // User 2 gets 99% of rewards + stakingRewards1 := delegationRewards. + AmountOf("ukava"). + QuoInt64(100). + RoundInt() + suite.BalanceEquals(userAddr1, preClaimBal1.Add(sdk.NewCoin("ukava", stakingRewards1))) + + // Total * 99 / 100 + stakingRewards2 := delegationRewards. + AmountOf("ukava"). + MulInt64(99). + QuoInt64(100). + TruncateInt() + suite.BalanceEquals(userAddr2, preClaimBal2.Add(sdk.NewCoin("ukava", stakingRewards2))) + + suite.Equal(delegationRewards.AmountOf("ukava").TruncateInt(), stakingRewards1.Add(stakingRewards2)) + + // Check that claimed coins have been removed from a claim's reward + 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/testutil/builder.go b/x/incentive/testutil/builder.go index 9c7d9b78..4b72f3b2 100644 --- a/x/incentive/testutil/builder.go +++ b/x/incentive/testutil/builder.go @@ -7,8 +7,10 @@ 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" ) const ( @@ -168,6 +170,24 @@ func (builder IncentiveGenesisBuilder) WithSimpleUSDXRewardPeriod(ctype string, )) } +// WithInitializedEarnRewardPeriod sets the genesis time as the previous accumulation time for the specified period. +// This can be helpful in tests. With no prev time set, the first block accrues no rewards as it just sets the prev time to the current. +func (builder IncentiveGenesisBuilder) WithInitializedEarnRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.EarnRewardPeriods = append(builder.Params.EarnRewardPeriods, period) + + accumulationTimeForPeriod := types.NewAccumulationTime(period.CollateralType, builder.genesisTime) + builder.EarnRewardState.AccumulationTimes = append( + builder.EarnRewardState.AccumulationTimes, + accumulationTimeForPeriod, + ) + + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleEarnRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedEarnRewardPeriod(builder.simpleRewardPeriod(ctype, rewardsPerSecond)) +} + func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.MultipliersPerDenoms) IncentiveGenesisBuilder { builder.Params.ClaimMultipliers = multipliers @@ -281,3 +301,75 @@ func (builder IncentiveGenesisBuilder) WithInitializedSavingsRewardPeriod(period func (builder IncentiveGenesisBuilder) WithSimpleSavingsRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { 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. +type SavingsGenesisBuilder struct { + savingstypes.GenesisState + genesisTime time.Time +} + +func NewSavingsGenesisBuilder() SavingsGenesisBuilder { + return SavingsGenesisBuilder{ + GenesisState: savingstypes.DefaultGenesisState(), + } +} + +func (builder SavingsGenesisBuilder) Build() savingstypes.GenesisState { + return builder.GenesisState +} + +func (builder SavingsGenesisBuilder) BuildMarshalled(cdc codec.JSONCodec) app.GenesisState { + built := builder.Build() + + return app.GenesisState{ + savingstypes.ModuleName: cdc.MustMarshalJSON(&built), + } +} + +func (builder SavingsGenesisBuilder) WithGenesisTime(genTime time.Time) SavingsGenesisBuilder { + builder.genesisTime = genTime + return builder +} + +func (builder SavingsGenesisBuilder) WithSupportedDenoms(denoms ...string) SavingsGenesisBuilder { + builder.Params.SupportedDenoms = append(builder.Params.SupportedDenoms, denoms...) + return builder +} diff --git a/x/incentive/testutil/integration.go b/x/incentive/testutil/integration.go index 5e59136e..dc01d9c9 100644 --- a/x/incentive/testutil/integration.go +++ b/x/incentive/testutil/integration.go @@ -6,6 +6,7 @@ import ( "time" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" 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" @@ -21,10 +22,14 @@ import ( cdptypes "github.com/kava-labs/kava/x/cdp/types" committeekeeper "github.com/kava-labs/kava/x/committee/keeper" committeetypes "github.com/kava-labs/kava/x/committee/types" + earnkeeper "github.com/kava-labs/kava/x/earn/keeper" + earntypes "github.com/kava-labs/kava/x/earn/types" hardkeeper "github.com/kava-labs/kava/x/hard/keeper" hardtypes "github.com/kava-labs/kava/x/hard/types" incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper" "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" swapkeeper "github.com/kava-labs/kava/x/swap/keeper" swaptypes "github.com/kava-labs/kava/x/swap/types" ) @@ -87,6 +92,8 @@ func (suite *IntegrationTester) DeliverIncentiveMsg(msg sdk.Msg) error { _, err = msgServer.ClaimUSDXMintingReward(sdk.WrapSDKContext(suite.Ctx), msg) case *types.MsgClaimDelegatorReward: _, err = msgServer.ClaimDelegatorReward(sdk.WrapSDKContext(suite.Ctx), msg) + case *types.MsgClaimEarnReward: + _, err = msgServer.ClaimEarnReward(sdk.WrapSDKContext(suite.Ctx), msg) default: panic("unhandled incentive msg") } @@ -194,6 +201,30 @@ func (suite *IntegrationTester) DeliverCDPMsgBorrow(owner sdk.AccAddress, collat return err } +func (suite *IntegrationTester) DeliverMsgMintDerivative( + sender sdk.AccAddress, + validator sdk.ValAddress, + amount 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 +} + +func (suite *IntegrationTester) DeliverEarnMsgDeposit( + depositor sdk.AccAddress, + amount sdk.Coin, + strategy earntypes.StrategyType, +) error { + msg := earntypes.NewMsgDeposit(depositor.String(), amount, strategy) + msgServer := earnkeeper.NewMsgServerImpl(suite.App.GetEarnKeeper()) + + _, err := msgServer.Deposit(sdk.WrapSDKContext(suite.Ctx), msg) + return err +} + func (suite *IntegrationTester) ProposeAndVoteOnNewParams(voter sdk.AccAddress, committeeID uint64, changes []proposaltypes.ParamChange) { propose, err := committeetypes.NewMsgSubmitProposal( proposaltypes.NewParameterChangeProposal( @@ -292,3 +323,18 @@ func (suite *IntegrationTester) USDXRewardEquals(owner sdk.AccAddress, expected suite.Require().Truef(found, "expected delegator claim to be found for %s", owner) suite.Equalf(expected, claim.Reward, "expected delegator claim reward to be %s, but got %s", expected, claim.Reward) } + +func (suite *IntegrationTester) EarnRewardEquals(owner sdk.AccAddress, expected sdk.Coins) { + claim, found := suite.App.GetIncentiveKeeper().GetEarnClaim(suite.Ctx, owner) + suite.Require().Truef(found, "expected earn claim to be found for %s", owner) + suite.Truef(expected.IsEqual(claim.Reward), "expected earn claim reward to be %s, but got %s", expected, claim.Reward) +} + +// AddTestAddrsFromPubKeys adds the addresses into the SimApp providing only the public keys. +func (suite *IntegrationTester) AddTestAddrsFromPubKeys(ctx sdk.Context, pubKeys []cryptotypes.PubKey, accAmt sdk.Int) { + initCoins := sdk.NewCoins(sdk.NewCoin(suite.App.GetStakingKeeper().BondDenom(ctx), accAmt)) + + for _, pk := range pubKeys { + suite.App.FundAccount(ctx, sdk.AccAddress(pk.Address()), initCoins) + } +}