mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 04:37:26 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									89f07e92b4
								
							
						
					
					
						commit
						6c0890d5ff
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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++ {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -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...)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user