Harvest: interest rate logic (#720)

* initial feature scaffolding

* implement interest keeper logic

* basic AccrueInterest

* accrue interest on borrow

* update borrow index formula

* update sample reserve factor

* move AccrueInterest to begin blocker

* refactor interest rate updates for accrue interest

* use interest rate model from store

* refactor begin blocker state machine

* add reserve factor to interest model params

* update comment

* store money market instead of interest rate models

* update test suite

* use BorrowedCoins store key

* update public functions and alias

* unit tests, keeper test scaffolding

* demo panic

* address revisions

* add 'normal no jump' test case

* spy = 1 + borrow rate

* update comment

* APYToSPY unit test

* per user borrow index list

* interest keeper test

* test: interest applied on successive borrows

* varied snapshot times

* test: multiple, varied snapshots

* address revisions

* add pending interest before validating new borrow

* update makefile

* address revisions

* fix test
This commit is contained in:
Denali Marsh 2020-12-03 22:50:35 +01:00 committed by GitHub
parent 9c69ee2fbf
commit 49d62dd076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1247 additions and 86 deletions

View File

@ -101,7 +101,7 @@ clean:
# Set to exclude riot links as they trigger false positives # Set to exclude riot links as they trigger false positives
link-check: link-check:
@go get -u github.com/raviqqe/liche@f57a5d1c5be4856454cb26de155a65a4fd856ee3 @go get -u github.com/raviqqe/liche@f57a5d1c5be4856454cb26de155a65a4fd856ee3
liche -r . --exclude "^http://127.*|^https://riot.im/app*|^http://kava-testnet*|^https://testnet-dex*|^https://kava3.data.kava.io*|^https://ipfs.io*|^https://apps.apple.com*" liche -r . --exclude "^http://127.*|^https://riot.im/app*|^http://kava-testnet*|^https://testnet-dex*|^https://kava3.data.kava.io*|^https://ipfs.io*|^https://apps.apple.com*|^https://kava.quicksync.io*"
lint: lint:

View File

@ -50,6 +50,10 @@ var (
// function aliases // function aliases
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier NewQuerier = keeper.NewQuerier
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
CalculateBorrowRate = keeper.CalculateBorrowRate
CalculateInterestFactor = keeper.CalculateInterestFactor
APYToSPY = keeper.APYToSPY
ClaimKey = types.ClaimKey ClaimKey = types.ClaimKey
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
DefaultParams = types.DefaultParams DefaultParams = types.DefaultParams

View File

@ -11,6 +11,17 @@ import (
// Borrow funds // Borrow funds
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
for _, coin := range coins {
_, foundBorrowIndex := k.GetBorrowIndex(ctx, coin.Denom)
if !foundBorrowIndex {
k.SetBorrowIndex(ctx, coin.Denom, sdk.OneDec())
}
}
// Sync user's borrow balance (only for coins user is requesting to borrow)
k.SyncBorrowInterest(ctx, borrower, coins)
// 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)
if err != nil { if err != nil {
@ -34,22 +45,22 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
} }
} }
// Update user's borrow in store
borrow, found := k.GetBorrow(ctx, borrower) borrow, found := k.GetBorrow(ctx, borrower)
if !found { if !found {
borrow = types.NewBorrow(borrower, coins) return types.ErrBorrowNotFound // This should never happen
} else {
borrow.Amount = borrow.Amount.Add(coins...)
} }
// Add the newly borrowed coins to the user's borrow object
borrow.Amount = borrow.Amount.Add(coins...)
k.SetBorrow(ctx, borrow) k.SetBorrow(ctx, borrow)
// Update total borrowed amount // Update total borrowed amount by newly borrowed coins. Don't add user's pending interest as
// it has already been included in the total borrowed coins by the BeginBlocker.
k.IncrementBorrowedCoins(ctx, coins) k.IncrementBorrowedCoins(ctx, coins)
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeHarvestBorrow, types.EventTypeHarvestBorrow,
sdk.NewAttribute(types.AttributeKeyBorrower, borrow.Borrower.String()), sdk.NewAttribute(types.AttributeKeyBorrower, borrower.String()),
sdk.NewAttribute(types.AttributeKeyBorrowCoins, coins.String()), sdk.NewAttribute(types.AttributeKeyBorrowCoins, coins.String()),
), ),
) )
@ -57,6 +68,57 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
return nil return nil
} }
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state,
// returning an sdk.Coins object containing the amount of newly accumulated interest.
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) sdk.Coins {
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, borrower)
if !found { // User's first borrow
// Build borrow index list containing (denoms, borrow index value at borrow time)
var borrowIndexes types.BorrowIndexes
for _, coin := range coins {
borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom)
borrowIndex := types.NewBorrowIndexItem(coin.Denom, borrowIndexValue)
borrowIndexes = append(borrowIndexes, borrowIndex)
}
borrow = types.NewBorrow(borrower, sdk.Coins{}, borrowIndexes)
} else { // User has existing borrow
for _, coin := range coins {
// 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
}
}
borrowIndexValue, _ := k.GetBorrowIndex(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has borrowed this denom
borrow.Index = append(borrow.Index, types.NewBorrowIndexItem(coin.Denom, borrowIndexValue))
} 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))
userLastBorrowIndex := borrow.Index[foundAtIndex].Value
interest := (storedAmount.Quo(userLastBorrowIndex).Mul(borrowIndexValue)).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 = borrowIndexValue
}
}
// 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)
return totalNewInterest
}
// 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() {
@ -70,7 +132,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
moneyMarket, ok := moneyMarketCache[coin.Denom] moneyMarket, ok := moneyMarketCache[coin.Denom]
// Fetch money market and store in local cache // Fetch money market and store in local cache
if !ok { if !ok {
newMoneyMarket, found := k.GetMoneyMarket(ctx, coin.Denom) newMoneyMarket, found := k.GetMoneyMarketParam(ctx, coin.Denom)
if !found { if !found {
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", coin.Denom) return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", coin.Denom)
} }
@ -114,7 +176,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
moneyMarket, ok := moneyMarketCache[deposit.Amount.Denom] moneyMarket, ok := moneyMarketCache[deposit.Amount.Denom]
// Fetch money market and store in local cache // Fetch money market and store in local cache
if !ok { if !ok {
newMoneyMarket, found := k.GetMoneyMarket(ctx, deposit.Amount.Denom) newMoneyMarket, found := k.GetMoneyMarketParam(ctx, deposit.Amount.Denom)
if !found { if !found {
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", deposit.Amount.Denom) return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", deposit.Amount.Denom)
} }
@ -140,7 +202,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
moneyMarket, ok := moneyMarketCache[borrowedCoin.Denom] moneyMarket, ok := moneyMarketCache[borrowedCoin.Denom]
// Fetch money market and store in local cache // Fetch money market and store in local cache
if !ok { if !ok {
newMoneyMarket, found := k.GetMoneyMarket(ctx, borrowedCoin.Denom) newMoneyMarket, found := k.GetMoneyMarketParam(ctx, borrowedCoin.Denom)
if !found { if !found {
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", borrowedCoin.Denom) return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", borrowedCoin.Denom)
} }

View File

@ -275,12 +275,12 @@ func (suite *KeeperTestSuite) TestBorrow() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", true, tc.args.usdxBorrowLimit, sdk.MustNewDecFromStr("1"), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, tc.args.usdxBorrowLimit, sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("busd", false, sdk.NewDec(100000000*BUSD_CF), sdk.MustNewDecFromStr("1"), "busd:usd", sdk.NewInt(BUSD_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("busd", types.NewBorrowLimit(false, sdk.NewDec(100000000*BUSD_CF), sdk.MustNewDecFromStr("1")), "busd:usd", sdk.NewInt(BUSD_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(100000000*KAVA_CF), tc.args.loanToValueKAVA, "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), tc.args.loanToValueKAVA), "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("btcb", false, sdk.NewDec(100000000*BTCB_CF), tc.args.loanToValueBTCB, "btcb:usd", sdk.NewInt(BTCB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("btcb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), tc.args.loanToValueBTCB), "btcb:usd", sdk.NewInt(BTCB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("bnb", false, sdk.NewDec(100000000*BNB_CF), tc.args.loanToValueBNB, "bnb:usd", sdk.NewInt(BNB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), tc.args.loanToValueBNB), "bnb:usd", sdk.NewInt(BNB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("xyz", false, sdk.NewDec(1), tc.args.loanToValueBNB, "xyz:usd", sdk.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("xyz", types.NewBorrowLimit(false, sdk.NewDec(1), tc.args.loanToValueBNB), "xyz:usd", sdk.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)

View File

@ -265,8 +265,8 @@ func (suite *KeeperTestSuite) TestClaim() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})

View File

@ -10,7 +10,6 @@ import (
// Deposit deposit // Deposit deposit
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error { func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error {
err := k.ValidateDeposit(ctx, amount) err := k.ValidateDeposit(ctx, amount)
if err != nil { if err != nil {
return err return err
@ -59,6 +58,7 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.C
if !found { if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no %s deposit found for %s", amount.Denom, depositor) return sdkerrors.Wrapf(types.ErrDepositNotFound, "no %s deposit found for %s", amount.Denom, depositor)
} }
if !deposit.Amount.IsGTE(amount) { if !deposit.Amount.IsGTE(amount) {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "%s>%s", amount, deposit.Amount) return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "%s>%s", amount, deposit.Amount)
} }

View File

@ -108,8 +108,8 @@ func (suite *KeeperTestSuite) TestDeposit() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
@ -251,8 +251,8 @@ func (suite *KeeperTestSuite) TestWithdraw() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})

View File

@ -2,30 +2,192 @@ package keeper
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/harvest/types" "github.com/kava-labs/kava/x/harvest/types"
) )
// ApplyInterestRateUpdates translates the current interest rate models from the params to the store var (
scalingFactor = 1e18
secondsPerYear = 31536000
)
// ApplyInterestRateUpdates translates the current interest rate models from the params to the store,
// with each money market accruing interest.
func (k Keeper) ApplyInterestRateUpdates(ctx sdk.Context) { func (k Keeper) ApplyInterestRateUpdates(ctx sdk.Context) {
denomSet := map[string]bool{} denomSet := map[string]bool{}
params := k.GetParams(ctx) params := k.GetParams(ctx)
for _, mm := range params.MoneyMarkets { for _, mm := range params.MoneyMarkets {
model, found := k.GetInterestRateModel(ctx, mm.Denom) // Set any new money markets in the store
moneyMarket, found := k.GetMoneyMarket(ctx, mm.Denom)
if !found { if !found {
k.SetInterestRateModel(ctx, mm.Denom, mm.InterestRateModel) moneyMarket = mm
continue k.SetMoneyMarket(ctx, mm.Denom, moneyMarket)
} }
if !model.Equal(mm.InterestRateModel) {
k.SetInterestRateModel(ctx, mm.Denom, mm.InterestRateModel) // Accrue interest according to the current money markets in the store
err := k.AccrueInterest(ctx, mm.Denom)
if err != nil {
panic(err)
}
// Update the interest rate in the store if the params have changed
if !moneyMarket.Equal(mm) {
k.SetMoneyMarket(ctx, mm.Denom, mm)
} }
denomSet[mm.Denom] = true denomSet[mm.Denom] = true
} }
k.IterateInterestRateModels(ctx, func(denom string, i types.InterestRateModel) bool { // Edge case: money markets removed from params that still exist in the store
k.IterateMoneyMarkets(ctx, func(denom string, i types.MoneyMarket) bool {
if !denomSet[denom] { if !denomSet[denom] {
k.DeleteInterestRateModel(ctx, denom) // Accrue interest according to current store money market
err := k.AccrueInterest(ctx, denom)
if err != nil {
panic(err)
}
// Delete the money market from the store
k.DeleteMoneyMarket(ctx, denom)
} }
return false return false
}) })
} }
// AccrueInterest applies accrued interest to total borrows and reserves by calculating
// interest from the last checkpoint time and writing the updated values to the store.
func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, denom)
if !found {
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
timeElapsed := ctx.BlockTime().Unix() - previousAccrualTime.Unix()
if timeElapsed == 0 {
return nil
}
// Get available harvest module account cash on hand
cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
// Get prior borrows
borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
if foundBorrowCoinsPrior {
borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
}
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
if !foundReservesPrior {
newReservesPrior := sdk.NewCoin(denom, sdk.ZeroInt())
k.SetTotalReserves(ctx, denom, newReservesPrior)
reservesPrior = newReservesPrior
}
borrowIndexPrior, foundBorrowIndexPrior := k.GetBorrowIndex(ctx, denom)
if !foundBorrowIndexPrior {
newBorrowIndexPrior := sdk.MustNewDecFromStr("1.0")
k.SetBorrowIndex(ctx, denom, newBorrowIndexPrior)
borrowIndexPrior = newBorrowIndexPrior
}
// Fetch money market from the store
mm, found := k.GetMoneyMarket(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrMoneyMarketNotFound, "%s", denom)
}
// 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))
if err != nil {
return err
}
// Convert from APY to SPY, expressed as (1 + borrow rate)
borrowRateSpy, err := APYToSPY(sdk.OneDec().Add(borrowRateApy))
if err != nil {
return err
}
interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(borrowsPrior.Amount)).TruncateInt()).Sub(borrowsPrior.Amount)
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestAccumulated))
totalReservesNew := reservesPrior.Add(sdk.NewCoin(denom, sdk.NewDecFromInt(interestAccumulated).Mul(mm.ReserveFactor).TruncateInt()))
borrowIndexNew := borrowIndexPrior.Mul(interestFactor)
k.SetBorrowIndex(ctx, denom, borrowIndexNew)
k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
k.SetTotalReserves(ctx, denom, totalReservesNew)
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
// CalculateBorrowRate calculates the borrow rate, which is the current APY expressed as a decimal
// based on the current utilization.
func CalculateBorrowRate(model types.InterestRateModel, cash, borrows, reserves sdk.Dec) (sdk.Dec, error) {
utilRatio := CalculateUtilizationRatio(cash, borrows, reserves)
// Calculate normal borrow rate (under kink)
if utilRatio.LTE(model.Kink) {
return utilRatio.Mul(model.BaseMultiplier).Add(model.BaseRateAPY), nil
}
// Calculate jump borrow rate (over kink)
normalRate := model.Kink.Mul(model.BaseMultiplier).Add(model.BaseRateAPY)
excessUtil := utilRatio.Sub(model.Kink)
return excessUtil.Mul(model.JumpMultiplier).Add(normalRate), nil
}
// CalculateUtilizationRatio calculates an asset's current utilization rate
func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
// Utilization rate is 0 when there are no borrows
if borrows.Equal(sdk.ZeroDec()) {
return sdk.ZeroDec()
}
totalSupply := cash.Add(borrows).Sub(reserves)
if totalSupply.IsNegative() {
return sdk.OneDec()
}
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
}
// CalculateInterestFactor calculates the simple interest scaling factor,
// which is equal to: (per-second interest rate * number of seconds elapsed)
// Will return 1.000x, multiply by principal to get new principal with added interest
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
scalingFactorInt := sdk.NewInt(int64(scalingFactor))
// Convert per-second interest rate to a uint scaled by 1e18
interestMantissa := sdk.NewUint(perSecondInterestRate.MulInt(scalingFactorInt).RoundInt().Uint64())
// Convert seconds elapsed to uint (*not scaled*)
secondsElapsedUint := sdk.NewUint(secondsElapsed.Uint64())
// Calculate the interest factor as a uint scaled by 1e18
interestFactorMantissa := sdk.RelativePow(interestMantissa, secondsElapsedUint, scalingFactorUint)
// Convert interest factor to an unscaled sdk.Dec
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
}
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
// Note: any APY 179 or greater will cause an out-of-bounds error
root, err := apy.ApproxRoot(uint64(secondsPerYear))
if err != nil {
return sdk.ZeroDec(), err
}
return root, nil
}
// minInt64 returns the smaller of x or y
func minDec(x, y sdk.Dec) sdk.Dec {
if x.GT(y) {
return y
}
return x
}

View File

@ -0,0 +1,791 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
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/harvest"
"github.com/kava-labs/kava/x/harvest/types"
"github.com/kava-labs/kava/x/pricefeed"
)
type InterestTestSuite struct {
suite.Suite
}
func (suite *InterestTestSuite) TestCalculateUtilizationRatio() {
type args struct {
cash sdk.Dec
borrows sdk.Dec
reserves sdk.Dec
expectedValue sdk.Dec
}
type test struct {
name string
args args
}
testCases := []test{
{
"normal",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("100"),
expectedValue: sdk.MustNewDecFromStr("0.847457627118644068"),
},
},
{
"high util ratio",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("250000"),
reserves: sdk.MustNewDecFromStr("100"),
expectedValue: sdk.MustNewDecFromStr("0.996412913511359107"),
},
},
{
"very high util ratio",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("250000000000"),
reserves: sdk.MustNewDecFromStr("100"),
expectedValue: sdk.MustNewDecFromStr("0.999999996400000013"),
},
},
{
"low util ratio",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("50"),
reserves: sdk.MustNewDecFromStr("100"),
expectedValue: sdk.MustNewDecFromStr("0.052631578947368421"),
},
},
{
"very low util ratio",
args{
cash: sdk.MustNewDecFromStr("10000000"),
borrows: sdk.MustNewDecFromStr("50"),
reserves: sdk.MustNewDecFromStr("100"),
expectedValue: sdk.MustNewDecFromStr("0.000005000025000125"),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
utilRatio := harvest.CalculateUtilizationRatio(tc.args.cash, tc.args.borrows, tc.args.reserves)
suite.Require().Equal(tc.args.expectedValue, utilRatio)
})
}
}
func (suite *InterestTestSuite) TestCalculateBorrowRate() {
type args struct {
cash sdk.Dec
borrows sdk.Dec
reserves sdk.Dec
model types.InterestRateModel
expectedValue sdk.Dec
}
type test struct {
name string
args args
}
// Normal model has:
// - BaseRateAPY: 0.0
// - BaseMultiplier: 0.1
// - Kink: 0.8
// - JumpMultiplier: 0.5
normalModel := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
testCases := []test{
{
"normal no jump",
args{
cash: sdk.MustNewDecFromStr("5000"),
borrows: sdk.MustNewDecFromStr("1000"),
reserves: sdk.MustNewDecFromStr("1000"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.020000000000000000"),
},
},
{
"normal with jump",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("100"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.103728813559322034"),
},
},
{
"high cash",
args{
cash: sdk.MustNewDecFromStr("10000000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("100"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.000049975511999120"),
},
},
{
"high borrows",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000000000000"),
reserves: sdk.MustNewDecFromStr("100"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.179999999910000000"),
},
},
{
"high reserves",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("1000000000000"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.180000000000000000"),
},
},
{
"random numbers",
args{
cash: sdk.MustNewDecFromStr("125"),
borrows: sdk.MustNewDecFromStr("11"),
reserves: sdk.MustNewDecFromStr("82"),
model: normalModel,
expectedValue: sdk.MustNewDecFromStr("0.020370370370370370"),
},
},
{
"increased base multiplier",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("100"),
model: types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.5"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("1.0")),
expectedValue: sdk.MustNewDecFromStr("0.447457627118644068"),
},
},
{
"decreased kink",
args{
cash: sdk.MustNewDecFromStr("1000"),
borrows: sdk.MustNewDecFromStr("5000"),
reserves: sdk.MustNewDecFromStr("100"),
model: types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.5"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("1.0")),
expectedValue: sdk.MustNewDecFromStr("0.797457627118644068"),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
borrowRate, err := harvest.CalculateBorrowRate(tc.args.model, tc.args.cash, tc.args.borrows, tc.args.reserves)
suite.Require().NoError(err)
suite.Require().Equal(tc.args.expectedValue, borrowRate)
})
}
}
func (suite *InterestTestSuite) TestCalculateInterestFactor() {
type args struct {
perSecondInterestRate sdk.Dec
timeElapsed sdk.Int
expectedValue sdk.Dec
}
type test struct {
name string
args args
}
oneYearInSeconds := int64(31536000)
testCases := []test{
{
"1 year",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.191463614477847370"),
},
},
{
"10 year",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds * 10),
expectedValue: sdk.MustNewDecFromStr("5.765113233897391189"),
},
},
{
"1 month",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds / 12),
expectedValue: sdk.MustNewDecFromStr("1.014705619075717373"),
},
},
{
"1 day",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds / 365),
expectedValue: sdk.MustNewDecFromStr("1.000480067194057924"),
},
},
{
"1 year: low interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.017656545925063632"),
},
},
{
"1 year, lower interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000055"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.001735985079841390"),
},
},
{
"1 year, lowest interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000005"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.000157692432076670"),
},
},
{
"1 year: high interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000055555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("5.766022095987868825"),
},
},
{
"1 year: higher interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000555555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("40628388.864535408465693310"),
},
},
// If we raise the per second interest rate too much we'll cause an integer overflow.
// For example, perSecondInterestRate: '1.000005555555' will cause a panic.
{
"1 year: highest interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000001555555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("2017093013158200407564.613502861572552603"),
},
},
}
for _, tc := range testCases {
interestFactor := harvest.CalculateInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
suite.Require().Equal(tc.args.expectedValue, interestFactor)
}
}
func (suite *InterestTestSuite) TestAPYToSPY() {
type args struct {
apy sdk.Dec
expectedValue sdk.Dec
}
type test struct {
name string
args args
expectError bool
}
testCases := []test{
{
"lowest apy",
args{
apy: sdk.MustNewDecFromStr("0.005"),
expectedValue: sdk.MustNewDecFromStr("0.999999831991472557"),
},
false,
},
{
"lower apy",
args{
apy: sdk.MustNewDecFromStr("0.05"),
expectedValue: sdk.MustNewDecFromStr("0.999999905005957279"),
},
false,
},
{
"medium-low apy",
args{
apy: sdk.MustNewDecFromStr("0.5"),
expectedValue: sdk.MustNewDecFromStr("0.999999978020447332"),
},
false,
},
{
"medium-high apy",
args{
apy: sdk.MustNewDecFromStr("5"),
expectedValue: sdk.MustNewDecFromStr("1.000000051034942717"),
},
false,
},
{
"high apy",
args{
apy: sdk.MustNewDecFromStr("50"),
expectedValue: sdk.MustNewDecFromStr("1.000000124049443433"),
},
false,
},
{
"highest apy",
args{
apy: sdk.MustNewDecFromStr("170"),
expectedValue: sdk.MustNewDecFromStr("1.000000162855113371"),
},
false,
},
{
"out of bounds error after 178",
args{
apy: sdk.MustNewDecFromStr("178"),
expectedValue: sdk.ZeroDec(),
},
true,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
spy, err := harvest.APYToSPY(tc.args.apy)
if tc.expectError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(tc.args.expectedValue, spy)
}
})
}
}
type ExpectedInterest struct {
elapsedTime int64
shouldBorrow bool
borrowCoin sdk.Coin
}
func (suite *KeeperTestSuite) TestInterest() {
type args struct {
user sdk.AccAddress
initialBorrowerCoins sdk.Coins
initialModuleCoins sdk.Coins
borrowCoinDenom string
borrowCoins sdk.Coins
interestRateModel types.InterestRateModel
reserveFactor sdk.Dec
expectedInterestSnaphots []ExpectedInterest
}
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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneDayInSeconds,
shouldBorrow: false,
borrowCoin: 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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneWeekInSeconds,
shouldBorrow: false,
borrowCoin: 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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
borrowCoin: 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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"0 reserve factor",
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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"borrow 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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: true,
borrowCoin: sdk.NewCoin("ukava", sdk.NewInt(1*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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
borrowCoin: 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))),
borrowCoinDenom: "ukava",
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
{
elapsedTime: oneDayInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
{
elapsedTime: oneWeekInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
borrowCoin: sdk.Coin{},
},
{
elapsedTime: oneYearInSeconds,
shouldBorrow: false,
borrowCoin: 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},
)
// 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
tc.args.interestRateModel,
tc.args.reserveFactor), // Reserve Factor
},
), 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},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
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)
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
var err error
// Run begin blocker and store initial block time
harvest.BeginBlocker(suite.ctx, suite.keeper)
// Deposit 2x as many coins for each coin we intend to borrow
for _, coin := range tc.args.borrowCoins {
err = suite.keeper.Deposit(suite.ctx, tc.args.user, sdk.NewCoin(coin.Denom, coin.Amount.Mul(sdk.NewInt(2))))
suite.Require().NoError(err)
}
// Borrow coins
err = suite.keeper.Borrow(suite.ctx, tc.args.user, tc.args.borrowCoins)
suite.Require().NoError(err)
// Check that the initial module-level borrow balance is correct and store it
initialBorrowedCoins, _ := suite.keeper.GetBorrowedCoins(suite.ctx)
suite.Require().Equal(tc.args.borrowCoins, initialBorrowedCoins)
// Check interest levels for each snapshot
prevCtx := suite.ctx
for _, snapshot := range tc.args.expectedInterestSnaphots {
// ---------------------------- Calculate expected interest ----------------------------
// 1. Get cash, borrows, reserves, and borrow index
cashPrior := suite.getModuleAccountAtCtx(types.ModuleName, prevCtx).GetCoins().AmountOf(tc.args.borrowCoinDenom)
borrowCoinsPrior, borrowCoinsPriorFound := suite.keeper.GetBorrowedCoins(prevCtx)
suite.Require().True(borrowCoinsPriorFound)
borrowCoinPriorAmount := borrowCoinsPrior.AmountOf(tc.args.borrowCoinDenom)
reservesPrior, foundReservesPrior := suite.keeper.GetTotalReserves(prevCtx, tc.args.borrowCoinDenom)
if !foundReservesPrior {
reservesPrior = sdk.NewCoin(tc.args.borrowCoinDenom, sdk.ZeroInt())
}
borrowIndexPrior, foundBorrowIndexPrior := suite.keeper.GetBorrowIndex(prevCtx, tc.args.borrowCoinDenom)
suite.Require().True(foundBorrowIndexPrior)
// 2. Calculate expected interest owed
borrowRateApy, err := harvest.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 := harvest.APYToSPY(sdk.OneDec().Add(borrowRateApy))
suite.Require().NoError(err)
interestFactor := harvest.CalculateInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
expectedInterest := (interestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
expectedReserves := reservesPrior.Add(sdk.NewCoin(tc.args.borrowCoinDenom, sdk.NewDecFromInt(expectedInterest).Mul(tc.args.reserveFactor).TruncateInt()))
expectedBorrowIndex := borrowIndexPrior.Mul(interestFactor)
// -------------------------------------------------------------------------------------
// Set up snapshot chain context and run begin blocker
runAtTime := time.Unix(prevCtx.BlockTime().Unix()+(snapshot.elapsedTime), 0)
snapshotCtx := prevCtx.WithBlockTime(runAtTime)
harvest.BeginBlocker(snapshotCtx, suite.keeper)
// Check that the total amount of borrowed coins has increased by expected interest amount
expectedBorrowedCoins := borrowCoinsPrior.AmountOf(tc.args.borrowCoinDenom).Add(expectedInterest)
currBorrowedCoins, _ := suite.keeper.GetBorrowedCoins(snapshotCtx)
suite.Require().Equal(expectedBorrowedCoins, currBorrowedCoins.AmountOf(tc.args.borrowCoinDenom))
// Check that the total reserves have changed as expected
currTotalReserves, _ := suite.keeper.GetTotalReserves(snapshotCtx, tc.args.borrowCoinDenom)
suite.Require().Equal(expectedReserves, currTotalReserves)
// Check that the borrow index has increased as expected
currIndexPrior, _ := suite.keeper.GetBorrowIndex(snapshotCtx, tc.args.borrowCoinDenom)
suite.Require().Equal(expectedBorrowIndex, currIndexPrior)
// After borrowing again user's borrow balance should have any outstanding interest applied
if snapshot.shouldBorrow {
borrowCoinsBefore, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
expectedInterestCoins := sdk.NewCoin(tc.args.borrowCoinDenom, expectedInterest)
expectedBorrowCoinsAfter := borrowCoinsBefore.Amount.Add(snapshot.borrowCoin).Add(expectedInterestCoins)
err = suite.keeper.Borrow(snapshotCtx, tc.args.user, sdk.NewCoins(snapshot.borrowCoin))
suite.Require().NoError(err)
borrowCoinsAfter, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
suite.Require().Equal(expectedBorrowCoinsAfter, borrowCoinsAfter.Amount)
}
// Update previous context to this snapshot's context, segmenting time periods between snapshots
prevCtx = snapshotCtx
}
})
}
}
func TestInterestTestSuite(t *testing.T) {
suite.Run(t, new(InterestTestSuite))
}

View File

@ -256,42 +256,99 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
return borrowedCoins, true return borrowedCoins, true
} }
// GetInterestRateModel returns an interest rate model from the store for a denom // GetMoneyMarket returns a money market from the store for a denom
func (k Keeper) GetInterestRateModel(ctx sdk.Context, denom string) (types.InterestRateModel, bool) { func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix) store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
bz := store.Get([]byte(denom)) bz := store.Get([]byte(denom))
if bz == nil { if bz == nil {
return types.InterestRateModel{}, false return types.MoneyMarket{}, false
} }
var interestRateModel types.InterestRateModel var moneyMarket types.MoneyMarket
k.cdc.MustUnmarshalBinaryBare(bz, &interestRateModel) k.cdc.MustUnmarshalBinaryBare(bz, &moneyMarket)
return interestRateModel, true return moneyMarket, true
} }
// SetInterestRateModel sets an interest rate model in the store for a denom // SetMoneyMarket sets a money market in the store for a denom
func (k Keeper) SetInterestRateModel(ctx sdk.Context, denom string, interestRateModel types.InterestRateModel) { func (k Keeper) SetMoneyMarket(ctx sdk.Context, denom string, moneyMarket types.MoneyMarket) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix) store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
bz := k.cdc.MustMarshalBinaryBare(interestRateModel) bz := k.cdc.MustMarshalBinaryBare(moneyMarket)
store.Set([]byte(denom), bz) store.Set([]byte(denom), bz)
} }
// DeleteInterestRateModel deletes an interest rate model from the store // DeleteMoneyMarket deletes a money market from the store
func (k Keeper) DeleteInterestRateModel(ctx sdk.Context, denom string) { func (k Keeper) DeleteMoneyMarket(ctx sdk.Context, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix) store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
store.Delete([]byte(denom)) store.Delete([]byte(denom))
} }
// IterateInterestRateModels iterates over all interest rate model objects in the store and performs a callback function // IterateMoneyMarkets iterates over all money markets objects in the store and performs a callback function
// that returns both the interest rate model value and the key it's stored under // that returns both the money market and the key (denom) it's stored under
func (k Keeper) IterateInterestRateModels(ctx sdk.Context, cb func(denom string, interestRateModel types.InterestRateModel) (stop bool)) { func (k Keeper) IterateMoneyMarkets(ctx sdk.Context, cb func(denom string, moneyMarket types.MoneyMarket) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix) store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{}) iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close() defer iterator.Close()
for ; iterator.Valid(); iterator.Next() { for ; iterator.Valid(); iterator.Next() {
var interestRateModel types.InterestRateModel var moneyMarket types.MoneyMarket
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &interestRateModel) k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &moneyMarket)
if cb(string(iterator.Key()), interestRateModel) { if cb(string(iterator.Key()), moneyMarket) {
break break
} }
} }
} }
// GetPreviousAccrualTime returns the last time an individual market accrued interest
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, denom string) (time.Time, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return time.Time{}, false
}
var previousAccrualTime time.Time
k.cdc.MustUnmarshalBinaryBare(bz, &previousAccrualTime)
return previousAccrualTime, true
}
// SetPreviousAccrualTime sets the most recent accrual time for a particular market
func (k Keeper) SetPreviousAccrualTime(ctx sdk.Context, denom string, previousAccrualTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
bz := k.cdc.MustMarshalBinaryBare(previousAccrualTime)
store.Set([]byte(denom), bz)
}
// GetTotalReserves returns the total reserves for an individual market
func (k Keeper) GetTotalReserves(ctx sdk.Context, denom string) (sdk.Coin, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.TotalReservesPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return sdk.Coin{}, false
}
var totalReserves sdk.Coin
k.cdc.MustUnmarshalBinaryBare(bz, &totalReserves)
return totalReserves, true
}
// SetTotalReserves sets the total reserves for an individual market
func (k Keeper) SetTotalReserves(ctx sdk.Context, denom string, coin sdk.Coin) {
store := prefix.NewStore(ctx.KVStore(k.key), types.TotalReservesPrefix)
bz := k.cdc.MustMarshalBinaryBare(coin)
store.Set([]byte(denom), bz)
}
// GetBorrowIndex returns the current borrow index for an individual market
func (k Keeper) GetBorrowIndex(ctx sdk.Context, denom string) (sdk.Dec, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowIndexPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return sdk.ZeroDec(), false
}
var borrowIndex sdk.Dec
k.cdc.MustUnmarshalBinaryBare(bz, &borrowIndex)
return borrowIndex, true
}
// SetBorrowIndex sets the current borrow index for an individual market
func (k Keeper) SetBorrowIndex(ctx sdk.Context, denom string, borrowIndex sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowIndexPrefix)
bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
store.Set([]byte(denom), bz)
}

View File

@ -21,7 +21,6 @@ import (
// Test suite used for all keeper tests // Test suite used for all keeper tests
type KeeperTestSuite struct { type KeeperTestSuite struct {
suite.Suite suite.Suite
keeper keeper.Keeper keeper keeper.Keeper
app app.TestApp app app.TestApp
ctx sdk.Context ctx sdk.Context
@ -157,45 +156,52 @@ func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
func (suite *KeeperTestSuite) TestGetSetDeleteInterestRateModel() { func (suite *KeeperTestSuite) TestGetSetDeleteInterestRateModel() {
denom := "test" denom := "test"
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))
borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5"))
moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), model, sdk.MustNewDecFromStr("0.05"))
_, f := suite.keeper.GetInterestRateModel(suite.ctx, denom) _, f := suite.keeper.GetMoneyMarket(suite.ctx, denom)
suite.Require().False(f) suite.Require().False(f)
suite.keeper.SetInterestRateModel(suite.ctx, denom, model) suite.keeper.SetMoneyMarket(suite.ctx, denom, moneyMarket)
testInterestRateModel, f := suite.keeper.GetInterestRateModel(suite.ctx, denom) testMoneyMarket, f := suite.keeper.GetMoneyMarket(suite.ctx, denom)
suite.Require().True(f) suite.Require().True(f)
suite.Require().Equal(model, testInterestRateModel) suite.Require().Equal(moneyMarket, testMoneyMarket)
suite.Require().NotPanics(func() { suite.keeper.DeleteInterestRateModel(suite.ctx, denom) }) suite.Require().NotPanics(func() { suite.keeper.DeleteMoneyMarket(suite.ctx, denom) })
_, f = suite.keeper.GetInterestRateModel(suite.ctx, denom) _, f = suite.keeper.GetMoneyMarket(suite.ctx, denom)
suite.Require().False(f) suite.Require().False(f)
} }
func (suite *KeeperTestSuite) TestIterateInterestRateModels() { func (suite *KeeperTestSuite) TestIterateInterestRateModels() {
testDenom := "test" testDenom := "test"
var setModels types.InterestRateModels var setMMs types.MoneyMarkets
var setDenoms []string var setDenoms []string
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
// Initialize a new money market
denom := testDenom + strconv.Itoa(i) denom := testDenom + strconv.Itoa(i)
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))
suite.Require().NotPanics(func() { suite.keeper.SetInterestRateModel(suite.ctx, denom, model) }) borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5"))
moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), model, sdk.MustNewDecFromStr("0.05"))
// Store money market in the module's store
suite.Require().NotPanics(func() { suite.keeper.SetMoneyMarket(suite.ctx, denom, moneyMarket) })
// Save the denom and model // Save the denom and model
setDenoms = append(setDenoms, denom) setDenoms = append(setDenoms, denom)
setModels = append(setModels, model) setMMs = append(setMMs, moneyMarket)
} }
var seenModels types.InterestRateModels var seenMMs types.MoneyMarkets
var seenDenoms []string var seenDenoms []string
suite.keeper.IterateInterestRateModels(suite.ctx, func(denom string, i types.InterestRateModel) bool { suite.keeper.IterateMoneyMarkets(suite.ctx, func(denom string, i types.MoneyMarket) bool {
seenDenoms = append(seenDenoms, denom) seenDenoms = append(seenDenoms, denom)
seenModels = append(seenModels, i) seenMMs = append(seenMMs, i)
return false return false
}) })
suite.Require().Equal(setModels, seenModels) suite.Require().Equal(setMMs, seenMMs)
suite.Require().Equal(setDenoms, seenDenoms) suite.Require().Equal(setDenoms, seenDenoms)
} }
@ -204,11 +210,21 @@ func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Accou
return ak.GetAccount(suite.ctx, addr) return ak.GetAccount(suite.ctx, addr)
} }
func (suite *KeeperTestSuite) getAccountAtCtx(addr sdk.AccAddress, ctx sdk.Context) authexported.Account {
ak := suite.app.GetAccountKeeper()
return ak.GetAccount(ctx, addr)
}
func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI { func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
sk := suite.app.GetSupplyKeeper() sk := suite.app.GetSupplyKeeper()
return sk.GetModuleAccount(suite.ctx, name) return sk.GetModuleAccount(suite.ctx, name)
} }
func (suite *KeeperTestSuite) getModuleAccountAtCtx(name string, ctx sdk.Context) supplyexported.ModuleAccountI {
sk := suite.app.GetSupplyKeeper()
return sk.GetModuleAccount(ctx, name)
}
func TestKeeperTestSuite(t *testing.T) { func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite)) suite.Run(t, new(KeeperTestSuite))
} }

View File

@ -40,8 +40,8 @@ func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.Deleg
return types.DelegatorDistributionSchedule{}, false return types.DelegatorDistributionSchedule{}, false
} }
// GetMoneyMarket returns the corresponding Money Market param for a specific denom // GetMoneyMarketParam returns the corresponding Money Market param for a specific denom
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) { func (k Keeper) GetMoneyMarketParam(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
params := k.GetParams(ctx) params := k.GetParams(ctx)
for _, mm := range params.MoneyMarkets { for _, mm := range params.MoneyMarkets {
if mm.Denom == denom { if mm.Denom == denom {

View File

@ -75,8 +75,8 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), tc.args.previousBlockTime, types.DefaultDistributionTimes) ), tc.args.previousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
@ -443,8 +443,8 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), ),
types.DefaultPreviousBlockTime, types.DefaultPreviousBlockTime,

View File

@ -291,8 +291,8 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
), ),
}, },
types.MoneyMarkets{ types.MoneyMarkets{
types.NewMoneyMarket("usdx", false, sdk.NewDec(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
types.NewMoneyMarket("ukava", false, sdk.NewDec(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))), types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
}, },
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})

View File

@ -4,16 +4,35 @@ 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 harvest module account // Borrow defines an amount of coins borrowed from a harvest 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 BorrowIndexes `json:"index" yaml:"index"`
} }
// NewBorrow returns a new Borrow instance // NewBorrow returns a new Borrow instance
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins) Borrow { func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index BorrowIndexes) Borrow {
return Borrow{ return Borrow{
Borrower: borrower, Borrower: borrower,
Amount: amount, Amount: amount,
Index: index,
} }
} }

View File

@ -55,4 +55,8 @@ var (
ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation") ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation")
// ErrBorrowEmptyCoins error for when you cannot borrow empty coins // ErrBorrowEmptyCoins error for when you cannot borrow empty coins
ErrBorrowEmptyCoins = sdkerrors.Register(ModuleName, 25, "cannot borrow zero coins") ErrBorrowEmptyCoins = sdkerrors.Register(ModuleName, 25, "cannot borrow zero coins")
// ErrPreviousAccrualTimeNotFound error for no previous accrual time found in store
ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 26, "no previous accrual time found")
// ErrBorrowNotFound error for when borrow not found in store
ErrBorrowNotFound = sdkerrors.Register(ModuleName, 27, "no borrow found")
) )

View File

@ -37,7 +37,10 @@ var (
ClaimsKeyPrefix = []byte{0x04} ClaimsKeyPrefix = []byte{0x04}
BorrowsKeyPrefix = []byte{0x05} BorrowsKeyPrefix = []byte{0x05}
BorrowedCoinsPrefix = []byte{0x06} BorrowedCoinsPrefix = []byte{0x06}
InterestRateModelsPrefix = []byte{0x07} MoneyMarketsPrefix = []byte{0x07}
PreviousAccrualTimePrefix = []byte{0x08} // denom -> time
TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin
BorrowIndexPrefix = []byte{0x10} // denom -> sdk.Dec
sep = []byte(":") sep = []byte(":")
) )

View File

@ -251,6 +251,20 @@ func (bl BorrowLimit) Validate() error {
return nil return nil
} }
// Equal returns a boolean indicating if an BorrowLimit is equal to another BorrowLimit
func (bl BorrowLimit) Equal(blCompareTo BorrowLimit) bool {
if bl.HasMaxLimit != blCompareTo.HasMaxLimit {
return false
}
if !bl.MaximumLimit.Equal(blCompareTo.MaximumLimit) {
return false
}
if !bl.LoanToValue.Equal(blCompareTo.LoanToValue) {
return false
}
return true
}
// MoneyMarket is a money market for an individual asset // MoneyMarket is a money market for an individual asset
type MoneyMarket struct { type MoneyMarket struct {
Denom string `json:"denom" yaml:"denom"` Denom string `json:"denom" yaml:"denom"`
@ -258,17 +272,19 @@ type MoneyMarket struct {
SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
InterestRateModel InterestRateModel `json:"interest_rate_model" yaml:"interest_rate_model"` InterestRateModel InterestRateModel `json:"interest_rate_model" yaml:"interest_rate_model"`
ReserveFactor sdk.Dec `json:"reserve_factor" yaml:"reserve_factor"`
} }
// NewMoneyMarket returns a new MoneyMarket // NewMoneyMarket returns a new MoneyMarket
func NewMoneyMarket(denom string, hasMaxLimit bool, maximumLimit, loanToValue sdk.Dec, func NewMoneyMarket(denom string, borrowLimit BorrowLimit, spotMarketID string,
spotMarketID string, conversionFactor sdk.Int, interestRateModel InterestRateModel) MoneyMarket { conversionFactor sdk.Int, interestRateModel InterestRateModel, reserveFactor sdk.Dec) MoneyMarket {
return MoneyMarket{ return MoneyMarket{
Denom: denom, Denom: denom,
BorrowLimit: NewBorrowLimit(hasMaxLimit, maximumLimit, loanToValue), BorrowLimit: borrowLimit,
SpotMarketID: spotMarketID, SpotMarketID: spotMarketID,
ConversionFactor: conversionFactor, ConversionFactor: conversionFactor,
InterestRateModel: interestRateModel, InterestRateModel: interestRateModel,
ReserveFactor: reserveFactor,
} }
} }
@ -285,9 +301,36 @@ func (mm MoneyMarket) Validate() error {
if err := mm.InterestRateModel.Validate(); err != nil { if err := mm.InterestRateModel.Validate(); err != nil {
return err return err
} }
if mm.ReserveFactor.IsNegative() || mm.ReserveFactor.GT(sdk.OneDec()) {
return fmt.Errorf("Reserve factor must be between 0.0-1.0")
}
return nil return nil
} }
// Equal returns a boolean indicating if a MoneyMarket is equal to another MoneyMarket
func (mm MoneyMarket) Equal(mmCompareTo MoneyMarket) bool {
if mm.Denom != mmCompareTo.Denom {
return false
}
if !mm.BorrowLimit.Equal(mmCompareTo.BorrowLimit) {
return false
}
if mm.SpotMarketID != mmCompareTo.SpotMarketID {
return false
}
if !mm.ConversionFactor.Equal(mmCompareTo.ConversionFactor) {
return false
}
if !mm.InterestRateModel.Equal(mmCompareTo.InterestRateModel) {
return false
}
if !mm.ReserveFactor.Equal(mmCompareTo.ReserveFactor) {
return false
}
return true
}
// MoneyMarkets slice of MoneyMarket // MoneyMarkets slice of MoneyMarket
type MoneyMarkets []MoneyMarket type MoneyMarkets []MoneyMarket
@ -341,17 +384,17 @@ func (irm InterestRateModel) Validate() error {
} }
// Equal returns a boolean indicating if an InterestRateModel is equal to another InterestRateModel // Equal returns a boolean indicating if an InterestRateModel is equal to another InterestRateModel
func (irm InterestRateModel) Equal(comparisonIRM InterestRateModel) bool { func (irm InterestRateModel) Equal(irmCompareTo InterestRateModel) bool {
if !irm.BaseRateAPY.Equal(comparisonIRM.BaseRateAPY) { if !irm.BaseRateAPY.Equal(irmCompareTo.BaseRateAPY) {
return false return false
} }
if !irm.BaseMultiplier.Equal(comparisonIRM.BaseMultiplier) { if !irm.BaseMultiplier.Equal(irmCompareTo.BaseMultiplier) {
return false return false
} }
if !irm.Kink.Equal(comparisonIRM.Kink) { if !irm.Kink.Equal(irmCompareTo.Kink) {
return false return false
} }
if !irm.JumpMultiplier.Equal(comparisonIRM.JumpMultiplier) { if !irm.JumpMultiplier.Equal(irmCompareTo.JumpMultiplier) {
return false return false
} }
return true return true