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 {
|
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
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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++ {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
)
|
)
|
||||||
|
@ -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...)
|
||||||
|
Loading…
Reference in New Issue
Block a user