Hard: suppliers earn interest (#749)

* update to borrow interest factor

* add supply interest factor to accrue interest

* supply interest factor keeper methods

* fix potential bug with user borrow indexing

* sync supply interest on deposit/withdraw

* separate withdraw/deposit

* relocate interest sync methods

* update comment

* simplify supply interest statement

* check truncated int not zero

* add .sub(storedAmount) back

* add store key suppliedcoins

* increment/decrement supplied coins

* update withdraw with new accounting

* update withdraw test

* catch repay edge case

* unit tests

* TestSupplyInterest scaffolding

* test notes

* temp: interest test

* example test

* changes, test checks more state

* fix: calculate supply interest directly

* fix: catch divide by zero

* add state checks back into interest test

* add snapshot test cases

* test owed supplied interest paid at correct ratio

* test user supply syncs user's borrow interest

* remove print statements and clean up

* refactor indented logic

* test supply/borrow multiple coins

* update decoder test

Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
Denali Marsh 2021-01-07 11:23:05 +01:00 committed by GitHub
parent e9f5043c84
commit f7a73c9245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1458 additions and 607 deletions

View File

@ -53,7 +53,8 @@ var (
NewQuerier = keeper.NewQuerier
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
CalculateBorrowRate = keeper.CalculateBorrowRate
CalculateInterestFactor = keeper.CalculateInterestFactor
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
APYToSPY = keeper.APYToSPY
ClaimKey = types.ClaimKey
DefaultGenesisState = types.DefaultGenesisState

View File

@ -13,11 +13,11 @@ import (
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 {
_, foundInterestFactor := k.GetInterestFactor(ctx, coin.Denom)
_, foundInterestFactor := k.GetBorrowInterestFactor(ctx, coin.Denom)
if !foundInterestFactor {
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
if foundMm {
k.SetInterestFactor(ctx, coin.Denom, sdk.OneDec())
k.SetBorrowInterestFactor(ctx, coin.Denom, sdk.OneDec())
}
}
}
@ -28,11 +28,8 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
return err
}
// If the user has an existing borrow, sync its outstanding interest
_, found := k.GetBorrow(ctx, borrower)
if found {
k.SyncOutstandingInterest(ctx, borrower)
}
// Sync any outstanding interest
k.SyncBorrowInterest(ctx, borrower)
// Validate borrow amount within user and protocol limits
err = k.ValidateBorrow(ctx, borrower, coins)
@ -57,22 +54,39 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
}
}
// The first time a user borrows a denom we add it the user's borrow interest factor index
var borrowInterestFactors types.BorrowInterestFactors
currBorrow, foundBorrow := k.GetBorrow(ctx, borrower)
// On user's first borrow, build borrow index list containing denoms and current global borrow index value
// We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps.
if !found {
var interestFactors types.InterestFactors
if foundBorrow {
// If the coin denom to be borrowed is not in the user's existing borrow, we add it borrow index
for _, coin := range coins {
interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
interestFactor := types.NewInterestFactor(coin.Denom, interestFactorValue)
interestFactors = append(interestFactors, interestFactor)
if !sdk.NewCoins(coin).DenomsSubsetOf(currBorrow.Amount) {
borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
}
}
// Concatenate new borrow interest factors to existing borrow interest factors
borrowInterestFactors = append(borrowInterestFactors, currBorrow.Index...)
} else {
for _, coin := range coins {
borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
}
borrow := types.NewBorrow(borrower, sdk.Coins{}, interestFactors)
k.SetBorrow(ctx, borrow)
}
// Add the newly borrowed coins to the user's borrow object
borrow, _ := k.GetBorrow(ctx, borrower)
borrow.Amount = borrow.Amount.Add(coins...)
// Calculate new borrow amount
var amount sdk.Coins
if foundBorrow {
amount = currBorrow.Amount.Add(coins...)
} else {
amount = coins
}
// Update the borrower's amount and borrow interest factors in the store
borrow := types.NewBorrow(borrower, amount, borrowInterestFactors)
k.SetBorrow(ctx, borrow)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower)
@ -92,46 +106,6 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
return nil
}
// SyncOutstandingInterest updates the user's owed interest on newly borrowed coins to the latest global state
func (k Keeper) SyncOutstandingInterest(ctx sdk.Context, addr sdk.AccAddress) {
totalNewInterest := sdk.Coins{}
// Update user's borrow index list for each asset in the 'coins' array.
// We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps.
borrow, found := k.GetBorrow(ctx, addr)
if !found {
return
}
for _, coin := range borrow.Amount {
// Locate the borrow index item by coin denom in the user's list of borrow indexes
foundAtIndex := -1
for i := range borrow.Index {
if borrow.Index[i].Denom == coin.Denom {
foundAtIndex = i
break
}
}
interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has borrowed this denom
borrow.Index = append(borrow.Index, types.NewInterestFactor(coin.Denom, interestFactorValue))
} else { // User has an existing borrow index for this denom
// Calculate interest owed by user since asset's last borrow index update
storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom))
userLastInterestFactor := borrow.Index[foundAtIndex].Value
interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount)
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
// We're synced up, so update user's borrow index value to match the current global borrow index value
borrow.Index[foundAtIndex].Value = interestFactorValue
}
}
// Add all pending interest to user's borrow
borrow.Amount = borrow.Amount.Add(totalNewInterest...)
// Update user's borrow in the store
k.SetBorrow(ctx, borrow)
}
// ValidateBorrow validates a borrow request against borrower and protocol requirements
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
if amount.IsZero() {
@ -240,7 +214,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
return nil
}
// IncrementBorrowedCoins increments the amount of borrowed coins by the newCoins parameter
// IncrementBorrowedCoins increments the total amount of borrowed coins by the newCoins parameter
func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
borrowedCoins, found := k.GetBorrowedCoins(ctx)
if !found {
@ -252,7 +226,7 @@ func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
}
}
// DecrementBorrowedCoins decrements the amount of borrowed coins by the coins parameter
// DecrementBorrowedCoins decrements the total amount of borrowed coins by the coins parameter
func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
borrowedCoins, found := k.GetBorrowedCoins(ctx)
if !found {
@ -272,10 +246,13 @@ func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.Coins {
borrowBalance := sdk.Coins{}
borrow, found := k.GetBorrow(ctx, borrower)
if found {
if !found {
return borrowBalance
}
totalNewInterest := sdk.Coins{}
for _, coin := range borrow.Amount {
interestFactorValue, foundInterestFactorValue := k.GetInterestFactor(ctx, coin.Denom)
interestFactorValue, foundInterestFactorValue := k.GetBorrowInterestFactor(ctx, coin.Denom)
if foundInterestFactorValue {
// Locate the interest factor by coin denom in the user's list of interest factors
foundAtIndex := -1
@ -294,7 +271,6 @@ func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.C
}
}
}
borrowBalance = borrow.Amount.Add(totalNewInterest...)
}
return borrowBalance
return borrow.Amount.Add(totalNewInterest...)
}

View File

@ -12,13 +12,26 @@ import (
// Deposit deposit
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
// Set any new denoms' global supply index to 1.0
for _, coin := range coins {
_, foundInterestFactor := k.GetSupplyInterestFactor(ctx, coin.Denom)
if !foundInterestFactor {
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
if foundMm {
k.SetSupplyInterestFactor(ctx, coin.Denom, sdk.OneDec())
}
}
}
// Get current stored LTV based on stored borrows/deposits
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
if err != nil {
return err
}
k.SyncOutstandingInterest(ctx, depositor)
// Sync any outstanding interest
k.SyncBorrowInterest(ctx, depositor)
k.SyncSupplyInterest(ctx, depositor)
err = k.ValidateDeposit(ctx, coins)
if err != nil {
@ -44,17 +57,47 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi
return err
}
deposit, found := k.GetDeposit(ctx, depositor)
if !found {
deposit = types.NewDeposit(depositor, coins)
// The first time a user deposits a denom we add it the user's supply interest factor index
var supplyInterestFactors types.SupplyInterestFactors
currDeposit, foundDeposit := k.GetDeposit(ctx, depositor)
// On user's first deposit, build deposit index list containing denoms and current global deposit index value
if foundDeposit {
// If the coin denom to be deposited is not in the user's existing deposit, we add it deposit index
for _, coin := range coins {
if !sdk.NewCoins(coin).DenomsSubsetOf(currDeposit.Amount) {
supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
}
}
// Concatenate new deposit interest factors to existing deposit interest factors
supplyInterestFactors = append(supplyInterestFactors, currDeposit.Index...)
} else {
deposit.Amount = deposit.Amount.Add(coins...)
for _, coin := range coins {
supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
}
}
// Calculate new deposit amount
var amount sdk.Coins
if foundDeposit {
amount = currDeposit.Amount.Add(coins...)
} else {
amount = coins
}
// Update the depositer's amount and supply interest factors in the store
deposit := types.NewDeposit(depositor, amount, supplyInterestFactors)
k.SetDeposit(ctx, deposit)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
// Update total supplied amount by newly supplied coins. Don't add user's pending interest as
// it has already been included in the total supplied coins by the BeginBlocker.
k.IncrementSuppliedCoins(ctx, coins)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHardDeposit,
@ -84,76 +127,37 @@ func (k Keeper) ValidateDeposit(ctx sdk.Context, coins sdk.Coins) error {
return nil
}
// Withdraw returns some or all of a deposit back to original depositor
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
deposit, found := k.GetDeposit(ctx, depositor)
if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
}
// Get current stored LTV based on stored borrows/deposits
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
if err != nil {
return err
}
k.SyncOutstandingInterest(ctx, depositor)
borrow, found := k.GetBorrow(ctx, depositor)
if !found {
borrow = types.Borrow{}
}
proposedDepositAmount, isNegative := deposit.Amount.SafeSub(coins)
if isNegative {
return types.ErrNegativeBorrowedCoins
}
proposedDeposit := types.NewDeposit(deposit.Depositor, proposedDepositAmount)
valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
if err != nil {
return err
}
if !valid {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, coins)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHardWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
),
)
if deposit.Amount.IsEqual(coins) {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDeleteHardDeposit,
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
),
)
k.DeleteDeposit(ctx, deposit)
return nil
}
deposit.Amount = deposit.Amount.Sub(coins)
k.SetDeposit(ctx, deposit)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
return nil
}
// GetTotalDeposited returns the total amount deposited for the input deposit type and deposit denom
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositDenom string) (total sdk.Int) {
var macc supplyExported.ModuleAccountI
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
return macc.GetCoins().AmountOf(depositDenom)
}
// IncrementSuppliedCoins increments the total amount of supplied coins by the newCoins parameter
func (k Keeper) IncrementSuppliedCoins(ctx sdk.Context, newCoins sdk.Coins) {
suppliedCoins, found := k.GetSuppliedCoins(ctx)
if !found {
if !newCoins.Empty() {
k.SetSuppliedCoins(ctx, newCoins)
}
} else {
k.SetSuppliedCoins(ctx, suppliedCoins.Add(newCoins...))
}
}
// DecrementSuppliedCoins decrements the total amount of supplied coins by the coins parameter
func (k Keeper) DecrementSuppliedCoins(ctx sdk.Context, coins sdk.Coins) error {
suppliedCoins, found := k.GetSuppliedCoins(ctx)
if !found {
return sdkerrors.Wrapf(types.ErrSuppliedCoinsNotFound, "cannot withdraw if no coins are deposited")
}
updatedSuppliedCoins, isAnyNegative := suppliedCoins.SafeSub(coins)
if isAnyNegative {
return types.ErrNegativeSuppliedCoins
}
k.SetSuppliedCoins(ctx, updatedSuppliedCoins)
return nil
}

View File

@ -196,360 +196,3 @@ func (suite *KeeperTestSuite) TestDeposit() {
})
}
}
func (suite *KeeperTestSuite) TestWithdraw() {
type args struct {
depositor sdk.AccAddress
depositAmount sdk.Coins
withdrawAmount sdk.Coins
createDeposit bool
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
depositExists bool
finalDepositAmount sdk.Coins
}
type errArgs struct {
expectPass bool
contains string
}
type withdrawTest struct {
name string
args args
errArgs errArgs
}
testCases := []withdrawTest{
{
"valid partial withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
depositExists: true,
finalDepositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid full withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.Coins(nil),
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"withdraw invalid, denom not found",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "subtraction results in negative borrow amount",
},
},
{
"withdraw exceeds deposit",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "subtraction results in negative borrow amount",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
loanToValue := sdk.MustNewDecFromStr("0.6")
hardGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(100000000), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
},
0, // LTV counter
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
// Pricefeed module genesis state
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("10.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
},
}
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
if tc.args.createDeposit {
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
suite.Require().NoError(err)
}
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.depositor)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
if tc.args.depositExists {
suite.Require().True(f)
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
} else {
suite.Require().False(f)
}
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestLtvWithdraw() {
type args struct {
borrower sdk.AccAddress
keeper sdk.AccAddress
initialModuleCoins sdk.Coins
initialBorrowerCoins sdk.Coins
initialKeeperCoins sdk.Coins
depositCoins []sdk.Coin
borrowCoins sdk.Coins
futureTime int64
}
type errArgs struct {
expectPass bool
contains string
}
type liqTest struct {
name string
args args
errArgs errArgs
}
// Set up test constants
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
reserveFactor := sdk.MustNewDecFromStr("0.05")
oneMonthInSeconds := int64(2592000)
borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
keeper := sdk.AccAddress(crypto.AddressHash([]byte("testkeeper")))
testCases := []liqTest{
{
"invalid: withdraw is outside loan-to-value range",
args{
borrower: borrower,
keeper: keeper,
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))),
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))),
futureTime: oneMonthInSeconds,
},
errArgs{
expectPass: false,
contains: "proposed withdraw outside loan-to-value range",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// Auth module genesis state
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.borrower, tc.args.keeper},
[]sdk.Coins{tc.args.initialBorrowerCoins, tc.args.initialKeeperCoins},
)
// Harvest module genesis state
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("ukava",
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"kava:usd", // Market ID
sdk.NewInt(KAVA_CF), // Conversion Factor
sdk.NewInt(100000000*KAVA_CF), // Auction Size
model, // Interest Rate Model
reserveFactor, // Reserve Factor
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
},
0, // LTV counter
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
// Pricefeed module genesis state
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
},
}
// Initialize test application
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
// Mint coins to Harvest module account
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
auctionKeeper := tApp.GetAuctionKeeper()
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.auctionKeeper = auctionKeeper
var err error
// Run begin blocker to set up state
hard.BeginBlocker(suite.ctx, suite.keeper)
// Deposit coins
err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
suite.Require().NoError(err)
// Borrow coins
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
suite.Require().NoError(err)
// Attempting to withdraw fails
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Set up future chain context and run begin blocker, increasing user's owed borrow balance
runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
liqCtx := suite.ctx.WithBlockTime(runAtTime)
hard.BeginBlocker(liqCtx, suite.keeper)
// Attempted withdraw of 1 coin still fails
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Repay the initial principal
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
suite.Require().NoError(err)
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Withdrawing half the coins should succeed
withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(2))))
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
suite.Require().NoError(err)
})
}
}

View File

@ -69,14 +69,13 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
return nil
}
// Get available hard module account cash on hand
// Get current protocol state and hold in memory as 'prior'
cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
// Get prior borrows
borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
if foundBorrowCoinsPrior {
borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
borrowedPrior := sdk.NewCoin(denom, sdk.ZeroInt())
borrowedCoinsPrior, foundBorrowedCoinsPrior := k.GetBorrowedCoins(ctx)
if foundBorrowedCoinsPrior {
borrowedPrior = sdk.NewCoin(denom, borrowedCoinsPrior.AmountOf(denom))
}
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
@ -86,11 +85,18 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
reservesPrior = newReservesPrior
}
interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, denom)
if !foundInterestFactorPrior {
newInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
k.SetInterestFactor(ctx, denom, newInterestFactorPrior)
interestFactorPrior = newInterestFactorPrior
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := k.GetBorrowInterestFactor(ctx, denom)
if !foundBorrowInterestFactorPrior {
newBorrowInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
k.SetBorrowInterestFactor(ctx, denom, newBorrowInterestFactorPrior)
borrowInterestFactorPrior = newBorrowInterestFactorPrior
}
supplyInterestFactorPrior, foundSupplyInterestFactorPrior := k.GetSupplyInterestFactor(ctx, denom)
if !foundSupplyInterestFactorPrior {
newSupplyInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
k.SetSupplyInterestFactor(ctx, denom, newSupplyInterestFactorPrior)
supplyInterestFactorPrior = newSupplyInterestFactorPrior
}
// Fetch money market from the store
@ -100,7 +106,7 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
}
// GetBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowsPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowedPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
if err != nil {
return err
}
@ -111,16 +117,27 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
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()))
interestFactorNew := interestFactorPrior.Mul(interestFactor)
// Calculate borrow interest factor and update
borrowInterestFactor := CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
interestBorrowAccumulated := (borrowInterestFactor.Mul(sdk.NewDecFromInt(borrowedPrior.Amount)).TruncateInt()).Sub(borrowedPrior.Amount)
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestBorrowAccumulated))
reservesNew := interestBorrowAccumulated.ToDec().Mul(mm.ReserveFactor).TruncateInt()
borrowInterestFactorNew := borrowInterestFactorPrior.Mul(borrowInterestFactor)
k.SetBorrowInterestFactor(ctx, denom, borrowInterestFactorNew)
k.SetInterestFactor(ctx, denom, interestFactorNew)
// Calculate supply interest factor and update
supplyInterestNew := interestBorrowAccumulated.Sub(reservesNew)
supplyInterestFactor := CalculateSupplyInterestFactor(supplyInterestNew.ToDec(), cashPrior.ToDec(), borrowedPrior.Amount.ToDec(), reservesPrior.Amount.ToDec())
supplyInterestFactorNew := supplyInterestFactorPrior.Mul(supplyInterestFactor)
k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
// Update accural keys in store
k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
k.SetTotalReserves(ctx, denom, totalReservesNew)
k.IncrementSuppliedCoins(ctx, sdk.NewCoins(sdk.NewCoin(denom, supplyInterestNew)))
k.SetTotalReserves(ctx, denom, reservesPrior.Add(sdk.NewCoin(mm.Denom, reservesNew)))
k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
@ -155,10 +172,10 @@ func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
}
// CalculateInterestFactor calculates the simple interest scaling factor,
// CalculateBorrowInterestFactor calculates the simple interest scaling factor,
// which is equal to: (per-second interest rate * number of seconds elapsed)
// Will return 1.000x, multiply by principal to get new principal with added interest
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
func CalculateBorrowInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
scalingFactorInt := sdk.NewInt(int64(scalingFactor))
@ -173,6 +190,100 @@ func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.I
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
}
// CalculateSupplyInterestFactor calculates the supply interest factor, which is the percentage of borrow interest
// that flows to each unit of supply, i.e. at 50% utilization and 0% reserve factor, a 5% borrow interest will
// correspond to a 2.5% supply interest.
func CalculateSupplyInterestFactor(newInterest, cash, borrows, reserves sdk.Dec) sdk.Dec {
totalSupply := cash.Add(borrows).Sub(reserves)
if totalSupply.IsZero() {
return sdk.OneDec()
}
return (newInterest.Quo(totalSupply)).Add(sdk.OneDec())
}
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, addr sdk.AccAddress) {
totalNewInterest := sdk.Coins{}
// Update user's borrow interest factor list for each asset in the 'coins' array.
// We use a list of BorrowInterestFactors here because Amino doesn't support marshaling maps.
borrow, found := k.GetBorrow(ctx, addr)
if !found {
return
}
for _, coin := range borrow.Amount {
// Locate the borrow interest factor item by coin denom in the user's list of borrow indexes
foundAtIndex := -1
for i := range borrow.Index {
if borrow.Index[i].Denom == coin.Denom {
foundAtIndex = i
break
}
}
interestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has borrowed this denom
borrow.Index = append(borrow.Index, types.NewBorrowInterestFactor(coin.Denom, interestFactorValue))
} else { // User has an existing borrow index for this denom
// Calculate interest owed by user since asset's last borrow index update
storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom))
userLastInterestFactor := borrow.Index[foundAtIndex].Value
interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount)
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
// We're synced up, so update user's borrow index value to match the current global borrow index value
borrow.Index[foundAtIndex].Value = interestFactorValue
}
}
// Add all pending interest to user's borrow
borrow.Amount = borrow.Amount.Add(totalNewInterest...)
// Update user's borrow in the store
k.SetBorrow(ctx, borrow)
}
// SyncSupplyInterest updates the user's earned interest on supplied coins based on the latest global state
func (k Keeper) SyncSupplyInterest(ctx sdk.Context, addr sdk.AccAddress) {
totalNewInterest := sdk.Coins{}
// Update user's supply index list for each asset in the 'coins' array.
// We use a list of SupplyInterestFactors here because Amino doesn't support marshaling maps.
deposit, found := k.GetDeposit(ctx, addr)
if !found {
return
}
for _, coin := range deposit.Amount {
// Locate the deposit index item by coin denom in the user's list of deposit indexes
foundAtIndex := -1
for i := range deposit.Index {
if deposit.Index[i].Denom == coin.Denom {
foundAtIndex = i
break
}
}
interestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
if foundAtIndex == -1 { // First time user has supplied this denom
deposit.Index = append(deposit.Index, types.NewSupplyInterestFactor(coin.Denom, interestFactorValue))
} else { // User has an existing supply index for this denom
// Calculate interest earned by user since asset's last deposit index update
storedAmount := sdk.NewDecFromInt(deposit.Amount.AmountOf(coin.Denom))
userLastInterestFactor := deposit.Index[foundAtIndex].Value
interest := (storedAmount.Mul(interestFactorValue).Quo(userLastInterestFactor)).Sub(storedAmount)
if interest.TruncateInt().GT(sdk.ZeroInt()) {
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
}
// We're synced up, so update user's deposit index value to match the current global deposit index value
deposit.Index[foundAtIndex].Value = interestFactorValue
}
}
// Add all pending interest to user's deposit
deposit.Amount = deposit.Amount.Add(totalNewInterest...)
// Update user's deposit in the store
k.SetDeposit(ctx, deposit)
}
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {

View File

@ -202,7 +202,7 @@ func (suite *InterestTestSuite) TestCalculateBorrowRate() {
}
}
func (suite *InterestTestSuite) TestCalculateInterestFactor() {
func (suite *InterestTestSuite) TestCalculateBorrowInterestFactor() {
type args struct {
perSecondInterestRate sdk.Dec
timeElapsed sdk.Int
@ -302,7 +302,65 @@ func (suite *InterestTestSuite) TestCalculateInterestFactor() {
}
for _, tc := range testCases {
interestFactor := hard.CalculateInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
interestFactor := hard.CalculateBorrowInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
suite.Require().Equal(tc.args.expectedValue, interestFactor)
}
}
func (suite *InterestTestSuite) TestCalculateSupplyInterestFactor() {
type args struct {
newInterest sdk.Dec
cash sdk.Dec
borrows sdk.Dec
reserves sdk.Dec
reserveFactor sdk.Dec
expectedValue sdk.Dec
}
type test struct {
name string
args args
}
testCases := []test{
{
"low new interest",
args{
newInterest: sdk.MustNewDecFromStr("1"),
cash: sdk.MustNewDecFromStr("100.0"),
borrows: sdk.MustNewDecFromStr("1000.0"),
reserves: sdk.MustNewDecFromStr("10.0"),
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedValue: sdk.MustNewDecFromStr("1.000917431192660550"),
},
},
{
"medium new interest",
args{
newInterest: sdk.MustNewDecFromStr("5"),
cash: sdk.MustNewDecFromStr("100.0"),
borrows: sdk.MustNewDecFromStr("1000.0"),
reserves: sdk.MustNewDecFromStr("10.0"),
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedValue: sdk.MustNewDecFromStr("1.004587155963302752"),
},
},
{
"high new interest",
args{
newInterest: sdk.MustNewDecFromStr("10"),
cash: sdk.MustNewDecFromStr("100.0"),
borrows: sdk.MustNewDecFromStr("1000.0"),
reserves: sdk.MustNewDecFromStr("10.0"),
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedValue: sdk.MustNewDecFromStr("1.009174311926605505"),
},
},
}
for _, tc := range testCases {
interestFactor := hard.CalculateSupplyInterestFactor(tc.args.newInterest,
tc.args.cash, tc.args.borrows, tc.args.reserves)
suite.Require().Equal(tc.args.expectedValue, interestFactor)
}
}
@ -377,7 +435,6 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
true,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
spy, err := hard.APYToSPY(tc.args.apy)
@ -391,13 +448,13 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
}
}
type ExpectedInterest struct {
type ExpectedBorrowInterest struct {
elapsedTime int64
shouldBorrow bool
borrowCoin sdk.Coin
}
func (suite *KeeperTestSuite) TestInterest() {
func (suite *KeeperTestSuite) TestBorrowInterest() {
type args struct {
user sdk.AccAddress
initialBorrowerCoins sdk.Coins
@ -406,7 +463,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins sdk.Coins
interestRateModel types.InterestRateModel
reserveFactor sdk.Dec
expectedInterestSnaphots []ExpectedInterest
expectedInterestSnaphots []ExpectedBorrowInterest
}
type errArgs struct {
@ -438,7 +495,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneDayInSeconds,
shouldBorrow: false,
@ -461,7 +518,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneWeekInSeconds,
shouldBorrow: false,
@ -484,7 +541,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
@ -507,7 +564,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: false,
@ -530,7 +587,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: false,
@ -553,7 +610,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneYearInSeconds,
shouldBorrow: true,
@ -576,7 +633,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneMonthInSeconds,
shouldBorrow: false,
@ -604,7 +661,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedInterest{
expectedInterestSnaphots: []ExpectedBorrowInterest{
{
elapsedTime: oneDayInSeconds,
shouldBorrow: false,
@ -633,6 +690,7 @@ func (suite *KeeperTestSuite) TestInterest() {
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Initialize test app and set context
@ -737,7 +795,7 @@ func (suite *KeeperTestSuite) TestInterest() {
reservesPrior = sdk.NewCoin(tc.args.borrowCoinDenom, sdk.ZeroInt())
}
interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetInterestFactor(prevCtx, tc.args.borrowCoinDenom)
interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, tc.args.borrowCoinDenom)
suite.Require().True(foundInterestFactorPrior)
// 2. Calculate expected interest owed
@ -748,7 +806,7 @@ func (suite *KeeperTestSuite) TestInterest() {
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
suite.Require().NoError(err)
interestFactor := hard.CalculateInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
interestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
expectedInterest := (interestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
expectedReserves := reservesPrior.Add(sdk.NewCoin(tc.args.borrowCoinDenom, sdk.NewDecFromInt(expectedInterest).Mul(tc.args.reserveFactor).TruncateInt()))
expectedInterestFactor := interestFactorPrior.Mul(interestFactor)
@ -769,7 +827,7 @@ func (suite *KeeperTestSuite) TestInterest() {
suite.Require().Equal(expectedReserves, currTotalReserves)
// Check that the borrow index has increased as expected
currIndexPrior, _ := suite.keeper.GetInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
currIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
suite.Require().Equal(expectedInterestFactor, currIndexPrior)
// After borrowing again user's borrow balance should have any outstanding interest applied
@ -791,6 +849,495 @@ func (suite *KeeperTestSuite) TestInterest() {
}
}
type ExpectedSupplyInterest struct {
elapsedTime int64
shouldSupply bool
supplyCoin sdk.Coin
}
func (suite *KeeperTestSuite) TestSupplyInterest() {
type args struct {
user sdk.AccAddress
initialSupplierCoins sdk.Coins
initialBorrowerCoins sdk.Coins
initialModuleCoins sdk.Coins
depositCoins sdk.Coins
coinDenoms []string
borrowCoins sdk.Coins
interestRateModel types.InterestRateModel
reserveFactor sdk.Dec
expectedInterestSnaphots []ExpectedSupplyInterest
}
type errArgs struct {
expectPass bool
contains string
}
type interestTest struct {
name string
args args
errArgs errArgs
}
normalModel := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
oneDayInSeconds := int64(86400)
oneWeekInSeconds := int64(604800)
oneMonthInSeconds := int64(2592000)
oneYearInSeconds := int64(31536000)
testCases := []interestTest{
{
"one day",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneDayInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"one week",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneWeekInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"one month",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"one year",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneYearInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"supply/borrow multiple coins",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(20*BNB_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"supply during snapshot",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneMonthInSeconds,
shouldSupply: true,
supplyCoin: sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)),
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"multiple snapshots",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"varied snapshots",
args{
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
coinDenoms: []string{"ukava"},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
interestRateModel: normalModel,
reserveFactor: sdk.MustNewDecFromStr("0.05"),
expectedInterestSnaphots: []ExpectedSupplyInterest{
{
elapsedTime: oneMonthInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
{
elapsedTime: oneDayInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
{
elapsedTime: oneYearInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
{
elapsedTime: oneWeekInSeconds,
shouldSupply: false,
supplyCoin: sdk.Coin{},
},
},
},
errArgs{
expectPass: true,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// Auth module genesis state
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.user},
[]sdk.Coins{tc.args.initialBorrowerCoins},
)
// Hard module genesis state
hardGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("ukava",
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"kava:usd", // Market ID
sdk.NewInt(KAVA_CF), // Conversion Factor
sdk.NewInt(USDX_CF*1000), // Auction Size
tc.args.interestRateModel, // Interest Rate Model
tc.args.reserveFactor, // Reserve Factor
sdk.ZeroDec()), // Keeper Reward Percentage
types.NewMoneyMarket("bnb",
types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"bnb:usd", // Market ID
sdk.NewInt(BNB_CF), // Conversion Factor
sdk.NewInt(USDX_CF*1000), // Auction Size
tc.args.interestRateModel, // Interest Rate Model
tc.args.reserveFactor, // Reserve Factor
sdk.ZeroDec()), // Keeper Reward Percentage
},
0, // LTV counter
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
// Pricefeed module genesis state
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("20.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
},
}
// Initialize test application
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
// Mint coins to Hard module account
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.keeper.SetSuppliedCoins(ctx, tc.args.initialModuleCoins)
var err error
// Run begin blocker
hard.BeginBlocker(suite.ctx, suite.keeper)
// // Deposit coins
err = suite.keeper.Deposit(suite.ctx, tc.args.user, tc.args.depositCoins)
suite.Require().NoError(err)
// Borrow coins
err = suite.keeper.Borrow(suite.ctx, tc.args.user, tc.args.borrowCoins)
suite.Require().NoError(err)
// Check interest levels for each snapshot
prevCtx := suite.ctx
for _, snapshot := range tc.args.expectedInterestSnaphots {
for _, coinDenom := range tc.args.coinDenoms {
// ---------------------------- Calculate expected supply interest ----------------------------
// 1. Get cash, borrows, reserves, and borrow index
cashPrior := suite.getModuleAccountAtCtx(types.ModuleName, prevCtx).GetCoins().AmountOf(coinDenom)
var borrowCoinPriorAmount sdk.Int
borrowCoinsPrior, borrowCoinsPriorFound := suite.keeper.GetBorrowedCoins(prevCtx)
suite.Require().True(borrowCoinsPriorFound)
borrowCoinPriorAmount = borrowCoinsPrior.AmountOf(coinDenom)
var supplyCoinPriorAmount sdk.Int
supplyCoinsPrior, supplyCoinsPriorFound := suite.keeper.GetSuppliedCoins(prevCtx)
suite.Require().True(supplyCoinsPriorFound)
supplyCoinPriorAmount = supplyCoinsPrior.AmountOf(coinDenom)
reservesPrior, foundReservesPrior := suite.keeper.GetTotalReserves(prevCtx, coinDenom)
if !foundReservesPrior {
reservesPrior = sdk.NewCoin(coinDenom, sdk.ZeroInt())
}
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, coinDenom)
suite.Require().True(foundBorrowInterestFactorPrior)
supplyInterestFactorPrior, foundSupplyInterestFactorPrior := suite.keeper.GetSupplyInterestFactor(prevCtx, coinDenom)
suite.Require().True(foundSupplyInterestFactorPrior)
// 2. Calculate expected borrow interest owed
borrowRateApy, err := hard.CalculateBorrowRate(tc.args.interestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
suite.Require().NoError(err)
// Convert from APY to SPY, expressed as (1 + borrow rate)
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
suite.Require().NoError(err)
newBorrowInterestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
expectedBorrowInterest := (newBorrowInterestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
expectedReserves := reservesPrior.Add(sdk.NewCoin(coinDenom, sdk.NewDecFromInt(expectedBorrowInterest).Mul(tc.args.reserveFactor).TruncateInt())).Sub(reservesPrior)
expectedTotalReserves := expectedReserves.Add(reservesPrior)
expectedBorrowInterestFactor := borrowInterestFactorPrior.Mul(newBorrowInterestFactor)
expectedSupplyInterest := expectedBorrowInterest.Sub(expectedReserves.Amount)
newSupplyInterestFactor := hard.CalculateSupplyInterestFactor(expectedSupplyInterest.ToDec(), sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
expectedSupplyInterestFactor := supplyInterestFactorPrior.Mul(newSupplyInterestFactor)
// -------------------------------------------------------------------------------------
// Set up snapshot chain context and run begin blocker
runAtTime := time.Unix(prevCtx.BlockTime().Unix()+(snapshot.elapsedTime), 0)
snapshotCtx := prevCtx.WithBlockTime(runAtTime)
hard.BeginBlocker(snapshotCtx, suite.keeper)
borrowInterestFactor, _ := suite.keeper.GetBorrowInterestFactor(ctx, coinDenom)
suite.Require().Equal(expectedBorrowInterestFactor, borrowInterestFactor)
suite.Require().Equal(expectedBorrowInterest, expectedSupplyInterest.Add(expectedReserves.Amount))
// Check that the total amount of borrowed coins has increased by expected borrow interest amount
borrowCoinsPost, _ := suite.keeper.GetBorrowedCoins(snapshotCtx)
borrowCoinPostAmount := borrowCoinsPost.AmountOf(coinDenom)
suite.Require().Equal(borrowCoinPostAmount, borrowCoinPriorAmount.Add(expectedBorrowInterest))
// Check that the total amount of supplied coins has increased by expected supply interest amount
supplyCoinsPost, _ := suite.keeper.GetSuppliedCoins(prevCtx)
supplyCoinPostAmount := supplyCoinsPost.AmountOf(coinDenom)
suite.Require().Equal(supplyCoinPostAmount, supplyCoinPriorAmount.Add(expectedSupplyInterest))
// Check current total reserves
totalReserves, _ := suite.keeper.GetTotalReserves(snapshotCtx, coinDenom)
suite.Require().Equal(expectedTotalReserves, totalReserves)
// Check that the supply index has increased as expected
currSupplyIndexPrior, _ := suite.keeper.GetSupplyInterestFactor(snapshotCtx, coinDenom)
suite.Require().Equal(expectedSupplyInterestFactor, currSupplyIndexPrior)
// // Check that the borrow index has increased as expected
currBorrowIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, coinDenom)
suite.Require().Equal(expectedBorrowInterestFactor, currBorrowIndexPrior)
// After supplying again user's supplied balance should have owed supply interest applied
if snapshot.shouldSupply {
// Calculate percentage of supply interest profits owed to user
userSupplyBefore, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
userSupplyCoinAmount := userSupplyBefore.Amount.AmountOf(coinDenom)
userPercentOfTotalSupplied := userSupplyCoinAmount.ToDec().Quo(supplyCoinPriorAmount.ToDec())
userExpectedSupplyInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalSupplied.MulInt(expectedSupplyInterest).TruncateInt())
// Calculate percentage of borrow interest profits owed to user
userBorrowBefore, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
userBorrowCoinAmount := userBorrowBefore.Amount.AmountOf(coinDenom)
userPercentOfTotalBorrowed := userBorrowCoinAmount.ToDec().Quo(borrowCoinPriorAmount.ToDec())
userExpectedBorrowInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalBorrowed.MulInt(expectedBorrowInterest).TruncateInt())
expectedBorrowCoinsAfter := userBorrowBefore.Amount.Add(userExpectedBorrowInterestCoin)
// Supplying syncs user's owed supply and borrow interest
err = suite.keeper.Deposit(snapshotCtx, tc.args.user, sdk.NewCoins(snapshot.supplyCoin))
suite.Require().NoError(err)
// Fetch user's new borrow and supply balance post-interaction
userSupplyAfter, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
userBorrowAfter, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
// Confirm that user's supply index for the denom has increased as expected
var userSupplyAfterIndexFactor sdk.Dec
for _, indexFactor := range userSupplyAfter.Index {
if indexFactor.Denom == coinDenom {
userSupplyAfterIndexFactor = indexFactor.Value
}
}
suite.Require().Equal(userSupplyAfterIndexFactor, currSupplyIndexPrior)
// Check user's supplied amount increased by supply interest owed + the newly supplied coins
expectedSupplyCoinsAfter := userSupplyBefore.Amount.Add(snapshot.supplyCoin).Add(userExpectedSupplyInterestCoin)
suite.Require().Equal(expectedSupplyCoinsAfter, userSupplyAfter.Amount)
// Confirm that user's borrow index for the denom has increased as expected
var userBorrowAfterIndexFactor sdk.Dec
for _, indexFactor := range userBorrowAfter.Index {
if indexFactor.Denom == coinDenom {
userBorrowAfterIndexFactor = indexFactor.Value
}
}
suite.Require().Equal(userBorrowAfterIndexFactor, currBorrowIndexPrior)
// Check user's borrowed amount increased by borrow interest owed
suite.Require().Equal(expectedBorrowCoinsAfter, userBorrowAfter.Amount)
}
prevCtx = snapshotCtx
}
}
})
}
}
func TestInterestTestSuite(t *testing.T) {
suite.Run(t, new(InterestTestSuite))
}

View File

@ -248,6 +248,29 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
return borrowedCoins, true
}
// SetSuppliedCoins sets the total amount of coins currently supplied in the store
func (k Keeper) SetSuppliedCoins(ctx sdk.Context, suppliedCoins sdk.Coins) {
store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
if suppliedCoins.Empty() {
store.Set([]byte{}, []byte{})
} else {
bz := k.cdc.MustMarshalBinaryBare(suppliedCoins)
store.Set([]byte{}, bz)
}
}
// GetSuppliedCoins returns an sdk.Coins object from the store representing all currently supplied coins
func (k Keeper) GetSuppliedCoins(ctx sdk.Context) (sdk.Coins, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
bz := store.Get([]byte{})
if bz == nil {
return sdk.Coins{}, false
}
var suppliedCoins sdk.Coins
k.cdc.MustUnmarshalBinaryBare(bz, &suppliedCoins)
return suppliedCoins, true
}
// GetMoneyMarket returns a money market from the store for a denom
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
@ -326,22 +349,41 @@ func (k Keeper) SetTotalReserves(ctx sdk.Context, denom string, coin sdk.Coin) {
store.Set([]byte(denom), bz)
}
// GetInterestFactor returns the current interest factor for an individual market
func (k Keeper) GetInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
// GetBorrowInterestFactor returns the current borrow interest factor for an individual market
func (k Keeper) GetBorrowInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return sdk.ZeroDec(), false
}
var interestFactor sdk.Dec
k.cdc.MustUnmarshalBinaryBare(bz, &interestFactor)
return interestFactor, true
var borrowInterestFactor sdk.Dec
k.cdc.MustUnmarshalBinaryBare(bz, &borrowInterestFactor)
return borrowInterestFactor, true
}
// SetInterestFactor sets the current interest factor for an individual market
func (k Keeper) SetInterestFactor(ctx sdk.Context, denom string, borrowIndex sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
// SetBorrowInterestFactor sets the current borrow interest factor for an individual market
func (k Keeper) SetBorrowInterestFactor(ctx sdk.Context, denom string, borrowInterestFactor sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
bz := k.cdc.MustMarshalBinaryBare(borrowInterestFactor)
store.Set([]byte(denom), bz)
}
// GetSupplyInterestFactor returns the current supply interest factor for an individual market
func (k Keeper) GetSupplyInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return sdk.ZeroDec(), false
}
var supplyInterestFactor sdk.Dec
k.cdc.MustUnmarshalBinaryBare(bz, &supplyInterestFactor)
return supplyInterestFactor, true
}
// SetSupplyInterestFactor sets the current supply interest factor for an individual market
func (k Keeper) SetSupplyInterestFactor(ctx sdk.Context, denom string, supplyInterestFactor sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
bz := k.cdc.MustMarshalBinaryBare(supplyInterestFactor)
store.Set([]byte(denom), bz)
}

View File

@ -76,7 +76,8 @@ func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
}
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
types.SupplyInterestFactors{types.NewSupplyInterestFactor("", sdk.MustNewDecFromStr("0"))})
_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"))
suite.Require().False(f)
@ -96,7 +97,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
func (suite *KeeperTestSuite) TestIterateDeposits() {
for i := 0; i < 5; i++ {
dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), types.SupplyInterestFactors{})
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
}
var deposits []types.Deposit

View File

@ -39,7 +39,7 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
return false, err
}
k.SyncOutstandingInterest(ctx, borrower)
k.SyncBorrowInterest(ctx, borrower)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, borrower)

View File

@ -15,8 +15,8 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
return err
}
// Sync interest so loan is up-to-date
k.SyncOutstandingInterest(ctx, sender)
// Sync borrow interest so loan is up-to-date
k.SyncBorrowInterest(ctx, sender)
// Validate requested repay
err = k.ValidateRepay(ctx, sender, coins)
@ -30,7 +30,10 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
return types.ErrBorrowNotFound
}
payment := k.CalculatePaymentAmount(borrow.Amount, coins)
payment, err := k.CalculatePaymentAmount(borrow.Amount, coins)
if err != nil {
return err
}
// Sends coins from user to Hard module account
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment)
@ -73,8 +76,13 @@ func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.
}
// CalculatePaymentAmount prevents overpayment when repaying borrowed coins
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Coins {
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) (sdk.Coins, error) {
repayment := sdk.Coins{}
if !payment.DenomsSubsetOf(owed) {
return repayment, types.ErrInvalidRepaymentDenom
}
for _, coin := range payment {
if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
repayment = append(repayment, sdk.NewCoin(coin.Denom, owed.AmountOf(coin.Denom)))
@ -82,5 +90,5 @@ func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Co
repayment = append(repayment, coin)
}
}
return repayment
return repayment, nil
}

View File

@ -86,6 +86,21 @@ func (suite *KeeperTestSuite) TestRepay() {
contains: "",
},
},
{
"invalid: attempt to repay non-supplied coin",
args{
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))),
depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))},
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*KAVA_CF))),
},
errArgs{
expectPass: false,
contains: "account can only repay up to 0bnb",
},
},
{
"invalid: insufficent balance for repay",
args{
@ -98,7 +113,7 @@ func (suite *KeeperTestSuite) TestRepay() {
},
errArgs{
expectPass: false,
contains: "account can only repay up to",
contains: "account can only repay up to 50000000ukava",
},
},
}
@ -203,7 +218,8 @@ func (suite *KeeperTestSuite) TestRepay() {
if tc.errArgs.expectPass {
suite.Require().NoError(err)
// If we overpaid expect an adjustment
repaymentCoins := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
repaymentCoins, err := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
suite.Require().NoError(err)
// Check borrower balance
expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins)

View File

@ -84,7 +84,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
keeper := tApp.GetHardKeeper()
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount)
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount, types.SupplyInterestFactors{})
keeper.SetDeposit(ctx, deposit)
suite.app = tApp
suite.ctx = ctx

98
x/hard/keeper/withdraw.go Normal file
View File

@ -0,0 +1,98 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/hard/types"
)
// Withdraw returns some or all of a deposit back to original depositor
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
// Get current stored LTV based on stored borrows/deposits
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
if err != nil {
return err
}
k.SyncBorrowInterest(ctx, depositor)
k.SyncSupplyInterest(ctx, depositor)
deposit, found := k.GetDeposit(ctx, depositor)
if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
}
amount, err := k.CalculateWithdrawAmount(deposit.Amount, coins)
if err != nil {
return err
}
proposedDeposit := types.NewDeposit(deposit.Depositor, deposit.Amount.Sub(amount), types.SupplyInterestFactors{})
borrow, found := k.GetBorrow(ctx, depositor)
if !found {
borrow = types.Borrow{}
}
valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
if err != nil {
return err
}
if !valid {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, amount)
if err != nil {
return err
}
if deposit.Amount.IsEqual(amount) {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDeleteHardDeposit,
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
),
)
k.DeleteDeposit(ctx, deposit)
return nil
}
deposit.Amount = deposit.Amount.Sub(amount)
k.SetDeposit(ctx, deposit)
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
// Update total supplied amount
k.DecrementBorrowedCoins(ctx, amount)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHardWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
),
)
return nil
}
// CalculateWithdrawAmount enables full withdraw of deposited coins by adjusting withdraw amount
// to equal total deposit amount if the requested withdraw amount > current deposit amount
func (k Keeper) CalculateWithdrawAmount(available sdk.Coins, request sdk.Coins) (sdk.Coins, error) {
result := sdk.Coins{}
if !request.DenomsSubsetOf(available) {
return result, types.ErrInvalidWithdrawDenom
}
for _, coin := range request {
if coin.Amount.GT(available.AmountOf(coin.Denom)) {
result = append(result, sdk.NewCoin(coin.Denom, available.AmountOf(coin.Denom)))
} else {
result = append(result, coin)
}
}
return result, nil
}

View File

@ -0,0 +1,390 @@
package keeper_test
import (
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hard"
"github.com/kava-labs/kava/x/hard/types"
"github.com/kava-labs/kava/x/pricefeed"
)
func (suite *KeeperTestSuite) TestWithdraw() {
type args struct {
depositor sdk.AccAddress
initialModAccountBalance sdk.Coins
depositAmount sdk.Coins
withdrawAmount sdk.Coins
createDeposit bool
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
depositExists bool
finalDepositAmount sdk.Coins
}
type errArgs struct {
expectPass bool
contains string
}
type withdrawTest struct {
name string
args args
errArgs errArgs
}
testCases := []withdrawTest{
{
"valid: partial withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialModAccountBalance: sdk.Coins(nil),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
depositExists: true,
finalDepositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid: full withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialModAccountBalance: sdk.Coins(nil),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.Coins(nil),
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid: withdraw exceeds deposit but is adjusted to match max deposit",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid: withdraw non-supplied coin type",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialModAccountBalance: sdk.Coins(nil),
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
withdrawAmount: sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "no coins of this type deposited",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.depositor},
[]sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))},
)
loanToValue := sdk.MustNewDecFromStr("0.6")
hardGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(100000000), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
},
0, // LTV counter
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
// Pricefeed module genesis state
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("10.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
},
}
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
// Mint coins to Hard module account
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModAccountBalance)
if tc.args.createDeposit {
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
suite.Require().NoError(err)
}
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.depositor)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
if tc.args.depositExists {
suite.Require().True(f)
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
} else {
suite.Require().False(f)
}
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestLtvWithdraw() {
type args struct {
borrower sdk.AccAddress
initialModuleCoins sdk.Coins
initialBorrowerCoins sdk.Coins
depositCoins sdk.Coins
borrowCoins sdk.Coins
futureTime int64
}
type errArgs struct {
expectPass bool
contains string
}
type liqTest struct {
name string
args args
errArgs errArgs
}
// Set up test constants
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
reserveFactor := sdk.MustNewDecFromStr("0.05")
oneMonthInSeconds := int64(2592000)
borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
testCases := []liqTest{
{
"invalid: withdraw is outside loan-to-value range",
args{
borrower: borrower,
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // 10 * 2 = $20
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), // 8 * 2 = $16
futureTime: oneMonthInSeconds,
},
errArgs{
expectPass: false,
contains: "proposed withdraw outside loan-to-value range",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// Auth module genesis state
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.borrower},
[]sdk.Coins{tc.args.initialBorrowerCoins},
)
// Harvest module genesis state
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("ukava",
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"kava:usd", // Market ID
sdk.NewInt(KAVA_CF), // Conversion Factor
sdk.NewInt(100000000*KAVA_CF), // Auction Size
model, // Interest Rate Model
reserveFactor, // Reserve Factor
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
types.NewMoneyMarket("usdx",
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"usdx:usd", // Market ID
sdk.NewInt(KAVA_CF), // Conversion Factor
sdk.NewInt(100000000*KAVA_CF), // Auction Size
model, // Interest Rate Model
reserveFactor, // Reserve Factor
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
},
0, // LTV counter
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
// Pricefeed module genesis state
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(100 * time.Hour),
},
},
}
// Initialize test application
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
// Mint coins to Harvest module account
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
auctionKeeper := tApp.GetAuctionKeeper()
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.auctionKeeper = auctionKeeper
var err error
// Run begin blocker to set up state
hard.BeginBlocker(suite.ctx, suite.keeper)
// Borrower deposits coins
err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
suite.Require().NoError(err)
// Borrower borrows coins
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
suite.Require().NoError(err)
// Attempting to withdraw fails
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Set up future chain context and run begin blocker, increasing user's owed borrow balance
runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
liqCtx := suite.ctx.WithBlockTime(runAtTime)
hard.BeginBlocker(liqCtx, suite.keeper)
// Attempted withdraw of 1 coin still fails
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Repay the initial principal
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
suite.Require().NoError(err)
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Withdrawing half the coins should succeed
withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(2))))
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
suite.Require().NoError(err)
})
}
}

View File

@ -27,7 +27,9 @@ func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec()
prevBlockTime := time.Now().UTC()
deposit := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))))
deposit := types.NewDeposit(sdk.AccAddress("test"),
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))),
types.SupplyInterestFactors{types.NewSupplyInterestFactor("bnb", sdk.OneDec())})
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
kvPairs := kv.Pairs{

View File

@ -4,32 +4,15 @@ 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 hard module account
type Borrow struct {
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
Index InterestFactors `json:"index" yaml:"index"`
Index BorrowInterestFactors `json:"index" yaml:"index"`
}
// NewBorrow returns a new Borrow instance
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors) Borrow {
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index BorrowInterestFactors) Borrow {
return Borrow{
Borrower: borrower,
Amount: amount,
@ -37,19 +20,19 @@ func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors)
}
}
// InterestFactor defines an individual interest factor
type InterestFactor struct {
// BorrowInterestFactor defines an individual borrow interest factor
type BorrowInterestFactor struct {
Denom string `json:"denom" yaml:"denom"`
Value sdk.Dec `json:"value" yaml:"value"`
}
// NewInterestFactor returns a new InterestFactor instance
func NewInterestFactor(denom string, value sdk.Dec) InterestFactor {
return InterestFactor{
// NewBorrowInterestFactor returns a new BorrowInterestFactor instance
func NewBorrowInterestFactor(denom string, value sdk.Dec) BorrowInterestFactor {
return BorrowInterestFactor{
Denom: denom,
Value: value,
}
}
// InterestFactors is a slice of InterestFactor, because Amino won't marshal maps
type InterestFactors []InterestFactor
// BorrowInterestFactors is a slice of BorrowInterestFactor, because Amino won't marshal maps
type BorrowInterestFactors []BorrowInterestFactor

View File

@ -8,12 +8,31 @@ import (
type Deposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
Index SupplyInterestFactors `json:"index" yaml:"index"`
}
// NewDeposit returns a new deposit
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins) Deposit {
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins, indexes SupplyInterestFactors) Deposit {
return Deposit{
Depositor: depositor,
Amount: amount,
Index: indexes,
}
}
// SupplyInterestFactor defines an individual borrow interest factor
type SupplyInterestFactor struct {
Denom string `json:"denom" yaml:"denom"`
Value sdk.Dec `json:"value" yaml:"value"`
}
// NewSupplyInterestFactor returns a new SupplyInterestFactor instance
func NewSupplyInterestFactor(denom string, value sdk.Dec) SupplyInterestFactor {
return SupplyInterestFactor{
Denom: denom,
Value: value,
}
}
// SupplyInterestFactors is a slice of SupplyInterestFactor, because Amino won't marshal maps
type SupplyInterestFactors []SupplyInterestFactor

View File

@ -67,4 +67,12 @@ var (
ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins")
// ErrInsufficientBalanceForBorrow error for when the requested borrow exceeds user's balance
ErrInsufficientBalanceForBorrow = sdkerrors.Register(ModuleName, 31, "insufficient balance")
// ErrSuppliedCoinsNotFound error for when the total amount of supplied coins cannot be found
ErrSuppliedCoinsNotFound = sdkerrors.Register(ModuleName, 32, "no supplied coins found")
// ErrNegativeSuppliedCoins error for when substracting coins from the total supplied balance results in a negative amount
ErrNegativeSuppliedCoins = sdkerrors.Register(ModuleName, 33, "subtraction results in negative supplied amount")
// ErrInvalidWithdrawDenom error for when user attempts to withdraw a non-supplied coin type
ErrInvalidWithdrawDenom = sdkerrors.Register(ModuleName, 34, "no coins of this type deposited")
// ErrInvalidRepaymentDenom error for when user attempts to repay a non-borrowed coin type
ErrInvalidRepaymentDenom = sdkerrors.Register(ModuleName, 35, "no coins of this type borrowed")
)

View File

@ -40,11 +40,13 @@ var (
ClaimsKeyPrefix = []byte{0x04}
BorrowsKeyPrefix = []byte{0x05}
BorrowedCoinsPrefix = []byte{0x06}
MoneyMarketsPrefix = []byte{0x07}
PreviousAccrualTimePrefix = []byte{0x08} // denom -> time
TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin
InterestFactorPrefix = []byte{0x10} // denom -> sdk.Dec
LtvIndexPrefix = []byte{0x11}
SuppliedCoinsPrefix = []byte{0x07}
MoneyMarketsPrefix = []byte{0x08}
PreviousAccrualTimePrefix = []byte{0x09} // denom -> time
TotalReservesPrefix = []byte{0x10} // denom -> sdk.Coin
BorrowInterestFactorPrefix = []byte{0x11} // denom -> sdk.Dec
SupplyInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec
LtvIndexPrefix = []byte{0x13}
sep = []byte(":")
)