2020-12-07 21:51:35 +00:00
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
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-12-07 21:51:35 +00:00
)
// Repay borrowed funds
2021-02-08 12:51:42 +00:00
func ( k Keeper ) Repay ( ctx sdk . Context , sender , owner sdk . AccAddress , coins sdk . Coins ) error {
2021-01-21 13:52:09 +00:00
// Check borrow exists here to avoid duplicating store read in ValidateRepay
2021-02-08 12:51:42 +00:00
borrow , found := k . GetBorrow ( ctx , owner )
2021-01-21 13:52:09 +00:00
if ! found {
return types . ErrBorrowNotFound
}
// Call incentive hook
k . BeforeBorrowModified ( ctx , borrow )
2021-01-07 10:23:05 +00:00
// Sync borrow interest so loan is up-to-date
2021-02-08 12:51:42 +00:00
k . SyncBorrowInterest ( ctx , owner )
2020-12-07 21:51:35 +00:00
2021-02-12 20:56:03 +00:00
// Refresh borrow after syncing interest
borrow , _ = k . GetBorrow ( ctx , owner )
2021-03-11 04:13:21 +00:00
// cap the repayment by what's available to repay (the borrow amount)
payment , err := k . CalculatePaymentAmount ( borrow . Amount , coins )
2020-12-07 21:51:35 +00:00
if err != nil {
return err
}
2021-03-11 04:13:21 +00:00
// Validate that sender holds coins for repayment
err = k . ValidateRepay ( ctx , sender , owner , payment )
2021-01-07 10:23:05 +00:00
if err != nil {
return err
}
2020-12-07 21:51:35 +00:00
2020-12-21 17:18:55 +00:00
// Sends coins from user to Hard module account
2020-12-07 21:51:35 +00:00
err = k . supplyKeeper . SendCoinsFromAccountToModule ( ctx , sender , types . ModuleAccountName , payment )
if err != nil {
return err
}
2021-02-05 11:31:38 +00:00
// If any coin denoms have been completely repaid reset the denom's borrow index factor
for _ , coin := range payment {
if coin . Amount . Equal ( borrow . Amount . AmountOf ( coin . Denom ) ) {
borrowIndex , removed := borrow . Index . RemoveInterestFactor ( coin . Denom )
if ! removed {
return sdkerrors . Wrapf ( types . ErrInvalidIndexFactorDenom , "%s" , coin . Denom )
}
borrow . Index = borrowIndex
}
}
2020-12-07 21:51:35 +00:00
// Update user's borrow in store
borrow . Amount = borrow . Amount . Sub ( payment )
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-16 21:08:29 +00:00
2020-12-07 21:51:35 +00:00
// Update total borrowed amount
2021-02-12 20:07:32 +00:00
err = k . DecrementBorrowedCoins ( ctx , payment )
if err != nil {
return err
}
2020-12-07 21:51:35 +00:00
2021-01-21 13:52:09 +00:00
// Call incentive hook
if ! borrow . Amount . Empty ( ) {
k . AfterBorrowModified ( ctx , borrow )
}
2020-12-07 21:51:35 +00:00
ctx . EventManager ( ) . EmitEvent (
sdk . NewEvent (
2020-12-21 17:18:55 +00:00
types . EventTypeHardRepay ,
2020-12-07 21:51:35 +00:00
sdk . NewAttribute ( types . AttributeKeySender , sender . String ( ) ) ,
2021-02-08 12:51:42 +00:00
sdk . NewAttribute ( types . AttributeKeyOwner , owner . String ( ) ) ,
2020-12-07 21:51:35 +00:00
sdk . NewAttribute ( types . AttributeKeyRepayCoins , payment . String ( ) ) ,
) ,
)
return nil
}
// ValidateRepay validates a requested loan repay
2021-02-12 15:28:05 +00:00
func ( k Keeper ) ValidateRepay ( ctx sdk . Context , sender , owner sdk . AccAddress , coins sdk . Coins ) error {
assetPriceCache := map [ string ] sdk . Dec { }
// Get the total USD value of user's existing borrows
existingBorrowUSDValue := sdk . ZeroDec ( )
existingBorrow , found := k . GetBorrow ( ctx , owner )
if found {
2021-02-19 20:23:19 +00:00
for _ , coin := range existingBorrow . Amount {
moneyMarket , found := k . GetMoneyMarket ( ctx , coin . Denom )
if ! found {
return sdkerrors . Wrapf ( types . ErrMarketNotFound , "no money market found for denom %s" , coin . Denom )
2021-02-12 15:28:05 +00:00
}
2021-02-19 20:23:19 +00:00
assetPrice , ok := assetPriceCache [ coin . Denom ]
2021-02-12 15:28:05 +00:00
if ! ok { // Fetch current asset price and store in local cache
assetPriceInfo , err := k . pricefeedKeeper . GetCurrentPrice ( ctx , moneyMarket . SpotMarketID )
if err != nil {
return sdkerrors . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
}
2021-02-19 20:23:19 +00:00
assetPriceCache [ coin . Denom ] = assetPriceInfo . Price
2021-02-12 15:28:05 +00:00
assetPrice = assetPriceInfo . Price
}
// Calculate this borrow coin's USD value and add it to the total previous borrowed USD value
2021-02-19 20:23:19 +00:00
coinUSDValue := sdk . NewDecFromInt ( coin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPrice )
2021-02-12 15:28:05 +00:00
existingBorrowUSDValue = existingBorrowUSDValue . Add ( coinUSDValue )
}
}
2020-12-07 21:51:35 +00:00
senderAcc := k . accountKeeper . GetAccount ( ctx , sender )
2020-12-18 01:12:48 +00:00
senderCoins := senderAcc . SpendableCoins ( ctx . BlockTime ( ) )
2021-02-12 15:28:05 +00:00
repayTotalUSDValue := sdk . ZeroDec ( )
for _ , repayCoin := range coins {
// Check that sender holds enough tokens to make the proposed payment
if senderCoins . AmountOf ( repayCoin . Denom ) . LT ( repayCoin . Amount ) {
return sdkerrors . Wrapf ( types . ErrInsufficientBalanceForRepay , "account can only repay up to %s%s" , senderCoins . AmountOf ( repayCoin . Denom ) , repayCoin . Denom )
}
2020-12-18 01:12:48 +00:00
2021-02-19 20:23:19 +00:00
moneyMarket , found := k . GetMoneyMarket ( ctx , repayCoin . Denom )
if ! found {
return sdkerrors . Wrapf ( types . ErrMarketNotFound , "no money market found for denom %s" , repayCoin . Denom )
2020-12-07 21:51:35 +00:00
}
2021-02-12 15:28:05 +00:00
// Calculate this coin's USD value and add it to the repay's total USD value
assetPrice , ok := assetPriceCache [ repayCoin . Denom ]
if ! ok { // Fetch current asset price and store in local cache
assetPriceInfo , err := k . pricefeedKeeper . GetCurrentPrice ( ctx , moneyMarket . SpotMarketID )
if err != nil {
return sdkerrors . Wrapf ( types . ErrPriceNotFound , "no price found for market %s" , moneyMarket . SpotMarketID )
}
assetPriceCache [ repayCoin . Denom ] = assetPriceInfo . Price
assetPrice = assetPriceInfo . Price
}
coinUSDValue := sdk . NewDecFromInt ( repayCoin . Amount ) . Quo ( sdk . NewDecFromInt ( moneyMarket . ConversionFactor ) ) . Mul ( assetPrice )
repayTotalUSDValue = repayTotalUSDValue . Add ( coinUSDValue )
}
// If the proposed repayment would results in a borrowed USD value below the minimum borrow USD value, reject it.
// User can overpay their loan to close it out, but underpaying by such a margin that the USD value is in an
// invalid range is not allowed
2021-03-11 04:13:21 +00:00
// Unless the user is fully repaying their loan
2021-02-12 15:28:05 +00:00
proposedBorrowNewUSDValue := existingBorrowUSDValue . Sub ( repayTotalUSDValue )
2021-03-11 04:13:21 +00:00
isFullRepayment := coins . IsEqual ( existingBorrow . Amount )
if proposedBorrowNewUSDValue . LT ( k . GetMinimumBorrowUSDValue ( ctx ) ) && ! isFullRepayment {
2021-02-12 15:28:05 +00:00
return sdkerrors . Wrapf ( types . ErrBelowMinimumBorrowValue , "the proposed borrow's USD value $%s is below the minimum borrow limit $%s" , proposedBorrowNewUSDValue , k . GetMinimumBorrowUSDValue ( ctx ) )
2020-12-07 21:51:35 +00:00
}
return nil
}
// CalculatePaymentAmount prevents overpayment when repaying borrowed coins
2021-01-07 10:23:05 +00:00
func ( k Keeper ) CalculatePaymentAmount ( owed sdk . Coins , payment sdk . Coins ) ( sdk . Coins , error ) {
2020-12-07 21:51:35 +00:00
repayment := sdk . Coins { }
2021-01-07 10:23:05 +00:00
if ! payment . DenomsSubsetOf ( owed ) {
return repayment , types . ErrInvalidRepaymentDenom
}
2020-12-07 21:51:35 +00:00
for _ , coin := range payment {
if coin . Amount . GT ( owed . AmountOf ( coin . Denom ) ) {
repayment = append ( repayment , sdk . NewCoin ( coin . Denom , owed . AmountOf ( coin . Denom ) ) )
} else {
repayment = append ( repayment , coin )
}
}
2021-01-07 10:23:05 +00:00
return repayment , nil
2020-12-07 21:51:35 +00:00
}