package keeper_test import ( "testing" "github.com/kava-labs/kava/x/earn/testutil" "github.com/kava-labs/kava/x/earn/types" "github.com/kava-labs/kava/x/earn/types/mocks" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) type hookTestSuite struct { testutil.Suite } func (suite *hookTestSuite) SetupTest() { suite.Suite.SetupTest() suite.Keeper.SetParams(suite.Ctx, types.DefaultParams()) } func TestHookTestSuite(t *testing.T) { suite.Run(t, new(hookTestSuite)) } func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() { suite.Keeper.ClearHooks() earnHooks := mocks.NewEarnHooks(suite.T()) suite.Keeper.SetHooks(earnHooks) vault1Denom := "usdx" vault2Denom := "ukava" 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) acc := suite.CreateAccount(sdk.NewCoins( sdk.NewInt64Coin(vault1Denom, 1000), 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, acc1deposit1Amount.Denom, acc.GetAddress(), sdk.NewDecFromInt(acc1deposit1Amount.Amount), ).Once() err := suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), acc1deposit1Amount, 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 earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit1Amount.Denom, acc.GetAddress(), sdk.NewDecFromInt(acc1deposit1Amount.Amount), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), acc1deposit1Amount, types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) // get the shares from the store from the last deposit shareRecord, found := suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.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, acc1deposit1Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit1Amount.Denom), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), acc1deposit1Amount, types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) // new deposit denom into vault creates the deposit and calls AfterVaultDepositCreated earnHooks.On( "AfterVaultDepositCreated", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), sdk.NewDecFromInt(acc1deposit2Amount.Amount), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) // second deposit into vault calls BeforeVaultDepositModified with initial shares given earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), sdk.NewDecFromInt(acc1deposit2Amount.Amount), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) // get the shares from the store from the last deposit shareRecord, found = suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.GetAddress(), ) suite.Require().True(found) // third deposit into vault calls BeforeVaultDepositModified with shares from last deposit earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() err = suite.Keeper.Deposit( suite.Ctx, acc.GetAddress(), 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(), sdk.NewDecFromInt(acc2deposit1Amount.Amount), ).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(), sdk.NewDecFromInt(acc2deposit1Amount.Amount), ).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(), sdk.NewDecFromInt(acc2deposit2Amount.Amount), ).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(), sdk.NewDecFromInt(acc2deposit2Amount.Amount), ).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) // ------------------------------------------------------------ // test hooks with a full withdraw of all shares deposit 1 denom shareRecord, found = suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.GetAddress(), ) suite.Require().True(found) // all shares given to BeforeVaultDepositModified earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit1Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit1Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), // 3 deposits, multiply original deposit amount by 3 sdk.NewCoin(acc1deposit1Amount.Denom, acc1deposit1Amount.Amount.MulRaw(3)), types.STRATEGY_TYPE_HARD, ) suite.Require().NoError(err) // test hooks on partial withdraw shareRecord, found = suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.GetAddress(), ) suite.Require().True(found) // all shares given to before deposit modified even with partial withdraw earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) // test hooks on second partial withdraw shareRecord, found = suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.GetAddress(), ) suite.Require().True(found) // all shares given to before deposit modified even with partial withdraw earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) // test hooks withdraw all remaining shares shareRecord, found = suite.Keeper.GetVaultAccountShares( suite.Ctx, acc.GetAddress(), ) suite.Require().True(found) // all shares given to before deposit modified even with partial withdraw earnHooks.On( "BeforeVaultDepositModified", suite.Ctx, acc1deposit2Amount.Denom, acc.GetAddress(), shareRecord.AmountOf(acc1deposit2Amount.Denom), ).Once() _, err = suite.Keeper.Withdraw( suite.Ctx, acc.GetAddress(), acc1deposit2Amount, types.STRATEGY_TYPE_SAVINGS, ) suite.Require().NoError(err) // ------------------------------------------------------------ // 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() { suite.Keeper.ClearHooks() vaultDenom := "usdx" startBalance := sdk.NewInt64Coin(vaultDenom, 1000) depositAmount := sdk.NewInt64Coin(vaultDenom, 100) withdrawAmount := sdk.NewInt64Coin(vaultDenom, 100) suite.CreateVault(vaultDenom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) // AfterVaultDepositModified should not panic if no hooks are registered err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) // BeforeVaultDepositModified should not panic if no hooks are registered err = suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) // BeforeVaultDepositModified should not panic if no hooks are registered _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) } func (suite *hookTestSuite) TestHooks_HookOrdering() { suite.Keeper.ClearHooks() earnHooks := &mocks.EarnHooks{} suite.Keeper.SetHooks(earnHooks) vaultDenom := "usdx" startBalance := sdk.NewInt64Coin(vaultDenom, 1000) depositAmount := sdk.NewInt64Coin(vaultDenom, 100) suite.CreateVault(vaultDenom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) earnHooks.On("AfterVaultDepositCreated", suite.Ctx, depositAmount.Denom, acc.GetAddress(), sdk.NewDecFromInt(depositAmount.Amount)). Run(func(args mock.Arguments) { shares, found := suite.Keeper.GetVaultAccountShares(suite.Ctx, acc.GetAddress()) suite.Require().True(found, "expected after hook to be called after shares are updated") suite.Require().Equal(sdk.NewDecFromInt(depositAmount.Amount), shares.AmountOf(depositAmount.Denom)) }) err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) earnHooks.On("BeforeVaultDepositModified", suite.Ctx, depositAmount.Denom, acc.GetAddress(), sdk.NewDecFromInt(depositAmount.Amount)). Run(func(args mock.Arguments) { shares, found := suite.Keeper.GetVaultAccountShares(suite.Ctx, acc.GetAddress()) suite.Require().True(found, "expected after hook to be called after shares are updated") suite.Require().Equal(sdk.NewDecFromInt(depositAmount.Amount), shares.AmountOf(depositAmount.Denom)) }) err = suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) existingShares, found := suite.Keeper.GetVaultAccountShares(suite.Ctx, acc.GetAddress()) suite.Require().True(found) earnHooks.On("BeforeVaultDepositModified", suite.Ctx, depositAmount.Denom, acc.GetAddress(), existingShares.AmountOf(depositAmount.Denom)). Run(func(args mock.Arguments) { shares, found := suite.Keeper.GetVaultAccountShares(suite.Ctx, acc.GetAddress()) suite.Require().True(found, "expected after hook to be called after shares are updated") suite.Require().Equal(sdk.NewDecFromInt(depositAmount.Amount.MulRaw(2)), shares.AmountOf(depositAmount.Denom)) }) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) suite.Require().NoError(err) }