From f7a73c9245864a90f8996129c0d9cbc5134d0698 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Thu, 7 Jan 2021 11:23:05 +0100 Subject: [PATCH] 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 --- x/hard/alias.go | 3 +- x/hard/keeper/borrow.go | 138 +++---- x/hard/keeper/deposit.go | 148 ++++---- x/hard/keeper/deposit_test.go | 357 ------------------ x/hard/keeper/interest.go | 153 ++++++-- x/hard/keeper/interest_test.go | 581 +++++++++++++++++++++++++++++- x/hard/keeper/keeper.go | 62 +++- x/hard/keeper/keeper_test.go | 5 +- x/hard/keeper/liquidation.go | 2 +- x/hard/keeper/repay.go | 18 +- x/hard/keeper/repay_test.go | 20 +- x/hard/keeper/rewards_test.go | 2 +- x/hard/keeper/withdraw.go | 98 +++++ x/hard/keeper/withdraw_test.go | 390 ++++++++++++++++++++ x/hard/simulation/decoder_test.go | 4 +- x/hard/types/borrow.go | 39 +- x/hard/types/deposit.go | 25 +- x/hard/types/errors.go | 8 + x/hard/types/keys.go | 12 +- 19 files changed, 1458 insertions(+), 607 deletions(-) create mode 100644 x/hard/keeper/withdraw.go create mode 100644 x/hard/keeper/withdraw_test.go diff --git a/x/hard/alias.go b/x/hard/alias.go index 4c038d07..d4cf9a9e 100644 --- a/x/hard/alias.go +++ b/x/hard/alias.go @@ -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 diff --git a/x/hard/keeper/borrow.go b/x/hard/keeper/borrow.go index ff8458f6..03bf89c8 100644 --- a/x/hard/keeper/borrow.go +++ b/x/hard/keeper/borrow.go @@ -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,29 +246,31 @@ 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 { - totalNewInterest := sdk.Coins{} - for _, coin := range borrow.Amount { - interestFactorValue, foundInterestFactorValue := k.GetInterestFactor(ctx, coin.Denom) - if foundInterestFactorValue { - // Locate the interest factor by coin denom in the user's list of interest factors - foundAtIndex := -1 - for i := range borrow.Index { - if borrow.Index[i].Denom == coin.Denom { - foundAtIndex = i - break - } - } - // Calculate interest owed by user for this asset - if foundAtIndex != -1 { - storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom)) - userLastInterestFactor := borrow.Index[foundAtIndex].Value - coinInterest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount) - totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, coinInterest.TruncateInt())) + if !found { + return borrowBalance + } + + totalNewInterest := sdk.Coins{} + for _, coin := range borrow.Amount { + 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 + for i := range borrow.Index { + if borrow.Index[i].Denom == coin.Denom { + foundAtIndex = i + break } } + // Calculate interest owed by user for this asset + if foundAtIndex != -1 { + storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom)) + userLastInterestFactor := borrow.Index[foundAtIndex].Value + coinInterest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount) + totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, coinInterest.TruncateInt())) + } } - borrowBalance = borrow.Amount.Add(totalNewInterest...) } - return borrowBalance + + return borrow.Amount.Add(totalNewInterest...) } diff --git a/x/hard/keeper/deposit.go b/x/hard/keeper/deposit.go index 9e3c8ed7..99f56151 100644 --- a/x/hard/keeper/deposit.go +++ b/x/hard/keeper/deposit.go @@ -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 +} diff --git a/x/hard/keeper/deposit_test.go b/x/hard/keeper/deposit_test.go index 451e106a..39fdea5d 100644 --- a/x/hard/keeper/deposit_test.go +++ b/x/hard/keeper/deposit_test.go @@ -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) - }) - } -} diff --git a/x/hard/keeper/interest.go b/x/hard/keeper/interest.go index 44b64f29..703c774a 100644 --- a/x/hard/keeper/interest.go +++ b/x/hard/keeper/interest.go @@ -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) { diff --git a/x/hard/keeper/interest_test.go b/x/hard/keeper/interest_test.go index de186e9f..2bbedf63 100644 --- a/x/hard/keeper/interest_test.go +++ b/x/hard/keeper/interest_test.go @@ -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)) } diff --git a/x/hard/keeper/keeper.go b/x/hard/keeper/keeper.go index 2613ffa5..4caf3dea 100644 --- a/x/hard/keeper/keeper.go +++ b/x/hard/keeper/keeper.go @@ -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) } diff --git a/x/hard/keeper/keeper_test.go b/x/hard/keeper/keeper_test.go index 31dc7de7..55a5c0b9 100644 --- a/x/hard/keeper/keeper_test.go +++ b/x/hard/keeper/keeper_test.go @@ -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 diff --git a/x/hard/keeper/liquidation.go b/x/hard/keeper/liquidation.go index 3de1391f..a4130c46 100644 --- a/x/hard/keeper/liquidation.go +++ b/x/hard/keeper/liquidation.go @@ -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) diff --git a/x/hard/keeper/repay.go b/x/hard/keeper/repay.go index c1f5822a..239962c4 100644 --- a/x/hard/keeper/repay.go +++ b/x/hard/keeper/repay.go @@ -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 } diff --git a/x/hard/keeper/repay_test.go b/x/hard/keeper/repay_test.go index f124dabe..50739d6d 100644 --- a/x/hard/keeper/repay_test.go +++ b/x/hard/keeper/repay_test.go @@ -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) diff --git a/x/hard/keeper/rewards_test.go b/x/hard/keeper/rewards_test.go index 133deb1d..d9624a8d 100644 --- a/x/hard/keeper/rewards_test.go +++ b/x/hard/keeper/rewards_test.go @@ -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 diff --git a/x/hard/keeper/withdraw.go b/x/hard/keeper/withdraw.go new file mode 100644 index 00000000..61854ce7 --- /dev/null +++ b/x/hard/keeper/withdraw.go @@ -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 +} diff --git a/x/hard/keeper/withdraw_test.go b/x/hard/keeper/withdraw_test.go new file mode 100644 index 00000000..1907eb95 --- /dev/null +++ b/x/hard/keeper/withdraw_test.go @@ -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) + }) + } +} diff --git a/x/hard/simulation/decoder_test.go b/x/hard/simulation/decoder_test.go index 007afd23..21cae5cd 100644 --- a/x/hard/simulation/decoder_test.go +++ b/x/hard/simulation/decoder_test.go @@ -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{ diff --git a/x/hard/types/borrow.go b/x/hard/types/borrow.go index cec6e4d1..531e5968 100644 --- a/x/hard/types/borrow.go +++ b/x/hard/types/borrow.go @@ -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"` + Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + 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 diff --git a/x/hard/types/deposit.go b/x/hard/types/deposit.go index cf531165..bcc9bd88 100644 --- a/x/hard/types/deposit.go +++ b/x/hard/types/deposit.go @@ -6,14 +6,33 @@ import ( // Deposit defines an amount of coins deposited into a hard module account type Deposit struct { - Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` - Amount sdk.Coins `json:"amount" yaml:"amount"` + 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 diff --git a/x/hard/types/errors.go b/x/hard/types/errors.go index fd646785..20c7290c 100644 --- a/x/hard/types/errors.go +++ b/x/hard/types/errors.go @@ -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") ) diff --git a/x/hard/types/keys.go b/x/hard/types/keys.go index b7aaac29..ccf20bb6 100644 --- a/x/hard/types/keys.go +++ b/x/hard/types/keys.go @@ -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(":") )