diff --git a/x/harvest/keeper/borrow.go b/x/harvest/keeper/borrow.go index 7eaa9e9b..0d8538d8 100644 --- a/x/harvest/keeper/borrow.go +++ b/x/harvest/keeper/borrow.go @@ -15,15 +15,27 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins for _, coin := range coins { _, foundBorrowIndex := k.GetBorrowIndex(ctx, coin.Denom) if !foundBorrowIndex { - k.SetBorrowIndex(ctx, coin.Denom, sdk.OneDec()) + _, foundMM := k.GetMoneyMarket(ctx, coin.Denom) + if foundMM { + k.SetBorrowIndex(ctx, coin.Denom, sdk.OneDec()) + } } } - // Sync user's borrow balance (only for coins user is requesting to borrow) - k.SyncBorrowInterest(ctx, borrower, coins) + // Get current stored LTV based on stored borrows/deposits + prevLtv, shouldRemoveIndex, err := k.GetCurrentLTV(ctx, borrower) + if err != nil { + return err + } + + // If the user has an existing borrow, sync its outstanding interest + _, found := k.GetBorrow(ctx, borrower) + if found { + k.SyncOustandingInterest(ctx, borrower) + } // Validate borrow amount within user and protocol limits - err := k.ValidateBorrow(ctx, borrower, coins) + err = k.ValidateBorrow(ctx, borrower, coins) if err != nil { return err } @@ -45,14 +57,26 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins } } - borrow, found := k.GetBorrow(ctx, borrower) + // On user's first borrow, build borrow index list containing denoms and current global borrow index value + // We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps. if !found { - return types.ErrBorrowNotFound // This should never happen + var borrowIndexes types.BorrowIndexes + for _, coin := range coins { + borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom) + borrowIndex := types.NewBorrowIndexItem(coin.Denom, borrowIndexValue) + borrowIndexes = append(borrowIndexes, borrowIndex) + } + borrow := types.NewBorrow(borrower, sdk.Coins{}, borrowIndexes) + k.SetBorrow(ctx, borrow) } + // Add the newly borrowed coins to the user's borrow object + borrow, _ := k.GetBorrow(ctx, borrower) borrow.Amount = borrow.Amount.Add(coins...) k.SetBorrow(ctx, borrow) + k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower) + // Update total borrowed amount by newly borrowed coins. Don't add user's pending interest as // it has already been included in the total borrowed coins by the BeginBlocker. k.IncrementBorrowedCoins(ctx, coins) @@ -68,55 +92,44 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins return nil } -// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state, -// returning an sdk.Coins object containing the amount of newly accumulated interest. -func (k Keeper) SyncBorrowInterest(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) sdk.Coins { +// 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) { totalNewInterest := sdk.Coins{} // Update user's borrow index list for each asset in the 'coins' array. // We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps. - borrow, found := k.GetBorrow(ctx, borrower) - if !found { // User's first borrow - // Build borrow index list containing (denoms, borrow index value at borrow time) - var borrowIndexes types.BorrowIndexes - for _, coin := range coins { - borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom) - borrowIndex := types.NewBorrowIndexItem(coin.Denom, borrowIndexValue) - borrowIndexes = append(borrowIndexes, borrowIndex) - } - borrow = types.NewBorrow(borrower, sdk.Coins{}, borrowIndexes) - } else { // User has existing borrow - for _, coin := range coins { - // Locate the borrow index item by coin denom in the user's list of borrow indexes - foundAtIndex := -1 - for i := range borrow.Index { - if borrow.Index[i].Denom == coin.Denom { - foundAtIndex = i - break - } - } - - borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom) - if foundAtIndex == -1 { // First time user has borrowed this denom - borrow.Index = append(borrow.Index, types.NewBorrowIndexItem(coin.Denom, borrowIndexValue)) - } else { // User has an existing borrow index for this denom - // Calculate interest owed by user since asset's last borrow index update - storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom)) - userLastBorrowIndex := borrow.Index[foundAtIndex].Value - interest := (storedAmount.Quo(userLastBorrowIndex).Mul(borrowIndexValue)).Sub(storedAmount) - totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt())) - // We're synced up, so update user's borrow index value to match the current global borrow index value - borrow.Index[foundAtIndex].Value = borrowIndexValue - } - } - // Add all pending interest to user's borrow - borrow.Amount = borrow.Amount.Add(totalNewInterest...) + borrow, found := k.GetBorrow(ctx, addr) + if !found { + return } + for _, coin := range borrow.Amount { + // Locate the borrow index item by coin denom in the user's list of borrow indexes + foundAtIndex := -1 + for i := range borrow.Index { + if borrow.Index[i].Denom == coin.Denom { + foundAtIndex = i + break + } + } + + borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom) + if foundAtIndex == -1 { // First time user has borrowed this denom + borrow.Index = append(borrow.Index, types.NewBorrowIndexItem(coin.Denom, borrowIndexValue)) + } else { // User has an existing borrow index for this denom + // Calculate interest owed by user since asset's last borrow index update + storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom)) + userLastBorrowIndex := borrow.Index[foundAtIndex].Value + interest := (storedAmount.Quo(userLastBorrowIndex).Mul(borrowIndexValue)).Sub(storedAmount) + totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt())) + // We're synced up, so update user's borrow index value to match the current global borrow index value + borrow.Index[foundAtIndex].Value = borrowIndexValue + } + } + // Add all pending interest to user's borrow + borrow.Amount = borrow.Amount.Add(totalNewInterest...) // Update user's borrow in the store k.SetBorrow(ctx, borrow) - - return totalNewInterest } // ValidateBorrow validates a borrow request against borrower and protocol requirements diff --git a/x/harvest/keeper/borrow_test.go b/x/harvest/keeper/borrow_test.go index aa537500..10b04127 100644 --- a/x/harvest/keeper/borrow_test.go +++ b/x/harvest/keeper/borrow_test.go @@ -10,6 +10,7 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/harvest" "github.com/kava-labs/kava/x/harvest/types" "github.com/kava-labs/kava/x/pricefeed" ) @@ -348,6 +349,9 @@ func (suite *KeeperTestSuite) TestBorrow() { var err error + // Run BeginBlocker once to transition MoneyMarkets + harvest.BeginBlocker(suite.ctx, suite.keeper) + // Deposit coins to harvest depositedCoins := sdk.NewCoins() for _, depositCoin := range tc.args.depositCoins { diff --git a/x/harvest/keeper/deposit.go b/x/harvest/keeper/deposit.go index c16fe5e0..f709568d 100644 --- a/x/harvest/keeper/deposit.go +++ b/x/harvest/keeper/deposit.go @@ -10,7 +10,15 @@ import ( // Deposit deposit func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error { - err := k.ValidateDeposit(ctx, amount) + // Get current stored LTV based on stored borrows/deposits + prevLtv, shouldRemoveIndex, err := k.GetCurrentLTV(ctx, depositor) + if err != nil { + return err + } + + k.SyncOustandingInterest(ctx, depositor) + + err = k.ValidateDeposit(ctx, amount) if err != nil { return err } @@ -29,6 +37,8 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Co k.SetDeposit(ctx, deposit) + k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeHarvestDeposit, diff --git a/x/harvest/keeper/deposit_test.go b/x/harvest/keeper/deposit_test.go index 823a579f..38452205 100644 --- a/x/harvest/keeper/deposit_test.go +++ b/x/harvest/keeper/deposit_test.go @@ -10,7 +10,9 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/harvest" "github.com/kava-labs/kava/x/harvest/types" + "github.com/kava-labs/kava/x/pricefeed" ) func (suite *KeeperTestSuite) TestDeposit() { @@ -110,14 +112,61 @@ func (suite *KeeperTestSuite) TestDeposit() { types.MoneyMarkets{ 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()), + 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()), }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) - tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) + + // 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}, + {MarketID: "btcb:usd", BaseAsset: "btcb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "bnb:usd", BaseAsset: "bnb", 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(1 * time.Hour), + }, + { + MarketID: "kava:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + { + MarketID: "btcb:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("100.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + { + MarketID: "bnb:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("10.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + + tApp.InitializeFromGenesisStates(authGS, + app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}, + app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}, + ) keeper := tApp.GetHarvestKeeper() suite.app = tApp suite.ctx = ctx suite.keeper = keeper + // Run BeginBlocker once to transition MoneyMarkets + harvest.BeginBlocker(suite.ctx, suite.keeper) + // run the test var err error for i := 0; i < tc.args.numberDeposits; i++ { diff --git a/x/harvest/keeper/keeper.go b/x/harvest/keeper/keeper.go index 2662f6ff..064e5efb 100644 --- a/x/harvest/keeper/keeper.go +++ b/x/harvest/keeper/keeper.go @@ -358,3 +358,46 @@ func (k Keeper) SetBorrowIndex(ctx sdk.Context, denom string, borrowIndex sdk.De bz := k.cdc.MustMarshalBinaryBare(borrowIndex) store.Set([]byte(denom), bz) } + +// InsertIntoLtvIndex indexes a user's borrow object by its current LTV +func (k Keeper) InsertIntoLtvIndex(ctx sdk.Context, ltv sdk.Dec, borrower sdk.AccAddress) { + store := prefix.NewStore(ctx.KVStore(k.key), types.LtvIndexPrefix) + store.Set(types.GetBorrowByLtvKey(ltv, borrower), borrower) +} + +// RemoveFromLtvIndex removes a user's borrow object from the LTV index +func (k Keeper) RemoveFromLtvIndex(ctx sdk.Context, ltv sdk.Dec, borrower sdk.AccAddress) { + store := prefix.NewStore(ctx.KVStore(k.key), types.LtvIndexPrefix) + store.Delete(types.GetBorrowByLtvKey(ltv, borrower)) +} + +// IterateLtvIndex provides an iterator over the borrowers ordered by LTV. +// For results found before the cutoff count, the cb will be called and the item returned. +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) + count := 0 + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + + // Stop iteration after first 10 items + count = count + 1 + if count > cutoffCount { + break + } + + id := iterator.Value() + cb(id) + } +} + +// 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 { + addrs = append(addrs, addr) + return false + }) + return +} diff --git a/x/harvest/keeper/keeper_test.go b/x/harvest/keeper/keeper_test.go index ed6200a1..e8db0855 100644 --- a/x/harvest/keeper/keeper_test.go +++ b/x/harvest/keeper/keeper_test.go @@ -208,6 +208,54 @@ func (suite *KeeperTestSuite) TestIterateInterestRateModels() { suite.Require().Equal(setDenoms, seenDenoms) } +func (suite *KeeperTestSuite) TestSetDeleteLtvIndex() { + // LTV index should have 0 items + firstAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + suite.Require().Equal(0, len(firstAddrs)) + + // Add an item to the LTV index + addr := sdk.AccAddress("test") + ltv := sdk.MustNewDecFromStr("1.1") + suite.Require().NotPanics(func() { suite.keeper.InsertIntoLtvIndex(suite.ctx, ltv, addr) }) + + // LTV index should have 1 item + secondAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + suite.Require().Equal(1, len(secondAddrs)) + + // Attempt to remove invalid item from LTV index + fakeLtv := sdk.MustNewDecFromStr("1.2") + suite.Require().NotPanics(func() { suite.keeper.RemoveFromLtvIndex(suite.ctx, fakeLtv, addr) }) + + // LTV index should still have 1 item + thirdAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + 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) + suite.Require().Equal(0, len(fourthAddrs)) +} + +func (suite *KeeperTestSuite) TestIterateLtvIndex() { + var setAddrs []sdk.AccAddress + for i := 1; i <= 20; i++ { + addr := sdk.AccAddress("test" + fmt.Sprint(i)) + incrementalDec := sdk.NewDec(int64(i)).Quo(sdk.NewDec(10)) + ltv := sdk.OneDec().Add(incrementalDec) + + // Set the ltv-address pair in the store + suite.Require().NotPanics(func() { suite.keeper.InsertIntoLtvIndex(suite.ctx, ltv, addr) }) + + setAddrs = append(setAddrs, addr) + } + + // Only the first 10 addresses should be returned + sliceAddrs := suite.keeper.GetLtvIndexSlice(suite.ctx) + suite.Require().Equal(setAddrs[:10], sliceAddrs) +} + func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account { ak := suite.app.GetAccountKeeper() return ak.GetAccount(suite.ctx, addr) diff --git a/x/harvest/keeper/liquidation.go b/x/harvest/keeper/liquidation.go index 2ef5f8b9..86d98a09 100644 --- a/x/harvest/keeper/liquidation.go +++ b/x/harvest/keeper/liquidation.go @@ -253,6 +253,83 @@ 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) { + // Fetch deposits and parse coin denoms + deposits := k.GetDepositsByUser(ctx, addr) + depositDenoms := []string{} + for _, deposit := range deposits { + depositDenoms = append(depositDenoms, deposit.Amount.Denom) + } + + // Fetch borrow balances and parse coin denoms + borrowBalances := k.GetBorrowBalance(ctx, addr) + if borrowBalances.IsZero() { + return sdk.ZeroDec(), false, nil + } + borrowDenoms := getDenoms(borrowBalances) + + liqMap := make(map[string]LiqData) + + // Load required liquidation data for every deposit/borrow denom + denoms := removeDuplicates(borrowDenoms, depositDenoms) + for _, denom := range denoms { + mm, found := k.GetMoneyMarket(ctx, denom) + if !found { + return sdk.ZeroDec(), false, sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom) + } + + priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID) + if err != nil { + return sdk.ZeroDec(), false, err + } + + liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor} + } + + // Build valuation map to hold deposit coin USD valuations + depositCoinValues := types.NewValuationMap() + for _, deposit := range deposits { + dData := liqMap[deposit.Amount.Denom] + dCoinUsdValue := sdk.NewDecFromInt(deposit.Amount.Amount).Quo(sdk.NewDecFromInt(dData.conversionFactor)).Mul(dData.price) + depositCoinValues.Increment(deposit.Amount.Denom, dCoinUsdValue) + } + + // Build valuation map to hold borrow coin USD valuations + borrowCoinValues := types.NewValuationMap() + for _, bCoin := range borrowBalances { + bData := liqMap[bCoin.Denom] + bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price) + borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue) + } + + // User doesn't have any deposits, catch divide by 0 error + sumDeposits := depositCoinValues.Sum() + if sumDeposits.Equal(sdk.ZeroDec()) { + return sdk.ZeroDec(), false, nil + } + + // Loan-to-Value ratio + return borrowCoinValues.Sum().Quo(sumDeposits), true, nil +} + +// 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) + if err != nil { + return err + } + + if shouldRemoveIndex { + k.RemoveFromLtvIndex(ctx, prevLtv, borrower) + } + if shouldInsertIndex { + k.InsertIntoLtvIndex(ctx, currLtv, borrower) + } + return nil +} + func getDenoms(coins sdk.Coins) []string { denoms := []string{} for _, coin := range coins { diff --git a/x/harvest/keeper/repay.go b/x/harvest/keeper/repay.go index e7f88fea..1c51b809 100644 --- a/x/harvest/keeper/repay.go +++ b/x/harvest/keeper/repay.go @@ -9,11 +9,17 @@ 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) + if err != nil { + return err + } + // Sync interest so loan is up-to-date - k.SyncBorrowInterest(ctx, sender, coins) + k.SyncOustandingInterest(ctx, sender) // Validate requested repay - err := k.ValidateRepay(ctx, sender, coins) + err = k.ValidateRepay(ctx, sender, coins) if err != nil { return err } @@ -36,6 +42,8 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e borrow.Amount = borrow.Amount.Sub(payment) k.SetBorrow(ctx, borrow) + k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, sender) + // Update total borrowed amount k.DecrementBorrowedCoins(ctx, payment) diff --git a/x/harvest/keeper/repay_test.go b/x/harvest/keeper/repay_test.go index b16f5c1b..e1af3e45 100644 --- a/x/harvest/keeper/repay_test.go +++ b/x/harvest/keeper/repay_test.go @@ -10,6 +10,7 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/harvest" "github.com/kava-labs/kava/x/harvest/types" "github.com/kava-labs/kava/x/pricefeed" ) @@ -186,6 +187,9 @@ func (suite *KeeperTestSuite) TestRepay() { var err error + // Run BeginBlocker once to transition MoneyMarkets + harvest.BeginBlocker(suite.ctx, suite.keeper) + // Deposit coins to harvest depositedCoins := sdk.NewCoins() for _, depositCoin := range tc.args.depositCoins { diff --git a/x/harvest/types/errors.go b/x/harvest/types/errors.go index eb1f1e2e..4e7296a9 100644 --- a/x/harvest/types/errors.go +++ b/x/harvest/types/errors.go @@ -65,4 +65,6 @@ var ( ErrBorrowNotLiquidatable = sdkerrors.Register(ModuleName, 29, "borrow not liquidatable") // ErrInsufficientCoins error for when there are not enough coins for the operation ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins") + // ErrInsufficientBalanceForBorrow error for when the requested borrow exceeds user's balance + ErrInsufficientBalanceForBorrow = sdkerrors.Register(ModuleName, 31, "insufficient balance") ) diff --git a/x/harvest/types/keys.go b/x/harvest/types/keys.go index 05dd0b19..ecc54cc6 100644 --- a/x/harvest/types/keys.go +++ b/x/harvest/types/keys.go @@ -44,6 +44,7 @@ var ( PreviousAccrualTimePrefix = []byte{0x08} // denom -> time TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin BorrowIndexPrefix = []byte{0x10} // denom -> sdk.Dec + LtvIndexPrefix = []byte{0x11} sep = []byte(":") ) @@ -67,6 +68,11 @@ func ClaimTypeIteratorKey(depositType ClaimType, denom string) []byte { return createKey([]byte(depositType), sep, []byte(denom)) } +// GetBorrowByLtvKey is used by the LTV index +func GetBorrowByLtvKey(ltv sdk.Dec, borrower sdk.AccAddress) []byte { + return append(ltv.Bytes(), borrower...) +} + func createKey(bytes ...[]byte) (r []byte) { for _, b := range bytes { r = append(r, b...)