mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +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