mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 01:47:27 +00:00 
			
		
		
		
	Hard: suppliers earn interest (#749)
* update to borrow interest factor * add supply interest factor to accrue interest * supply interest factor keeper methods * fix potential bug with user borrow indexing * sync supply interest on deposit/withdraw * separate withdraw/deposit * relocate interest sync methods * update comment * simplify supply interest statement * check truncated int not zero * add .sub(storedAmount) back * add store key suppliedcoins * increment/decrement supplied coins * update withdraw with new accounting * update withdraw test * catch repay edge case * unit tests * TestSupplyInterest scaffolding * test notes * temp: interest test * example test * changes, test checks more state * fix: calculate supply interest directly * fix: catch divide by zero * add state checks back into interest test * add snapshot test cases * test owed supplied interest paid at correct ratio * test user supply syncs user's borrow interest * remove print statements and clean up * refactor indented logic * test supply/borrow multiple coins * update decoder test Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
		
							parent
							
								
									e9f5043c84
								
							
						
					
					
						commit
						f7a73c9245
					
				@ -53,7 +53,8 @@ var (
 | 
			
		||||
	NewQuerier                       = keeper.NewQuerier
 | 
			
		||||
	CalculateUtilizationRatio        = keeper.CalculateUtilizationRatio
 | 
			
		||||
	CalculateBorrowRate              = keeper.CalculateBorrowRate
 | 
			
		||||
	CalculateInterestFactor          = keeper.CalculateInterestFactor
 | 
			
		||||
	CalculateBorrowInterestFactor    = keeper.CalculateBorrowInterestFactor
 | 
			
		||||
	CalculateSupplyInterestFactor    = keeper.CalculateSupplyInterestFactor
 | 
			
		||||
	APYToSPY                         = keeper.APYToSPY
 | 
			
		||||
	ClaimKey                         = types.ClaimKey
 | 
			
		||||
	DefaultGenesisState              = types.DefaultGenesisState
 | 
			
		||||
 | 
			
		||||
@ -13,11 +13,11 @@ import (
 | 
			
		||||
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.GetInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		_, foundInterestFactor := k.GetBorrowInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		if !foundInterestFactor {
 | 
			
		||||
			_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
 | 
			
		||||
			if foundMm {
 | 
			
		||||
				k.SetInterestFactor(ctx, coin.Denom, sdk.OneDec())
 | 
			
		||||
				k.SetBorrowInterestFactor(ctx, coin.Denom, sdk.OneDec())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -28,11 +28,8 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the user has an existing borrow, sync its outstanding interest
 | 
			
		||||
	_, found := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	if found {
 | 
			
		||||
		k.SyncOutstandingInterest(ctx, borrower)
 | 
			
		||||
	}
 | 
			
		||||
	// Sync any outstanding interest
 | 
			
		||||
	k.SyncBorrowInterest(ctx, borrower)
 | 
			
		||||
 | 
			
		||||
	// Validate borrow amount within user and protocol limits
 | 
			
		||||
	err = k.ValidateBorrow(ctx, borrower, coins)
 | 
			
		||||
@ -57,22 +54,39 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The first time a user borrows a denom we add it the user's borrow interest factor index
 | 
			
		||||
	var borrowInterestFactors types.BorrowInterestFactors
 | 
			
		||||
	currBorrow, foundBorrow := 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 {
 | 
			
		||||
		var interestFactors types.InterestFactors
 | 
			
		||||
	if foundBorrow {
 | 
			
		||||
		// If the coin denom to be borrowed is not in the user's existing borrow, we add it borrow index
 | 
			
		||||
		for _, coin := range coins {
 | 
			
		||||
			interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
 | 
			
		||||
			interestFactor := types.NewInterestFactor(coin.Denom, interestFactorValue)
 | 
			
		||||
			interestFactors = append(interestFactors, interestFactor)
 | 
			
		||||
			if !sdk.NewCoins(coin).DenomsSubsetOf(currBorrow.Amount) {
 | 
			
		||||
				borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
 | 
			
		||||
				borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
 | 
			
		||||
				borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Concatenate new borrow interest factors to existing borrow interest factors
 | 
			
		||||
		borrowInterestFactors = append(borrowInterestFactors, currBorrow.Index...)
 | 
			
		||||
	} else {
 | 
			
		||||
		for _, coin := range coins {
 | 
			
		||||
			borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
 | 
			
		||||
			borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
 | 
			
		||||
			borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
 | 
			
		||||
		}
 | 
			
		||||
		borrow := types.NewBorrow(borrower, sdk.Coins{}, interestFactors)
 | 
			
		||||
		k.SetBorrow(ctx, borrow)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add the newly borrowed coins to the user's borrow object
 | 
			
		||||
	borrow, _ := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	borrow.Amount = borrow.Amount.Add(coins...)
 | 
			
		||||
	// Calculate new borrow amount
 | 
			
		||||
	var amount sdk.Coins
 | 
			
		||||
	if foundBorrow {
 | 
			
		||||
		amount = currBorrow.Amount.Add(coins...)
 | 
			
		||||
	} else {
 | 
			
		||||
		amount = coins
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update the borrower's amount and borrow interest factors in the store
 | 
			
		||||
	borrow := types.NewBorrow(borrower, amount, borrowInterestFactors)
 | 
			
		||||
	k.SetBorrow(ctx, borrow)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower)
 | 
			
		||||
@ -92,46 +106,6 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	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.
 | 
			
		||||
	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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		if foundAtIndex == -1 { // First time user has borrowed this denom
 | 
			
		||||
			borrow.Index = append(borrow.Index, types.NewInterestFactor(coin.Denom, interestFactorValue))
 | 
			
		||||
		} 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))
 | 
			
		||||
			userLastInterestFactor := borrow.Index[foundAtIndex].Value
 | 
			
		||||
			interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).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 = interestFactorValue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Add all pending interest to user's borrow
 | 
			
		||||
	borrow.Amount = borrow.Amount.Add(totalNewInterest...)
 | 
			
		||||
 | 
			
		||||
	// Update user's borrow in the store
 | 
			
		||||
	k.SetBorrow(ctx, borrow)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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() {
 | 
			
		||||
@ -240,7 +214,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IncrementBorrowedCoins increments the amount of borrowed coins by the newCoins parameter
 | 
			
		||||
// 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 {
 | 
			
		||||
@ -252,7 +226,7 @@ func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DecrementBorrowedCoins decrements the amount of borrowed coins by the coins parameter
 | 
			
		||||
// 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 {
 | 
			
		||||
@ -272,10 +246,13 @@ func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
 | 
			
		||||
func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.Coins {
 | 
			
		||||
	borrowBalance := sdk.Coins{}
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	if found {
 | 
			
		||||
	if !found {
 | 
			
		||||
		return borrowBalance
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	totalNewInterest := sdk.Coins{}
 | 
			
		||||
	for _, coin := range borrow.Amount {
 | 
			
		||||
			interestFactorValue, foundInterestFactorValue := k.GetInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		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
 | 
			
		||||
@ -294,7 +271,6 @@ func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.C
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
		borrowBalance = borrow.Amount.Add(totalNewInterest...)
 | 
			
		||||
	}
 | 
			
		||||
	return borrowBalance
 | 
			
		||||
 | 
			
		||||
	return borrow.Amount.Add(totalNewInterest...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,26 @@ import (
 | 
			
		||||
 | 
			
		||||
// Deposit deposit
 | 
			
		||||
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
 | 
			
		||||
	// Set any new denoms' global supply index to 1.0
 | 
			
		||||
	for _, coin := range coins {
 | 
			
		||||
		_, foundInterestFactor := k.GetSupplyInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		if !foundInterestFactor {
 | 
			
		||||
			_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
 | 
			
		||||
			if foundMm {
 | 
			
		||||
				k.SetSupplyInterestFactor(ctx, coin.Denom, sdk.OneDec())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current stored LTV based on stored borrows/deposits
 | 
			
		||||
	prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k.SyncOutstandingInterest(ctx, depositor)
 | 
			
		||||
	// Sync any outstanding interest
 | 
			
		||||
	k.SyncBorrowInterest(ctx, depositor)
 | 
			
		||||
	k.SyncSupplyInterest(ctx, depositor)
 | 
			
		||||
 | 
			
		||||
	err = k.ValidateDeposit(ctx, coins)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -44,17 +57,47 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, depositor)
 | 
			
		||||
	if !found {
 | 
			
		||||
		deposit = types.NewDeposit(depositor, coins)
 | 
			
		||||
	// The first time a user deposits a denom we add it the user's supply interest factor index
 | 
			
		||||
	var supplyInterestFactors types.SupplyInterestFactors
 | 
			
		||||
	currDeposit, foundDeposit := k.GetDeposit(ctx, depositor)
 | 
			
		||||
	// On user's first deposit, build deposit index list containing denoms and current global deposit index value
 | 
			
		||||
	if foundDeposit {
 | 
			
		||||
		// If the coin denom to be deposited is not in the user's existing deposit, we add it deposit index
 | 
			
		||||
		for _, coin := range coins {
 | 
			
		||||
			if !sdk.NewCoins(coin).DenomsSubsetOf(currDeposit.Amount) {
 | 
			
		||||
				supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
 | 
			
		||||
				supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
 | 
			
		||||
				supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Concatenate new deposit interest factors to existing deposit interest factors
 | 
			
		||||
		supplyInterestFactors = append(supplyInterestFactors, currDeposit.Index...)
 | 
			
		||||
	} else {
 | 
			
		||||
		deposit.Amount = deposit.Amount.Add(coins...)
 | 
			
		||||
		for _, coin := range coins {
 | 
			
		||||
			supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
 | 
			
		||||
			supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
 | 
			
		||||
			supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Calculate new deposit amount
 | 
			
		||||
	var amount sdk.Coins
 | 
			
		||||
	if foundDeposit {
 | 
			
		||||
		amount = currDeposit.Amount.Add(coins...)
 | 
			
		||||
	} else {
 | 
			
		||||
		amount = coins
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update the depositer's amount and supply interest factors in the store
 | 
			
		||||
	deposit := types.NewDeposit(depositor, amount, supplyInterestFactors)
 | 
			
		||||
	k.SetDeposit(ctx, deposit)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
 | 
			
		||||
 | 
			
		||||
	// Update total supplied amount by newly supplied coins. Don't add user's pending interest as
 | 
			
		||||
	// it has already been included in the total supplied coins by the BeginBlocker.
 | 
			
		||||
	k.IncrementSuppliedCoins(ctx, coins)
 | 
			
		||||
 | 
			
		||||
	ctx.EventManager().EmitEvent(
 | 
			
		||||
		sdk.NewEvent(
 | 
			
		||||
			types.EventTypeHardDeposit,
 | 
			
		||||
@ -84,76 +127,37 @@ func (k Keeper) ValidateDeposit(ctx sdk.Context, coins sdk.Coins) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Withdraw returns some or all of a deposit back to original depositor
 | 
			
		||||
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, depositor)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current stored LTV based on stored borrows/deposits
 | 
			
		||||
	prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k.SyncOutstandingInterest(ctx, depositor)
 | 
			
		||||
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, depositor)
 | 
			
		||||
	if !found {
 | 
			
		||||
		borrow = types.Borrow{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	proposedDepositAmount, isNegative := deposit.Amount.SafeSub(coins)
 | 
			
		||||
	if isNegative {
 | 
			
		||||
		return types.ErrNegativeBorrowedCoins
 | 
			
		||||
	}
 | 
			
		||||
	proposedDeposit := types.NewDeposit(deposit.Depositor, proposedDepositAmount)
 | 
			
		||||
 | 
			
		||||
	valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !valid {
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, coins)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.EventManager().EmitEvent(
 | 
			
		||||
		sdk.NewEvent(
 | 
			
		||||
			types.EventTypeHardWithdrawal,
 | 
			
		||||
			sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()),
 | 
			
		||||
			sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if deposit.Amount.IsEqual(coins) {
 | 
			
		||||
		ctx.EventManager().EmitEvent(
 | 
			
		||||
			sdk.NewEvent(
 | 
			
		||||
				types.EventTypeDeleteHardDeposit,
 | 
			
		||||
				sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
 | 
			
		||||
			),
 | 
			
		||||
		)
 | 
			
		||||
		k.DeleteDeposit(ctx, deposit)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	deposit.Amount = deposit.Amount.Sub(coins)
 | 
			
		||||
	k.SetDeposit(ctx, deposit)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTotalDeposited returns the total amount deposited for the input deposit type and deposit denom
 | 
			
		||||
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositDenom string) (total sdk.Int) {
 | 
			
		||||
	var macc supplyExported.ModuleAccountI
 | 
			
		||||
	macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
 | 
			
		||||
	return macc.GetCoins().AmountOf(depositDenom)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IncrementSuppliedCoins increments the total amount of supplied coins by the newCoins parameter
 | 
			
		||||
func (k Keeper) IncrementSuppliedCoins(ctx sdk.Context, newCoins sdk.Coins) {
 | 
			
		||||
	suppliedCoins, found := k.GetSuppliedCoins(ctx)
 | 
			
		||||
	if !found {
 | 
			
		||||
		if !newCoins.Empty() {
 | 
			
		||||
			k.SetSuppliedCoins(ctx, newCoins)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		k.SetSuppliedCoins(ctx, suppliedCoins.Add(newCoins...))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DecrementSuppliedCoins decrements the total amount of supplied coins by the coins parameter
 | 
			
		||||
func (k Keeper) DecrementSuppliedCoins(ctx sdk.Context, coins sdk.Coins) error {
 | 
			
		||||
	suppliedCoins, found := k.GetSuppliedCoins(ctx)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrSuppliedCoinsNotFound, "cannot withdraw if no coins are deposited")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatedSuppliedCoins, isAnyNegative := suppliedCoins.SafeSub(coins)
 | 
			
		||||
	if isAnyNegative {
 | 
			
		||||
		return types.ErrNegativeSuppliedCoins
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k.SetSuppliedCoins(ctx, updatedSuppliedCoins)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -196,360 +196,3 @@ func (suite *KeeperTestSuite) TestDeposit() {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		depositor                 sdk.AccAddress
 | 
			
		||||
		depositAmount             sdk.Coins
 | 
			
		||||
		withdrawAmount            sdk.Coins
 | 
			
		||||
		createDeposit             bool
 | 
			
		||||
		expectedAccountBalance    sdk.Coins
 | 
			
		||||
		expectedModAccountBalance sdk.Coins
 | 
			
		||||
		depositExists             bool
 | 
			
		||||
		finalDepositAmount        sdk.Coins
 | 
			
		||||
	}
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
		expectPass bool
 | 
			
		||||
		contains   string
 | 
			
		||||
	}
 | 
			
		||||
	type withdrawTest struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		errArgs errArgs
 | 
			
		||||
	}
 | 
			
		||||
	testCases := []withdrawTest{
 | 
			
		||||
		{
 | 
			
		||||
			"valid partial withdraw",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
 | 
			
		||||
				expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
				depositExists:             true,
 | 
			
		||||
				finalDepositAmount:        sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"valid full withdraw",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
 | 
			
		||||
				expectedModAccountBalance: sdk.Coins(nil),
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"withdraw invalid, denom not found",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.Coins{},
 | 
			
		||||
				expectedModAccountBalance: sdk.Coins{},
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "subtraction results in negative borrow amount",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"withdraw exceeds deposit",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.Coins{},
 | 
			
		||||
				expectedModAccountBalance: sdk.Coins{},
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "subtraction results in negative borrow amount",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// create new app with one funded account
 | 
			
		||||
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
			tApp := app.NewTestApp()
 | 
			
		||||
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
 | 
			
		||||
			authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
 | 
			
		||||
			loanToValue := sdk.MustNewDecFromStr("0.6")
 | 
			
		||||
			hardGS := types.NewGenesisState(types.NewParams(
 | 
			
		||||
				true,
 | 
			
		||||
				types.DistributionSchedules{
 | 
			
		||||
					types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
				},
 | 
			
		||||
				types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
 | 
			
		||||
					types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					time.Hour*24,
 | 
			
		||||
				),
 | 
			
		||||
				},
 | 
			
		||||
				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("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(100000000), 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()),
 | 
			
		||||
				},
 | 
			
		||||
				0, // LTV counter
 | 
			
		||||
			), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
 | 
			
		||||
 | 
			
		||||
			// 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: "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(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "kava:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("2.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "bnb:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("10.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS,
 | 
			
		||||
				app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
 | 
			
		||||
				app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
			suite.keeper = keeper
 | 
			
		||||
 | 
			
		||||
			if tc.args.createDeposit {
 | 
			
		||||
				err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
 | 
			
		||||
 | 
			
		||||
			if tc.errArgs.expectPass {
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
				acc := suite.getAccount(tc.args.depositor)
 | 
			
		||||
				suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
 | 
			
		||||
				mAcc := suite.getModuleAccount(types.ModuleAccountName)
 | 
			
		||||
				suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
 | 
			
		||||
				testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
 | 
			
		||||
				if tc.args.depositExists {
 | 
			
		||||
					suite.Require().True(f)
 | 
			
		||||
					suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
 | 
			
		||||
				} else {
 | 
			
		||||
					suite.Require().False(f)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				suite.Require().Error(err)
 | 
			
		||||
				suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestLtvWithdraw() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		borrower             sdk.AccAddress
 | 
			
		||||
		keeper               sdk.AccAddress
 | 
			
		||||
		initialModuleCoins   sdk.Coins
 | 
			
		||||
		initialBorrowerCoins sdk.Coins
 | 
			
		||||
		initialKeeperCoins   sdk.Coins
 | 
			
		||||
		depositCoins         []sdk.Coin
 | 
			
		||||
		borrowCoins          sdk.Coins
 | 
			
		||||
		futureTime           int64
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
		expectPass bool
 | 
			
		||||
		contains   string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type liqTest struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		errArgs errArgs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set up test constants
 | 
			
		||||
	model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
 | 
			
		||||
	reserveFactor := sdk.MustNewDecFromStr("0.05")
 | 
			
		||||
	oneMonthInSeconds := int64(2592000)
 | 
			
		||||
	borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
 | 
			
		||||
	keeper := sdk.AccAddress(crypto.AddressHash([]byte("testkeeper")))
 | 
			
		||||
 | 
			
		||||
	testCases := []liqTest{
 | 
			
		||||
		{
 | 
			
		||||
			"invalid: withdraw is outside loan-to-value range",
 | 
			
		||||
			args{
 | 
			
		||||
				borrower:             borrower,
 | 
			
		||||
				keeper:               keeper,
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialKeeperCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))),
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))),
 | 
			
		||||
				futureTime:           oneMonthInSeconds,
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "proposed withdraw outside loan-to-value range",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
			tApp := app.NewTestApp()
 | 
			
		||||
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
 | 
			
		||||
 | 
			
		||||
			// Auth module genesis state
 | 
			
		||||
			authGS := app.NewAuthGenState(
 | 
			
		||||
				[]sdk.AccAddress{tc.args.borrower, tc.args.keeper},
 | 
			
		||||
				[]sdk.Coins{tc.args.initialBorrowerCoins, tc.args.initialKeeperCoins},
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			// Harvest module genesis state
 | 
			
		||||
			harvestGS := types.NewGenesisState(types.NewParams(
 | 
			
		||||
				true,
 | 
			
		||||
				types.DistributionSchedules{
 | 
			
		||||
					types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
				},
 | 
			
		||||
				types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
 | 
			
		||||
					types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					time.Hour*24,
 | 
			
		||||
				),
 | 
			
		||||
				},
 | 
			
		||||
				types.MoneyMarkets{
 | 
			
		||||
					types.NewMoneyMarket("ukava",
 | 
			
		||||
						types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
 | 
			
		||||
						"kava:usd",                     // Market ID
 | 
			
		||||
						sdk.NewInt(KAVA_CF),            // Conversion Factor
 | 
			
		||||
						sdk.NewInt(100000000*KAVA_CF),  // Auction Size
 | 
			
		||||
						model,                          // Interest Rate Model
 | 
			
		||||
						reserveFactor,                  // Reserve Factor
 | 
			
		||||
						sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
 | 
			
		||||
				},
 | 
			
		||||
				0, // LTV counter
 | 
			
		||||
			), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
 | 
			
		||||
 | 
			
		||||
			// 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},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				PostedPrices: []pricefeed.PostedPrice{
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "usdx:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("1.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "kava:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("2.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Initialize test application
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS,
 | 
			
		||||
				app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
 | 
			
		||||
				app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
 | 
			
		||||
 | 
			
		||||
			// Mint coins to Harvest module account
 | 
			
		||||
			supplyKeeper := tApp.GetSupplyKeeper()
 | 
			
		||||
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
 | 
			
		||||
 | 
			
		||||
			auctionKeeper := tApp.GetAuctionKeeper()
 | 
			
		||||
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
			suite.keeper = keeper
 | 
			
		||||
			suite.auctionKeeper = auctionKeeper
 | 
			
		||||
 | 
			
		||||
			var err error
 | 
			
		||||
 | 
			
		||||
			// Run begin blocker to set up state
 | 
			
		||||
			hard.BeginBlocker(suite.ctx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
			// Deposit coins
 | 
			
		||||
			err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Borrow coins
 | 
			
		||||
			err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Attempting to withdraw fails
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Set up future chain context and run begin blocker, increasing user's owed borrow balance
 | 
			
		||||
			runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
 | 
			
		||||
			liqCtx := suite.ctx.WithBlockTime(runAtTime)
 | 
			
		||||
			hard.BeginBlocker(liqCtx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
			// Attempted withdraw of 1 coin still fails
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Repay the initial principal
 | 
			
		||||
			err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Withdrawing half the coins should succeed
 | 
			
		||||
			withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(2))))
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -69,14 +69,13 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get available hard module account cash on hand
 | 
			
		||||
	// Get current protocol state and hold in memory as 'prior'
 | 
			
		||||
	cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
 | 
			
		||||
 | 
			
		||||
	// Get prior borrows
 | 
			
		||||
	borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
 | 
			
		||||
	borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
 | 
			
		||||
	if foundBorrowCoinsPrior {
 | 
			
		||||
		borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
 | 
			
		||||
	borrowedPrior := sdk.NewCoin(denom, sdk.ZeroInt())
 | 
			
		||||
	borrowedCoinsPrior, foundBorrowedCoinsPrior := k.GetBorrowedCoins(ctx)
 | 
			
		||||
	if foundBorrowedCoinsPrior {
 | 
			
		||||
		borrowedPrior = sdk.NewCoin(denom, borrowedCoinsPrior.AmountOf(denom))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
 | 
			
		||||
@ -86,11 +85,18 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
 | 
			
		||||
		reservesPrior = newReservesPrior
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, denom)
 | 
			
		||||
	if !foundInterestFactorPrior {
 | 
			
		||||
		newInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
 | 
			
		||||
		k.SetInterestFactor(ctx, denom, newInterestFactorPrior)
 | 
			
		||||
		interestFactorPrior = newInterestFactorPrior
 | 
			
		||||
	borrowInterestFactorPrior, foundBorrowInterestFactorPrior := k.GetBorrowInterestFactor(ctx, denom)
 | 
			
		||||
	if !foundBorrowInterestFactorPrior {
 | 
			
		||||
		newBorrowInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
 | 
			
		||||
		k.SetBorrowInterestFactor(ctx, denom, newBorrowInterestFactorPrior)
 | 
			
		||||
		borrowInterestFactorPrior = newBorrowInterestFactorPrior
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	supplyInterestFactorPrior, foundSupplyInterestFactorPrior := k.GetSupplyInterestFactor(ctx, denom)
 | 
			
		||||
	if !foundSupplyInterestFactorPrior {
 | 
			
		||||
		newSupplyInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
 | 
			
		||||
		k.SetSupplyInterestFactor(ctx, denom, newSupplyInterestFactorPrior)
 | 
			
		||||
		supplyInterestFactorPrior = newSupplyInterestFactorPrior
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch money market from the store
 | 
			
		||||
@ -100,7 +106,7 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GetBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
 | 
			
		||||
	borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowsPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
 | 
			
		||||
	borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowedPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@ -111,16 +117,27 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
 | 
			
		||||
	interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(borrowsPrior.Amount)).TruncateInt()).Sub(borrowsPrior.Amount)
 | 
			
		||||
	totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestAccumulated))
 | 
			
		||||
	totalReservesNew := reservesPrior.Add(sdk.NewCoin(denom, sdk.NewDecFromInt(interestAccumulated).Mul(mm.ReserveFactor).TruncateInt()))
 | 
			
		||||
	interestFactorNew := interestFactorPrior.Mul(interestFactor)
 | 
			
		||||
	// Calculate borrow interest factor and update
 | 
			
		||||
	borrowInterestFactor := CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
 | 
			
		||||
	interestBorrowAccumulated := (borrowInterestFactor.Mul(sdk.NewDecFromInt(borrowedPrior.Amount)).TruncateInt()).Sub(borrowedPrior.Amount)
 | 
			
		||||
	totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestBorrowAccumulated))
 | 
			
		||||
	reservesNew := interestBorrowAccumulated.ToDec().Mul(mm.ReserveFactor).TruncateInt()
 | 
			
		||||
	borrowInterestFactorNew := borrowInterestFactorPrior.Mul(borrowInterestFactor)
 | 
			
		||||
	k.SetBorrowInterestFactor(ctx, denom, borrowInterestFactorNew)
 | 
			
		||||
 | 
			
		||||
	k.SetInterestFactor(ctx, denom, interestFactorNew)
 | 
			
		||||
	// Calculate supply interest factor and update
 | 
			
		||||
	supplyInterestNew := interestBorrowAccumulated.Sub(reservesNew)
 | 
			
		||||
	supplyInterestFactor := CalculateSupplyInterestFactor(supplyInterestNew.ToDec(), cashPrior.ToDec(), borrowedPrior.Amount.ToDec(), reservesPrior.Amount.ToDec())
 | 
			
		||||
	supplyInterestFactorNew := supplyInterestFactorPrior.Mul(supplyInterestFactor)
 | 
			
		||||
	k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
 | 
			
		||||
 | 
			
		||||
	// Update accural keys in store
 | 
			
		||||
	k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
 | 
			
		||||
	k.SetTotalReserves(ctx, denom, totalReservesNew)
 | 
			
		||||
	k.IncrementSuppliedCoins(ctx, sdk.NewCoins(sdk.NewCoin(denom, supplyInterestNew)))
 | 
			
		||||
	k.SetTotalReserves(ctx, denom, reservesPrior.Add(sdk.NewCoin(mm.Denom, reservesNew)))
 | 
			
		||||
	k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
 | 
			
		||||
	k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -155,10 +172,10 @@ func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
 | 
			
		||||
	return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateInterestFactor calculates the simple interest scaling factor,
 | 
			
		||||
// CalculateBorrowInterestFactor calculates the simple interest scaling factor,
 | 
			
		||||
// which is equal to: (per-second interest rate * number of seconds elapsed)
 | 
			
		||||
// Will return 1.000x, multiply by principal to get new principal with added interest
 | 
			
		||||
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
 | 
			
		||||
func CalculateBorrowInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
 | 
			
		||||
	scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
 | 
			
		||||
	scalingFactorInt := sdk.NewInt(int64(scalingFactor))
 | 
			
		||||
 | 
			
		||||
@ -173,6 +190,100 @@ func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.I
 | 
			
		||||
	return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateSupplyInterestFactor calculates the supply interest factor, which is the percentage of borrow interest
 | 
			
		||||
// that flows to each unit of supply, i.e. at 50% utilization and 0% reserve factor, a 5% borrow interest will
 | 
			
		||||
// correspond to a 2.5% supply interest.
 | 
			
		||||
func CalculateSupplyInterestFactor(newInterest, cash, borrows, reserves sdk.Dec) sdk.Dec {
 | 
			
		||||
	totalSupply := cash.Add(borrows).Sub(reserves)
 | 
			
		||||
	if totalSupply.IsZero() {
 | 
			
		||||
		return sdk.OneDec()
 | 
			
		||||
	}
 | 
			
		||||
	return (newInterest.Quo(totalSupply)).Add(sdk.OneDec())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state
 | 
			
		||||
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, addr sdk.AccAddress) {
 | 
			
		||||
	totalNewInterest := sdk.Coins{}
 | 
			
		||||
 | 
			
		||||
	// Update user's borrow interest factor list for each asset in the 'coins' array.
 | 
			
		||||
	// We use a list of BorrowInterestFactors here because Amino doesn't support marshaling maps.
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, coin := range borrow.Amount {
 | 
			
		||||
		// Locate the borrow interest factor 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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		interestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		if foundAtIndex == -1 { // First time user has borrowed this denom
 | 
			
		||||
			borrow.Index = append(borrow.Index, types.NewBorrowInterestFactor(coin.Denom, interestFactorValue))
 | 
			
		||||
		} 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))
 | 
			
		||||
			userLastInterestFactor := borrow.Index[foundAtIndex].Value
 | 
			
		||||
			interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).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 = interestFactorValue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Add all pending interest to user's borrow
 | 
			
		||||
	borrow.Amount = borrow.Amount.Add(totalNewInterest...)
 | 
			
		||||
 | 
			
		||||
	// Update user's borrow in the store
 | 
			
		||||
	k.SetBorrow(ctx, borrow)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncSupplyInterest updates the user's earned interest on supplied coins based on the latest global state
 | 
			
		||||
func (k Keeper) SyncSupplyInterest(ctx sdk.Context, addr sdk.AccAddress) {
 | 
			
		||||
	totalNewInterest := sdk.Coins{}
 | 
			
		||||
 | 
			
		||||
	// Update user's supply index list for each asset in the 'coins' array.
 | 
			
		||||
	// We use a list of SupplyInterestFactors here because Amino doesn't support marshaling maps.
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, coin := range deposit.Amount {
 | 
			
		||||
		// Locate the deposit index item by coin denom in the user's list of deposit indexes
 | 
			
		||||
		foundAtIndex := -1
 | 
			
		||||
		for i := range deposit.Index {
 | 
			
		||||
			if deposit.Index[i].Denom == coin.Denom {
 | 
			
		||||
				foundAtIndex = i
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		interestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
 | 
			
		||||
		if foundAtIndex == -1 { // First time user has supplied this denom
 | 
			
		||||
			deposit.Index = append(deposit.Index, types.NewSupplyInterestFactor(coin.Denom, interestFactorValue))
 | 
			
		||||
		} else { // User has an existing supply index for this denom
 | 
			
		||||
			// Calculate interest earned by user since asset's last deposit index update
 | 
			
		||||
			storedAmount := sdk.NewDecFromInt(deposit.Amount.AmountOf(coin.Denom))
 | 
			
		||||
			userLastInterestFactor := deposit.Index[foundAtIndex].Value
 | 
			
		||||
			interest := (storedAmount.Mul(interestFactorValue).Quo(userLastInterestFactor)).Sub(storedAmount)
 | 
			
		||||
			if interest.TruncateInt().GT(sdk.ZeroInt()) {
 | 
			
		||||
				totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
 | 
			
		||||
			}
 | 
			
		||||
			// We're synced up, so update user's deposit index value to match the current global deposit index value
 | 
			
		||||
			deposit.Index[foundAtIndex].Value = interestFactorValue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Add all pending interest to user's deposit
 | 
			
		||||
	deposit.Amount = deposit.Amount.Add(totalNewInterest...)
 | 
			
		||||
 | 
			
		||||
	// Update user's deposit in the store
 | 
			
		||||
	k.SetDeposit(ctx, deposit)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
 | 
			
		||||
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
 | 
			
		||||
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
 | 
			
		||||
 | 
			
		||||
@ -202,7 +202,7 @@ func (suite *InterestTestSuite) TestCalculateBorrowRate() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *InterestTestSuite) TestCalculateInterestFactor() {
 | 
			
		||||
func (suite *InterestTestSuite) TestCalculateBorrowInterestFactor() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		perSecondInterestRate sdk.Dec
 | 
			
		||||
		timeElapsed           sdk.Int
 | 
			
		||||
@ -302,7 +302,65 @@ func (suite *InterestTestSuite) TestCalculateInterestFactor() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		interestFactor := hard.CalculateInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
 | 
			
		||||
		interestFactor := hard.CalculateBorrowInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
 | 
			
		||||
		suite.Require().Equal(tc.args.expectedValue, interestFactor)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *InterestTestSuite) TestCalculateSupplyInterestFactor() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		newInterest   sdk.Dec
 | 
			
		||||
		cash          sdk.Dec
 | 
			
		||||
		borrows       sdk.Dec
 | 
			
		||||
		reserves      sdk.Dec
 | 
			
		||||
		reserveFactor sdk.Dec
 | 
			
		||||
		expectedValue sdk.Dec
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type test struct {
 | 
			
		||||
		name string
 | 
			
		||||
		args args
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testCases := []test{
 | 
			
		||||
		{
 | 
			
		||||
			"low new interest",
 | 
			
		||||
			args{
 | 
			
		||||
				newInterest:   sdk.MustNewDecFromStr("1"),
 | 
			
		||||
				cash:          sdk.MustNewDecFromStr("100.0"),
 | 
			
		||||
				borrows:       sdk.MustNewDecFromStr("1000.0"),
 | 
			
		||||
				reserves:      sdk.MustNewDecFromStr("10.0"),
 | 
			
		||||
				reserveFactor: sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedValue: sdk.MustNewDecFromStr("1.000917431192660550"),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"medium new interest",
 | 
			
		||||
			args{
 | 
			
		||||
				newInterest:   sdk.MustNewDecFromStr("5"),
 | 
			
		||||
				cash:          sdk.MustNewDecFromStr("100.0"),
 | 
			
		||||
				borrows:       sdk.MustNewDecFromStr("1000.0"),
 | 
			
		||||
				reserves:      sdk.MustNewDecFromStr("10.0"),
 | 
			
		||||
				reserveFactor: sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedValue: sdk.MustNewDecFromStr("1.004587155963302752"),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"high new interest",
 | 
			
		||||
			args{
 | 
			
		||||
				newInterest:   sdk.MustNewDecFromStr("10"),
 | 
			
		||||
				cash:          sdk.MustNewDecFromStr("100.0"),
 | 
			
		||||
				borrows:       sdk.MustNewDecFromStr("1000.0"),
 | 
			
		||||
				reserves:      sdk.MustNewDecFromStr("10.0"),
 | 
			
		||||
				reserveFactor: sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedValue: sdk.MustNewDecFromStr("1.009174311926605505"),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		interestFactor := hard.CalculateSupplyInterestFactor(tc.args.newInterest,
 | 
			
		||||
			tc.args.cash, tc.args.borrows, tc.args.reserves)
 | 
			
		||||
		suite.Require().Equal(tc.args.expectedValue, interestFactor)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -377,7 +435,6 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
 | 
			
		||||
			true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			spy, err := hard.APYToSPY(tc.args.apy)
 | 
			
		||||
@ -391,13 +448,13 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExpectedInterest struct {
 | 
			
		||||
type ExpectedBorrowInterest struct {
 | 
			
		||||
	elapsedTime  int64
 | 
			
		||||
	shouldBorrow bool
 | 
			
		||||
	borrowCoin   sdk.Coin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
func (suite *KeeperTestSuite) TestBorrowInterest() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		user                     sdk.AccAddress
 | 
			
		||||
		initialBorrowerCoins     sdk.Coins
 | 
			
		||||
@ -406,7 +463,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
		borrowCoins              sdk.Coins
 | 
			
		||||
		interestRateModel        types.InterestRateModel
 | 
			
		||||
		reserveFactor            sdk.Dec
 | 
			
		||||
		expectedInterestSnaphots []ExpectedInterest
 | 
			
		||||
		expectedInterestSnaphots []ExpectedBorrowInterest
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
@ -438,7 +495,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneDayInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -461,7 +518,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneWeekInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -484,7 +541,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -507,7 +564,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneYearInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -530,7 +587,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneYearInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -553,7 +610,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneYearInSeconds,
 | 
			
		||||
						shouldBorrow: true,
 | 
			
		||||
@ -576,7 +633,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -604,7 +661,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedInterest{
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedBorrowInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneDayInSeconds,
 | 
			
		||||
						shouldBorrow: false,
 | 
			
		||||
@ -633,6 +690,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
@ -737,7 +795,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
					reservesPrior = sdk.NewCoin(tc.args.borrowCoinDenom, sdk.ZeroInt())
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetInterestFactor(prevCtx, tc.args.borrowCoinDenom)
 | 
			
		||||
				interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, tc.args.borrowCoinDenom)
 | 
			
		||||
				suite.Require().True(foundInterestFactorPrior)
 | 
			
		||||
 | 
			
		||||
				// 2. Calculate expected interest owed
 | 
			
		||||
@ -748,7 +806,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
				interestFactor := hard.CalculateInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
 | 
			
		||||
				interestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
 | 
			
		||||
				expectedInterest := (interestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
 | 
			
		||||
				expectedReserves := reservesPrior.Add(sdk.NewCoin(tc.args.borrowCoinDenom, sdk.NewDecFromInt(expectedInterest).Mul(tc.args.reserveFactor).TruncateInt()))
 | 
			
		||||
				expectedInterestFactor := interestFactorPrior.Mul(interestFactor)
 | 
			
		||||
@ -769,7 +827,7 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
				suite.Require().Equal(expectedReserves, currTotalReserves)
 | 
			
		||||
 | 
			
		||||
				// Check that the borrow index has increased as expected
 | 
			
		||||
				currIndexPrior, _ := suite.keeper.GetInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
 | 
			
		||||
				currIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
 | 
			
		||||
				suite.Require().Equal(expectedInterestFactor, currIndexPrior)
 | 
			
		||||
 | 
			
		||||
				// After borrowing again user's borrow balance should have any outstanding interest applied
 | 
			
		||||
@ -791,6 +849,495 @@ func (suite *KeeperTestSuite) TestInterest() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExpectedSupplyInterest struct {
 | 
			
		||||
	elapsedTime  int64
 | 
			
		||||
	shouldSupply bool
 | 
			
		||||
	supplyCoin   sdk.Coin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestSupplyInterest() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		user                     sdk.AccAddress
 | 
			
		||||
		initialSupplierCoins     sdk.Coins
 | 
			
		||||
		initialBorrowerCoins     sdk.Coins
 | 
			
		||||
		initialModuleCoins       sdk.Coins
 | 
			
		||||
		depositCoins             sdk.Coins
 | 
			
		||||
		coinDenoms               []string
 | 
			
		||||
		borrowCoins              sdk.Coins
 | 
			
		||||
		interestRateModel        types.InterestRateModel
 | 
			
		||||
		reserveFactor            sdk.Dec
 | 
			
		||||
		expectedInterestSnaphots []ExpectedSupplyInterest
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
		expectPass bool
 | 
			
		||||
		contains   string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type interestTest struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		errArgs errArgs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	normalModel := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
 | 
			
		||||
 | 
			
		||||
	oneDayInSeconds := int64(86400)
 | 
			
		||||
	oneWeekInSeconds := int64(604800)
 | 
			
		||||
	oneMonthInSeconds := int64(2592000)
 | 
			
		||||
	oneYearInSeconds := int64(31536000)
 | 
			
		||||
 | 
			
		||||
	testCases := []interestTest{
 | 
			
		||||
		{
 | 
			
		||||
			"one day",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneDayInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"one week",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneWeekInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"one month",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"one year",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneYearInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"supply/borrow multiple coins",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(20*BNB_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"supply during snapshot",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: true,
 | 
			
		||||
						supplyCoin:   sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"multiple snapshots",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"varied snapshots",
 | 
			
		||||
			args{
 | 
			
		||||
				user:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				coinDenoms:           []string{"ukava"},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
 | 
			
		||||
				interestRateModel:    normalModel,
 | 
			
		||||
				reserveFactor:        sdk.MustNewDecFromStr("0.05"),
 | 
			
		||||
				expectedInterestSnaphots: []ExpectedSupplyInterest{
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneMonthInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneDayInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneYearInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						elapsedTime:  oneWeekInSeconds,
 | 
			
		||||
						shouldSupply: false,
 | 
			
		||||
						supplyCoin:   sdk.Coin{},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
			tApp := app.NewTestApp()
 | 
			
		||||
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
 | 
			
		||||
 | 
			
		||||
			// Auth module genesis state
 | 
			
		||||
			authGS := app.NewAuthGenState(
 | 
			
		||||
				[]sdk.AccAddress{tc.args.user},
 | 
			
		||||
				[]sdk.Coins{tc.args.initialBorrowerCoins},
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			// Hard module genesis state
 | 
			
		||||
			hardGS := types.NewGenesisState(types.NewParams(
 | 
			
		||||
				true,
 | 
			
		||||
				types.DistributionSchedules{
 | 
			
		||||
					types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
				},
 | 
			
		||||
				types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
 | 
			
		||||
					types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					time.Hour*24,
 | 
			
		||||
				),
 | 
			
		||||
				},
 | 
			
		||||
				types.MoneyMarkets{
 | 
			
		||||
					types.NewMoneyMarket("ukava",
 | 
			
		||||
						types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
 | 
			
		||||
						"kava:usd",                // Market ID
 | 
			
		||||
						sdk.NewInt(KAVA_CF),       // Conversion Factor
 | 
			
		||||
						sdk.NewInt(USDX_CF*1000),  // Auction Size
 | 
			
		||||
						tc.args.interestRateModel, // Interest Rate Model
 | 
			
		||||
						tc.args.reserveFactor,     // Reserve Factor
 | 
			
		||||
						sdk.ZeroDec()),            // Keeper Reward Percentage
 | 
			
		||||
					types.NewMoneyMarket("bnb",
 | 
			
		||||
						types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
 | 
			
		||||
						"bnb:usd",                 // Market ID
 | 
			
		||||
						sdk.NewInt(BNB_CF),        // Conversion Factor
 | 
			
		||||
						sdk.NewInt(USDX_CF*1000),  // Auction Size
 | 
			
		||||
						tc.args.interestRateModel, // Interest Rate Model
 | 
			
		||||
						tc.args.reserveFactor,     // Reserve Factor
 | 
			
		||||
						sdk.ZeroDec()),            // Keeper Reward Percentage
 | 
			
		||||
				},
 | 
			
		||||
				0, // LTV counter
 | 
			
		||||
			), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
 | 
			
		||||
 | 
			
		||||
			// Pricefeed module genesis state
 | 
			
		||||
			pricefeedGS := pricefeed.GenesisState{
 | 
			
		||||
				Params: pricefeed.Params{
 | 
			
		||||
					Markets: []pricefeed.Market{
 | 
			
		||||
						{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
 | 
			
		||||
						{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				PostedPrices: []pricefeed.PostedPrice{
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "kava:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("2.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "bnb:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("20.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Initialize test application
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS,
 | 
			
		||||
				app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
 | 
			
		||||
				app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
 | 
			
		||||
 | 
			
		||||
			// Mint coins to Hard module account
 | 
			
		||||
			supplyKeeper := tApp.GetSupplyKeeper()
 | 
			
		||||
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
 | 
			
		||||
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
			suite.keeper = keeper
 | 
			
		||||
			suite.keeper.SetSuppliedCoins(ctx, tc.args.initialModuleCoins)
 | 
			
		||||
 | 
			
		||||
			var err error
 | 
			
		||||
 | 
			
		||||
			// Run begin blocker
 | 
			
		||||
			hard.BeginBlocker(suite.ctx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
			// // Deposit coins
 | 
			
		||||
			err = suite.keeper.Deposit(suite.ctx, tc.args.user, tc.args.depositCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Borrow coins
 | 
			
		||||
			err = suite.keeper.Borrow(suite.ctx, tc.args.user, tc.args.borrowCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Check interest levels for each snapshot
 | 
			
		||||
			prevCtx := suite.ctx
 | 
			
		||||
			for _, snapshot := range tc.args.expectedInterestSnaphots {
 | 
			
		||||
				for _, coinDenom := range tc.args.coinDenoms {
 | 
			
		||||
					// ---------------------------- Calculate expected supply interest ----------------------------
 | 
			
		||||
					// 1. Get cash, borrows, reserves, and borrow index
 | 
			
		||||
					cashPrior := suite.getModuleAccountAtCtx(types.ModuleName, prevCtx).GetCoins().AmountOf(coinDenom)
 | 
			
		||||
 | 
			
		||||
					var borrowCoinPriorAmount sdk.Int
 | 
			
		||||
					borrowCoinsPrior, borrowCoinsPriorFound := suite.keeper.GetBorrowedCoins(prevCtx)
 | 
			
		||||
					suite.Require().True(borrowCoinsPriorFound)
 | 
			
		||||
					borrowCoinPriorAmount = borrowCoinsPrior.AmountOf(coinDenom)
 | 
			
		||||
 | 
			
		||||
					var supplyCoinPriorAmount sdk.Int
 | 
			
		||||
					supplyCoinsPrior, supplyCoinsPriorFound := suite.keeper.GetSuppliedCoins(prevCtx)
 | 
			
		||||
					suite.Require().True(supplyCoinsPriorFound)
 | 
			
		||||
					supplyCoinPriorAmount = supplyCoinsPrior.AmountOf(coinDenom)
 | 
			
		||||
 | 
			
		||||
					reservesPrior, foundReservesPrior := suite.keeper.GetTotalReserves(prevCtx, coinDenom)
 | 
			
		||||
					if !foundReservesPrior {
 | 
			
		||||
						reservesPrior = sdk.NewCoin(coinDenom, sdk.ZeroInt())
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					borrowInterestFactorPrior, foundBorrowInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, coinDenom)
 | 
			
		||||
					suite.Require().True(foundBorrowInterestFactorPrior)
 | 
			
		||||
 | 
			
		||||
					supplyInterestFactorPrior, foundSupplyInterestFactorPrior := suite.keeper.GetSupplyInterestFactor(prevCtx, coinDenom)
 | 
			
		||||
					suite.Require().True(foundSupplyInterestFactorPrior)
 | 
			
		||||
 | 
			
		||||
					// 2. Calculate expected borrow interest owed
 | 
			
		||||
					borrowRateApy, err := hard.CalculateBorrowRate(tc.args.interestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
 | 
			
		||||
					suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
					// Convert from APY to SPY, expressed as (1 + borrow rate)
 | 
			
		||||
					borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
 | 
			
		||||
					suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
					newBorrowInterestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
 | 
			
		||||
					expectedBorrowInterest := (newBorrowInterestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
 | 
			
		||||
					expectedReserves := reservesPrior.Add(sdk.NewCoin(coinDenom, sdk.NewDecFromInt(expectedBorrowInterest).Mul(tc.args.reserveFactor).TruncateInt())).Sub(reservesPrior)
 | 
			
		||||
					expectedTotalReserves := expectedReserves.Add(reservesPrior)
 | 
			
		||||
 | 
			
		||||
					expectedBorrowInterestFactor := borrowInterestFactorPrior.Mul(newBorrowInterestFactor)
 | 
			
		||||
					expectedSupplyInterest := expectedBorrowInterest.Sub(expectedReserves.Amount)
 | 
			
		||||
 | 
			
		||||
					newSupplyInterestFactor := hard.CalculateSupplyInterestFactor(expectedSupplyInterest.ToDec(), sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
 | 
			
		||||
					expectedSupplyInterestFactor := supplyInterestFactorPrior.Mul(newSupplyInterestFactor)
 | 
			
		||||
					// -------------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
					// Set up snapshot chain context and run begin blocker
 | 
			
		||||
					runAtTime := time.Unix(prevCtx.BlockTime().Unix()+(snapshot.elapsedTime), 0)
 | 
			
		||||
					snapshotCtx := prevCtx.WithBlockTime(runAtTime)
 | 
			
		||||
					hard.BeginBlocker(snapshotCtx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
					borrowInterestFactor, _ := suite.keeper.GetBorrowInterestFactor(ctx, coinDenom)
 | 
			
		||||
					suite.Require().Equal(expectedBorrowInterestFactor, borrowInterestFactor)
 | 
			
		||||
					suite.Require().Equal(expectedBorrowInterest, expectedSupplyInterest.Add(expectedReserves.Amount))
 | 
			
		||||
 | 
			
		||||
					// Check that the total amount of borrowed coins has increased by expected borrow interest amount
 | 
			
		||||
					borrowCoinsPost, _ := suite.keeper.GetBorrowedCoins(snapshotCtx)
 | 
			
		||||
					borrowCoinPostAmount := borrowCoinsPost.AmountOf(coinDenom)
 | 
			
		||||
					suite.Require().Equal(borrowCoinPostAmount, borrowCoinPriorAmount.Add(expectedBorrowInterest))
 | 
			
		||||
 | 
			
		||||
					// Check that the total amount of supplied coins has increased by expected supply interest amount
 | 
			
		||||
					supplyCoinsPost, _ := suite.keeper.GetSuppliedCoins(prevCtx)
 | 
			
		||||
					supplyCoinPostAmount := supplyCoinsPost.AmountOf(coinDenom)
 | 
			
		||||
					suite.Require().Equal(supplyCoinPostAmount, supplyCoinPriorAmount.Add(expectedSupplyInterest))
 | 
			
		||||
 | 
			
		||||
					// Check current total reserves
 | 
			
		||||
					totalReserves, _ := suite.keeper.GetTotalReserves(snapshotCtx, coinDenom)
 | 
			
		||||
					suite.Require().Equal(expectedTotalReserves, totalReserves)
 | 
			
		||||
 | 
			
		||||
					// Check that the supply index has increased as expected
 | 
			
		||||
					currSupplyIndexPrior, _ := suite.keeper.GetSupplyInterestFactor(snapshotCtx, coinDenom)
 | 
			
		||||
					suite.Require().Equal(expectedSupplyInterestFactor, currSupplyIndexPrior)
 | 
			
		||||
 | 
			
		||||
					// // Check that the borrow index has increased as expected
 | 
			
		||||
					currBorrowIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, coinDenom)
 | 
			
		||||
					suite.Require().Equal(expectedBorrowInterestFactor, currBorrowIndexPrior)
 | 
			
		||||
 | 
			
		||||
					// After supplying again user's supplied balance should have owed supply interest applied
 | 
			
		||||
					if snapshot.shouldSupply {
 | 
			
		||||
						// Calculate percentage of supply interest profits owed to user
 | 
			
		||||
						userSupplyBefore, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
 | 
			
		||||
						userSupplyCoinAmount := userSupplyBefore.Amount.AmountOf(coinDenom)
 | 
			
		||||
						userPercentOfTotalSupplied := userSupplyCoinAmount.ToDec().Quo(supplyCoinPriorAmount.ToDec())
 | 
			
		||||
						userExpectedSupplyInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalSupplied.MulInt(expectedSupplyInterest).TruncateInt())
 | 
			
		||||
 | 
			
		||||
						// Calculate percentage of borrow interest profits owed to user
 | 
			
		||||
						userBorrowBefore, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
 | 
			
		||||
						userBorrowCoinAmount := userBorrowBefore.Amount.AmountOf(coinDenom)
 | 
			
		||||
						userPercentOfTotalBorrowed := userBorrowCoinAmount.ToDec().Quo(borrowCoinPriorAmount.ToDec())
 | 
			
		||||
						userExpectedBorrowInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalBorrowed.MulInt(expectedBorrowInterest).TruncateInt())
 | 
			
		||||
						expectedBorrowCoinsAfter := userBorrowBefore.Amount.Add(userExpectedBorrowInterestCoin)
 | 
			
		||||
 | 
			
		||||
						// Supplying syncs user's owed supply and borrow interest
 | 
			
		||||
						err = suite.keeper.Deposit(snapshotCtx, tc.args.user, sdk.NewCoins(snapshot.supplyCoin))
 | 
			
		||||
						suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
						// Fetch user's new borrow and supply balance post-interaction
 | 
			
		||||
						userSupplyAfter, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
 | 
			
		||||
						userBorrowAfter, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
 | 
			
		||||
 | 
			
		||||
						// Confirm that user's supply index for the denom has increased as expected
 | 
			
		||||
						var userSupplyAfterIndexFactor sdk.Dec
 | 
			
		||||
						for _, indexFactor := range userSupplyAfter.Index {
 | 
			
		||||
							if indexFactor.Denom == coinDenom {
 | 
			
		||||
								userSupplyAfterIndexFactor = indexFactor.Value
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						suite.Require().Equal(userSupplyAfterIndexFactor, currSupplyIndexPrior)
 | 
			
		||||
 | 
			
		||||
						// Check user's supplied amount increased by supply interest owed + the newly supplied coins
 | 
			
		||||
						expectedSupplyCoinsAfter := userSupplyBefore.Amount.Add(snapshot.supplyCoin).Add(userExpectedSupplyInterestCoin)
 | 
			
		||||
						suite.Require().Equal(expectedSupplyCoinsAfter, userSupplyAfter.Amount)
 | 
			
		||||
 | 
			
		||||
						// Confirm that user's borrow index for the denom has increased as expected
 | 
			
		||||
						var userBorrowAfterIndexFactor sdk.Dec
 | 
			
		||||
						for _, indexFactor := range userBorrowAfter.Index {
 | 
			
		||||
							if indexFactor.Denom == coinDenom {
 | 
			
		||||
								userBorrowAfterIndexFactor = indexFactor.Value
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						suite.Require().Equal(userBorrowAfterIndexFactor, currBorrowIndexPrior)
 | 
			
		||||
 | 
			
		||||
						// Check user's borrowed amount increased by borrow interest owed
 | 
			
		||||
						suite.Require().Equal(expectedBorrowCoinsAfter, userBorrowAfter.Amount)
 | 
			
		||||
					}
 | 
			
		||||
					prevCtx = snapshotCtx
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInterestTestSuite(t *testing.T) {
 | 
			
		||||
	suite.Run(t, new(InterestTestSuite))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -248,6 +248,29 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
 | 
			
		||||
	return borrowedCoins, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSuppliedCoins sets the total amount of coins currently supplied in the store
 | 
			
		||||
func (k Keeper) SetSuppliedCoins(ctx sdk.Context, suppliedCoins sdk.Coins) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
 | 
			
		||||
	if suppliedCoins.Empty() {
 | 
			
		||||
		store.Set([]byte{}, []byte{})
 | 
			
		||||
	} else {
 | 
			
		||||
		bz := k.cdc.MustMarshalBinaryBare(suppliedCoins)
 | 
			
		||||
		store.Set([]byte{}, bz)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSuppliedCoins returns an sdk.Coins object from the store representing all currently supplied coins
 | 
			
		||||
func (k Keeper) GetSuppliedCoins(ctx sdk.Context) (sdk.Coins, bool) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
 | 
			
		||||
	bz := store.Get([]byte{})
 | 
			
		||||
	if bz == nil {
 | 
			
		||||
		return sdk.Coins{}, false
 | 
			
		||||
	}
 | 
			
		||||
	var suppliedCoins sdk.Coins
 | 
			
		||||
	k.cdc.MustUnmarshalBinaryBare(bz, &suppliedCoins)
 | 
			
		||||
	return suppliedCoins, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMoneyMarket returns a money market from the store for a denom
 | 
			
		||||
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
 | 
			
		||||
@ -326,22 +349,41 @@ func (k Keeper) SetTotalReserves(ctx sdk.Context, denom string, coin sdk.Coin) {
 | 
			
		||||
	store.Set([]byte(denom), bz)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetInterestFactor returns the current interest factor for an individual market
 | 
			
		||||
func (k Keeper) GetInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
 | 
			
		||||
// GetBorrowInterestFactor returns the current borrow interest factor for an individual market
 | 
			
		||||
func (k Keeper) GetBorrowInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
 | 
			
		||||
	bz := store.Get([]byte(denom))
 | 
			
		||||
	if bz == nil {
 | 
			
		||||
		return sdk.ZeroDec(), false
 | 
			
		||||
	}
 | 
			
		||||
	var interestFactor sdk.Dec
 | 
			
		||||
	k.cdc.MustUnmarshalBinaryBare(bz, &interestFactor)
 | 
			
		||||
	return interestFactor, true
 | 
			
		||||
	var borrowInterestFactor sdk.Dec
 | 
			
		||||
	k.cdc.MustUnmarshalBinaryBare(bz, &borrowInterestFactor)
 | 
			
		||||
	return borrowInterestFactor, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetInterestFactor sets the current interest factor for an individual market
 | 
			
		||||
func (k Keeper) SetInterestFactor(ctx sdk.Context, denom string, borrowIndex sdk.Dec) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
 | 
			
		||||
	bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
 | 
			
		||||
// SetBorrowInterestFactor sets the current borrow interest factor for an individual market
 | 
			
		||||
func (k Keeper) SetBorrowInterestFactor(ctx sdk.Context, denom string, borrowInterestFactor sdk.Dec) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
 | 
			
		||||
	bz := k.cdc.MustMarshalBinaryBare(borrowInterestFactor)
 | 
			
		||||
	store.Set([]byte(denom), bz)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSupplyInterestFactor returns the current supply interest factor for an individual market
 | 
			
		||||
func (k Keeper) GetSupplyInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
 | 
			
		||||
	bz := store.Get([]byte(denom))
 | 
			
		||||
	if bz == nil {
 | 
			
		||||
		return sdk.ZeroDec(), false
 | 
			
		||||
	}
 | 
			
		||||
	var supplyInterestFactor sdk.Dec
 | 
			
		||||
	k.cdc.MustUnmarshalBinaryBare(bz, &supplyInterestFactor)
 | 
			
		||||
	return supplyInterestFactor, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSupplyInterestFactor sets the current supply interest factor for an individual market
 | 
			
		||||
func (k Keeper) SetSupplyInterestFactor(ctx sdk.Context, denom string, supplyInterestFactor sdk.Dec) {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
 | 
			
		||||
	bz := k.cdc.MustMarshalBinaryBare(supplyInterestFactor)
 | 
			
		||||
	store.Set([]byte(denom), bz)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,8 @@ func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
 | 
			
		||||
	dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
 | 
			
		||||
	dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
		types.SupplyInterestFactors{types.NewSupplyInterestFactor("", sdk.MustNewDecFromStr("0"))})
 | 
			
		||||
 | 
			
		||||
	_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"))
 | 
			
		||||
	suite.Require().False(f)
 | 
			
		||||
@ -96,7 +97,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestIterateDeposits() {
 | 
			
		||||
	for i := 0; i < 5; i++ {
 | 
			
		||||
		dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
 | 
			
		||||
		dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), types.SupplyInterestFactors{})
 | 
			
		||||
		suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
 | 
			
		||||
	}
 | 
			
		||||
	var deposits []types.Deposit
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k.SyncOutstandingInterest(ctx, borrower)
 | 
			
		||||
	k.SyncBorrowInterest(ctx, borrower)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, borrower)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,8 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sync interest so loan is up-to-date
 | 
			
		||||
	k.SyncOutstandingInterest(ctx, sender)
 | 
			
		||||
	// Sync borrow interest so loan is up-to-date
 | 
			
		||||
	k.SyncBorrowInterest(ctx, sender)
 | 
			
		||||
 | 
			
		||||
	// Validate requested repay
 | 
			
		||||
	err = k.ValidateRepay(ctx, sender, coins)
 | 
			
		||||
@ -30,7 +30,10 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
 | 
			
		||||
		return types.ErrBorrowNotFound
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	payment := k.CalculatePaymentAmount(borrow.Amount, coins)
 | 
			
		||||
	payment, err := k.CalculatePaymentAmount(borrow.Amount, coins)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sends coins from user to Hard module account
 | 
			
		||||
	err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment)
 | 
			
		||||
@ -73,8 +76,13 @@ func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculatePaymentAmount prevents overpayment when repaying borrowed coins
 | 
			
		||||
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Coins {
 | 
			
		||||
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) (sdk.Coins, error) {
 | 
			
		||||
	repayment := sdk.Coins{}
 | 
			
		||||
 | 
			
		||||
	if !payment.DenomsSubsetOf(owed) {
 | 
			
		||||
		return repayment, types.ErrInvalidRepaymentDenom
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, coin := range payment {
 | 
			
		||||
		if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
 | 
			
		||||
			repayment = append(repayment, sdk.NewCoin(coin.Denom, owed.AmountOf(coin.Denom)))
 | 
			
		||||
@ -82,5 +90,5 @@ func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Co
 | 
			
		||||
			repayment = append(repayment, coin)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return repayment
 | 
			
		||||
	return repayment, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,21 @@ func (suite *KeeperTestSuite) TestRepay() {
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"invalid: attempt to repay non-supplied coin",
 | 
			
		||||
			args{
 | 
			
		||||
				borrower:             sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))),
 | 
			
		||||
				depositCoins:         []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))},
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
 | 
			
		||||
				repayCoins:           sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*KAVA_CF))),
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "account can only repay up to 0bnb",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"invalid: insufficent balance for repay",
 | 
			
		||||
			args{
 | 
			
		||||
@ -98,7 +113,7 @@ func (suite *KeeperTestSuite) TestRepay() {
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "account can only repay up to",
 | 
			
		||||
				contains:   "account can only repay up to 50000000ukava",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@ -203,7 +218,8 @@ func (suite *KeeperTestSuite) TestRepay() {
 | 
			
		||||
			if tc.errArgs.expectPass {
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
				// If we overpaid expect an adjustment
 | 
			
		||||
				repaymentCoins := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
 | 
			
		||||
				repaymentCoins, err := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
				// Check borrower balance
 | 
			
		||||
				expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins)
 | 
			
		||||
 | 
			
		||||
@ -84,7 +84,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
 | 
			
		||||
			supplyKeeper := tApp.GetSupplyKeeper()
 | 
			
		||||
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount)
 | 
			
		||||
			deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount, types.SupplyInterestFactors{})
 | 
			
		||||
			keeper.SetDeposit(ctx, deposit)
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								x/hard/keeper/withdraw.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								x/hard/keeper/withdraw.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
package keeper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/kava-labs/kava/x/hard/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Withdraw returns some or all of a deposit back to original depositor
 | 
			
		||||
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
 | 
			
		||||
	// Get current stored LTV based on stored borrows/deposits
 | 
			
		||||
	prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k.SyncBorrowInterest(ctx, depositor)
 | 
			
		||||
	k.SyncSupplyInterest(ctx, depositor)
 | 
			
		||||
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, depositor)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	amount, err := k.CalculateWithdrawAmount(deposit.Amount, coins)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	proposedDeposit := types.NewDeposit(deposit.Depositor, deposit.Amount.Sub(amount), types.SupplyInterestFactors{})
 | 
			
		||||
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, depositor)
 | 
			
		||||
	if !found {
 | 
			
		||||
		borrow = types.Borrow{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !valid {
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, amount)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if deposit.Amount.IsEqual(amount) {
 | 
			
		||||
		ctx.EventManager().EmitEvent(
 | 
			
		||||
			sdk.NewEvent(
 | 
			
		||||
				types.EventTypeDeleteHardDeposit,
 | 
			
		||||
				sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
 | 
			
		||||
			),
 | 
			
		||||
		)
 | 
			
		||||
		k.DeleteDeposit(ctx, deposit)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	deposit.Amount = deposit.Amount.Sub(amount)
 | 
			
		||||
	k.SetDeposit(ctx, deposit)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
 | 
			
		||||
 | 
			
		||||
	// Update total supplied amount
 | 
			
		||||
	k.DecrementBorrowedCoins(ctx, amount)
 | 
			
		||||
 | 
			
		||||
	ctx.EventManager().EmitEvent(
 | 
			
		||||
		sdk.NewEvent(
 | 
			
		||||
			types.EventTypeHardWithdrawal,
 | 
			
		||||
			sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
 | 
			
		||||
			sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateWithdrawAmount enables full withdraw of deposited coins by adjusting withdraw amount
 | 
			
		||||
// to equal total deposit amount if the requested withdraw amount > current deposit amount
 | 
			
		||||
func (k Keeper) CalculateWithdrawAmount(available sdk.Coins, request sdk.Coins) (sdk.Coins, error) {
 | 
			
		||||
	result := sdk.Coins{}
 | 
			
		||||
 | 
			
		||||
	if !request.DenomsSubsetOf(available) {
 | 
			
		||||
		return result, types.ErrInvalidWithdrawDenom
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, coin := range request {
 | 
			
		||||
		if coin.Amount.GT(available.AmountOf(coin.Denom)) {
 | 
			
		||||
			result = append(result, sdk.NewCoin(coin.Denom, available.AmountOf(coin.Denom)))
 | 
			
		||||
		} else {
 | 
			
		||||
			result = append(result, coin)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										390
									
								
								x/hard/keeper/withdraw_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										390
									
								
								x/hard/keeper/withdraw_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,390 @@
 | 
			
		||||
package keeper_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	abci "github.com/tendermint/tendermint/abci/types"
 | 
			
		||||
	"github.com/tendermint/tendermint/crypto"
 | 
			
		||||
	tmtime "github.com/tendermint/tendermint/types/time"
 | 
			
		||||
 | 
			
		||||
	"github.com/kava-labs/kava/app"
 | 
			
		||||
	"github.com/kava-labs/kava/x/hard"
 | 
			
		||||
	"github.com/kava-labs/kava/x/hard/types"
 | 
			
		||||
	"github.com/kava-labs/kava/x/pricefeed"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		depositor                 sdk.AccAddress
 | 
			
		||||
		initialModAccountBalance  sdk.Coins
 | 
			
		||||
		depositAmount             sdk.Coins
 | 
			
		||||
		withdrawAmount            sdk.Coins
 | 
			
		||||
		createDeposit             bool
 | 
			
		||||
		expectedAccountBalance    sdk.Coins
 | 
			
		||||
		expectedModAccountBalance sdk.Coins
 | 
			
		||||
		depositExists             bool
 | 
			
		||||
		finalDepositAmount        sdk.Coins
 | 
			
		||||
	}
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
		expectPass bool
 | 
			
		||||
		contains   string
 | 
			
		||||
	}
 | 
			
		||||
	type withdrawTest struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		errArgs errArgs
 | 
			
		||||
	}
 | 
			
		||||
	testCases := []withdrawTest{
 | 
			
		||||
		{
 | 
			
		||||
			"valid: partial withdraw",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialModAccountBalance:  sdk.Coins(nil),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
 | 
			
		||||
				expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
				depositExists:             true,
 | 
			
		||||
				finalDepositAmount:        sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"valid: full withdraw",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialModAccountBalance:  sdk.Coins(nil),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
 | 
			
		||||
				expectedModAccountBalance: sdk.Coins(nil),
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"valid: withdraw exceeds deposit but is adjusted to match max deposit",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialModAccountBalance:  sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
 | 
			
		||||
				expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: true,
 | 
			
		||||
				contains:   "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"invalid: withdraw non-supplied coin type",
 | 
			
		||||
			args{
 | 
			
		||||
				depositor:                 sdk.AccAddress(crypto.AddressHash([]byte("test"))),
 | 
			
		||||
				initialModAccountBalance:  sdk.Coins(nil),
 | 
			
		||||
				depositAmount:             sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
 | 
			
		||||
				withdrawAmount:            sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
 | 
			
		||||
				createDeposit:             true,
 | 
			
		||||
				expectedAccountBalance:    sdk.Coins{},
 | 
			
		||||
				expectedModAccountBalance: sdk.Coins{},
 | 
			
		||||
				depositExists:             false,
 | 
			
		||||
				finalDepositAmount:        sdk.Coins{},
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "no coins of this type deposited",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// create new app with one funded account
 | 
			
		||||
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
			tApp := app.NewTestApp()
 | 
			
		||||
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
 | 
			
		||||
			authGS := app.NewAuthGenState(
 | 
			
		||||
				[]sdk.AccAddress{tc.args.depositor},
 | 
			
		||||
				[]sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))},
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			loanToValue := sdk.MustNewDecFromStr("0.6")
 | 
			
		||||
			hardGS := types.NewGenesisState(types.NewParams(
 | 
			
		||||
				true,
 | 
			
		||||
				types.DistributionSchedules{
 | 
			
		||||
					types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
				},
 | 
			
		||||
				types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
 | 
			
		||||
					types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					time.Hour*24,
 | 
			
		||||
				),
 | 
			
		||||
				},
 | 
			
		||||
				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("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(100000000), 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()),
 | 
			
		||||
				},
 | 
			
		||||
				0, // LTV counter
 | 
			
		||||
			), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
 | 
			
		||||
 | 
			
		||||
			// 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: "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(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "kava:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("2.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "bnb:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("10.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS,
 | 
			
		||||
				app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
 | 
			
		||||
				app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
			suite.keeper = keeper
 | 
			
		||||
 | 
			
		||||
			// Mint coins to Hard module account
 | 
			
		||||
			supplyKeeper := tApp.GetSupplyKeeper()
 | 
			
		||||
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModAccountBalance)
 | 
			
		||||
 | 
			
		||||
			if tc.args.createDeposit {
 | 
			
		||||
				err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
 | 
			
		||||
 | 
			
		||||
			if tc.errArgs.expectPass {
 | 
			
		||||
				suite.Require().NoError(err)
 | 
			
		||||
				acc := suite.getAccount(tc.args.depositor)
 | 
			
		||||
				suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
 | 
			
		||||
				mAcc := suite.getModuleAccount(types.ModuleAccountName)
 | 
			
		||||
				suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
 | 
			
		||||
				testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
 | 
			
		||||
				if tc.args.depositExists {
 | 
			
		||||
					suite.Require().True(f)
 | 
			
		||||
					suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
 | 
			
		||||
				} else {
 | 
			
		||||
					suite.Require().False(f)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				suite.Require().Error(err)
 | 
			
		||||
				suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *KeeperTestSuite) TestLtvWithdraw() {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		borrower             sdk.AccAddress
 | 
			
		||||
		initialModuleCoins   sdk.Coins
 | 
			
		||||
		initialBorrowerCoins sdk.Coins
 | 
			
		||||
		depositCoins         sdk.Coins
 | 
			
		||||
		borrowCoins          sdk.Coins
 | 
			
		||||
		futureTime           int64
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type errArgs struct {
 | 
			
		||||
		expectPass bool
 | 
			
		||||
		contains   string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type liqTest struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		errArgs errArgs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set up test constants
 | 
			
		||||
	model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
 | 
			
		||||
	reserveFactor := sdk.MustNewDecFromStr("0.05")
 | 
			
		||||
	oneMonthInSeconds := int64(2592000)
 | 
			
		||||
	borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
 | 
			
		||||
 | 
			
		||||
	testCases := []liqTest{
 | 
			
		||||
		{
 | 
			
		||||
			"invalid: withdraw is outside loan-to-value range",
 | 
			
		||||
			args{
 | 
			
		||||
				borrower:             borrower,
 | 
			
		||||
				initialModuleCoins:   sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))),
 | 
			
		||||
				depositCoins:         sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // 10 * 2 = $20
 | 
			
		||||
				borrowCoins:          sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))),  // 8 * 2 = $16
 | 
			
		||||
				futureTime:           oneMonthInSeconds,
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "proposed withdraw outside loan-to-value range",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		suite.Run(tc.name, func() {
 | 
			
		||||
			// Initialize test app and set context
 | 
			
		||||
			tApp := app.NewTestApp()
 | 
			
		||||
			ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
 | 
			
		||||
 | 
			
		||||
			// Auth module genesis state
 | 
			
		||||
			authGS := app.NewAuthGenState(
 | 
			
		||||
				[]sdk.AccAddress{tc.args.borrower},
 | 
			
		||||
				[]sdk.Coins{tc.args.initialBorrowerCoins},
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			// Harvest module genesis state
 | 
			
		||||
			harvestGS := types.NewGenesisState(types.NewParams(
 | 
			
		||||
				true,
 | 
			
		||||
				types.DistributionSchedules{
 | 
			
		||||
					types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
				},
 | 
			
		||||
				types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
 | 
			
		||||
					types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
 | 
			
		||||
					time.Hour*24,
 | 
			
		||||
				),
 | 
			
		||||
				},
 | 
			
		||||
				types.MoneyMarkets{
 | 
			
		||||
					types.NewMoneyMarket("ukava",
 | 
			
		||||
						types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
 | 
			
		||||
						"kava:usd",                     // Market ID
 | 
			
		||||
						sdk.NewInt(KAVA_CF),            // Conversion Factor
 | 
			
		||||
						sdk.NewInt(100000000*KAVA_CF),  // Auction Size
 | 
			
		||||
						model,                          // Interest Rate Model
 | 
			
		||||
						reserveFactor,                  // Reserve Factor
 | 
			
		||||
						sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
 | 
			
		||||
					types.NewMoneyMarket("usdx",
 | 
			
		||||
						types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
 | 
			
		||||
						"usdx:usd",                     // Market ID
 | 
			
		||||
						sdk.NewInt(KAVA_CF),            // Conversion Factor
 | 
			
		||||
						sdk.NewInt(100000000*KAVA_CF),  // Auction Size
 | 
			
		||||
						model,                          // Interest Rate Model
 | 
			
		||||
						reserveFactor,                  // Reserve Factor
 | 
			
		||||
						sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
 | 
			
		||||
				},
 | 
			
		||||
				0, // LTV counter
 | 
			
		||||
			), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
 | 
			
		||||
 | 
			
		||||
			// 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},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				PostedPrices: []pricefeed.PostedPrice{
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "usdx:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("1.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						MarketID:      "kava:usd",
 | 
			
		||||
						OracleAddress: sdk.AccAddress{},
 | 
			
		||||
						Price:         sdk.MustNewDecFromStr("2.00"),
 | 
			
		||||
						Expiry:        time.Now().Add(100 * time.Hour),
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Initialize test application
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS,
 | 
			
		||||
				app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
 | 
			
		||||
				app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
 | 
			
		||||
 | 
			
		||||
			// Mint coins to Harvest module account
 | 
			
		||||
			supplyKeeper := tApp.GetSupplyKeeper()
 | 
			
		||||
			supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
 | 
			
		||||
 | 
			
		||||
			auctionKeeper := tApp.GetAuctionKeeper()
 | 
			
		||||
 | 
			
		||||
			keeper := tApp.GetHardKeeper()
 | 
			
		||||
			suite.app = tApp
 | 
			
		||||
			suite.ctx = ctx
 | 
			
		||||
			suite.keeper = keeper
 | 
			
		||||
			suite.auctionKeeper = auctionKeeper
 | 
			
		||||
 | 
			
		||||
			var err error
 | 
			
		||||
 | 
			
		||||
			// Run begin blocker to set up state
 | 
			
		||||
			hard.BeginBlocker(suite.ctx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
			// Borrower deposits coins
 | 
			
		||||
			err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Borrower borrows coins
 | 
			
		||||
			err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Attempting to withdraw fails
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Set up future chain context and run begin blocker, increasing user's owed borrow balance
 | 
			
		||||
			runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
 | 
			
		||||
			liqCtx := suite.ctx.WithBlockTime(runAtTime)
 | 
			
		||||
			hard.BeginBlocker(liqCtx, suite.keeper)
 | 
			
		||||
 | 
			
		||||
			// Attempted withdraw of 1 coin still fails
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Repay the initial principal
 | 
			
		||||
			err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
 | 
			
		||||
			suite.Require().Error(err)
 | 
			
		||||
			suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
 | 
			
		||||
 | 
			
		||||
			// Withdrawing half the coins should succeed
 | 
			
		||||
			withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(2))))
 | 
			
		||||
			err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -27,7 +27,9 @@ func TestDecodeDistributionStore(t *testing.T) {
 | 
			
		||||
	cdc := makeTestCodec()
 | 
			
		||||
 | 
			
		||||
	prevBlockTime := time.Now().UTC()
 | 
			
		||||
	deposit := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))))
 | 
			
		||||
	deposit := types.NewDeposit(sdk.AccAddress("test"),
 | 
			
		||||
		sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))),
 | 
			
		||||
		types.SupplyInterestFactors{types.NewSupplyInterestFactor("bnb", sdk.OneDec())})
 | 
			
		||||
	claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
 | 
			
		||||
 | 
			
		||||
	kvPairs := kv.Pairs{
 | 
			
		||||
 | 
			
		||||
@ -4,32 +4,15 @@ import (
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BorrowIndexItem defines an individual borrow index
 | 
			
		||||
type BorrowIndexItem struct {
 | 
			
		||||
	Denom string  `json:"denom" yaml:"denom"`
 | 
			
		||||
	Value sdk.Dec `json:"value" yaml:"value"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBorrowIndexItem returns a new BorrowIndexItem instance
 | 
			
		||||
func NewBorrowIndexItem(denom string, value sdk.Dec) BorrowIndexItem {
 | 
			
		||||
	return BorrowIndexItem{
 | 
			
		||||
		Denom: denom,
 | 
			
		||||
		Value: value,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BorrowIndexes is a slice of BorrowIndexItem, because Amino won't marshal maps
 | 
			
		||||
type BorrowIndexes []BorrowIndexItem
 | 
			
		||||
 | 
			
		||||
// Borrow defines an amount of coins borrowed from a hard module account
 | 
			
		||||
type Borrow struct {
 | 
			
		||||
	Borrower sdk.AccAddress        `json:"borrower" yaml:"borrower"`
 | 
			
		||||
	Amount   sdk.Coins             `json:"amount" yaml:"amount"`
 | 
			
		||||
	Index    InterestFactors `json:"index" yaml:"index"`
 | 
			
		||||
	Index    BorrowInterestFactors `json:"index" yaml:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBorrow returns a new Borrow instance
 | 
			
		||||
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors) Borrow {
 | 
			
		||||
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index BorrowInterestFactors) Borrow {
 | 
			
		||||
	return Borrow{
 | 
			
		||||
		Borrower: borrower,
 | 
			
		||||
		Amount:   amount,
 | 
			
		||||
@ -37,19 +20,19 @@ func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InterestFactor defines an individual interest factor
 | 
			
		||||
type InterestFactor struct {
 | 
			
		||||
// BorrowInterestFactor defines an individual borrow interest factor
 | 
			
		||||
type BorrowInterestFactor struct {
 | 
			
		||||
	Denom string  `json:"denom" yaml:"denom"`
 | 
			
		||||
	Value sdk.Dec `json:"value" yaml:"value"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewInterestFactor returns a new InterestFactor instance
 | 
			
		||||
func NewInterestFactor(denom string, value sdk.Dec) InterestFactor {
 | 
			
		||||
	return InterestFactor{
 | 
			
		||||
// NewBorrowInterestFactor returns a new BorrowInterestFactor instance
 | 
			
		||||
func NewBorrowInterestFactor(denom string, value sdk.Dec) BorrowInterestFactor {
 | 
			
		||||
	return BorrowInterestFactor{
 | 
			
		||||
		Denom: denom,
 | 
			
		||||
		Value: value,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InterestFactors is a slice of InterestFactor, because Amino won't marshal maps
 | 
			
		||||
type InterestFactors []InterestFactor
 | 
			
		||||
// BorrowInterestFactors is a slice of BorrowInterestFactor, because Amino won't marshal maps
 | 
			
		||||
type BorrowInterestFactors []BorrowInterestFactor
 | 
			
		||||
 | 
			
		||||
@ -8,12 +8,31 @@ import (
 | 
			
		||||
type Deposit struct {
 | 
			
		||||
	Depositor sdk.AccAddress        `json:"depositor" yaml:"depositor"`
 | 
			
		||||
	Amount    sdk.Coins             `json:"amount" yaml:"amount"`
 | 
			
		||||
	Index     SupplyInterestFactors `json:"index" yaml:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewDeposit returns a new deposit
 | 
			
		||||
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins) Deposit {
 | 
			
		||||
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins, indexes SupplyInterestFactors) Deposit {
 | 
			
		||||
	return Deposit{
 | 
			
		||||
		Depositor: depositor,
 | 
			
		||||
		Amount:    amount,
 | 
			
		||||
		Index:     indexes,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SupplyInterestFactor defines an individual borrow interest factor
 | 
			
		||||
type SupplyInterestFactor struct {
 | 
			
		||||
	Denom string  `json:"denom" yaml:"denom"`
 | 
			
		||||
	Value sdk.Dec `json:"value" yaml:"value"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSupplyInterestFactor returns a new SupplyInterestFactor instance
 | 
			
		||||
func NewSupplyInterestFactor(denom string, value sdk.Dec) SupplyInterestFactor {
 | 
			
		||||
	return SupplyInterestFactor{
 | 
			
		||||
		Denom: denom,
 | 
			
		||||
		Value: value,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SupplyInterestFactors is a slice of SupplyInterestFactor, because Amino won't marshal maps
 | 
			
		||||
type SupplyInterestFactors []SupplyInterestFactor
 | 
			
		||||
 | 
			
		||||
@ -67,4 +67,12 @@ var (
 | 
			
		||||
	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")
 | 
			
		||||
	// ErrSuppliedCoinsNotFound error for when the total amount of supplied coins cannot be found
 | 
			
		||||
	ErrSuppliedCoinsNotFound = sdkerrors.Register(ModuleName, 32, "no supplied coins found")
 | 
			
		||||
	// ErrNegativeSuppliedCoins error for when substracting coins from the total supplied balance results in a negative amount
 | 
			
		||||
	ErrNegativeSuppliedCoins = sdkerrors.Register(ModuleName, 33, "subtraction results in negative supplied amount")
 | 
			
		||||
	// ErrInvalidWithdrawDenom error for when user attempts to withdraw a non-supplied coin type
 | 
			
		||||
	ErrInvalidWithdrawDenom = sdkerrors.Register(ModuleName, 34, "no coins of this type deposited")
 | 
			
		||||
	// ErrInvalidRepaymentDenom error for when user attempts to repay a non-borrowed coin type
 | 
			
		||||
	ErrInvalidRepaymentDenom = sdkerrors.Register(ModuleName, 35, "no coins of this type borrowed")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -40,11 +40,13 @@ var (
 | 
			
		||||
	ClaimsKeyPrefix                   = []byte{0x04}
 | 
			
		||||
	BorrowsKeyPrefix                  = []byte{0x05}
 | 
			
		||||
	BorrowedCoinsPrefix               = []byte{0x06}
 | 
			
		||||
	MoneyMarketsPrefix                = []byte{0x07}
 | 
			
		||||
	PreviousAccrualTimePrefix         = []byte{0x08} // denom -> time
 | 
			
		||||
	TotalReservesPrefix               = []byte{0x09} // denom -> sdk.Coin
 | 
			
		||||
	InterestFactorPrefix              = []byte{0x10} // denom -> sdk.Dec
 | 
			
		||||
	LtvIndexPrefix                    = []byte{0x11}
 | 
			
		||||
	SuppliedCoinsPrefix               = []byte{0x07}
 | 
			
		||||
	MoneyMarketsPrefix                = []byte{0x08}
 | 
			
		||||
	PreviousAccrualTimePrefix         = []byte{0x09} // denom -> time
 | 
			
		||||
	TotalReservesPrefix               = []byte{0x10} // denom -> sdk.Coin
 | 
			
		||||
	BorrowInterestFactorPrefix        = []byte{0x11} // denom -> sdk.Dec
 | 
			
		||||
	SupplyInterestFactorPrefix        = []byte{0x12} // denom -> sdk.Dec
 | 
			
		||||
	LtvIndexPrefix                    = []byte{0x13}
 | 
			
		||||
	sep                               = []byte(":")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user