2020-10-30 09:59:47 +00:00
package keeper
import (
2020-11-11 15:05:17 +00:00
"strings"
2020-10-30 09:59:47 +00:00
sdk "github.com/cosmos/cosmos-sdk/types"
2020-11-03 09:46:08 +00:00
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
2020-10-30 09:59:47 +00:00
"github.com/kava-labs/kava/x/harvest/types"
)
// Borrow funds
func ( k Keeper ) Borrow ( ctx sdk . Context , borrower sdk . AccAddress , coins sdk . Coins ) error {
2020-12-03 21:50:35 +00:00
// Set any new denoms' global borrow index to 1.0
for _ , coin := range coins {
_ , foundBorrowIndex := k . GetBorrowIndex ( ctx , coin . Denom )
if ! foundBorrowIndex {
2020-12-16 21:08:29 +00:00
_ , foundMM := k . GetMoneyMarket ( ctx , coin . Denom )
if foundMM {
k . SetBorrowIndex ( ctx , coin . Denom , sdk . OneDec ( ) )
}
2020-12-03 21:50:35 +00:00
}
}
2020-12-16 21:08:29 +00:00
// Get current stored LTV based on stored borrows/deposits
2020-12-18 01:12:48 +00:00
prevLtv , shouldRemoveIndex , err := k . GetStoreLTV ( ctx , borrower )
2020-12-16 21:08:29 +00:00
if err != nil {
return err
}
// If the user has an existing borrow, sync its outstanding interest
_ , found := k . GetBorrow ( ctx , borrower )
if found {
2020-12-18 01:12:48 +00:00
k . SyncOutstandingInterest ( ctx , borrower )
2020-12-16 21:08:29 +00:00
}
2020-12-03 21:50:35 +00:00
2020-11-12 15:50:54 +00:00
// Validate borrow amount within user and protocol limits
2020-12-16 21:08:29 +00:00
err = k . ValidateBorrow ( ctx , borrower , coins )
2020-11-03 09:46:08 +00:00
if err != nil {
return err
}
2020-11-12 15:50:54 +00:00
// Sends coins from Harvest module account to user
2020-11-03 09:46:08 +00:00
err = k . supplyKeeper . SendCoinsFromModuleToAccount ( ctx , types . ModuleAccountName , borrower , coins )
2020-10-30 09:59:47 +00:00
if err != nil {
2020-11-11 15:05:17 +00:00
if strings . Contains ( err . Error ( ) , "insufficient account funds" ) {
modAccCoins := k . supplyKeeper . GetModuleAccount ( ctx , types . ModuleAccountName ) . GetCoins ( )
for _ , coin := range coins {
_ , isNegative := modAccCoins . SafeSub ( sdk . NewCoins ( coin ) )
if isNegative {
return sdkerrors . Wrapf ( types . ErrBorrowExceedsAvailableBalance ,
"the requested borrow amount of %s exceeds the total amount of %s%s available to borrow" ,
coin , modAccCoins . AmountOf ( coin . Denom ) , coin . Denom ,
)
}
}
}
2020-10-30 09:59:47 +00:00
}
2020-12-16 21:08:29 +00:00
// 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.
2020-10-30 09:59:47 +00:00
if ! found {
2020-12-16 21:08:29 +00:00
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 )
2020-10-30 09:59:47 +00:00
}
2020-12-16 21:08:29 +00:00
2020-12-03 21:50:35 +00:00
// Add the newly borrowed coins to the user's borrow object
2020-12-16 21:08:29 +00:00
borrow , _ := k . GetBorrow ( ctx , borrower )
2020-12-03 21:50:35 +00:00
borrow . Amount = borrow . Amount . Add ( coins ... )
2020-10-30 09:59:47 +00:00
k . SetBorrow ( ctx , borrow )
2020-12-16 21:08:29 +00:00
k . UpdateItemInLtvIndex ( ctx , prevLtv , shouldRemoveIndex , borrower )
2020-12-03 21:50:35 +00:00
// 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.
2020-11-12 15:50:54 +00:00
k . IncrementBorrowedCoins ( ctx , coins )
2020-10-30 09:59:47 +00:00
ctx . EventManager ( ) . EmitEvent (
sdk . NewEvent (
types . EventTypeHarvestBorrow ,
2020-12-03 21:50:35 +00:00
sdk . NewAttribute ( types . AttributeKeyBorrower , borrower . String ( ) ) ,
2020-10-30 09:59:47 +00:00
sdk . NewAttribute ( types . AttributeKeyBorrowCoins , coins . String ( ) ) ,
) ,
)
return nil
}
2020-11-03 09:46:08 +00:00
2020-12-18 01:12:48 +00:00
// SyncOutstandingInterest updates the user's owed interest on newly borrowed coins to the latest global state
func ( k Keeper ) SyncOutstandingInterest ( ctx sdk . Context , addr sdk . AccAddress ) {
2020-12-03 21:50:35 +00:00
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.
2020-12-16 21:08:29 +00:00
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
2020-12-03 21:50:35 +00:00
}
2020-12-16 21:08:29 +00:00
}
2020-12-03 21:50:35 +00:00
2020-12-16 21:08:29 +00:00
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
2020-12-03 21:50:35 +00:00
}
}
2020-12-16 21:08:29 +00:00
// Add all pending interest to user's borrow
borrow . Amount = borrow . Amount . Add ( totalNewInterest ... )
2020-12-03 21:50:35 +00:00
// Update user's borrow in the store
k . SetBorrow ( ctx , borrow )
}
2020-11-03 09:46:08 +00:00
// ValidateBorrow validates a borrow request against borrower and protocol requirements
2020-11-09 21:52:08 +00:00
func ( k Keeper ) ValidateBorrow ( ctx sdk . Context , borrower sdk . AccAddress , amount sdk . Coins ) error {
2020-11-12 15:50:54 +00:00
if amount . IsZero ( ) {
return types . ErrBorrowEmptyCoins
}
2020-11-09 21:52:08 +00:00
// Get the proposed borrow USD value
moneyMarketCache := map [ string ] types . MoneyMarket { }
proprosedBorrowUSDValue := sdk . ZeroDec ( )
for _ , coin := range amount {
moneyMarket , ok := moneyMarketCache [ coin . Denom ]
// Fetch money market and store in local cache
if ! ok {
2020-12-03 21:50:35 +00:00
newMoneyMarket , found := k . GetMoneyMarketParam ( ctx , coin . Denom )
2020-11-09 21:52:08 +00:00
if ! found {
return sdkerrors . Wrapf ( types . ErrMarketNotFound , "no market found for denom %s" , coin . Denom )
}
moneyMarketCache [ coin . Denom ] = newMoneyMarket
moneyMarket = newMoneyMarket
}
// Calculate this coin's USD value and add it borrow's total USD value
assetPriceInfo , err := k . pricefeedKeeper . GetCurrentPrice ( ctx , moneyMarket . SpotMarketID )
if err != nil {
return sdkerrors . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
}
coinUSDValue := sdk . NewDecFromInt ( coin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPriceInfo . Price )
2020-11-12 15:50:54 +00:00
// Validate the requested borrow value for the asset against the money market's global borrow limit
if moneyMarket . BorrowLimit . HasMaxLimit {
var assetTotalBorrowedAmount sdk . Int
totalBorrowedCoins , found := k . GetBorrowedCoins ( ctx )
if ! found {
assetTotalBorrowedAmount = sdk . ZeroInt ( )
} else {
assetTotalBorrowedAmount = totalBorrowedCoins . AmountOf ( coin . Denom )
}
newProposedAssetTotalBorrowedAmount := sdk . NewDecFromInt ( assetTotalBorrowedAmount . Add ( coin . Amount ) )
if newProposedAssetTotalBorrowedAmount . GT ( moneyMarket . BorrowLimit . MaximumLimit ) {
return sdkerrors . Wrapf ( types . ErrGreaterThanAssetBorrowLimit ,
"proposed borrow would result in %s borrowed, but the maximum global asset borrow limit is %s" ,
newProposedAssetTotalBorrowedAmount , moneyMarket . BorrowLimit . MaximumLimit )
}
}
2020-11-09 21:52:08 +00:00
proprosedBorrowUSDValue = proprosedBorrowUSDValue . Add ( coinUSDValue )
2020-11-03 09:46:08 +00:00
}
2020-11-09 21:52:08 +00:00
// Get the total borrowable USD amount at user's existing deposits
2020-12-18 16:05:21 +00:00
deposit , found := k . GetDeposit ( ctx , borrower )
if ! found {
2020-11-03 09:46:08 +00:00
return sdkerrors . Wrapf ( types . ErrDepositsNotFound , "no deposits found for %s" , borrower )
}
2020-11-05 17:36:49 +00:00
totalBorrowableAmount := sdk . ZeroDec ( )
2020-12-18 16:05:21 +00:00
for _ , depCoin := range deposit . Amount {
moneyMarket , ok := moneyMarketCache [ depCoin . Denom ]
2020-11-09 21:52:08 +00:00
// Fetch money market and store in local cache
if ! ok {
2020-12-18 16:05:21 +00:00
newMoneyMarket , found := k . GetMoneyMarketParam ( ctx , depCoin . Denom )
2020-11-09 21:52:08 +00:00
if ! found {
2020-12-18 16:05:21 +00:00
return sdkerrors . Wrapf ( types . ErrMarketNotFound , "no market found for denom %s" , depCoin . Denom )
2020-11-09 21:52:08 +00:00
}
2020-12-18 16:05:21 +00:00
moneyMarketCache [ depCoin . Denom ] = newMoneyMarket
2020-11-09 21:52:08 +00:00
moneyMarket = newMoneyMarket
}
// Calculate the borrowable amount and add it to the user's total borrowable amount
assetPriceInfo , err := k . pricefeedKeeper . GetCurrentPrice ( ctx , moneyMarket . SpotMarketID )
2020-11-05 17:36:49 +00:00
if err != nil {
2020-11-09 21:52:08 +00:00
sdkerrors . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
2020-11-05 17:36:49 +00:00
}
2020-12-18 16:05:21 +00:00
depositUSDValue := sdk . NewDecFromInt ( depCoin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPriceInfo . Price )
2020-11-09 21:52:08 +00:00
borrowableAmountForDeposit := depositUSDValue . Mul ( moneyMarket . BorrowLimit . LoanToValue )
2020-11-05 17:36:49 +00:00
totalBorrowableAmount = totalBorrowableAmount . Add ( borrowableAmountForDeposit )
2020-11-03 09:46:08 +00:00
}
2020-11-09 21:52:08 +00:00
// Get the total USD value of user's existing borrows
existingBorrowUSDValue := sdk . ZeroDec ( )
existingBorrow , found := k . GetBorrow ( ctx , borrower )
2020-11-03 09:46:08 +00:00
if found {
2020-11-09 21:52:08 +00:00
for _ , borrowedCoin := range existingBorrow . Amount {
moneyMarket , ok := moneyMarketCache [ borrowedCoin . Denom ]
// Fetch money market and store in local cache
if ! ok {
2020-12-03 21:50:35 +00:00
newMoneyMarket , found := k . GetMoneyMarketParam ( ctx , borrowedCoin . Denom )
2020-11-09 21:52:08 +00:00
if ! found {
return sdkerrors . Wrapf ( types . ErrMarketNotFound , "no market found for denom %s" , borrowedCoin . Denom )
}
moneyMarketCache [ borrowedCoin . Denom ] = newMoneyMarket
moneyMarket = newMoneyMarket
}
// Calculate this borrow coin's USD value and add it to the total previous borrowed USD value
assetPriceInfo , err := k . pricefeedKeeper . GetCurrentPrice ( ctx , moneyMarket . SpotMarketID )
if err != nil {
return sdkerrors . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
}
coinUSDValue := sdk . NewDecFromInt ( borrowedCoin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPriceInfo . Price )
existingBorrowUSDValue = existingBorrowUSDValue . Add ( coinUSDValue )
2020-11-03 09:46:08 +00:00
}
}
2020-11-05 17:36:49 +00:00
// Validate that the proposed borrow's USD value is within user's borrowable limit
2020-11-09 21:52:08 +00:00
if proprosedBorrowUSDValue . GT ( totalBorrowableAmount . Sub ( existingBorrowUSDValue ) ) {
2020-12-08 13:28:01 +00:00
return sdkerrors . Wrapf ( types . ErrInsufficientLoanToValue , "requested borrow %s exceeds the allowable amount as determined by the collateralization ratio" , amount )
2020-11-03 09:46:08 +00:00
}
return nil
}
2020-11-12 15:50:54 +00:00
// IncrementBorrowedCoins increments the amount of borrowed coins by the newCoins parameter
func ( k Keeper ) IncrementBorrowedCoins ( ctx sdk . Context , newCoins sdk . Coins ) {
borrowedCoins , found := k . GetBorrowedCoins ( ctx )
if ! found {
2020-12-04 14:35:26 +00:00
if ! newCoins . Empty ( ) {
k . SetBorrowedCoins ( ctx , newCoins )
}
2020-11-12 15:50:54 +00:00
} else {
k . SetBorrowedCoins ( ctx , borrowedCoins . Add ( newCoins ... ) )
}
}
// DecrementBorrowedCoins decrements the amount of borrowed coins by the coins parameter
func ( k Keeper ) DecrementBorrowedCoins ( ctx sdk . Context , coins sdk . Coins ) error {
borrowedCoins , found := k . GetBorrowedCoins ( ctx )
if ! found {
return sdkerrors . Wrapf ( types . ErrBorrowedCoinsNotFound , "cannot repay coins if no coins are currently borrowed" )
}
updatedBorrowedCoins , isAnyNegative := borrowedCoins . SafeSub ( coins )
if isAnyNegative {
return types . ErrNegativeBorrowedCoins
}
k . SetBorrowedCoins ( ctx , updatedBorrowedCoins )
return nil
}
2020-12-04 19:04:05 +00:00
// GetBorrowBalance gets the user's total borrow balance (borrow balance + pending interest)
func ( k Keeper ) GetBorrowBalance ( ctx sdk . Context , borrower sdk . AccAddress ) sdk . Coins {
borrowBalance := sdk . Coins { }
borrow , found := k . GetBorrow ( ctx , borrower )
if found {
totalNewInterest := sdk . Coins { }
for _ , coin := range borrow . Amount {
borrowIndexValue , foundBorrowIndexValue := k . GetBorrowIndex ( ctx , coin . Denom )
if foundBorrowIndexValue {
// 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
}
}
// Calculate interest owed by user for this asset
if foundAtIndex != - 1 {
storedAmount := sdk . NewDecFromInt ( borrow . Amount . AmountOf ( coin . Denom ) )
userLastBorrowIndex := borrow . Index [ foundAtIndex ] . Value
coinInterest := ( storedAmount . Quo ( userLastBorrowIndex ) . Mul ( borrowIndexValue ) ) . Sub ( storedAmount )
totalNewInterest = totalNewInterest . Add ( sdk . NewCoin ( coin . Denom , coinInterest . TruncateInt ( ) ) )
}
}
}
borrowBalance = borrow . Amount . Add ( totalNewInterest ... )
}
return borrowBalance
}