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:
Denali Marsh 2021-01-07 11:23:05 +01:00 committed by GitHub
parent e9f5043c84
commit f7a73c9245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1458 additions and 607 deletions

View File

@ -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

View File

@ -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
} }

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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) {

View File

@ -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))
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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)

View File

@ -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
} }

View File

@ -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)

View File

@ -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
View 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
}

View 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)
})
}
}

View File

@ -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{

View File

@ -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

View File

@ -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

View File

@ -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")
) )

View File

@ -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(":")
) )