mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 16:25:21 +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
|
NewQuerier = keeper.NewQuerier
|
||||||
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
|
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
|
||||||
CalculateBorrowRate = keeper.CalculateBorrowRate
|
CalculateBorrowRate = keeper.CalculateBorrowRate
|
||||||
CalculateInterestFactor = keeper.CalculateInterestFactor
|
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
|
||||||
|
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
|
||||||
APYToSPY = keeper.APYToSPY
|
APYToSPY = keeper.APYToSPY
|
||||||
ClaimKey = types.ClaimKey
|
ClaimKey = types.ClaimKey
|
||||||
DefaultGenesisState = types.DefaultGenesisState
|
DefaultGenesisState = types.DefaultGenesisState
|
||||||
|
@ -13,11 +13,11 @@ import (
|
|||||||
func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error {
|
func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error {
|
||||||
// Set any new denoms' global borrow index to 1.0
|
// Set any new denoms' global borrow index to 1.0
|
||||||
for _, coin := range coins {
|
for _, coin := range coins {
|
||||||
_, foundInterestFactor := k.GetInterestFactor(ctx, coin.Denom)
|
_, foundInterestFactor := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||||
if !foundInterestFactor {
|
if !foundInterestFactor {
|
||||||
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
|
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
|
||||||
if foundMm {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user has an existing borrow, sync its outstanding interest
|
// Sync any outstanding interest
|
||||||
_, found := k.GetBorrow(ctx, borrower)
|
k.SyncBorrowInterest(ctx, borrower)
|
||||||
if found {
|
|
||||||
k.SyncOutstandingInterest(ctx, borrower)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate borrow amount within user and protocol limits
|
// Validate borrow amount within user and protocol limits
|
||||||
err = k.ValidateBorrow(ctx, borrower, coins)
|
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
|
// 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 foundBorrow {
|
||||||
if !found {
|
// If the coin denom to be borrowed is not in the user's existing borrow, we add it borrow index
|
||||||
var interestFactors types.InterestFactors
|
|
||||||
for _, coin := range coins {
|
for _, coin := range coins {
|
||||||
interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
|
if !sdk.NewCoins(coin).DenomsSubsetOf(currBorrow.Amount) {
|
||||||
interestFactor := types.NewInterestFactor(coin.Denom, interestFactorValue)
|
borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||||
interestFactors = append(interestFactors, interestFactor)
|
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
|
// Calculate new borrow amount
|
||||||
borrow, _ := k.GetBorrow(ctx, borrower)
|
var amount sdk.Coins
|
||||||
borrow.Amount = borrow.Amount.Add(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.SetBorrow(ctx, borrow)
|
||||||
|
|
||||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower)
|
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
|
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
|
// ValidateBorrow validates a borrow request against borrower and protocol requirements
|
||||||
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
|
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
|
||||||
if amount.IsZero() {
|
if amount.IsZero() {
|
||||||
@ -240,7 +214,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
|
|||||||
return nil
|
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) {
|
func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
|
||||||
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
||||||
if !found {
|
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 {
|
func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
|
||||||
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
||||||
if !found {
|
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 {
|
func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.Coins {
|
||||||
borrowBalance := sdk.Coins{}
|
borrowBalance := sdk.Coins{}
|
||||||
borrow, found := k.GetBorrow(ctx, borrower)
|
borrow, found := k.GetBorrow(ctx, borrower)
|
||||||
if found {
|
if !found {
|
||||||
|
return borrowBalance
|
||||||
|
}
|
||||||
|
|
||||||
totalNewInterest := sdk.Coins{}
|
totalNewInterest := sdk.Coins{}
|
||||||
for _, coin := range borrow.Amount {
|
for _, coin := range borrow.Amount {
|
||||||
interestFactorValue, foundInterestFactorValue := k.GetInterestFactor(ctx, coin.Denom)
|
interestFactorValue, foundInterestFactorValue := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||||
if foundInterestFactorValue {
|
if foundInterestFactorValue {
|
||||||
// Locate the interest factor by coin denom in the user's list of interest factors
|
// Locate the interest factor by coin denom in the user's list of interest factors
|
||||||
foundAtIndex := -1
|
foundAtIndex := -1
|
||||||
@ -294,7 +271,6 @@ func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
borrowBalance = borrow.Amount.Add(totalNewInterest...)
|
|
||||||
}
|
return borrow.Amount.Add(totalNewInterest...)
|
||||||
return borrowBalance
|
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,26 @@ import (
|
|||||||
|
|
||||||
// Deposit deposit
|
// Deposit deposit
|
||||||
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
|
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
|
// Get current stored LTV based on stored borrows/deposits
|
||||||
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
|
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
k.SyncOutstandingInterest(ctx, depositor)
|
// Sync any outstanding interest
|
||||||
|
k.SyncBorrowInterest(ctx, depositor)
|
||||||
|
k.SyncSupplyInterest(ctx, depositor)
|
||||||
|
|
||||||
err = k.ValidateDeposit(ctx, coins)
|
err = k.ValidateDeposit(ctx, coins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -44,17 +57,47 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
deposit, found := k.GetDeposit(ctx, depositor)
|
// The first time a user deposits a denom we add it the user's supply interest factor index
|
||||||
if !found {
|
var supplyInterestFactors types.SupplyInterestFactors
|
||||||
deposit = types.NewDeposit(depositor, coins)
|
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 {
|
} 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.SetDeposit(ctx, deposit)
|
||||||
|
|
||||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
|
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(
|
ctx.EventManager().EmitEvent(
|
||||||
sdk.NewEvent(
|
sdk.NewEvent(
|
||||||
types.EventTypeHardDeposit,
|
types.EventTypeHardDeposit,
|
||||||
@ -84,76 +127,37 @@ func (k Keeper) ValidateDeposit(ctx sdk.Context, coins sdk.Coins) error {
|
|||||||
return nil
|
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
|
// 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) {
|
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositDenom string) (total sdk.Int) {
|
||||||
var macc supplyExported.ModuleAccountI
|
var macc supplyExported.ModuleAccountI
|
||||||
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||||
return macc.GetCoins().AmountOf(depositDenom)
|
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
|
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)
|
cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
|
||||||
|
|
||||||
// Get prior borrows
|
borrowedPrior := sdk.NewCoin(denom, sdk.ZeroInt())
|
||||||
borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
|
borrowedCoinsPrior, foundBorrowedCoinsPrior := k.GetBorrowedCoins(ctx)
|
||||||
borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
|
if foundBorrowedCoinsPrior {
|
||||||
if foundBorrowCoinsPrior {
|
borrowedPrior = sdk.NewCoin(denom, borrowedCoinsPrior.AmountOf(denom))
|
||||||
borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
|
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
|
||||||
@ -86,11 +85,18 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
|||||||
reservesPrior = newReservesPrior
|
reservesPrior = newReservesPrior
|
||||||
}
|
}
|
||||||
|
|
||||||
interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, denom)
|
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := k.GetBorrowInterestFactor(ctx, denom)
|
||||||
if !foundInterestFactorPrior {
|
if !foundBorrowInterestFactorPrior {
|
||||||
newInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
|
newBorrowInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
|
||||||
k.SetInterestFactor(ctx, denom, newInterestFactorPrior)
|
k.SetBorrowInterestFactor(ctx, denom, newBorrowInterestFactorPrior)
|
||||||
interestFactorPrior = newInterestFactorPrior
|
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
|
// 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)
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -111,16 +117,27 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
|
// Calculate borrow interest factor and update
|
||||||
interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(borrowsPrior.Amount)).TruncateInt()).Sub(borrowsPrior.Amount)
|
borrowInterestFactor := CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
|
||||||
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestAccumulated))
|
interestBorrowAccumulated := (borrowInterestFactor.Mul(sdk.NewDecFromInt(borrowedPrior.Amount)).TruncateInt()).Sub(borrowedPrior.Amount)
|
||||||
totalReservesNew := reservesPrior.Add(sdk.NewCoin(denom, sdk.NewDecFromInt(interestAccumulated).Mul(mm.ReserveFactor).TruncateInt()))
|
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestBorrowAccumulated))
|
||||||
interestFactorNew := interestFactorPrior.Mul(interestFactor)
|
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.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())
|
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +172,10 @@ func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
|
|||||||
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
|
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)
|
// 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
|
// 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))
|
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
|
||||||
scalingFactorInt := sdk.NewInt(int64(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)
|
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.
|
// 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.
|
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
|
||||||
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
|
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 {
|
type args struct {
|
||||||
perSecondInterestRate sdk.Dec
|
perSecondInterestRate sdk.Dec
|
||||||
timeElapsed sdk.Int
|
timeElapsed sdk.Int
|
||||||
@ -302,7 +302,65 @@ func (suite *InterestTestSuite) TestCalculateInterestFactor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
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)
|
suite.Require().Equal(tc.args.expectedValue, interestFactor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,7 +435,6 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
|
|||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
suite.Run(tc.name, func() {
|
suite.Run(tc.name, func() {
|
||||||
spy, err := hard.APYToSPY(tc.args.apy)
|
spy, err := hard.APYToSPY(tc.args.apy)
|
||||||
@ -391,13 +448,13 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpectedInterest struct {
|
type ExpectedBorrowInterest struct {
|
||||||
elapsedTime int64
|
elapsedTime int64
|
||||||
shouldBorrow bool
|
shouldBorrow bool
|
||||||
borrowCoin sdk.Coin
|
borrowCoin sdk.Coin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *KeeperTestSuite) TestInterest() {
|
func (suite *KeeperTestSuite) TestBorrowInterest() {
|
||||||
type args struct {
|
type args struct {
|
||||||
user sdk.AccAddress
|
user sdk.AccAddress
|
||||||
initialBorrowerCoins sdk.Coins
|
initialBorrowerCoins sdk.Coins
|
||||||
@ -406,7 +463,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins sdk.Coins
|
borrowCoins sdk.Coins
|
||||||
interestRateModel types.InterestRateModel
|
interestRateModel types.InterestRateModel
|
||||||
reserveFactor sdk.Dec
|
reserveFactor sdk.Dec
|
||||||
expectedInterestSnaphots []ExpectedInterest
|
expectedInterestSnaphots []ExpectedBorrowInterest
|
||||||
}
|
}
|
||||||
|
|
||||||
type errArgs struct {
|
type errArgs struct {
|
||||||
@ -438,7 +495,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneDayInSeconds,
|
elapsedTime: oneDayInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -461,7 +518,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneWeekInSeconds,
|
elapsedTime: oneWeekInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -484,7 +541,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneMonthInSeconds,
|
elapsedTime: oneMonthInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -507,7 +564,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneYearInSeconds,
|
elapsedTime: oneYearInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -530,7 +587,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0"),
|
reserveFactor: sdk.MustNewDecFromStr("0"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneYearInSeconds,
|
elapsedTime: oneYearInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -553,7 +610,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneYearInSeconds,
|
elapsedTime: oneYearInSeconds,
|
||||||
shouldBorrow: true,
|
shouldBorrow: true,
|
||||||
@ -576,7 +633,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneMonthInSeconds,
|
elapsedTime: oneMonthInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -604,7 +661,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||||
interestRateModel: normalModel,
|
interestRateModel: normalModel,
|
||||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||||
expectedInterestSnaphots: []ExpectedInterest{
|
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||||
{
|
{
|
||||||
elapsedTime: oneDayInSeconds,
|
elapsedTime: oneDayInSeconds,
|
||||||
shouldBorrow: false,
|
shouldBorrow: false,
|
||||||
@ -633,6 +690,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
suite.Run(tc.name, func() {
|
suite.Run(tc.name, func() {
|
||||||
// Initialize test app and set context
|
// Initialize test app and set context
|
||||||
@ -737,7 +795,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
reservesPrior = sdk.NewCoin(tc.args.borrowCoinDenom, sdk.ZeroInt())
|
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)
|
suite.Require().True(foundInterestFactorPrior)
|
||||||
|
|
||||||
// 2. Calculate expected interest owed
|
// 2. Calculate expected interest owed
|
||||||
@ -748,7 +806,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
|
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
|
||||||
suite.Require().NoError(err)
|
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)
|
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()))
|
expectedReserves := reservesPrior.Add(sdk.NewCoin(tc.args.borrowCoinDenom, sdk.NewDecFromInt(expectedInterest).Mul(tc.args.reserveFactor).TruncateInt()))
|
||||||
expectedInterestFactor := interestFactorPrior.Mul(interestFactor)
|
expectedInterestFactor := interestFactorPrior.Mul(interestFactor)
|
||||||
@ -769,7 +827,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
|||||||
suite.Require().Equal(expectedReserves, currTotalReserves)
|
suite.Require().Equal(expectedReserves, currTotalReserves)
|
||||||
|
|
||||||
// Check that the borrow index has increased as expected
|
// 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)
|
suite.Require().Equal(expectedInterestFactor, currIndexPrior)
|
||||||
|
|
||||||
// After borrowing again user's borrow balance should have any outstanding interest applied
|
// 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) {
|
func TestInterestTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(InterestTestSuite))
|
suite.Run(t, new(InterestTestSuite))
|
||||||
}
|
}
|
||||||
|
@ -248,6 +248,29 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
|||||||
return borrowedCoins, true
|
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
|
// GetMoneyMarket returns a money market from the store for a denom
|
||||||
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
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)
|
store.Set([]byte(denom), bz)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInterestFactor returns the current interest factor for an individual market
|
// GetBorrowInterestFactor returns the current borrow interest factor for an individual market
|
||||||
func (k Keeper) GetInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
|
func (k Keeper) GetBorrowInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
|
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
|
||||||
bz := store.Get([]byte(denom))
|
bz := store.Get([]byte(denom))
|
||||||
if bz == nil {
|
if bz == nil {
|
||||||
return sdk.ZeroDec(), false
|
return sdk.ZeroDec(), false
|
||||||
}
|
}
|
||||||
var interestFactor sdk.Dec
|
var borrowInterestFactor sdk.Dec
|
||||||
k.cdc.MustUnmarshalBinaryBare(bz, &interestFactor)
|
k.cdc.MustUnmarshalBinaryBare(bz, &borrowInterestFactor)
|
||||||
return interestFactor, true
|
return borrowInterestFactor, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetInterestFactor sets the current interest factor for an individual market
|
// SetBorrowInterestFactor sets the current borrow interest factor for an individual market
|
||||||
func (k Keeper) SetInterestFactor(ctx sdk.Context, denom string, borrowIndex sdk.Dec) {
|
func (k Keeper) SetBorrowInterestFactor(ctx sdk.Context, denom string, borrowInterestFactor sdk.Dec) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
|
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
|
||||||
bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
|
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)
|
store.Set([]byte(denom), bz)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,8 @@ func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
|
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"))
|
_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"))
|
||||||
suite.Require().False(f)
|
suite.Require().False(f)
|
||||||
@ -96,7 +97,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
|
|||||||
|
|
||||||
func (suite *KeeperTestSuite) TestIterateDeposits() {
|
func (suite *KeeperTestSuite) TestIterateDeposits() {
|
||||||
for i := 0; i < 5; i++ {
|
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) })
|
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
|
||||||
}
|
}
|
||||||
var deposits []types.Deposit
|
var deposits []types.Deposit
|
||||||
|
@ -39,7 +39,7 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
k.SyncOutstandingInterest(ctx, borrower)
|
k.SyncBorrowInterest(ctx, borrower)
|
||||||
|
|
||||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync interest so loan is up-to-date
|
// Sync borrow interest so loan is up-to-date
|
||||||
k.SyncOutstandingInterest(ctx, sender)
|
k.SyncBorrowInterest(ctx, sender)
|
||||||
|
|
||||||
// Validate requested repay
|
// Validate requested repay
|
||||||
err = k.ValidateRepay(ctx, sender, coins)
|
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
|
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
|
// Sends coins from user to Hard module account
|
||||||
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment)
|
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
|
// 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{}
|
repayment := sdk.Coins{}
|
||||||
|
|
||||||
|
if !payment.DenomsSubsetOf(owed) {
|
||||||
|
return repayment, types.ErrInvalidRepaymentDenom
|
||||||
|
}
|
||||||
|
|
||||||
for _, coin := range payment {
|
for _, coin := range payment {
|
||||||
if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
|
if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
|
||||||
repayment = append(repayment, sdk.NewCoin(coin.Denom, 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)
|
repayment = append(repayment, coin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return repayment
|
return repayment, nil
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,21 @@ func (suite *KeeperTestSuite) TestRepay() {
|
|||||||
contains: "",
|
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",
|
"invalid: insufficent balance for repay",
|
||||||
args{
|
args{
|
||||||
@ -98,7 +113,7 @@ func (suite *KeeperTestSuite) TestRepay() {
|
|||||||
},
|
},
|
||||||
errArgs{
|
errArgs{
|
||||||
expectPass: false,
|
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 {
|
if tc.errArgs.expectPass {
|
||||||
suite.Require().NoError(err)
|
suite.Require().NoError(err)
|
||||||
// If we overpaid expect an adjustment
|
// 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
|
// Check borrower balance
|
||||||
expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins)
|
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 := tApp.GetSupplyKeeper()
|
||||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
|
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
|
||||||
keeper := tApp.GetHardKeeper()
|
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)
|
keeper.SetDeposit(ctx, deposit)
|
||||||
suite.app = tApp
|
suite.app = tApp
|
||||||
suite.ctx = ctx
|
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()
|
cdc := makeTestCodec()
|
||||||
|
|
||||||
prevBlockTime := time.Now().UTC()
|
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")
|
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
|
||||||
|
|
||||||
kvPairs := kv.Pairs{
|
kvPairs := kv.Pairs{
|
||||||
|
@ -4,32 +4,15 @@ import (
|
|||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
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
|
// Borrow defines an amount of coins borrowed from a hard module account
|
||||||
type Borrow struct {
|
type Borrow struct {
|
||||||
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
||||||
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
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
|
// 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{
|
return Borrow{
|
||||||
Borrower: borrower,
|
Borrower: borrower,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
@ -37,19 +20,19 @@ func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterestFactor defines an individual interest factor
|
// BorrowInterestFactor defines an individual borrow interest factor
|
||||||
type InterestFactor struct {
|
type BorrowInterestFactor struct {
|
||||||
Denom string `json:"denom" yaml:"denom"`
|
Denom string `json:"denom" yaml:"denom"`
|
||||||
Value sdk.Dec `json:"value" yaml:"value"`
|
Value sdk.Dec `json:"value" yaml:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInterestFactor returns a new InterestFactor instance
|
// NewBorrowInterestFactor returns a new BorrowInterestFactor instance
|
||||||
func NewInterestFactor(denom string, value sdk.Dec) InterestFactor {
|
func NewBorrowInterestFactor(denom string, value sdk.Dec) BorrowInterestFactor {
|
||||||
return InterestFactor{
|
return BorrowInterestFactor{
|
||||||
Denom: denom,
|
Denom: denom,
|
||||||
Value: value,
|
Value: value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterestFactors is a slice of InterestFactor, because Amino won't marshal maps
|
// BorrowInterestFactors is a slice of BorrowInterestFactor, because Amino won't marshal maps
|
||||||
type InterestFactors []InterestFactor
|
type BorrowInterestFactors []BorrowInterestFactor
|
||||||
|
@ -8,12 +8,31 @@ import (
|
|||||||
type Deposit struct {
|
type Deposit struct {
|
||||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||||
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
||||||
|
Index SupplyInterestFactors `json:"index" yaml:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeposit returns a new deposit
|
// 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{
|
return Deposit{
|
||||||
Depositor: depositor,
|
Depositor: depositor,
|
||||||
Amount: amount,
|
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")
|
ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins")
|
||||||
// ErrInsufficientBalanceForBorrow error for when the requested borrow exceeds user's balance
|
// ErrInsufficientBalanceForBorrow error for when the requested borrow exceeds user's balance
|
||||||
ErrInsufficientBalanceForBorrow = sdkerrors.Register(ModuleName, 31, "insufficient 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}
|
ClaimsKeyPrefix = []byte{0x04}
|
||||||
BorrowsKeyPrefix = []byte{0x05}
|
BorrowsKeyPrefix = []byte{0x05}
|
||||||
BorrowedCoinsPrefix = []byte{0x06}
|
BorrowedCoinsPrefix = []byte{0x06}
|
||||||
MoneyMarketsPrefix = []byte{0x07}
|
SuppliedCoinsPrefix = []byte{0x07}
|
||||||
PreviousAccrualTimePrefix = []byte{0x08} // denom -> time
|
MoneyMarketsPrefix = []byte{0x08}
|
||||||
TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin
|
PreviousAccrualTimePrefix = []byte{0x09} // denom -> time
|
||||||
InterestFactorPrefix = []byte{0x10} // denom -> sdk.Dec
|
TotalReservesPrefix = []byte{0x10} // denom -> sdk.Coin
|
||||||
LtvIndexPrefix = []byte{0x11}
|
BorrowInterestFactorPrefix = []byte{0x11} // denom -> sdk.Dec
|
||||||
|
SupplyInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec
|
||||||
|
LtvIndexPrefix = []byte{0x13}
|
||||||
sep = []byte(":")
|
sep = []byte(":")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user