diff --git a/x/harvest/abci.go b/x/harvest/abci.go index ec130d30..d0e9453e 100644 --- a/x/harvest/abci.go +++ b/x/harvest/abci.go @@ -12,5 +12,6 @@ func BeginBlocker(ctx sdk.Context, k Keeper) { k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), k.BondDenom(ctx)) } k.ApplyInterestRateUpdates(ctx) + k.AttemptIndexLiquidations(ctx) k.SetPreviousBlockTime(ctx, ctx.BlockTime()) } diff --git a/x/harvest/keeper/borrow.go b/x/harvest/keeper/borrow.go index 0d8538d8..df9f4fd7 100644 --- a/x/harvest/keeper/borrow.go +++ b/x/harvest/keeper/borrow.go @@ -23,7 +23,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins } // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetCurrentLTV(ctx, borrower) + prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, borrower) if err != nil { return err } @@ -31,7 +31,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins // If the user has an existing borrow, sync its outstanding interest _, found := k.GetBorrow(ctx, borrower) if found { - k.SyncOustandingInterest(ctx, borrower) + k.SyncOutstandingInterest(ctx, borrower) } // Validate borrow amount within user and protocol limits @@ -92,8 +92,8 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins return nil } -// SyncOustandingInterest updates the user's owed interest on newly borrowed coins to the latest global state -func (k Keeper) SyncOustandingInterest(ctx sdk.Context, addr sdk.AccAddress) { +// SyncOutstandingInterest updates the user's owed interest on newly borrowed coins to the latest global state +func (k Keeper) SyncOutstandingInterest(ctx sdk.Context, addr sdk.AccAddress) { totalNewInterest := sdk.Coins{} // Update user's borrow index list for each asset in the 'coins' array. diff --git a/x/harvest/keeper/borrow_test.go b/x/harvest/keeper/borrow_test.go index 10b04127..9586ccdf 100644 --- a/x/harvest/keeper/borrow_test.go +++ b/x/harvest/keeper/borrow_test.go @@ -283,6 +283,7 @@ func (suite *KeeperTestSuite) TestBorrow() { types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), tc.args.loanToValueBNB), "bnb:usd", sdk.NewInt(BNB_CF), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("xyz", types.NewBorrowLimit(false, sdk.NewDec(1), tc.args.loanToValueBNB), "xyz:usd", sdk.NewInt(1), sdk.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) // Pricefeed module genesis state diff --git a/x/harvest/keeper/claim_test.go b/x/harvest/keeper/claim_test.go index 0bb9464c..ccee4432 100644 --- a/x/harvest/keeper/claim_test.go +++ b/x/harvest/keeper/claim_test.go @@ -268,6 +268,7 @@ func (suite *KeeperTestSuite) TestClaim() { types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) if tc.args.validatorVesting { diff --git a/x/harvest/keeper/deposit.go b/x/harvest/keeper/deposit.go index f709568d..d064b25d 100644 --- a/x/harvest/keeper/deposit.go +++ b/x/harvest/keeper/deposit.go @@ -11,12 +11,12 @@ import ( // Deposit deposit func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error { // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetCurrentLTV(ctx, depositor) + prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor) if err != nil { return err } - k.SyncOustandingInterest(ctx, depositor) + k.SyncOutstandingInterest(ctx, depositor) err = k.ValidateDeposit(ctx, amount) if err != nil { diff --git a/x/harvest/keeper/deposit_test.go b/x/harvest/keeper/deposit_test.go index 38452205..580d998f 100644 --- a/x/harvest/keeper/deposit_test.go +++ b/x/harvest/keeper/deposit_test.go @@ -115,6 +115,7 @@ func (suite *KeeperTestSuite) TestDeposit() { types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(1000000), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("btcb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "btcb:usd", sdk.NewInt(1000000), sdk.NewInt(BTCB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) // Pricefeed module genesis state @@ -303,6 +304,7 @@ func (suite *KeeperTestSuite) TestWithdraw() { types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) keeper := tApp.GetHarvestKeeper() diff --git a/x/harvest/keeper/interest_test.go b/x/harvest/keeper/interest_test.go index 2551bbee..04286909 100644 --- a/x/harvest/keeper/interest_test.go +++ b/x/harvest/keeper/interest_test.go @@ -666,6 +666,7 @@ func (suite *KeeperTestSuite) TestInterest() { tc.args.reserveFactor, // Reserve Factor sdk.ZeroDec()), // Keeper Reward Percentage }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) // Pricefeed module genesis state diff --git a/x/harvest/keeper/keeper.go b/x/harvest/keeper/keeper.go index 064e5efb..34dbcc6d 100644 --- a/x/harvest/keeper/keeper.go +++ b/x/harvest/keeper/keeper.go @@ -376,7 +376,7 @@ func (k Keeper) RemoveFromLtvIndex(ctx sdk.Context, ltv sdk.Dec, borrower sdk.Ac func (k Keeper) IterateLtvIndex(ctx sdk.Context, cutoffCount int, cb func(addr sdk.AccAddress) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.key), types.LtvIndexPrefix) - iterator := store.Iterator(nil, nil) + iterator := store.ReverseIterator(nil, nil) count := 0 defer iterator.Close() @@ -394,8 +394,8 @@ func (k Keeper) IterateLtvIndex(ctx sdk.Context, cutoffCount int, } // GetLtvIndexSlice returns the first 10 items in the LTV index from the store -func (k Keeper) GetLtvIndexSlice(ctx sdk.Context) (addrs []sdk.AccAddress) { - k.IterateLtvIndex(ctx, 10, func(addr sdk.AccAddress) bool { +func (k Keeper) GetLtvIndexSlice(ctx sdk.Context, count int) (addrs []sdk.AccAddress) { + k.IterateLtvIndex(ctx, count, func(addr sdk.AccAddress) bool { addrs = append(addrs, addr) return false }) diff --git a/x/harvest/keeper/keeper_test.go b/x/harvest/keeper/keeper_test.go index e8db0855..bf7c0969 100644 --- a/x/harvest/keeper/keeper_test.go +++ b/x/harvest/keeper/keeper_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "fmt" + "sort" "strconv" "testing" @@ -160,7 +161,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteInterestRateModel() { denom := "test" model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5")) - moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) + moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(1000000000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) _, f := suite.keeper.GetMoneyMarket(suite.ctx, denom) suite.Require().False(f) @@ -186,7 +187,7 @@ func (suite *KeeperTestSuite) TestIterateInterestRateModels() { denom := testDenom + strconv.Itoa(i) model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5")) - moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) + moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(1000000000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) // Store money market in the module's store suite.Require().NotPanics(func() { suite.keeper.SetMoneyMarket(suite.ctx, denom, moneyMarket) }) @@ -210,7 +211,7 @@ func (suite *KeeperTestSuite) TestIterateInterestRateModels() { func (suite *KeeperTestSuite) TestSetDeleteLtvIndex() { // LTV index should have 0 items - firstAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + firstAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) suite.Require().Equal(0, len(firstAddrs)) // Add an item to the LTV index @@ -219,7 +220,7 @@ func (suite *KeeperTestSuite) TestSetDeleteLtvIndex() { suite.Require().NotPanics(func() { suite.keeper.InsertIntoLtvIndex(suite.ctx, ltv, addr) }) // LTV index should have 1 item - secondAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + secondAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) suite.Require().Equal(1, len(secondAddrs)) // Attempt to remove invalid item from LTV index @@ -227,14 +228,14 @@ func (suite *KeeperTestSuite) TestSetDeleteLtvIndex() { suite.Require().NotPanics(func() { suite.keeper.RemoveFromLtvIndex(suite.ctx, fakeLtv, addr) }) // LTV index should still have 1 item - thirdAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + thirdAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) suite.Require().Equal(1, len(thirdAddrs)) // Attempt to remove valid item from LTV index suite.Require().NotPanics(func() { suite.keeper.RemoveFromLtvIndex(suite.ctx, ltv, addr) }) // LTV index should still have 0 items - fourthAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + fourthAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) suite.Require().Equal(0, len(fourthAddrs)) } @@ -252,8 +253,23 @@ func (suite *KeeperTestSuite) TestIterateLtvIndex() { } // Only the first 10 addresses should be returned - sliceAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) - suite.Require().Equal(setAddrs[:10], sliceAddrs) + sliceAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) + suite.Require().Equal(addressSort(setAddrs[10:20]), addressSort(sliceAddrs)) + + // Insert an additional item into the LTV index that should be returned in the first 10 elements + addr := sdk.AccAddress("test" + fmt.Sprint(21)) + ltv := sdk.OneDec().Add(sdk.MustNewDecFromStr("15").Quo(sdk.NewDec(10))) + suite.Require().NotPanics(func() { suite.keeper.InsertIntoLtvIndex(suite.ctx, ltv, addr) }) + + // Fetch the updated LTV index + updatedSliceAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx, 10) + sawAddr := false + for _, updatedSliceAddr := range updatedSliceAddrs { + if updatedSliceAddr.Equals(addr) { + sawAddr = true + } + } + suite.Require().Equal(true, sawAddr) } func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account { @@ -276,6 +292,21 @@ func (suite *KeeperTestSuite) getModuleAccountAtCtx(name string, ctx sdk.Context return sk.GetModuleAccount(ctx, name) } +func addressSort(addrs []sdk.AccAddress) (sortedAddrs []sdk.AccAddress) { + addrStrs := []string{} + for _, addr := range addrs { + addrStrs = append(addrStrs, addr.String()) + } + + sort.Strings(addrStrs) + + for _, addrStr := range addrStrs { + addr, _ := sdk.AccAddressFromBech32(addrStr) + sortedAddrs = append(sortedAddrs, addr) + } + return sortedAddrs +} + func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } diff --git a/x/harvest/keeper/liquidation.go b/x/harvest/keeper/liquidation.go index 86d98a09..0da0bf25 100644 --- a/x/harvest/keeper/liquidation.go +++ b/x/harvest/keeper/liquidation.go @@ -1,6 +1,8 @@ package keeper import ( + "errors" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -16,23 +18,31 @@ type LiqData struct { // AttemptIndexLiquidations attempts to liquidate the lowest LTV borrows func (k Keeper) AttemptIndexLiquidations(ctx sdk.Context) error { - // use moneyMarketCache := map[string]types.MoneyMarket{} - - // Iterate over index - // Get borrower's address - // Use borrower's address to fetch borrow object - // Calculate outstanding interest and add to borrow balances - // Use current asset prices from pricefeed to calculate current LTV for each asset - // If LTV of any asset is over the max, liquidate it by - // Sending coins to auction module - // (?) Removing borrow from the store - // (?) Removing borrow LTV from LTV index + params := k.GetParams(ctx) + borrowers := k.GetLtvIndexSlice(ctx, params.CheckLtvIndexCount) + for _, borrower := range borrowers { + _, err := k.AttemptKeeperLiquidation(ctx, sdk.AccAddress(types.LiquidatorAccount), borrower) + if err != nil { + if !errors.Is(err, types.ErrBorrowNotLiquidatable) { + panic(err) + } + } + } return nil } // AttemptKeeperLiquidation enables a keeper to liquidate an individual borrower's position -func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, borrower sdk.AccAddress) error { +func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, borrower sdk.AccAddress) (bool, error) { + prevLtv, shouldInsertIndex, err := k.GetStoreLTV(ctx, borrower) + if err != nil { + return false, err + } + + k.SyncOutstandingInterest(ctx, borrower) + + k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, borrower) + // Fetch deposits and parse coin denoms deposits := k.GetDepositsByUser(ctx, borrower) depositDenoms := []string{} @@ -41,8 +51,11 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, } // Fetch borrow balances and parse coin denoms - borrowBalances := k.GetBorrowBalance(ctx, borrower) - borrowDenoms := getDenoms(borrowBalances) + borrows, found := k.GetBorrow(ctx, borrower) + if !found { + return false, types.ErrBorrowNotFound + } + borrowDenoms := getDenoms(borrows.Amount) liqMap := make(map[string]LiqData) @@ -51,12 +64,12 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, for _, denom := range denoms { mm, found := k.GetMoneyMarket(ctx, denom) if !found { - return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom) + return false, sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom) } priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID) if err != nil { - return err + return false, err } liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor} @@ -73,7 +86,7 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, } totalBorrowedUSDAmount := sdk.ZeroDec() - for _, coin := range borrowBalances { + for _, coin := range borrows.Amount { lData := liqMap[coin.Denom] usdValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(lData.conversionFactor)).Mul(lData.price) totalBorrowedUSDAmount = totalBorrowedUSDAmount.Add(usdValue) @@ -81,23 +94,29 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, // Validate that the proposed borrow's USD value is within user's borrowable limit if totalBorrowedUSDAmount.LTE(totalBorrowableUSDAmount) { - return sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "borrowed %s <= borrowable %s", totalBorrowedUSDAmount, totalBorrowableUSDAmount) + return false, sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "borrowed %s <= borrowable %s", totalBorrowedUSDAmount, totalBorrowableUSDAmount) } - // Sending coins to auction module with keeper address getting % of the profits - borrow, _ := k.GetBorrow(ctx, borrower) - err := k.SeizeDeposits(ctx, keeper, liqMap, deposits, borrowBalances, depositDenoms, borrowDenoms) + // Seize deposits and auciton them off + err = k.SeizeDeposits(ctx, keeper, liqMap, deposits, borrows.Amount, depositDenoms, borrowDenoms) if err != nil { - return err + return false, err } + currLtv, _, err := k.GetStoreLTV(ctx, borrower) + if err != nil { + return false, err + } + k.RemoveFromLtvIndex(ctx, currLtv, borrower) + + borrow, _ := k.GetBorrow(ctx, borrower) k.DeleteBorrow(ctx, borrow) for _, oldDeposit := range deposits { k.DeleteDeposit(ctx, oldDeposit) } - return nil + return true, err } // SeizeDeposits seizes a list of deposits and sends them to auction @@ -111,16 +130,20 @@ func (k Keeper) SeizeDeposits(ctx sdk.Context, keeper sdk.AccAddress, liqMap map amount := deposit.Amount.Amount mm, _ := k.GetMoneyMarket(ctx, denom) - keeperReward := mm.KeeperRewardPercentage.MulInt(amount).TruncateInt() - if keeperReward.GT(sdk.ZeroInt()) { - // Send keeper their reward - keeperCoin := sdk.NewCoin(denom, keeperReward) - err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, keeper, sdk.NewCoins(keeperCoin)) - if err != nil { - return err + // No rewards for anyone if liquidated by LTV index + if !keeper.Equals(sdk.AccAddress(types.LiquidatorAccount)) { + keeperReward := mm.KeeperRewardPercentage.MulInt(amount).TruncateInt() + if keeperReward.GT(sdk.ZeroInt()) { + // Send keeper their reward + keeperCoin := sdk.NewCoin(denom, keeperReward) + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, keeper, sdk.NewCoins(keeperCoin)) + if err != nil { + return err + } + amount = amount.Sub(keeperReward) } - amount = amount.Sub(keeperReward) } + // Add remaining deposit coin to aucDeposits aucDeposits = aucDeposits.Add(sdk.NewCoin(denom, amount)) } @@ -253,8 +276,9 @@ func (k Keeper) StartAuctions(ctx sdk.Context, borrower sdk.AccAddress, borrows, return nil } -// GetCurrentLTV calculates the user's current LTV based on their deposits/borrows in the store -func (k Keeper) GetCurrentLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bool, error) { +// GetStoreLTV calculates the user's current LTV based on their deposits/borrows in the store +// and does not include any outsanding interest. +func (k Keeper) GetStoreLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bool, error) { // Fetch deposits and parse coin denoms deposits := k.GetDepositsByUser(ctx, addr) depositDenoms := []string{} @@ -263,11 +287,11 @@ func (k Keeper) GetCurrentLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bo } // Fetch borrow balances and parse coin denoms - borrowBalances := k.GetBorrowBalance(ctx, addr) - if borrowBalances.IsZero() { + borrows, found := k.GetBorrow(ctx, addr) + if !found { return sdk.ZeroDec(), false, nil } - borrowDenoms := getDenoms(borrowBalances) + borrowDenoms := getDenoms(borrows.Amount) liqMap := make(map[string]LiqData) @@ -297,7 +321,7 @@ func (k Keeper) GetCurrentLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bo // Build valuation map to hold borrow coin USD valuations borrowCoinValues := types.NewValuationMap() - for _, bCoin := range borrowBalances { + for _, bCoin := range borrows.Amount { bData := liqMap[bCoin.Denom] bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price) borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue) @@ -316,7 +340,7 @@ func (k Keeper) GetCurrentLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bo // UpdateItemInLtvIndex updates the key a borrower's address is stored under in the LTV index func (k Keeper) UpdateItemInLtvIndex(ctx sdk.Context, prevLtv sdk.Dec, shouldRemoveIndex bool, borrower sdk.AccAddress) error { - currLtv, shouldInsertIndex, err := k.GetCurrentLTV(ctx, borrower) + currLtv, shouldInsertIndex, err := k.GetStoreLTV(ctx, borrower) if err != nil { return err } diff --git a/x/harvest/keeper/liquidation_test.go b/x/harvest/keeper/liquidation_test.go index 8c464a71..b4ead9c8 100644 --- a/x/harvest/keeper/liquidation_test.go +++ b/x/harvest/keeper/liquidation_test.go @@ -16,6 +16,755 @@ import ( "github.com/kava-labs/kava/x/pricefeed" ) +func (suite *KeeperTestSuite) TestIndexLiquidation() { + type args struct { + borrower sdk.AccAddress + initialModuleCoins sdk.Coins + initialBorrowerCoins sdk.Coins + depositCoins []sdk.Coin + borrowCoins sdk.Coins + beginBlockerTime int64 + ltvIndexCount int + expectedBorrowerCoins sdk.Coins // additional coins (if any) the borrower address should have after successfully liquidating position + expectedAuctions auctypes.Auctions // the auctions we should expect to find have been started + } + + type errArgs struct { + expectLiquidate bool + contains string + } + + type liqTest struct { + name string + args args + errArgs errArgs + } + + // Set up test constants + model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5")) + reserveFactor := sdk.MustNewDecFromStr("0.05") + oneMonthInSeconds := int64(2592000) + borrower := sdk.AccAddress(crypto.AddressHash([]byte("randomaddr"))) + + // Set up auction constants + layout := "2006-01-02T15:04:05.000Z" + endTimeStr := "9000-01-01T00:00:00.000Z" + endTime, _ := time.Parse(layout, endTimeStr) + + lotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{borrower}, []sdk.Int{sdk.NewInt(100)}) + + testCases := []liqTest{ + { + "valid: LTV index liquidates borrow", + args{ + borrower: borrower, + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + beginBlockerTime: oneMonthInSeconds, + ltvIndexCount: int(10), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(98*KAVA_CF))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8004766), + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectLiquidate: true, + contains: "", + }, + }, + { + "invalid: borrow not over limit, LTV index does not liquidate", + args{ + borrower: borrower, + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(7*KAVA_CF))), + beginBlockerTime: oneMonthInSeconds, + ltvIndexCount: int(10), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(97*KAVA_CF))), // initial - deposit + borrow + expectedAuctions: auctypes.Auctions{}, + }, + errArgs{ + expectLiquidate: false, + contains: "", + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Auth module genesis state + authGS := app.NewAuthGenState( + []sdk.AccAddress{tc.args.borrower}, + []sdk.Coins{tc.args.initialBorrowerCoins}, + ) + + // Harvest module genesis state + harvestGS := types.NewGenesisState(types.NewParams( + true, + types.DistributionSchedules{ + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "usdc", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "usdt", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "dai", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "btc", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + }, + types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule( + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + time.Hour*24, + ), + }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdx:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("usdt", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdt:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("usdc", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdc:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("dai", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "dai:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("ukava", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "kava:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("bnb", + types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "bnb:usd", // Market ID + sdk.NewInt(BNB_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("btc", + types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "btc:usd", // Market ID + sdk.NewInt(BTCB_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + }, + tc.args.ltvIndexCount, // LTV counter + ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) + + // Pricefeed module genesis state + pricefeedGS := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + {MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "usdt:usd", BaseAsset: "usdt", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "usdc:usd", BaseAsset: "usdc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "dai:usd", BaseAsset: "dai", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + { + MarketID: "usdx:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "usdt:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "usdc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "dai:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "kava:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "bnb:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("10.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "btc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("100.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + }, + } + + // Initialize test application + tApp.InitializeFromGenesisStates(authGS, + app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}, + app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) + + // Mint coins to Harvest module account + supplyKeeper := tApp.GetSupplyKeeper() + supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins) + + auctionKeeper := tApp.GetAuctionKeeper() + + keeper := tApp.GetHarvestKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + suite.auctionKeeper = auctionKeeper + + var err error + + // Run begin blocker to set up state + harvest.BeginBlocker(suite.ctx, suite.keeper) + + // Deposit coins + for _, coin := range tc.args.depositCoins { + err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, coin) + suite.Require().NoError(err) + } + + // Borrow coins + err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins) + suite.Require().NoError(err) + + // Check borrow exists before liquidation + _, foundBorrowBefore := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower) + suite.Require().True(foundBorrowBefore) + + // Check that the user's deposits exist before liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositBefore := suite.keeper.GetDeposit(suite.ctx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositBefore) + } + + // Liquidate the borrow by running begin blocker + runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.beginBlockerTime), 0) + liqCtx := suite.ctx.WithBlockTime(runAtTime) + harvest.BeginBlocker(liqCtx, suite.keeper) + + if tc.errArgs.expectLiquidate { + // Check borrow does not exist after liquidation + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().False(foundBorrowAfter) + // Check deposits do not exist after liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().False(foundDepositAfter) + } + + // Check that borrower's balance contains the expected coins + accBorrower := suite.getAccountAtCtx(tc.args.borrower, liqCtx) + suite.Require().Equal(tc.args.expectedBorrowerCoins, accBorrower.GetCoins()) + + // Check that the expected auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) > 0) + suite.Require().Equal(tc.args.expectedAuctions, auctions) + } else { + // Check that the user's borrow exists + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().True(foundBorrowAfter) + // Check that the user's deposits exist + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositAfter) + } + + // Check that no auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) == 0) + } + }) + } +} + +func (suite *KeeperTestSuite) TestFullIndexLiquidation() { + type args struct { + borrower sdk.AccAddress + otherBorrowers []sdk.AccAddress + initialModuleCoins sdk.Coins + initialBorrowerCoins sdk.Coins + depositCoins []sdk.Coin + borrowCoins sdk.Coins + otherBorrowCoins sdk.Coins + beginBlockerTime int64 + ltvIndexCount int + expectedBorrowerCoins sdk.Coins // additional coins (if any) the borrower address should have after successfully liquidating position + expectedAuctions auctypes.Auctions // the auctions we should expect to find have been started + } + + type errArgs struct { + expectLiquidate bool + expectLiquidateOtherBorrowers bool + contains string + } + + type liqTest struct { + name string + args args + errArgs errArgs + } + + // Set up test constants + model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5")) + reserveFactor := sdk.MustNewDecFromStr("0.05") + oneMonthInSeconds := int64(2592000) + borrower := sdk.AccAddress(crypto.AddressHash([]byte("randomaddr"))) + otherBorrower1 := sdk.AccAddress(crypto.AddressHash([]byte("AotherBorrower1"))) + otherBorrower2 := sdk.AccAddress(crypto.AddressHash([]byte("BotherBorrower2"))) + otherBorrower3 := sdk.AccAddress(crypto.AddressHash([]byte("CotherBorrower3"))) + otherBorrower4 := sdk.AccAddress(crypto.AddressHash([]byte("DotherBorrower4"))) + otherBorrower5 := sdk.AccAddress(crypto.AddressHash([]byte("EotherBorrower5"))) + otherBorrower6 := sdk.AccAddress(crypto.AddressHash([]byte("FotherBorrower6"))) + otherBorrower7 := sdk.AccAddress(crypto.AddressHash([]byte("GotherBorrower7"))) + otherBorrower8 := sdk.AccAddress(crypto.AddressHash([]byte("HotherBorrower8"))) + otherBorrower9 := sdk.AccAddress(crypto.AddressHash([]byte("IotherBorrower9"))) + otherBorrower10 := sdk.AccAddress(crypto.AddressHash([]byte("JotherBorrower10"))) + + // Set up auction constants + layout := "2006-01-02T15:04:05.000Z" + endTimeStr := "9000-01-01T00:00:00.000Z" + endTime, _ := time.Parse(layout, endTimeStr) + + lotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{borrower}, []sdk.Int{sdk.NewInt(100)}) + otherBorrower1LotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{otherBorrower1}, []sdk.Int{sdk.NewInt(100)}) + otherBorrower2LotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{otherBorrower2}, []sdk.Int{sdk.NewInt(100)}) + otherBorrower3LotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{otherBorrower3}, []sdk.Int{sdk.NewInt(100)}) + + testCases := []liqTest{ + { + "valid: LTV index only liquidates positions over LTV", + args{ + borrower: borrower, + otherBorrowers: []sdk.AccAddress{otherBorrower1, otherBorrower2, otherBorrower3}, + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + otherBorrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(7*KAVA_CF))), + beginBlockerTime: oneMonthInSeconds, + ltvIndexCount: int(10), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(98*KAVA_CF))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8013492), // TODO: why isn't this 8004766 + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectLiquidate: true, + expectLiquidateOtherBorrowers: false, + contains: "", + }, + }, + { + "valid: LTV liquidates multiple positions over LTV", + args{ + borrower: borrower, + otherBorrowers: []sdk.AccAddress{otherBorrower1, otherBorrower2, otherBorrower3}, + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + otherBorrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + beginBlockerTime: oneMonthInSeconds, + ltvIndexCount: int(10), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(98*KAVA_CF))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8014873), // TODO: Why isn't this 8013492 + LotReturns: otherBorrower3LotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 2, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8014873), + LotReturns: otherBorrower2LotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 3, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8014873), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 4, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 10*KAVA_CF), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8014873), + LotReturns: otherBorrower1LotReturns, + }, + }, + }, + errArgs{ + expectLiquidate: true, + expectLiquidateOtherBorrowers: true, + contains: "", + }, + }, + { + "valid: LTV index doesn't liquidate over limit positions outside of top 10", + args{ + borrower: borrower, + otherBorrowers: []sdk.AccAddress{otherBorrower1, otherBorrower2, otherBorrower3, otherBorrower4, otherBorrower5, otherBorrower6, otherBorrower7, otherBorrower8, otherBorrower9, otherBorrower10}, + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(7.99*KAVA_CF))), + otherBorrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + beginBlockerTime: oneMonthInSeconds, + ltvIndexCount: int(10), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(98*KAVA_CF))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{}, // Ignoring other borrower auctions for this test + }, + errArgs{ + expectLiquidate: false, + expectLiquidateOtherBorrowers: true, + contains: "", + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + otherBorrowersCoins := make([]sdk.Coins, len(tc.args.otherBorrowers)) + i := 0 + for i < len(tc.args.otherBorrowers) { + otherBorrowersCoins[i] = tc.args.initialBorrowerCoins + i++ + } + appCoins := append([]sdk.Coins{tc.args.initialBorrowerCoins}, otherBorrowersCoins...) + appAddrs := append([]sdk.AccAddress{tc.args.borrower}, tc.args.otherBorrowers...) + + // Auth module genesis state + authGS := app.NewAuthGenState(appAddrs, appCoins) + + // Harvest module genesis state + harvestGS := types.NewGenesisState(types.NewParams( + true, + types.DistributionSchedules{ + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + }, + types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule( + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + time.Hour*24, + ), + }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdx:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + types.NewMoneyMarket("ukava", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "kava:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + sdk.NewInt(100000*KAVA_CF), // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + }, + tc.args.ltvIndexCount, // LTV counter + ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) + + // Pricefeed module genesis state + pricefeedGS := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + {MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + { + MarketID: "usdx:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "kava:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + }, + } + + // Initialize test application + tApp.InitializeFromGenesisStates(authGS, + app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}, + app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) + + // Mint coins to Harvest module account + supplyKeeper := tApp.GetSupplyKeeper() + supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins) + + auctionKeeper := tApp.GetAuctionKeeper() + + keeper := tApp.GetHarvestKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + suite.auctionKeeper = auctionKeeper + + var err error + + // Run begin blocker to set up state + harvest.BeginBlocker(suite.ctx, suite.keeper) + + // ----------- Users get inserted into the LTV index ----------- + + // Other borrowers take out positions by depositing and borrowing coins + for _, otherBorrower := range tc.args.otherBorrowers { + for _, coin := range tc.args.depositCoins { + err = suite.keeper.Deposit(suite.ctx, otherBorrower, coin) + suite.Require().NoError(err) + } + + err = suite.keeper.Borrow(suite.ctx, otherBorrower, tc.args.otherBorrowCoins) + suite.Require().NoError(err) + } + + // Primary borrower deposits and borrows + for _, coin := range tc.args.depositCoins { + err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, coin) + suite.Require().NoError(err) + } + + err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins) + suite.Require().NoError(err) + + // ----------- Check state before liquidation ----------- + // Other borrowers + for _, otherBorrower := range tc.args.otherBorrowers { + _, foundBorrowBefore := suite.keeper.GetBorrow(suite.ctx, otherBorrower) + suite.Require().True(foundBorrowBefore) + + for _, coin := range tc.args.depositCoins { + _, foundDepositBefore := suite.keeper.GetDeposit(suite.ctx, otherBorrower, coin.Denom) + suite.Require().True(foundDepositBefore) + } + } + + // Primary borrower + _, foundBorrowBefore := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower) + suite.Require().True(foundBorrowBefore) + + for _, coin := range tc.args.depositCoins { + _, foundDepositBefore := suite.keeper.GetDeposit(suite.ctx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositBefore) + } + + // ----------- Liquidate and check state ----------- + // Liquidate the borrow by running begin blocker + runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.beginBlockerTime), 0) + liqCtx := suite.ctx.WithBlockTime(runAtTime) + harvest.BeginBlocker(liqCtx, suite.keeper) + + if tc.errArgs.expectLiquidate { + // Check borrow does not exist after liquidation + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().False(foundBorrowAfter) + // Check deposits do not exist after liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().False(foundDepositAfter) + } + + // Check that borrower's balance contains the expected coins + accBorrower := suite.getAccountAtCtx(tc.args.borrower, liqCtx) + suite.Require().Equal(tc.args.expectedBorrowerCoins, accBorrower.GetCoins()) + + // Check that the expected auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) > 0) + suite.Require().Equal(tc.args.expectedAuctions, auctions) + } else { + // Check that the user's borrow exists + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().True(foundBorrowAfter) + // Check that the user's deposits exist + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositAfter) + } + + if !tc.errArgs.expectLiquidateOtherBorrowers { + // Check that no auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) == 0) + } + } + + // Check other borrowers + if tc.errArgs.expectLiquidateOtherBorrowers { + for _, otherBorrower := range tc.args.otherBorrowers { + // Check borrow does not exist after liquidation + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, otherBorrower) + suite.Require().False(foundBorrowAfter) + + // Check deposits do not exist after liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, otherBorrower, coin.Denom) + suite.Require().False(foundDepositAfter) + } + } + + var expectedLtvIndexItemCount int + if tc.errArgs.expectLiquidate { + expectedLtvIndexItemCount = 0 + } else { + expectedLtvIndexItemCount = 1 + } + indexAddrs := suite.keeper.GetLtvIndexSlice(liqCtx, 1000) // Get all items in the index... + suite.Require().Equal(expectedLtvIndexItemCount, len(indexAddrs)) + } else { + for _, otherBorrower := range tc.args.otherBorrowers { + // Check borrow does not exist after liquidation + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, otherBorrower) + suite.Require().True(foundBorrowAfter) + + // Check deposits do not exist after liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, otherBorrower, coin.Denom) + suite.Require().True(foundDepositAfter) + } + } + + var expectedLtvIndexItemCount int + if tc.errArgs.expectLiquidate { + expectedLtvIndexItemCount = len(tc.args.otherBorrowers) + } else { + expectedLtvIndexItemCount = len(tc.args.otherBorrowers) + 1 + } + indexAddrs := suite.keeper.GetLtvIndexSlice(liqCtx, tc.args.ltvIndexCount) + suite.Require().Equal(expectedLtvIndexItemCount, len(indexAddrs)) + } + }) + } +} + func (suite *KeeperTestSuite) TestKeeperLiquidation() { type args struct { borrower sdk.AccAddress @@ -526,6 +1275,7 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { reserveFactor, // Reserve Factor tc.args.keeperRewardPercent), // Keeper Reward Percent }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) // Pricefeed module genesis state @@ -634,8 +1384,9 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { } // Attempt to liquidate - err = suite.keeper.AttemptKeeperLiquidation(liqCtx, tc.args.keeper, tc.args.borrower) + liquidated, err := suite.keeper.AttemptKeeperLiquidation(liqCtx, tc.args.keeper, tc.args.borrower) if tc.errArgs.expectPass { + suite.Require().True(liquidated) suite.Require().NoError(err) // Check borrow does not exist after liquidation @@ -660,6 +1411,7 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { suite.Require().True(len(auctions) > 0) suite.Require().Equal(tc.args.expectedAuctions, auctions) } else { + suite.Require().False(liquidated) suite.Require().Error(err) suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) diff --git a/x/harvest/keeper/repay.go b/x/harvest/keeper/repay.go index 1c51b809..a20b9349 100644 --- a/x/harvest/keeper/repay.go +++ b/x/harvest/keeper/repay.go @@ -10,13 +10,13 @@ import ( // Repay borrowed funds func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetCurrentLTV(ctx, sender) + prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, sender) if err != nil { return err } // Sync interest so loan is up-to-date - k.SyncOustandingInterest(ctx, sender) + k.SyncOutstandingInterest(ctx, sender) // Validate requested repay err = k.ValidateRepay(ctx, sender, coins) @@ -61,7 +61,8 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e // ValidateRepay validates a requested loan repay func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { senderAcc := k.accountKeeper.GetAccount(ctx, sender) - senderCoins := senderAcc.GetCoins() + senderCoins := senderAcc.SpendableCoins(ctx.BlockTime()) + for _, coin := range coins { if senderCoins.AmountOf(coin.Denom).LT(coin.Amount) { return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(coin.Denom), coin.Denom) diff --git a/x/harvest/keeper/repay_test.go b/x/harvest/keeper/repay_test.go index e1af3e45..af47f37f 100644 --- a/x/harvest/keeper/repay_test.go +++ b/x/harvest/keeper/repay_test.go @@ -144,6 +144,7 @@ func (suite *KeeperTestSuite) TestRepay() { sdk.MustNewDecFromStr("0.05"), // Reserve Factor sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) // Pricefeed module genesis state diff --git a/x/harvest/keeper/rewards_test.go b/x/harvest/keeper/rewards_test.go index c5ad8087..db6b2d31 100644 --- a/x/harvest/keeper/rewards_test.go +++ b/x/harvest/keeper/rewards_test.go @@ -78,6 +78,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() { types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), tc.args.previousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) supplyKeeper := tApp.GetSupplyKeeper() @@ -446,6 +447,7 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState { types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes, diff --git a/x/harvest/keeper/timelock_test.go b/x/harvest/keeper/timelock_test.go index 09d42927..5615c20b 100644 --- a/x/harvest/keeper/timelock_test.go +++ b/x/harvest/keeper/timelock_test.go @@ -294,6 +294,7 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() { types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + 0, // LTV counter ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) if tc.args.accArgs.vestingAccountBefore { diff --git a/x/harvest/types/genesis_test.go b/x/harvest/types/genesis_test.go index aaf2052e..f2f48010 100644 --- a/x/harvest/types/genesis_test.go +++ b/x/harvest/types/genesis_test.go @@ -52,6 +52,7 @@ func (suite *GenesisTestSuite) TestGenesisValidation() { ), }, types.DefaultMoneyMarkets, + types.DefaultCheckLtvIndexCount, ), pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), pdts: types.GenesisDistributionTimes{ @@ -75,6 +76,7 @@ func (suite *GenesisTestSuite) TestGenesisValidation() { ), }, types.DefaultMoneyMarkets, + types.DefaultCheckLtvIndexCount, ), pbt: time.Time{}, pdts: types.GenesisDistributionTimes{ @@ -98,6 +100,7 @@ func (suite *GenesisTestSuite) TestGenesisValidation() { ), }, types.DefaultMoneyMarkets, + types.DefaultCheckLtvIndexCount, ), pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), pdts: types.GenesisDistributionTimes{ diff --git a/x/harvest/types/params.go b/x/harvest/types/params.go index e0ed1795..9f2d12c9 100644 --- a/x/harvest/types/params.go +++ b/x/harvest/types/params.go @@ -17,11 +17,13 @@ var ( KeyLPSchedules = []byte("LPSchedules") KeyDelegatorSchedule = []byte("DelegatorSchedule") KeyMoneyMarkets = []byte("MoneyMarkets") + KeyCheckLtvIndexCount = []byte("CheckLtvIndexCount") DefaultActive = true DefaultGovSchedules = DistributionSchedules{} DefaultLPSchedules = DistributionSchedules{} DefaultDelegatorSchedules = DelegatorDistributionSchedules{} DefaultMoneyMarkets = MoneyMarkets{} + DefaultCheckLtvIndexCount = 10 GovDenom = cdptypes.DefaultGovDenom ) @@ -31,6 +33,7 @@ type Params struct { LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"` DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"` MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"` + CheckLtvIndexCount int `json:"check_ltv_index_count" yaml:"check_ltv_index_count"` } // DistributionSchedule distribution schedule for liquidity providers @@ -423,18 +426,21 @@ func (irm InterestRateModel) Equal(irmCompareTo InterestRateModel) bool { type InterestRateModels []InterestRateModel // NewParams returns a new params object -func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules, moneyMarkets MoneyMarkets) Params { +func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules, + moneyMarkets MoneyMarkets, checkLtvIndexCount int) Params { return Params{ Active: active, LiquidityProviderSchedules: lps, DelegatorDistributionSchedules: dds, MoneyMarkets: moneyMarkets, + CheckLtvIndexCount: checkLtvIndexCount, } } // DefaultParams returns default params for harvest module func DefaultParams() Params { - return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules, DefaultMoneyMarkets) + return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules, + DefaultMoneyMarkets, DefaultCheckLtvIndexCount) } // String implements fmt.Stringer @@ -443,8 +449,10 @@ func (p Params) String() string { Active: %t Liquidity Provider Distribution Schedules %s Delegator Distribution Schedule %s - Money Markets %v`, - p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, p.MoneyMarkets) + Money Markets %v + Check LTV Index Count: %v`, + p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, + p.MoneyMarkets, p.CheckLtvIndexCount) } // ParamKeyTable Key declaration for parameters @@ -459,6 +467,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams), params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams), params.NewParamSetPair(KeyMoneyMarkets, &p.MoneyMarkets, validateMoneyMarketParams), + params.NewParamSetPair(KeyCheckLtvIndexCount, &p.CheckLtvIndexCount, validateCheckLtvIndexCount), } } @@ -472,7 +481,15 @@ func (p Params) Validate() error { return err } - return validateLPParams(p.LiquidityProviderSchedules) + if err := validateLPParams(p.LiquidityProviderSchedules); err != nil { + return err + } + + if err := validateMoneyMarketParams(p.MoneyMarkets); err != nil { + return err + } + + return validateCheckLtvIndexCount(p.CheckLtvIndexCount) } func validateActiveParam(i interface{}) error { @@ -517,3 +534,16 @@ func validateMoneyMarketParams(i interface{}) error { return mm.Validate() } + +func validateCheckLtvIndexCount(i interface{}) error { + ltvCheckCount, ok := i.(int) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if ltvCheckCount < 0 { + return fmt.Errorf("CheckLtvIndexCount param must be positive, got: %d", ltvCheckCount) + } + + return nil +} diff --git a/x/harvest/types/params_test.go b/x/harvest/types/params_test.go index f2219c17..10f75fae 100644 --- a/x/harvest/types/params_test.go +++ b/x/harvest/types/params_test.go @@ -18,12 +18,12 @@ type ParamTestSuite struct { func (suite *ParamTestSuite) TestParamValidation() { type args struct { - lps types.DistributionSchedules - gds types.DistributionSchedules - dds types.DelegatorDistributionSchedules - mms types.MoneyMarkets - kpr sdk.Dec - active bool + lps types.DistributionSchedules + gds types.DistributionSchedules + dds types.DelegatorDistributionSchedules + mms types.MoneyMarkets + ltvCounter int + active bool } testCases := []struct { name string @@ -52,9 +52,9 @@ func (suite *ParamTestSuite) TestParamValidation() { time.Hour*24, ), }, - mms: types.DefaultMoneyMarkets, - kpr: sdk.MustNewDecFromStr("0.05"), - active: true, + mms: types.DefaultMoneyMarkets, + ltvCounter: 10, + active: true, }, expectPass: true, expectedErr: "", @@ -70,9 +70,9 @@ func (suite *ParamTestSuite) TestParamValidation() { time.Hour*24, ), }, - mms: types.DefaultMoneyMarkets, - kpr: sdk.MustNewDecFromStr("0.05"), - active: true, + mms: types.DefaultMoneyMarkets, + ltvCounter: 10, + active: true, }, expectPass: false, expectedErr: "reward denom should be hard", @@ -80,7 +80,7 @@ func (suite *ParamTestSuite) TestParamValidation() { } for _, tc := range testCases { suite.Run(tc.name, func() { - params := types.NewParams(tc.args.active, tc.args.lps, tc.args.dds, tc.args.mms) + params := types.NewParams(tc.args.active, tc.args.lps, tc.args.dds, tc.args.mms, tc.args.ltvCounter) err := params.Validate() if tc.expectPass { suite.NoError(err)