mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05: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
|
# 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:
|
||||||
@ -201,4 +201,4 @@ docs-develop:
|
|||||||
docs-build:
|
docs-build:
|
||||||
@cd docs && \
|
@cd docs && \
|
||||||
npm install && \
|
npm install && \
|
||||||
npm run build
|
npm run build
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
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
|
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)
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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)})
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
)
|
)
|
||||||
|
@ -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(":")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user