mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-15 12:35:20 +00:00
916ec6d30c
* add additional tests that attempt to borrow funds from the insolvent market(should fail), and attempt to borrow funds from the not insolvent market (it will fail, but shouldn't). The not insolvent market should continue to processs borrows * remove unused code * make tests less specific for string contains * change the structure for new and old tests and add more verbosity for other tests * remove print * remove unneeded code * add paren * remove duplicate test case with invalid test name, and update to use error contains in places where it was validating if true for strings contains --------- Co-authored-by: Sam Sheffield <sam.sheffield@kavalabs.io>
302 lines
11 KiB
Go
302 lines
11 KiB
Go
package keeper
|
|
|
|
import (
|
|
"errors"
|
|
|
|
errorsmod "cosmossdk.io/errors"
|
|
sdkmath "cosmossdk.io/math"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
"github.com/kava-labs/kava/x/hard/types"
|
|
)
|
|
|
|
// Borrow funds
|
|
func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error {
|
|
// Set any new denoms' global borrow index to 1.0
|
|
for _, coin := range coins {
|
|
_, foundInterestFactor := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
|
if !foundInterestFactor {
|
|
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
|
|
if foundMm {
|
|
k.SetBorrowInterestFactor(ctx, coin.Denom, sdk.OneDec())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call incentive hooks
|
|
existingDeposit, hasExistingDeposit := k.GetDeposit(ctx, borrower)
|
|
if hasExistingDeposit {
|
|
k.BeforeDepositModified(ctx, existingDeposit)
|
|
}
|
|
existingBorrow, hasExistingBorrow := k.GetBorrow(ctx, borrower)
|
|
if hasExistingBorrow {
|
|
k.BeforeBorrowModified(ctx, existingBorrow)
|
|
}
|
|
|
|
k.SyncSupplyInterest(ctx, borrower)
|
|
k.SyncBorrowInterest(ctx, borrower)
|
|
|
|
// Validate borrow amount within user and protocol limits
|
|
err := k.ValidateBorrow(ctx, borrower, coins)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sends coins from Hard module account to user
|
|
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, borrower, coins)
|
|
if err != nil {
|
|
if errors.Is(err, sdkerrors.ErrInsufficientFunds) {
|
|
macc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
|
modAccCoins := k.bankKeeper.GetAllBalances(ctx, macc.GetAddress())
|
|
for _, coin := range coins {
|
|
_, isNegative := modAccCoins.SafeSub(coin)
|
|
if isNegative {
|
|
return errorsmod.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,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
interestFactors := types.BorrowInterestFactors{}
|
|
currBorrow, foundBorrow := k.GetBorrow(ctx, borrower)
|
|
if foundBorrow {
|
|
interestFactors = currBorrow.Index
|
|
}
|
|
for _, coin := range coins {
|
|
interestFactorValue, foundValue := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
|
if foundValue {
|
|
interestFactors = interestFactors.SetInterestFactor(coin.Denom, interestFactorValue)
|
|
}
|
|
}
|
|
|
|
// Calculate new borrow amount
|
|
var amount sdk.Coins
|
|
if foundBorrow {
|
|
amount = currBorrow.Amount.Add(coins...)
|
|
} else {
|
|
amount = coins
|
|
}
|
|
|
|
// Construct the user's new/updated borrow with amount and interest factors
|
|
borrow := types.NewBorrow(borrower, amount, interestFactors)
|
|
if borrow.Amount.Empty() {
|
|
k.DeleteBorrow(ctx, borrow)
|
|
} else {
|
|
k.SetBorrow(ctx, borrow)
|
|
}
|
|
|
|
// 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)
|
|
|
|
if !hasExistingBorrow {
|
|
k.AfterBorrowCreated(ctx, borrow)
|
|
} else {
|
|
k.AfterBorrowModified(ctx, borrow)
|
|
}
|
|
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventTypeHardBorrow,
|
|
sdk.NewAttribute(types.AttributeKeyBorrower, borrower.String()),
|
|
sdk.NewAttribute(types.AttributeKeyBorrowCoins, coins.String()),
|
|
),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateBorrow validates a borrow request against borrower and protocol requirements
|
|
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
|
|
if amount.IsZero() {
|
|
return types.ErrBorrowEmptyCoins
|
|
}
|
|
|
|
// The reserve coins aren't available for users to borrow
|
|
macc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
|
|
hardMaccCoins := k.bankKeeper.GetAllBalances(ctx, macc.GetAddress())
|
|
reserveCoins, foundReserveCoins := k.GetTotalReserves(ctx)
|
|
if !foundReserveCoins {
|
|
reserveCoins = sdk.NewCoins()
|
|
}
|
|
fundsAvailableToBorrow, isNegative := hardMaccCoins.SafeSub(reserveCoins...)
|
|
if isNegative {
|
|
return errorsmod.Wrapf(types.ErrReservesExceedCash, "reserves %s > cash %s", reserveCoins, hardMaccCoins)
|
|
}
|
|
if amount.IsAnyGT(fundsAvailableToBorrow) {
|
|
return errorsmod.Wrapf(types.ErrExceedsProtocolBorrowableBalance, "requested borrow %s > available to borrow %s", amount, fundsAvailableToBorrow)
|
|
}
|
|
|
|
// Get the proposed borrow USD value
|
|
proprosedBorrowUSDValue := sdk.ZeroDec()
|
|
for _, coin := range amount {
|
|
moneyMarket, found := k.GetMoneyMarket(ctx, coin.Denom)
|
|
if !found {
|
|
return errorsmod.Wrapf(types.ErrMarketNotFound, "no money market found for denom %s", coin.Denom)
|
|
}
|
|
|
|
// 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 errorsmod.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID)
|
|
}
|
|
coinUSDValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPriceInfo.Price)
|
|
|
|
// Validate the requested borrow value for the asset against the money market's global borrow limit
|
|
if moneyMarket.BorrowLimit.HasMaxLimit {
|
|
var assetTotalBorrowedAmount sdkmath.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 errorsmod.Wrapf(types.ErrGreaterThanAssetBorrowLimit,
|
|
"proposed borrow would result in %s borrowed, but the maximum global asset borrow limit is %s",
|
|
newProposedAssetTotalBorrowedAmount, moneyMarket.BorrowLimit.MaximumLimit)
|
|
}
|
|
}
|
|
proprosedBorrowUSDValue = proprosedBorrowUSDValue.Add(coinUSDValue)
|
|
}
|
|
|
|
// Get the total borrowable USD amount at user's existing deposits
|
|
deposit, found := k.GetDeposit(ctx, borrower)
|
|
if !found {
|
|
return errorsmod.Wrapf(types.ErrDepositsNotFound, "no deposits found for %s", borrower)
|
|
}
|
|
totalBorrowableAmount := sdk.ZeroDec()
|
|
for _, coin := range deposit.Amount {
|
|
moneyMarket, found := k.GetMoneyMarket(ctx, coin.Denom)
|
|
if !found {
|
|
return errorsmod.Wrapf(types.ErrMarketNotFound, "no money market found for denom %s", coin.Denom)
|
|
}
|
|
|
|
// Calculate the borrowable amount and add it to the user's total borrowable amount
|
|
assetPriceInfo, err := k.pricefeedKeeper.GetCurrentPrice(ctx, moneyMarket.SpotMarketID)
|
|
if err != nil {
|
|
return errorsmod.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID)
|
|
}
|
|
depositUSDValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPriceInfo.Price)
|
|
borrowableAmountForDeposit := depositUSDValue.Mul(moneyMarket.BorrowLimit.LoanToValue)
|
|
totalBorrowableAmount = totalBorrowableAmount.Add(borrowableAmountForDeposit)
|
|
}
|
|
|
|
// Get the total USD value of user's existing borrows
|
|
existingBorrowUSDValue := sdk.ZeroDec()
|
|
existingBorrow, found := k.GetBorrow(ctx, borrower)
|
|
if found {
|
|
for _, coin := range existingBorrow.Amount {
|
|
moneyMarket, found := k.GetMoneyMarket(ctx, coin.Denom)
|
|
if !found {
|
|
return errorsmod.Wrapf(types.ErrMarketNotFound, "no money market found for denom %s", coin.Denom)
|
|
}
|
|
|
|
// 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 errorsmod.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID)
|
|
}
|
|
coinUSDValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPriceInfo.Price)
|
|
existingBorrowUSDValue = existingBorrowUSDValue.Add(coinUSDValue)
|
|
}
|
|
}
|
|
|
|
// 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)) {
|
|
return errorsmod.Wrapf(types.ErrBelowMinimumBorrowValue, "the proposed borrow's USD value $%s is below the minimum borrow limit $%s", totalBorrowUSDValue, k.GetMinimumBorrowUSDValue(ctx))
|
|
}
|
|
|
|
// Validate that the proposed borrow's USD value is within user's borrowable limit
|
|
if proprosedBorrowUSDValue.GT(totalBorrowableAmount.Sub(existingBorrowUSDValue)) {
|
|
return errorsmod.Wrapf(types.ErrInsufficientLoanToValue, "requested borrow %s exceeds the allowable amount as determined by the collateralization ratio", amount)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IncrementBorrowedCoins increments the total 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 {
|
|
if !newCoins.Empty() {
|
|
k.SetBorrowedCoins(ctx, newCoins)
|
|
}
|
|
} else {
|
|
k.SetBorrowedCoins(ctx, borrowedCoins.Add(newCoins...))
|
|
}
|
|
}
|
|
|
|
// DecrementBorrowedCoins decrements the total 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 errorsmod.Wrapf(types.ErrBorrowedCoinsNotFound, "cannot repay coins if no coins are currently borrowed")
|
|
}
|
|
|
|
updatedBorrowedCoins, isNegative := borrowedCoins.SafeSub(coins...)
|
|
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)
|
|
}
|
|
}
|
|
updatedBorrowedCoins = borrowedCoins.Sub(coinsToSubtract...)
|
|
}
|
|
|
|
k.SetBorrowedCoins(ctx, updatedBorrowedCoins)
|
|
return nil
|
|
}
|
|
|
|
// GetSyncedBorrow returns a borrow object containing current balances and indexes
|
|
func (k Keeper) GetSyncedBorrow(ctx sdk.Context, borrower sdk.AccAddress) (types.Borrow, bool) {
|
|
borrow, found := k.GetBorrow(ctx, borrower)
|
|
if !found {
|
|
return types.Borrow{}, false
|
|
}
|
|
|
|
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 {
|
|
totalNewInterest := sdk.Coins{}
|
|
newBorrowIndexes := types.BorrowInterestFactors{}
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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()))
|
|
}
|
|
}
|
|
|
|
borrowIndex := types.NewBorrowInterestFactor(coin.Denom, interestFactorValue)
|
|
newBorrowIndexes = append(newBorrowIndexes, borrowIndex)
|
|
}
|
|
|
|
return types.NewBorrow(borrow.Borrower, borrow.Amount.Add(totalNewInterest...), newBorrowIndexes)
|
|
}
|