2020-10-30 09:59:47 +00:00
package keeper
import (
2021-03-15 15:15:19 +00:00
"errors"
2020-11-11 15:05:17 +00:00
2023-04-05 23:21:59 +00:00
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
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-12-21 17:18:55 +00:00
"github.com/kava-labs/kava/x/hard/types"
2020-10-30 09:59:47 +00:00
)
// 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 {
2021-01-07 10:23:05 +00:00
_ , foundInterestFactor := k . GetBorrowInterestFactor ( ctx , coin . Denom )
2020-12-21 17:07:02 +00:00
if ! foundInterestFactor {
_ , foundMm := k . GetMoneyMarket ( ctx , coin . Denom )
if foundMm {
2021-01-07 10:23:05 +00:00
k . SetBorrowInterestFactor ( ctx , coin . Denom , sdk . OneDec ( ) )
2020-12-16 21:08:29 +00:00
}
2020-12-03 21:50:35 +00:00
}
}
2021-02-08 12:23:37 +00:00
// Call incentive hooks
existingDeposit , hasExistingDeposit := k . GetDeposit ( ctx , borrower )
if hasExistingDeposit {
k . BeforeDepositModified ( ctx , existingDeposit )
2020-12-16 21:08:29 +00:00
}
2021-01-21 13:52:09 +00:00
existingBorrow , hasExistingBorrow := k . GetBorrow ( ctx , borrower )
if hasExistingBorrow {
k . BeforeBorrowModified ( ctx , existingBorrow )
}
2021-02-04 16:54:13 +00:00
k . SyncSupplyInterest ( ctx , borrower )
2021-01-07 10:23:05 +00:00
k . SyncBorrowInterest ( ctx , borrower )
2020-12-03 21:50:35 +00:00
2020-11-12 15:50:54 +00:00
// Validate borrow amount within user and protocol limits
2021-02-08 12:23:37 +00:00
err := k . ValidateBorrow ( ctx , borrower , coins )
2020-11-03 09:46:08 +00:00
if err != nil {
return err
}
2020-12-21 17:18:55 +00:00
// Sends coins from Hard module account to user
2022-01-08 00:39:27 +00:00
err = k . bankKeeper . SendCoinsFromModuleToAccount ( ctx , types . ModuleAccountName , borrower , coins )
2020-10-30 09:59:47 +00:00
if err != nil {
2021-03-15 15:15:19 +00:00
if errors . Is ( err , sdkerrors . ErrInsufficientFunds ) {
2022-01-08 00:39:27 +00:00
macc := k . accountKeeper . GetModuleAccount ( ctx , types . ModuleAccountName )
modAccCoins := k . bankKeeper . GetAllBalances ( ctx , macc . GetAddress ( ) )
2020-11-11 15:05:17 +00:00
for _ , coin := range coins {
2023-04-04 00:08:45 +00:00
_ , isNegative := modAccCoins . SafeSub ( coin )
2020-11-11 15:05:17 +00:00
if isNegative {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrBorrowExceedsAvailableBalance ,
2020-11-11 15:05:17 +00:00
"the requested borrow amount of %s exceeds the total amount of %s%s available to borrow" ,
coin , modAccCoins . AmountOf ( coin . Denom ) , coin . Denom ,
)
}
}
}
2021-02-15 15:30:41 +00:00
return err
}
2020-10-30 09:59:47 +00:00
2021-02-05 11:31:38 +00:00
interestFactors := types . BorrowInterestFactors { }
2021-01-07 10:23:05 +00:00
currBorrow , foundBorrow := k . GetBorrow ( ctx , borrower )
if foundBorrow {
2021-02-05 11:31:38 +00:00
interestFactors = currBorrow . Index
}
for _ , coin := range coins {
interestFactorValue , foundValue := k . GetBorrowInterestFactor ( ctx , coin . Denom )
if foundValue {
interestFactors = interestFactors . SetInterestFactor ( coin . Denom , interestFactorValue )
2020-12-16 21:08:29 +00:00
}
2020-10-30 09:59:47 +00:00
}
2020-12-16 21:08:29 +00:00
2021-01-07 10:23:05 +00:00
// Calculate new borrow amount
var amount sdk . Coins
if foundBorrow {
amount = currBorrow . Amount . Add ( coins ... )
} else {
amount = coins
}
2021-02-08 12:23:37 +00:00
2021-01-07 21:40:25 +00:00
// Construct the user's new/updated borrow with amount and interest factors
2021-02-05 11:31:38 +00:00
borrow := types . NewBorrow ( borrower , amount , interestFactors )
2021-02-08 12:23:37 +00:00
if borrow . Amount . Empty ( ) {
k . DeleteBorrow ( ctx , borrow )
} else {
k . SetBorrow ( ctx , borrow )
2021-01-07 21:40:25 +00:00
}
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 )
2021-01-21 13:52:09 +00:00
if ! hasExistingBorrow {
k . AfterBorrowCreated ( ctx , borrow )
} else {
k . AfterBorrowModified ( ctx , borrow )
}
2020-10-30 09:59:47 +00:00
ctx . EventManager ( ) . EmitEvent (
sdk . NewEvent (
2020-12-21 17:18:55 +00:00
types . EventTypeHardBorrow ,
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
// 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
}
2021-02-16 14:45:57 +00:00
// The reserve coins aren't available for users to borrow
2022-01-08 00:39:27 +00:00
macc := k . accountKeeper . GetModuleAccount ( ctx , types . ModuleName )
2024-07-30 17:08:48 +00:00
hardMaccCoins := FilterCoinsByDenoms ( k . bankKeeper . GetAllBalances ( ctx , macc . GetAddress ( ) ) , amount )
2021-02-16 14:45:57 +00:00
reserveCoins , foundReserveCoins := k . GetTotalReserves ( ctx )
if ! foundReserveCoins {
reserveCoins = sdk . NewCoins ( )
2024-07-30 17:08:48 +00:00
} else {
reserveCoins = FilterCoinsByDenoms ( reserveCoins , amount )
2021-02-16 14:45:57 +00:00
}
2024-07-30 17:08:48 +00:00
2023-04-04 00:08:45 +00:00
fundsAvailableToBorrow , isNegative := hardMaccCoins . SafeSub ( reserveCoins ... )
2021-02-16 14:45:57 +00:00
if isNegative {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrReservesExceedCash , "reserves %s > cash %s" , reserveCoins , hardMaccCoins )
2021-02-16 14:45:57 +00:00
}
if amount . IsAnyGT ( fundsAvailableToBorrow ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrExceedsProtocolBorrowableBalance , "requested borrow %s > available to borrow %s" , amount , fundsAvailableToBorrow )
2021-02-16 14:45:57 +00:00
}
2020-11-09 21:52:08 +00:00
// Get the proposed borrow USD value
proprosedBorrowUSDValue := sdk . ZeroDec ( )
for _ , coin := range amount {
2021-02-19 20:23:19 +00:00
moneyMarket , found := k . GetMoneyMarket ( ctx , coin . Denom )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrMarketNotFound , "no money market found for denom %s" , coin . Denom )
2020-11-09 21:52:08 +00:00
}
// 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 {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
2020-11-09 21:52:08 +00:00
}
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 {
2023-04-05 23:21:59 +00:00
var assetTotalBorrowedAmount sdkmath . Int
2020-11-12 15:50:54 +00:00
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 ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrGreaterThanAssetBorrowLimit ,
2020-11-12 15:50:54 +00:00
"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 {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrDepositsNotFound , "no deposits found for %s" , borrower )
2020-11-03 09:46:08 +00:00
}
2020-11-05 17:36:49 +00:00
totalBorrowableAmount := sdk . ZeroDec ( )
2021-02-19 20:23:19 +00:00
for _ , coin := range deposit . Amount {
moneyMarket , found := k . GetMoneyMarket ( ctx , coin . Denom )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrMarketNotFound , "no money market found for denom %s" , coin . Denom )
2020-11-09 21:52:08 +00:00
}
// 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 {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
2020-11-05 17:36:49 +00:00
}
2021-02-19 20:23:19 +00:00
depositUSDValue := sdk . NewDecFromInt ( coin . 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 {
2021-02-19 20:23:19 +00:00
for _ , coin := range existingBorrow . Amount {
moneyMarket , found := k . GetMoneyMarket ( ctx , coin . Denom )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrMarketNotFound , "no money market found for denom %s" , coin . Denom )
2020-11-09 21:52:08 +00:00
}
// 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 {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
2020-11-09 21:52:08 +00:00
}
2021-02-19 20:23:19 +00:00
coinUSDValue := sdk . NewDecFromInt ( coin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPriceInfo . Price )
2020-11-09 21:52:08 +00:00
existingBorrowUSDValue = existingBorrowUSDValue . Add ( coinUSDValue )
2020-11-03 09:46:08 +00:00
}
}
2021-02-12 15:28:05 +00:00
// Borrow's updated total USD value must be greater than the minimum global USD borrow limit
totalBorrowUSDValue := proprosedBorrowUSDValue . Add ( existingBorrowUSDValue )
if totalBorrowUSDValue . LT ( k . GetMinimumBorrowUSDValue ( ctx ) ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrBelowMinimumBorrowValue , "the proposed borrow's USD value $%s is below the minimum borrow limit $%s" , totalBorrowUSDValue , k . GetMinimumBorrowUSDValue ( ctx ) )
2021-02-12 15:28:05 +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 ) ) {
2023-04-05 23:21:59 +00:00
return errorsmod . 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
2024-07-30 17:08:48 +00:00
// FilterCoinsByDenoms filters the given coins by retaining only those whose denoms
// are present in the filterByCoins list.
//
// Parameters:
// - coins: The list of coins to be filtered.
// - filterByCoins: The list of coins whose denoms will be used as a filter.
//
// Returns:
// - A new list of coins that includes only those coins whose denom is in the filterByCoins list.
func FilterCoinsByDenoms ( coins , filterByCoins sdk . Coins ) sdk . Coins {
// Create a map to store the denoms that we want to filter by.
denoms := make ( map [ string ] struct { } )
// Populate the map with denoms from filterByCoins.
for _ , denom := range filterByCoins . Denoms ( ) {
denoms [ denom ] = struct { } { }
}
// Prepare a slice to hold the filtered coins.
filteredCoins := make ( sdk . Coins , 0 , len ( coins ) )
// Iterate through the list of coins and add those that have a denom in the denoms map.
for _ , coin := range coins {
if _ , exists := denoms [ coin . Denom ] ; exists {
filteredCoins = append ( filteredCoins , coin )
}
}
return filteredCoins
}
2021-01-07 10:23:05 +00:00
// IncrementBorrowedCoins increments the total amount of borrowed coins by the newCoins parameter
2020-11-12 15:50:54 +00:00
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 ... ) )
}
}
2021-01-07 10:23:05 +00:00
// DecrementBorrowedCoins decrements the total amount of borrowed coins by the coins parameter
2020-11-12 15:50:54 +00:00
func ( k Keeper ) DecrementBorrowedCoins ( ctx sdk . Context , coins sdk . Coins ) error {
borrowedCoins , found := k . GetBorrowedCoins ( ctx )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrBorrowedCoinsNotFound , "cannot repay coins if no coins are currently borrowed" )
2020-11-12 15:50:54 +00:00
}
2023-04-04 00:08:45 +00:00
updatedBorrowedCoins , isNegative := borrowedCoins . SafeSub ( coins ... )
2021-03-10 17:56:08 +00:00
if isNegative {
coinsToSubtract := sdk . NewCoins ( )
for _ , coin := range coins {
if borrowedCoins . AmountOf ( coin . Denom ) . LT ( coin . Amount ) {
if borrowedCoins . AmountOf ( coin . Denom ) . GT ( sdk . ZeroInt ( ) ) {
coinsToSubtract = coinsToSubtract . Add ( sdk . NewCoin ( coin . Denom , borrowedCoins . AmountOf ( coin . Denom ) ) )
}
} else {
coinsToSubtract = coinsToSubtract . Add ( coin )
}
2021-02-12 20:07:32 +00:00
}
2023-04-04 00:08:45 +00:00
updatedBorrowedCoins = borrowedCoins . Sub ( coinsToSubtract ... )
2020-11-12 15:50:54 +00:00
}
k . SetBorrowedCoins ( ctx , updatedBorrowedCoins )
return nil
}
2020-12-04 19:04:05 +00:00
2021-01-13 18:14:58 +00:00
// GetSyncedBorrow returns a borrow object containing current balances and indexes
func ( k Keeper ) GetSyncedBorrow ( ctx sdk . Context , borrower sdk . AccAddress ) ( types . Borrow , bool ) {
2020-12-04 19:04:05 +00:00
borrow , found := k . GetBorrow ( ctx , borrower )
2021-01-07 10:23:05 +00:00
if ! found {
2021-01-13 18:14:58 +00:00
return types . Borrow { } , false
2021-01-07 10:23:05 +00:00
}
2021-01-13 18:14:58 +00:00
return k . loadSyncedBorrow ( ctx , borrow ) , true
}
// loadSyncedBorrow calculates a user's synced borrow, but does not update state
func ( k Keeper ) loadSyncedBorrow ( ctx sdk . Context , borrow types . Borrow ) types . Borrow {
2021-01-07 10:23:05 +00:00
totalNewInterest := sdk . Coins { }
2021-01-13 18:14:58 +00:00
newBorrowIndexes := types . BorrowInterestFactors { }
2021-01-07 10:23:05 +00:00
for _ , coin := range borrow . Amount {
interestFactorValue , foundInterestFactorValue := k . GetBorrowInterestFactor ( ctx , coin . Denom )
if foundInterestFactorValue {
// Locate the interest factor by coin denom in the user's list of interest factors
foundAtIndex := - 1
for i := range borrow . Index {
if borrow . Index [ i ] . Denom == coin . Denom {
foundAtIndex = i
break
2020-12-04 19:04:05 +00:00
}
}
2021-01-13 18:14:58 +00:00
2021-01-07 10:23:05 +00:00
// Calculate interest owed by user for this asset
if foundAtIndex != - 1 {
storedAmount := sdk . NewDecFromInt ( borrow . Amount . AmountOf ( coin . Denom ) )
userLastInterestFactor := borrow . Index [ foundAtIndex ] . Value
coinInterest := ( storedAmount . Quo ( userLastInterestFactor ) . Mul ( interestFactorValue ) ) . Sub ( storedAmount )
totalNewInterest = totalNewInterest . Add ( sdk . NewCoin ( coin . Denom , coinInterest . TruncateInt ( ) ) )
}
2020-12-04 19:04:05 +00:00
}
2021-01-13 18:14:58 +00:00
borrowIndex := types . NewBorrowInterestFactor ( coin . Denom , interestFactorValue )
newBorrowIndexes = append ( newBorrowIndexes , borrowIndex )
2020-12-04 19:04:05 +00:00
}
2021-01-07 10:23:05 +00:00
2021-01-13 18:14:58 +00:00
return types . NewBorrow ( borrow . Borrower , borrow . Amount . Add ( totalNewInterest ... ) , newBorrowIndexes )
2020-12-04 19:04:05 +00:00
}