mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 07:45:18 +00:00
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:
parent
9c69ee2fbf
commit
49d62dd076
4
Makefile
4
Makefile
@ -101,7 +101,7 @@ clean:
|
||||
# Set to exclude riot links as they trigger false positives
|
||||
link-check:
|
||||
@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:
|
||||
@ -201,4 +201,4 @@ docs-develop:
|
||||
docs-build:
|
||||
@cd docs && \
|
||||
npm install && \
|
||||
npm run build
|
||||
npm run build
|
||||
|
@ -50,6 +50,10 @@ var (
|
||||
// function aliases
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
|
||||
CalculateBorrowRate = keeper.CalculateBorrowRate
|
||||
CalculateInterestFactor = keeper.CalculateInterestFactor
|
||||
APYToSPY = keeper.APYToSPY
|
||||
ClaimKey = types.ClaimKey
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
DefaultParams = types.DefaultParams
|
||||
|
@ -11,6 +11,17 @@ import (
|
||||
|
||||
// Borrow funds
|
||||
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
|
||||
err := k.ValidateBorrow(ctx, borrower, coins)
|
||||
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)
|
||||
if !found {
|
||||
borrow = types.NewBorrow(borrower, coins)
|
||||
} else {
|
||||
borrow.Amount = borrow.Amount.Add(coins...)
|
||||
return types.ErrBorrowNotFound // This should never happen
|
||||
}
|
||||
// Add the newly borrowed coins to the user's borrow object
|
||||
borrow.Amount = borrow.Amount.Add(coins...)
|
||||
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)
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHarvestBorrow,
|
||||
sdk.NewAttribute(types.AttributeKeyBorrower, borrow.Borrower.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyBorrower, borrower.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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
|
||||
if amount.IsZero() {
|
||||
@ -70,7 +132,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
|
||||
moneyMarket, ok := moneyMarketCache[coin.Denom]
|
||||
// Fetch money market and store in local cache
|
||||
if !ok {
|
||||
newMoneyMarket, found := k.GetMoneyMarket(ctx, coin.Denom)
|
||||
newMoneyMarket, found := k.GetMoneyMarketParam(ctx, coin.Denom)
|
||||
if !found {
|
||||
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]
|
||||
// Fetch money market and store in local cache
|
||||
if !ok {
|
||||
newMoneyMarket, found := k.GetMoneyMarket(ctx, deposit.Amount.Denom)
|
||||
newMoneyMarket, found := k.GetMoneyMarketParam(ctx, deposit.Amount.Denom)
|
||||
if !found {
|
||||
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]
|
||||
// Fetch money market and store in local cache
|
||||
if !ok {
|
||||
newMoneyMarket, found := k.GetMoneyMarket(ctx, borrowedCoin.Denom)
|
||||
newMoneyMarket, found := k.GetMoneyMarketParam(ctx, borrowedCoin.Denom)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", borrowedCoin.Denom)
|
||||
}
|
||||
|
@ -275,12 +275,12 @@ func (suite *KeeperTestSuite) TestBorrow() {
|
||||
),
|
||||
},
|
||||
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("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("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("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("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("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("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", 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", 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", 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", 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", 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)
|
||||
|
||||
|
@ -265,8 +265,8 @@ func (suite *KeeperTestSuite) TestClaim() {
|
||||
),
|
||||
},
|
||||
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("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("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", 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)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
|
||||
// Deposit deposit
|
||||
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin) error {
|
||||
|
||||
err := k.ValidateDeposit(ctx, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -59,6 +58,7 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.C
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no %s deposit found for %s", amount.Denom, depositor)
|
||||
}
|
||||
|
||||
if !deposit.Amount.IsGTE(amount) {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "%s>%s", amount, deposit.Amount)
|
||||
}
|
||||
|
@ -108,8 +108,8 @@ func (suite *KeeperTestSuite) TestDeposit() {
|
||||
),
|
||||
},
|
||||
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("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("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", 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)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
@ -251,8 +251,8 @@ func (suite *KeeperTestSuite) TestWithdraw() {
|
||||
),
|
||||
},
|
||||
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("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("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", 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)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
|
@ -2,30 +2,192 @@ package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"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) {
|
||||
denomSet := map[string]bool{}
|
||||
|
||||
params := k.GetParams(ctx)
|
||||
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 {
|
||||
k.SetInterestRateModel(ctx, mm.Denom, mm.InterestRateModel)
|
||||
continue
|
||||
moneyMarket = mm
|
||||
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
|
||||
}
|
||||
|
||||
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] {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
791
x/harvest/keeper/interest_test.go
Normal file
791
x/harvest/keeper/interest_test.go
Normal 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))
|
||||
}
|
@ -256,42 +256,99 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
||||
return borrowedCoins, true
|
||||
}
|
||||
|
||||
// GetInterestRateModel returns an interest rate model from the store for a denom
|
||||
func (k Keeper) GetInterestRateModel(ctx sdk.Context, denom string) (types.InterestRateModel, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix)
|
||||
// GetMoneyMarket returns a money market from the store for a denom
|
||||
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
||||
bz := store.Get([]byte(denom))
|
||||
if bz == nil {
|
||||
return types.InterestRateModel{}, false
|
||||
return types.MoneyMarket{}, false
|
||||
}
|
||||
var interestRateModel types.InterestRateModel
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &interestRateModel)
|
||||
return interestRateModel, true
|
||||
var moneyMarket types.MoneyMarket
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &moneyMarket)
|
||||
return moneyMarket, true
|
||||
}
|
||||
|
||||
// SetInterestRateModel sets an interest rate model in the store for a denom
|
||||
func (k Keeper) SetInterestRateModel(ctx sdk.Context, denom string, interestRateModel types.InterestRateModel) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(interestRateModel)
|
||||
// SetMoneyMarket sets a money market in the store for a denom
|
||||
func (k Keeper) SetMoneyMarket(ctx sdk.Context, denom string, moneyMarket types.MoneyMarket) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(moneyMarket)
|
||||
store.Set([]byte(denom), bz)
|
||||
}
|
||||
|
||||
// DeleteInterestRateModel deletes an interest rate model from the store
|
||||
func (k Keeper) DeleteInterestRateModel(ctx sdk.Context, denom string) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix)
|
||||
// DeleteMoneyMarket deletes a money market from the store
|
||||
func (k Keeper) DeleteMoneyMarket(ctx sdk.Context, denom string) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
||||
store.Delete([]byte(denom))
|
||||
}
|
||||
|
||||
// IterateInterestRateModels iterates over all interest rate model objects in the store and performs a callback function
|
||||
// that returns both the interest rate model value and the key it's stored under
|
||||
func (k Keeper) IterateInterestRateModels(ctx sdk.Context, cb func(denom string, interestRateModel types.InterestRateModel) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestRateModelsPrefix)
|
||||
// IterateMoneyMarkets iterates over all money markets objects in the store and performs a callback function
|
||||
// that returns both the money market and the key (denom) it's stored under
|
||||
func (k Keeper) IterateMoneyMarkets(ctx sdk.Context, cb func(denom string, moneyMarket types.MoneyMarket) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, []byte{})
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var interestRateModel types.InterestRateModel
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &interestRateModel)
|
||||
if cb(string(iterator.Key()), interestRateModel) {
|
||||
var moneyMarket types.MoneyMarket
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &moneyMarket)
|
||||
if cb(string(iterator.Key()), moneyMarket) {
|
||||
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)
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
// Test suite used for all keeper tests
|
||||
type KeeperTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
keeper keeper.Keeper
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
@ -157,45 +156,52 @@ func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
|
||||
func (suite *KeeperTestSuite) TestGetSetDeleteInterestRateModel() {
|
||||
denom := "test"
|
||||
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.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().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)
|
||||
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestIterateInterestRateModels() {
|
||||
testDenom := "test"
|
||||
var setModels types.InterestRateModels
|
||||
var setMMs types.MoneyMarkets
|
||||
var setDenoms []string
|
||||
for i := 0; i < 5; i++ {
|
||||
// Initialize a new money market
|
||||
denom := testDenom + strconv.Itoa(i)
|
||||
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
|
||||
setDenoms = append(setDenoms, denom)
|
||||
setModels = append(setModels, model)
|
||||
setMMs = append(setMMs, moneyMarket)
|
||||
}
|
||||
|
||||
var seenModels types.InterestRateModels
|
||||
var seenMMs types.MoneyMarkets
|
||||
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)
|
||||
seenModels = append(seenModels, i)
|
||||
seenMMs = append(seenMMs, i)
|
||||
return false
|
||||
})
|
||||
|
||||
suite.Require().Equal(setModels, seenModels)
|
||||
suite.Require().Equal(setMMs, seenMMs)
|
||||
suite.Require().Equal(setDenoms, seenDenoms)
|
||||
}
|
||||
|
||||
@ -204,11 +210,21 @@ func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Accou
|
||||
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 {
|
||||
sk := suite.app.GetSupplyKeeper()
|
||||
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) {
|
||||
suite.Run(t, new(KeeperTestSuite))
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.Deleg
|
||||
return types.DelegatorDistributionSchedule{}, false
|
||||
}
|
||||
|
||||
// GetMoneyMarket returns the corresponding Money Market param for a specific denom
|
||||
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
||||
// GetMoneyMarketParam returns the corresponding Money Market param for a specific denom
|
||||
func (k Keeper) GetMoneyMarketParam(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
||||
params := k.GetParams(ctx)
|
||||
for _, mm := range params.MoneyMarkets {
|
||||
if mm.Denom == denom {
|
||||
|
@ -75,8 +75,8 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
|
||||
),
|
||||
},
|
||||
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("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("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", 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)
|
||||
tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
@ -443,8 +443,8 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState {
|
||||
),
|
||||
},
|
||||
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("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("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", 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,
|
||||
|
@ -291,8 +291,8 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
|
||||
),
|
||||
},
|
||||
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("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("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", 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)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
|
@ -4,16 +4,35 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// BorrowIndexItem defines an individual borrow index
|
||||
type BorrowIndexItem struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
Value sdk.Dec `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// NewBorrowIndexItem returns a new BorrowIndexItem instance
|
||||
func NewBorrowIndexItem(denom string, value sdk.Dec) BorrowIndexItem {
|
||||
return BorrowIndexItem{
|
||||
Denom: denom,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// BorrowIndexes is a slice of BorrowIndexItem, because Amino won't marshal maps
|
||||
type BorrowIndexes []BorrowIndexItem
|
||||
|
||||
// Borrow defines an amount of coins borrowed from a harvest module account
|
||||
type Borrow struct {
|
||||
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
||||
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
||||
Index BorrowIndexes `json:"index" yaml:"index"`
|
||||
}
|
||||
|
||||
// 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{
|
||||
Borrower: borrower,
|
||||
Amount: amount,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
@ -55,4 +55,8 @@ var (
|
||||
ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation")
|
||||
// ErrBorrowEmptyCoins error for when you cannot borrow empty 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")
|
||||
)
|
||||
|
@ -37,7 +37,10 @@ var (
|
||||
ClaimsKeyPrefix = []byte{0x04}
|
||||
BorrowsKeyPrefix = []byte{0x05}
|
||||
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(":")
|
||||
)
|
||||
|
||||
|
@ -251,6 +251,20 @@ func (bl BorrowLimit) Validate() error {
|
||||
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
|
||||
type MoneyMarket struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
@ -258,17 +272,19 @@ type MoneyMarket struct {
|
||||
SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"`
|
||||
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
|
||||
InterestRateModel InterestRateModel `json:"interest_rate_model" yaml:"interest_rate_model"`
|
||||
ReserveFactor sdk.Dec `json:"reserve_factor" yaml:"reserve_factor"`
|
||||
}
|
||||
|
||||
// NewMoneyMarket returns a new MoneyMarket
|
||||
func NewMoneyMarket(denom string, hasMaxLimit bool, maximumLimit, loanToValue sdk.Dec,
|
||||
spotMarketID string, conversionFactor sdk.Int, interestRateModel InterestRateModel) MoneyMarket {
|
||||
func NewMoneyMarket(denom string, borrowLimit BorrowLimit, spotMarketID string,
|
||||
conversionFactor sdk.Int, interestRateModel InterestRateModel, reserveFactor sdk.Dec) MoneyMarket {
|
||||
return MoneyMarket{
|
||||
Denom: denom,
|
||||
BorrowLimit: NewBorrowLimit(hasMaxLimit, maximumLimit, loanToValue),
|
||||
BorrowLimit: borrowLimit,
|
||||
SpotMarketID: spotMarketID,
|
||||
ConversionFactor: conversionFactor,
|
||||
InterestRateModel: interestRateModel,
|
||||
ReserveFactor: reserveFactor,
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,9 +301,36 @@ func (mm MoneyMarket) Validate() error {
|
||||
if err := mm.InterestRateModel.Validate(); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
func (irm InterestRateModel) Equal(comparisonIRM InterestRateModel) bool {
|
||||
if !irm.BaseRateAPY.Equal(comparisonIRM.BaseRateAPY) {
|
||||
func (irm InterestRateModel) Equal(irmCompareTo InterestRateModel) bool {
|
||||
if !irm.BaseRateAPY.Equal(irmCompareTo.BaseRateAPY) {
|
||||
return false
|
||||
}
|
||||
if !irm.BaseMultiplier.Equal(comparisonIRM.BaseMultiplier) {
|
||||
if !irm.BaseMultiplier.Equal(irmCompareTo.BaseMultiplier) {
|
||||
return false
|
||||
}
|
||||
if !irm.Kink.Equal(comparisonIRM.Kink) {
|
||||
if !irm.Kink.Equal(irmCompareTo.Kink) {
|
||||
return false
|
||||
}
|
||||
if !irm.JumpMultiplier.Equal(comparisonIRM.JumpMultiplier) {
|
||||
if !irm.JumpMultiplier.Equal(irmCompareTo.JumpMultiplier) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
Loading…
Reference in New Issue
Block a user