Hard: introduce LTV index (#742)

* hotfix

* update params, keys

* liquidation by keeper

* refactor GetPendingBorrowBalance

* fix app build

* elegant handling of denom arrays

* auction deposit in lots

* add error msg

* update tests with new params

* happy path liquidation test

* update liquidator macc name

* refactor reward % to money market params

* refactor tests for updated params

* compile: harvest liquidator module account

* add liquidate msg

* liquidation approach

* update liquidations

* return remaining deposit coins to original borrowr

* check keeper reward before sending

* introduce ValuationMap

* convert Ints <> Decs

* implement double-loop

* ModuleAccountName

* sort keys for deterministic auctions

* test: correct auctions created

* test: preset keeper coins

* ensure deterministic iteration

* test cases

* update repay test

* auction fixes, tests

* LTV index

* user actions sync interest and update ltv index

* tests: all deposits must have money markets

* reorder borrow logic

* check mm before setting borrow index

* insert into LTV index even when LTV is 0
This commit is contained in:
Denali Marsh 2020-12-16 22:08:29 +01:00 committed by GitHub
parent 89f07e92b4
commit 6c0890d5ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 315 additions and 51 deletions

View File

@ -15,15 +15,27 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
for _, coin := range coins { for _, coin := range coins {
_, foundBorrowIndex := k.GetBorrowIndex(ctx, coin.Denom) _, foundBorrowIndex := k.GetBorrowIndex(ctx, coin.Denom)
if !foundBorrowIndex { 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) // Get current stored LTV based on stored borrows/deposits
k.SyncBorrowInterest(ctx, borrower, coins) 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 // Validate borrow amount within user and protocol limits
err := k.ValidateBorrow(ctx, borrower, coins) err = k.ValidateBorrow(ctx, borrower, coins)
if err != nil { if err != nil {
return err 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 { 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 // Add the newly borrowed coins to the user's borrow object
borrow, _ := k.GetBorrow(ctx, borrower)
borrow.Amount = borrow.Amount.Add(coins...) borrow.Amount = borrow.Amount.Add(coins...)
k.SetBorrow(ctx, borrow) 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 // 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. // it has already been included in the total borrowed coins by the BeginBlocker.
k.IncrementBorrowedCoins(ctx, coins) k.IncrementBorrowedCoins(ctx, coins)
@ -68,55 +92,44 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
return nil return nil
} }
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state, // SyncOustandingInterest 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) SyncOustandingInterest(ctx sdk.Context, addr sdk.AccAddress) {
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) sdk.Coins {
totalNewInterest := sdk.Coins{} totalNewInterest := sdk.Coins{}
// Update user's borrow index list for each asset in the 'coins' array. // 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. // We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps.
borrow, found := k.GetBorrow(ctx, borrower) borrow, found := k.GetBorrow(ctx, addr)
if !found { // User's first borrow if !found {
// Build borrow index list containing (denoms, borrow index value at borrow time) return
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...)
} }
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 // Update user's borrow in the store
k.SetBorrow(ctx, borrow) k.SetBorrow(ctx, borrow)
return totalNewInterest
} }
// ValidateBorrow validates a borrow request against borrower and protocol requirements // ValidateBorrow validates a borrow request against borrower and protocol requirements

View File

@ -10,6 +10,7 @@ import (
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "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/harvest/types"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
) )
@ -348,6 +349,9 @@ func (suite *KeeperTestSuite) TestBorrow() {
var err error var err error
// Run BeginBlocker once to transition MoneyMarkets
harvest.BeginBlocker(suite.ctx, suite.keeper)
// Deposit coins to harvest // Deposit coins to harvest
depositedCoins := sdk.NewCoins() depositedCoins := sdk.NewCoins()
for _, depositCoin := range tc.args.depositCoins { for _, depositCoin := range tc.args.depositCoins {

View File

@ -10,7 +10,15 @@ import (
// Deposit deposit // Deposit deposit
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error { 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 { if err != nil {
return err return err
} }
@ -29,6 +37,8 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Co
k.SetDeposit(ctx, deposit) k.SetDeposit(ctx, deposit)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeHarvestDeposit, types.EventTypeHarvestDeposit,

View File

@ -10,7 +10,9 @@ import (
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "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/harvest/types"
"github.com/kava-labs/kava/x/pricefeed"
) )
func (suite *KeeperTestSuite) TestDeposit() { func (suite *KeeperTestSuite) TestDeposit() {
@ -110,14 +112,61 @@ func (suite *KeeperTestSuite) TestDeposit() {
types.MoneyMarkets{ 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("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("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) ), 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() keeper := tApp.GetHarvestKeeper()
suite.app = tApp suite.app = tApp
suite.ctx = ctx suite.ctx = ctx
suite.keeper = keeper suite.keeper = keeper
// Run BeginBlocker once to transition MoneyMarkets
harvest.BeginBlocker(suite.ctx, suite.keeper)
// run the test // run the test
var err error var err error
for i := 0; i < tc.args.numberDeposits; i++ { for i := 0; i < tc.args.numberDeposits; i++ {

View File

@ -358,3 +358,46 @@ func (k Keeper) SetBorrowIndex(ctx sdk.Context, denom string, borrowIndex sdk.De
bz := k.cdc.MustMarshalBinaryBare(borrowIndex) bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
store.Set([]byte(denom), bz) 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
}

View File

@ -208,6 +208,54 @@ func (suite *KeeperTestSuite) TestIterateInterestRateModels() {
suite.Require().Equal(setDenoms, seenDenoms) 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 { func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.app.GetAccountKeeper() ak := suite.app.GetAccountKeeper()
return ak.GetAccount(suite.ctx, addr) return ak.GetAccount(suite.ctx, addr)

View File

@ -253,6 +253,83 @@ func (k Keeper) StartAuctions(ctx sdk.Context, borrower sdk.AccAddress, borrows,
return nil 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 { func getDenoms(coins sdk.Coins) []string {
denoms := []string{} denoms := []string{}
for _, coin := range coins { for _, coin := range coins {

View File

@ -9,11 +9,17 @@ import (
// Repay borrowed funds // Repay borrowed funds
func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { 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 // Sync interest so loan is up-to-date
k.SyncBorrowInterest(ctx, sender, coins) k.SyncOustandingInterest(ctx, sender)
// Validate requested repay // Validate requested repay
err := k.ValidateRepay(ctx, sender, coins) err = k.ValidateRepay(ctx, sender, coins)
if err != nil { if err != nil {
return err 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) borrow.Amount = borrow.Amount.Sub(payment)
k.SetBorrow(ctx, borrow) k.SetBorrow(ctx, borrow)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, sender)
// Update total borrowed amount // Update total borrowed amount
k.DecrementBorrowedCoins(ctx, payment) k.DecrementBorrowedCoins(ctx, payment)

View File

@ -10,6 +10,7 @@ import (
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "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/harvest/types"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
) )
@ -186,6 +187,9 @@ func (suite *KeeperTestSuite) TestRepay() {
var err error var err error
// Run BeginBlocker once to transition MoneyMarkets
harvest.BeginBlocker(suite.ctx, suite.keeper)
// Deposit coins to harvest // Deposit coins to harvest
depositedCoins := sdk.NewCoins() depositedCoins := sdk.NewCoins()
for _, depositCoin := range tc.args.depositCoins { for _, depositCoin := range tc.args.depositCoins {

View File

@ -65,4 +65,6 @@ var (
ErrBorrowNotLiquidatable = sdkerrors.Register(ModuleName, 29, "borrow not liquidatable") ErrBorrowNotLiquidatable = sdkerrors.Register(ModuleName, 29, "borrow not liquidatable")
// ErrInsufficientCoins error for when there are not enough coins for the operation // ErrInsufficientCoins error for when there are not enough coins for the operation
ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins") 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")
) )

View File

@ -44,6 +44,7 @@ var (
PreviousAccrualTimePrefix = []byte{0x08} // denom -> time PreviousAccrualTimePrefix = []byte{0x08} // denom -> time
TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin
BorrowIndexPrefix = []byte{0x10} // denom -> sdk.Dec BorrowIndexPrefix = []byte{0x10} // denom -> sdk.Dec
LtvIndexPrefix = []byte{0x11}
sep = []byte(":") sep = []byte(":")
) )
@ -67,6 +68,11 @@ func ClaimTypeIteratorKey(depositType ClaimType, denom string) []byte {
return createKey([]byte(depositType), sep, []byte(denom)) 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) { func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes { for _, b := range bytes {
r = append(r, b...) r = append(r, b...)